Makefiles
We’ll look at GNU Make to manage building projects that are split across multiple files. Make allows us to specify rules for how to compile and link the code.
Note
A popular alternative to GNU Make is CMake.
We’ll use this to manage our planets + multiple files example.
A first GNUmakefile
Here’s a basic makefile, which we’ll name GNUmakefile
:
# by default, make will try to build the first target it encounters.
# here we make up a dummy name "ALL" (note: this is not a special make
# name, it is just commonly used).
ALL: planet_sort_split
# explicitly list the rules for making each of the object files
planet_sort_split.o: planet_sort_split.cpp planet.H
g++ -c planet_sort_split.cpp
planet.o: planet.cpp planet.H
g++ -c planet.cpp
# explicitly write the rule for linking together the executable
planet_sort_split: planet_sort_split.o planet.o
g++ -o planet_sort_split planet_sort_split.o planet.o
There are a few different types of lines, but the most important are the rules that are of the form:
target: dependencies ...
commands
...
Note
make uses a tab character to indent the commands, not spaces.
For example, the rule:
planet_sort_split.o: planet_sort_split.cpp planet.H
g++ -c planet_sort_split.cpp
says that planet_sort_split.o
depends on the files
planet_sort_split.cpp
and planet.H
. And the rule to make it
is g++ -c planet_sort_split.cpp
. By specifying the dependencies,
make
knows whether it needs to recompile a file by looking at
whether its dependencies changed.
It is common to put a target called ALL
at the top, since by
default make
will try to build the first target it encounters.
We build the project by doing:
make
try it…
If you type make
again, it doesn’t do anything, since none of the source
files changed, so it does not need to update the build.
If you modify one file, e.g.,
touch planet.cpp
and then do make again:
make
Then it will only execute the rules that depend on the file that changed.
Generalizing our GNUmakefile
Our first makefile required us to write a rule for every .cpp
file
explicitly. Instead, we can use pattern rules
and wildcards
to automate a lot of the logic.
Here’s the improved GNUmakefile
:
# by default, make will try to build the first target it encounters.
# here we make up a dummy name "ALL" (note: this is not a special make
# name, it is just commonly used).
ALL: planet_sort_split
# find all of the source files and header files
SOURCES := $(wildcard *.cpp)
HEADERS := $(wildcard *.H)
# create a list of object files by replacing .cpp with .o
OBJECTS := $(SOURCES:.cpp=.o)
# a recipe for making an object file from a .cpp file
# Note: this makes every header file a dependency of every object file,
# which is not ideal, but it is safe.
%.o : %.cpp ${HEADERS}
g++ -c $<
# explicitly write the rule for linking together the executable
planet_sort_split: ${OBJECTS}
g++ -o $@ ${OBJECTS}
The first thing we do is find all of the sources and headers:
SOURCES := $(wildcard *.cpp)
HEADERS := $(wildcard *.H)
The :=
operator evaluates these expressions immediately when
make
encounters them and stores the results in the variables
SOURCES
and HEADERS
. To reference the list of files in
these variables, we use ${}
, e.g., ${SOURCES}
.
The next trick is that we create a list of object files by automatically
replacing the .cpp
extension with a .o
extension via the make
expression:
OBJECTS := $(SOURCES:.cpp=.o)
Finally, we have a generic rule to compile a C++ file:
%.o : %.cpp ${HEADERS}
g++ -c $<
Note that we make every header file a dependency for every object file, whether or not it is included in the source file. This is not necessary, but simplifies how we specify the dependencies.
We use a make automatic variable,
$<
to indicate the first name in the dependencies.
We use another automatic variable in the final rule, $@
to indicate
the name of the target, to be used as the executable name:
planet_sort_split: ${OBJECTS}
g++ -o $@ ${OBJECTS}
Going further
There are more things we can do to generalize our makefile.
We can use g++ to automatically determine dependencies. Also see the GNU make automatic prerequisites.
We can have the makefile create code at build time, for instance, writing a function that contains the current git hash of the code
We can different levels of build: optimized and debug
try it…
Modify the GNUmakefile
to add the warning flags we discussed earlier to
the compilation of each source.
Tip
A common target in makefiles is clean
, which will delete the intermediate
*.o
files and the executable.
Tip
Here’s a GNUmakefile I use with some of the in-class examples to build several executables in the same directory.