At some point during software development, you must have read a Makefile or two. Maybe you get to write them, and here are a few notes when writing Makefles for GCC/Clang.
Rules and Files
A Makefile commonly has bits that say:
.PHONY: all all: another-target another-target: blah
This says rule
all depends on
another-target and to get
blah must be built first.
Usually, the rule (thing on the left of
:) represents a file; resulting object of compiling source code (often with
.o extension), the final application from linking all objects, etc. If dependency (things on the right of
:) of a rule isn't changed, then the rule is not built. However, not every rule ends in a file: usually the rule named
all is never a file. So we put such rule under
.PHONY and dependencies of these those rules will always be checked.
Finally, the first rule of a Makefile is the default one invoked when you type
make without any arguments.
You can define variables like so:
VAR := value
If you define variable with
:=, and the thing on the right is another variable, then it is expanded once. If you use
=, then it is always expanded.
If a variable must have some default value, but you want to make it something user can override:
VAR ?= default
You can reference the text in variable by:
Under each rule, you can execute shell commands to do work. That command will be printed out so you can follow along. But if you want to just the command's output, then add a
@ in front of the command:
all: @echo "Hello World!"
Whenever a command in a Makefile emits error,
make stops and gives you line number where it encountered the error. If you know it is possible for the command to fail you can keep
make going by prefixing
- in front of the command.
all: -rm non-existent-file
The most common usage is for working with files that will be generated later. Such as including dependencies. Sometimes you don't need it if the shell command always succeeds; for example
rm -f, even if the file you are trying to delete does not exist.
Conditionals And Looking For Things
These will have indentation on the same level as the rules. For example, checking if a variable is undefined:
my-rule: ifndef VAR ... endif
Do something if variable
VAR has value
my-rule: ifeq ($(VAR),yes) ... endif
Similarly, do something if
FILE does not exist:
my-rule: ifeq (, $(wildcard FILE)) ... endif
$(wildcard) returns a list of files matching the pattern, so you can use it also to collect all .cpp source files under directory PATH/:
CXX_SRCS := $(wildcard PATH/*.cpp)
Identifying the list of source files is useful, often the list is used to transform into list of objects we want to build.
SOURCES, a list of
.cpp files, into a list of
.o files and store the transformed result in variable
OBJECTS is done this way:
OBJECTS := $(SOURCES:%.cpp=%.o)
Then we'll just have define rules to deal with those
.o files to compile your code. Sometimes your project include different types code from another project and you want to invoke different tools to handle each type. Not sure what is the best way to handle this, but this worked for me:
C_SRCS := $(wildcard *.c) CXX_SRCS := $(wildcard *.cpp) CC_SRCS := $(wildcard *.cc) OBJECTS := $(C_SRCS:%.c=%.c.o) OBJECTS += $(CXX_SRCS:%.cpp=%.cpp.o) OBJECTS += $(CC_SRCS:%.cc=%.cc.o)
Above fragment will transform different types of sources distinguished by extension all into
.o extension. This is useful for dependency tracking, where we generate list of dependencies (often file with
.d extension) by:
DEPENDS := $(OBJECTS:%.o=%.d)
Assuming your objects are transformed with the rules from above section, compile all of them automatically without having to specify unique rules for each object file:
%.c.o: %.c $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ %.cpp.o: %.cpp $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@ %.cc.o: %.cc $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
These rules say that, use C compiler (specified by variable
CC) to build
.c.o from source file with extension
.c sharing the same name. Then use C++ compiler (specified by variable
CXX) to compile
make find those source files? It consults variable
VPATH and checks directories specified in that variable and compile the first file with matching name and extension.
It is not desirable to rebuild everything every time you invoke
make. Objects don't depend on each other, and so if its source does not change,
make knows to re-use last compiled object. However, C/C++ sources must be rebuilt if one of the included header files change.
If you were to do this by hand, your rule would have to depend on the header files also, and this is obviously not manageable. This is the reason why I compiled source files with the option
-MMD -MP. Those compiler options will generate dependencies in the form of text files and store them in
.d files. If you open one of them, they look like:
blah.cpp: blah.h ...
Usually a very long list, including system and library header files. It looks like a Makefile rule, and indeed you can include it to say that
blah.cpp depends on
blah.h and other things.
Remembering we transformed all
.d before? That was so that we can do this:
DEPENDS := $(OBJECTS:%.o=%.d) -include $(DEPENDS)
This fragment with
include directive tells
make to include all the rules in those
.d files for dependency tracking, as if contents of the
.d files are part of my Makefile. The
- is important because the first time you build, you will not have those
.d files since they are generated only after you compile sources. So the
- makes sure that your build does not fail because those
.d files are missing. For that same reason, if incremental build isn't working but
.d files have the right dependencies, it is usually because you did not include them. It is easy to transform a bunch of stuff with the wrong paths, for example, so
echo the list of
.d files will help you identify the problem.
echo the variables to see if you have the right transformations.
You can also invoke a rule, let's say if you want to know what's going on when building
blah.o with the
$ make -d blah.o > output.txt 2>&1
It'll print a lot of stuff, that is why I redirected output to file
output.txt so that I can look at it in more detail with a text editor.
Finally, if you want to interrupt
make command because you know it isn't going to build the right thing and you want the last error to be at where in the Makefile when you run the problem:
env-check: ifndef KEY $(error KEY must be set) endif
Above fragment stops
make execution when it detects variable
KEY is undefined.
That's it! Build systems are not my favorite, because they are usually the first thing in between me and making code do interesting things. Hope this page helps you get to coding sooner.