Makefile

Notes about Makefiles: rules, dependency tracking, conditionals.

Makefile

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 those rule's dependencies 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)

This way 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 detected 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 that get in the way of me and making code do interesting things.  Hope this page helps you get to coding sooner.

References