Makefile
Notes about Makefiles: rules, dependency tracking, conditionals.
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 another-target
, 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.
Variables
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:
$(VAR)
Executing Commands
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 yes
:
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.
Transforming
To turn 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)
Generating Objects
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 .cpp
and .cc
files.
Where will 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.
Incremental Builds
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 .o
to .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.
Debugging Makefiles
You can 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 -d
switch:
$ 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.
Good Luck
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.
References
- GNU Make Manual: in various formats.
- Using
include
directive for dependency tracking by Tom Tromey and Paul D. Smith.