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. 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:
Their exact contents do not matter, but
array.hpp and both
array.cpp include their respective headers,
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 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
maketo compile the
vector-testbinary from specified sources
make cleanto 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:
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
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 cleanremoves build artifacts
- And it works even if a file named
cleanis present in the folder
- And it works even if a file named
- Default target, so
makewill build the desired binary
make allwill 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,
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
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
Next up are 4 targets,
all, a wildcard target,
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).
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.
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. ↩︎
.DEFAULT_GOALis not set, then the first non-special target is built when make is called without a goal.
.DEFAULT_GOALalso cannot contain more than 1 target. ↩︎
This prevents compiling object files from multiple inputs if the object file has a dependency. ↩︎