The end of a semester is here and, as I grade our student's semestral works, I get to use Makefiles and CMakeLists of dubious quality[1]. After seeing the same errors repeated over and over again, I decided to write a short tutorial towards writing simple Makefiles and CMakeLists. This is the Make tutorial, the CMake one can be found here.
Through these tutorials, I'll use a very simple example from one of our labs. It is start of an implementation of growing array (ala std::vector
), consisting of 5 files:
main.cpp
vector.hpp
vector.cpp
array.hpp
array.cpp
Their exact contents do not matter[2], but main.cpp
includes vector.hpp
, vector.cpp
includes array.hpp
and both vector.cpp
and array.cpp
include their respective headers, vector.hpp
and array.hpp
.
It is important to note that these tutorials are not meant to build a bottom-up understanding of either of the two, but rather provide a person with an easy-to-modify template that they can use for themselves and quickly get back to the interesting part -- their code.
Make
Make is a general purpose task automation tool, but it's most common use is in building things from source. It even has some built-in rules, e.g. for building .o
files from .cpp
files, but relying on these is often considered bad form.
Makefiles are a set of targets, where each target can have many dependencies, and each target has a set of commands that will satisfy this target. The structure of a single target is:
target: dependency1 dependency2 dependency3 ...
command1
command2
...
Note that commands are preceded by a tab, not spaces!
An example Makefile that is fairly common in online examples looks something like this:
CXXFLAGS += -Wall -Wextra -std=c++14
SOURCES = array.cpp vector.cpp main.cpp
OBJECTS = $(SOURCES:.cpp=.o)
.PHONY: clean all
all: vector-test
%.o: %.cpp
$(CXX) $(CXXFLAGS) $^ -o $@ -c
vector-test: $(OBJECTS)
$(CXX) $(CXXFLAGS) $^ -o $@
clean:
rm -f *.o vector-test
This Makefile lets the user call
make
to compile thevector-test
binary from specified sourcesmake clean
to remove all build artifacts (object files and the final binary)
I will go over how it works later, as this Makefile has a very important problem: it does not track dependencies between implementation files and header files, so if vector.hpp
changes, it will not recompile any object files. We can fix this in two ways, the most straightforward of which is tracking the dependencies manually, by adding these targets to our Makefile:
array.o: array.hpp
vector.o: vector.hpp array.hpp
main.o: vector.hpp
This works but obviously does not scale well. What we can do instead, is to fill in the dependencies automatically, with compiler's help. The compiler has to resolve all dependencies of an object file during compilation (as it has to include each header it depends on) anyway, so all we need is to get the information into a Make-friendly format.
Luckily both GCC and Clang can already output dependencies in Make format, so all we need is to change our Makefile somewhat and add these two lines:
CXXFLAGS += -MMD -MP
-include $(OBJECTS:.o=.d)
-MMD
tells the compiler to output a Makefile snippet for each compiled .cpp
file, and save it into a .d
file with the same name. As an example, for main.cpp
it will output this snippet:
main.o: main.cpp vector.hpp
-MP
then tells the compiler to also output a non-dependent target for each header file it encounters. This prevents Make errors if header files are renamed/deleted/moved, and it attempts to use the old dependency information, as it thinks it can create the missing header. The output for main.cpp
will now look like this:
main.o: main.cpp vector.hpp
vector.hpp:
Finally, include $(OBJECTS:%.o=%.d)
tells Make to include all .d
files created by compiling object files, and the -
prefix tells it to ignore error during inclusions -- this prevents errors when compiling the project from scratch when corresponding .d
files have not yet been created.
We should also extend the clean
target to remove the generated .d
files.
The final Makefile should look something like this:
CXXFLAGS += -Wall -Wextra -std=c++14 -MMD -MP
SOURCES = array.cpp vector.cpp main.cpp
OBJECTS = $(SOURCES:.cpp=.o)
.PHONY: clean all
.DEFAULT_GOAL := all
all: vector-test
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@ -c
vector-test: $(OBJECTS)
$(CXX) $(CXXFLAGS) $^ -o $@
clean:
rm -f *.o vector-test *.d
-include $(OBJECTS:.o=.d)
This Makefile provides basic functionality, that is
- Functional header dependency tracking -> if a header changes, all dependent files will be recompiled
- And only the dependent ones
make clean
removes build artifacts- And it works even if a file named
clean
is present in the folder
- And it works even if a file named
- Default target, so
make
will build the desired binary - An
all
target, somake all
will build all binaries - The files will be compiled with reasonable warnings and C++ standard enabled
How does it work?
The first line appends extra flags -Wall -Wextra -std=c++14 -MMD -MP
to variable CXXFLAGS
. The reason the flags are appended is that it allows users of the Makefile to add to the flags easily. E.g. CXXFLAGS=-Weverything make all
would mean that CXXFLAGS
would expand into -Weverything -Wall -Wextra -std=c++14 -MMD -MP
inside the makefile.
The third line defines variable SOURCES
as a list of three files, main.cpp
, vector.cpp
and array.cpp
. Fourth line the defines a variable OBJECTS
as a list of files create from SOURCES
by replacing the .cpp
suffix of each file with .o
suffix.
Next up we use a special target called .PHONY
to denote that certain targets are not files, but rather names for a set of tasks. This means that they will be run even if a file with this name already exists. Next, we modify what happens when make
is invoked without a target, by setting special variable .DEFAULT_GOAL
. By convention, plain make
invocation is expected to build everything, so we explicitly set it to all
.[3]
Next up are 4 targets, all
, a wildcard target, vector-test
and clean
. By convention, all makefiles should provide a target named all
, that builds all binaries in the makefile. Similarly, all makefiles should provide a target named clean
, that returns the directory into the original state (i.e. deletes build artifacts and generated files).
Target vector-test
describes how to build the desired binary. It depends on all of the object files and is created by invoking $(CXX) $(CXXFLAGS) $^ -o $@
on the command line. $^
is an implicit variable containing all dependencies, $@
is another implicit variable containing target's name. Desugared a bit, the command becomes $(CXX) $(CXXFLAGS) $(OBJECTS) -o vector-test
. This is another convention used in makefiles, binaries have the same name as the target that generates them (or targets have the same name as binaries that they generate).
The last undescribed build rule is a wildcard rule %.o: %.cpp
. This rule applies to every file ending in .o
(or to all object files). It says that each object file depends on an implementation file of the same name, and is generated by invoking $(CXX) $(CXXFLAGS) $< -o $@ -c
. This command uses another implicit variable, $<
, containing the name of the first dependency.[4]
Closing words
I think that both Make and CMake are terrible. Make is horrible because it does not handle spaces in paths, contains some very strong assumptions about running on Linux (and maybe other POSIX systems) and there are many incompatible dialects (GNU Make, BSD Make, NMake, the other NMake, etc.). The syntax isn't anything to write home about either.
CMake then has absolutely horrendous syntax, contains a large amount of backward compatibility cruft and lot of design decisions in it are absolutely mindboggling -- across my contributions to OSS projects I've encountered enough crazy things that they need to be in their own post.
Still, I am strongly in favour of using CMake, if just for supporting various IDEs well and being able to deal with Windows properly.
Last time we didn't teach either, but we will probably add a short introduction to both to the next semester. ↩︎
In case you do care, here is a link to one of the zips that contains them. ↩︎
If
.DEFAULT_GOAL
is not set, then the first non-special target is built when make is called without a goal..DEFAULT_GOAL
also cannot contain more than 1 target. ↩︎This prevents compiling object files from multiple inputs if the object file has a dependency. ↩︎