Working with Makefiles
Manually running the compiler and linker at the command-line gets quite tedious. It’s also quite easy to make mistakes (e.g., recompile and not relink, or vice versa; or recompile the wrong files). Fortunately, we can use a tool called make
to automate much of this work.
To tell the make
command what to do, we create a configuration
file for our project with the name Makefile
. A Makefile lets you specify rules of the form “if any of these ‹prerequisites› change, then ‹target› needs to be updated by running one or more ‹commands›.” For example, here is a Makefile that will compile Homework 3’s segfaultGenerator
code:
generateSegfault: generateSegfault.o helloSayer.o
clang++ -o generateSegfault -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.o helloSayer.o
generateSegfault.o: generateSegfault.cpp helloSayer.hpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.cpp
helloSayer.o: helloSayer.cpp helloSayer.hpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra helloSayer.cpp
Each section of the Makefile has the form
‹target› : ‹prerequisites›
‹commands›
Make is smart in two ways:
- It can detect when some or all of your code has not changed and skip commands related to that code.
- It operates recursively. If
helloSayer.cpp
changes and we try to makegenerateSegfault
, thenhelloSayer.o
will be regenerated. After that, the rule for buildinggenerateSegfault
will run.
There are a couple of tricks to getting your Makefile working:
- It’s impossible to see in our example code above, but every
‹command›
line in theMakefile
must start with a literal “tab” character! If you try to use eight spaces instead of one tab character, your Makefile will not work at all. Thus, Makefiles are exempt from the usual CS 70 rule on indenting with spaces rather than tabs. - Of course, you also need to get the prerequisites right. In general, a
.o
file will depend on the corresponding.cpp
file and any user-written header files that.cpp
file includes (and any header files they include and so on…). Similarly, a runnable program likeshuffle
will depend on the.o
files to be linked.
Phony Targets
In addition to the names of files or programs that we want to keep up-to-date, we can define “phony” targets in our Makefiles. It is idiomatic to include the following “phony” targets:
clean
, which gets rid of all generated files (e.g.,.o
files and compiled executables)all
, which has all relevant programs as prerequisites, but runs no commands itself. Having this target ensures all its prerequisites are up to date (as usual), but then doesn’t do anything else. It is idiomatic forall
to be the first target in a Makefile, so that runningmake
will bring all executables up to date.
We can add these phony targets to our example Makefile from above:
all: generateSegfault
generateSegfault: generateSegfault.o helloSayer.o
clang++ -o generateSegfault -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.o helloSayer.o
generateSegfault.o: generateSegfault.cpp helloSayer.hpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.cpp
helloSayer.o: helloSayer.cpp helloSayer.hpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra helloSayer.cpp
clean:
rm -rf *.o generateSegfault
Eliminating Redundancy
One problem with the Makefile
above is its redundancy. If we wanted to use the g++
compiler instead of clang++
, or if we wanted to turn on even more compiler warnings, we’d have to make lots of changes in lots of places, which is tedious and error-prone.
We can reduce that redundancy by using variables (which make
calls macros). Specifically, if we have the line
VARIABLE=some text
in the Makefile, then any appearance of $(VARIABLE)
is automatically replaced by some text
.
We can update our Makefile from above to include variables/macros:
CXX = clang++
CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra
generateSegfault: generateSegfault.o helloSayer.o
$(CXX) -o generateSegfault $(CXXFLAGS) generateSegfault.o helloSayer.o
generateSegfault.o: generateSegfault.cpp helloSayer.hpp
$(CXX) -c $(CXXFLAGS) generateSegfault.cpp
helloSayer.o: helloSayer.cpp helloSayer.hpp
$(CXX) -c $(CXXFLAGS) helloSayer.cpp
clean:
rm -rf *.o generateSegfault
Special Macros
In addition to defining our own macros, make
provides some predefined macros based on the target and prerequisites. The following three are especially useful for us:
$@
: The target$<
: The first prerequisite$^
: All of the prerequisites
We can update our Makefile from above to make use of these special macros:
CXX = clang++
CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra
generateSegfault: generateSegfault.o helloSayer.o
$(CXX) -o $@ $(CXXFLAGS) $^
generateSegfault.o: generateSegfault.cpp helloSayer.hpp
$(CXX) -c $(CXXFLAGS) $<
helloSayer.o: helloSayer.cpp helloSayer.hpp
$(CXX) -c $(CXXFLAGS) $<
clean:
rm -rf *.o generateSegfault
You can read about more special macros here.
(When logged in, completion status appears here.)