Makefiles are the granddaddy of build systems. Though falling out of favour relative to more modern systems like SCons and ant, make is still the lingua franca of software builds, particularly in the C and C++ parts of the open source world. Because of this, it is imperative to have at least a basic understanding of makefiles and their use.
There are plenty of tutorials introducing the fundamentals of makefile syntax, and a handful that show off some advanced features. There are very few, however, that actually show how to write a useful makefile, or that introduce makefile conventions and patterns. For me, this meant that writing makefiles became an arduous process of stringing together snippets from various places, and hoping they interoperated harmoniously. Frustratingly, I'd often learn of a new feature months later and rip out half of the file and replace it with a single line. Worst of all, I had no idea if what I was doing was “conventional” or even passable as a serious makefile.
I therefore want to put out this guide to basic makefile usage and conventions, and in the process, develop a basic makefile template that can be used for most small projects or as a starting point for more elaborate build systems. The resulting makefile will also roughly adhere to the GNU makefile conventions, but only where it makes sense for a small project and where support is not too onerous. For the purposes of the guide, we'll be writing a makefile for a C program, but the ideas are easily applicable to other languages. So if you'll oblige me by firing up your text editors, I'll get started.
Build Variables
At the top of our makefile, we will want to
declare the variables used in the build process. Keeping everything in
variables allows for easy modification of multiple build rules at once, as
well as exporting variables from higher-level makefiles in the case of
recursive builds (ones where this makefile is just building a particular
component). For portability, we can start out by declaring our
SHELL
and compiler. These two variables are among Make's many
special names, and are used implicitly in certain situations, so it is good
practice to specify them. We can do so with the following snippet:
SHELL = /bin/sh
CC = gcc
Next we'll define variables for the actual compiler flags used for
building. My personal system is to break these up into four parts:
FLAGS
, used for mandatory flags without which the project will
not build, RELEASEFLAGS
and DEBUGFLAGS
, for public
release and debugging flags, respectively, and CFLAGS
for
user-defined C compiler settings. This last one is a standard that some users
like to define for themselves, and so it should always use that name. I use it
for things that are not essential but that I would always like to have in
place when building. For this makefile, I've defined these variables as
follows:
FLAGS = -std=gnu99 -Iinclude
CFLAGS = -pedantic -Wall -Wextra -march=native -ggdb3
DEBUGFLAGS = -O0 -D _DEBUG
RELEASEFLAGS = -O2 -D NDEBUG -combine -fwhole-program
You can see above the usage of the different variables. Without the
FLAGS
settings (which specify to use the GNU variant of the C99
standard, and to look for #include
d files in the “include”
directory, respectively) the hypothetical code would likely not compile
correctly (logically, we assume that this hypothetical code does in fact keep
header files in that location, and does make use of C99 extensions). The debug
and release flags, on the other hand, contain various optimization directives
and declarations: the NDEBUG definition causes assert()
s to be
taken out (among other things); the combine
and
fwhole-program
flags instruct GCC to assume that the files it is
working on comprise the whole program, and to optimize accordingly (this only
works for C at present); and the O number specifies the level of optimization
to apply. Finally, CFLAGS
holds user-optional choices, as
promised. In this example, I have chosen to make the compiler very strict
about errors and warnings (pedantic, Wall, Wextra
), instructed it
to tune the output program for my specific machine architecture, and finally
asked for the inclusion of copious amounts of GDB-specific debugger
information. For maximum portability you should not assume GDB, but in
practice it is fine for me.
Now let's define some variables to hold important files related to the build. We'll need the name of the program we're building, which I've called “foomatic-widget”, a list of source files, header files, common headers on which all files depend, and the object files that our sources will compile to. The application name and common headers we can just specify, but keeping track of all our source and header files could be a pain. I've therefore used a Make feature where we can call out to the shell, in this case to get a list of all files ending in “.c” and “.h”. Likewise, the list of object files is built by taking all the source files, and replacing their extensions with “.o”. This all looks like this:
TARGET = foomatic-widget
SOURCES = $(shell echo src/*.c)
COMMON = include/definitions.h include/debug.h
HEADERS = $(shell echo include/*.h)
OBJECTS = $(SOURCES:.c=.o)
Finally, we define some paths used for installing our program in a more
permanent fashion. By convention, the DESTDIR
variable is used,
even though we don't declare it, as this allows the user to test installation
to any directory by specifying a DESTDIR
on the
command-line. These variables are defined this way:
PREFIX = $(DESTDIR)/usr/local
BINDIR = $(PREFIX)/bin
Build Targets
Now we get on to the main business of Make: building things. Make uses the
concept of targets to represent sets of instructions that you want it
to run. The first target listed is the default one used if Make is invoked
without specifying a target. Otherwise, you can run a different one with
make targetname
. By convention, the “all” target builds the
project fully, and is the default. Targets can also have prerequisites:
targets that will be processed prior to the current one, or files that, if
changed, will cause the target to be rerun. If the target itself is a file,
Make intelligently determines whether it needs to be rerun based on its
prerequisites' times of last modification.
I typically just make “all” depend on the actual name of the
executable, defined in $(TARGET)
above. This ensures that the
executable is built if you simply run Make. Optionally, you can also define
other targets as prerequisites; for example, I often include a run of the
indent
or cppcheck
utilities, depending on the
nature of the project.
Now we have to let Make know how to build the $(TARGET)
that
all
depends on. We do this by defining it as a new target, which
depends on the $(OBJECT)
files of each component, as well as the
common headers. This target, however, actually contains a rule on how to build
it, which will be run when all the prerequisites have been satisfied. This
rule is on a new line, and must be indented with tabs. This is a
common pitfall, though many editors will make sure that you don't accidentally
use spaces here unless you really want to. The rule simply consists of a call
to our compiler, defined above, with all the flags that we also defined, and a
list of the object files to link. The first two rules then look like this:
all: $(TARGET)
$(TARGET): $(OBJECTS) $(COMMON)
$(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -o $(TARGET) $(OBJECTS)
Now, you may have noticed that we're building with the debug settings. How
then, do you produce something for day-to-day usage? Why, with another target,
invoked with make release
and looking like this:
release: $(SOURCES) $(HEADERS) $(COMMON)
$(CC) $(FLAGS) $(CFLAGS) $(RELEASEFLAGS) -o $(TARGET) $(SOURCES)
You may also, later in the development cycle, wish to compile your program
with profiling information. The way I've implemented this functionality is
with another feature of Make, namely modifying variables. The first target
below causes the CFLAGS
variable to include a profiling option,
and then the actual target causes the application to be built with the new set
of flags.
profile: CFLAGS += -pg
profile: $(TARGET)
Administrative Targets
We should also define some “administrative” targets, which will let us move files around or remove them as needed. A subset of the ones suggested by the GNU Makefile conventions are below:
install: release
install -D $(TARGET) $(BINDIR)/$(TARGET)
install-strip: release
install -D -s $(TARGET) $(BINDIR)/$(TARGET)
uninstall:
-rm $(BINDIR)/$(TARGET)
clean:
-rm -f $(OBJECTS)
-rm -f gmon.out
distclean: clean
-rm -f $(TARGET)
The install
and install-strip
targets provide us
with a mechanism to put our final built binary in some appropriate path, as
defined in the environment variables above, and using the standard
install
utility (the naming is a bit confusing: we have both an
“install” Make target and a system utility). The latter
option strips debugging symbols from the binary in the process. Both targets
depend on the release
target, so we can expect that to be built
as per the process described above. Uninstall
provides the
reverse functionality.
The two cleaning-related options are also standard; they differ only in
that distclean
restores the directory to the pristine state it
would be distributed in, i.e. the compiled binary is also removed. The
commands in these targets are preceded by a minus sign, telling Make to
continue even if the command yields an error (like if the files don't
exist).
With these targets in place, we should also take a moment to consider what
would happen if we were to actually create a file named, for example,
“release” or “install”. Make would start deciding whether to run
these targets based on the freshness of those files — clearly not the
behaviour we want. We can work around this by defining these targets as
PHONY
, which tells make to always execute them (solving our
problem) and to not bother searching for prerequisites (slightly improving
performance). We do this as follows:
.PHONY : all profile release \
install install-strip uninstall clean distclean
Objects
Our application target above depends on a whole bunch of object files. We could list them all individually, or we could allow Make to build them implicitly (it's pretty smart and can mostly figure it out), but we can do even better. We can define a wildcard rule that will match all object files, and build them just the way we want. We could also define one or two object files individually, if they were special cases for some reason.
This wildcard rule makes use of a few special variables. The first one
you'll see is %.o
. That is the actual wildcard that matches
object files. We can use a similar syntax to make it depend on the right
source file as a prerequisite. We also need to know about the $@
and $<
variables, which refer to the current target and the
first prerequisite, respectively. The rule can then be built like this:
%.o: %.c $(HEADERS) $(COMMON)
$(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
You may have noticed that the above rule has all header files as a prerequisite. This is to be on the safe side, in case other parts of the program that are relevant to that file were changed. Depending on the size of your project, that may represent a significant amount of time wasted needlessly. If you're not averse to some really gruesome syntax, and want to rectify the problem, and if you're using GNU Make only, you can do better.
Using a feature of GNU Make known as “second expansion”, you can
dynamically determine the specific headers to care about by calling out to GCC
with the -MM
option, which makes it list the headers included by
a particular file. Second expansion allows us to evaluate variables a second
time, later on in their lifecycle, where the surrounding context may have
changed. For details on the “deep magic” going on here, consult the
actual manual, but you should be able to get a rough idea of what's going on
from the following implementation:
.SECONDEXPANSION:
$(foreach OBJ,$(OBJECTS),$(eval $(OBJ)_DEPS = $(shell gcc -MM $(OBJ:.o=.c) | sed s/.*://)))
%.o: %.c $$($$@_DEPS)
$(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
The final product
Our shiny new makefile is reproduced below in its entirety:
SHELL = /bin/sh
CC = gcc
FLAGS = -std=gnu99 -Iinclude
CFLAGS = -pedantic -Wall -Wextra -march=native -ggdb3
DEBUGFLAGS = -O0 -D _DEBUG
RELEASEFLAGS = -O2 -D NDEBUG -combine -fwhole-program
TARGET = foomatic-widget
SOURCES = $(shell echo src/*.c)
COMMON = include/definitions.h include/debug.h
HEADERS = $(shell echo include/*.h)
OBJECTS = $(SOURCES:.c=.o)
PREFIX = $(DESTDIR)/usr/local
BINDIR = $(PREFIX)/bin
all: $(TARGET)
$(TARGET): $(OBJECTS) $(COMMON)
$(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -o $(TARGET) $(OBJECTS)
release: $(SOURCES) $(HEADERS) $(COMMON)
$(CC) $(FLAGS) $(CFLAGS) $(RELEASEFLAGS) -o $(TARGET) $(SOURCES)
profile: CFLAGS += -pg
profile: $(TARGET)
install: release
install -D $(TARGET) $(BINDIR)/$(TARGET)
install-strip: release
install -D -s $(TARGET) $(BINDIR)/$(TARGET)
uninstall:
-rm $(BINDIR)/$(TARGET)
clean:
-rm -f $(OBJECTS)
-rm -f gmon.out
distclean: clean
-rm -f $(TARGET)
.SECONDEXPANSION:
$(foreach OBJ,$(OBJECTS),$(eval $(OBJ)_DEPS = $(shell gcc -MM $(OBJ:.o=.c) | sed s/.*://)))
%.o: %.c $$($$@_DEPS)
$(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
# %.o: %.c $(HEADERS) $(COMMON)
# $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
.PHONY : all profile release \
install install-strip uninstall clean distclean
For more detailed documentation, consult the GNU Make Manual or the GNU Makefile Conventions document.