Makefile tutorial 2.0 2021-06-16
The new and improved Makefile tutorial, for beginners and intermediates alike. Topics covered include pattern matching, compiling from and to different directories, header dependencies, and variables.
Translations: English | français | Português (Brasil)
Assumed knowledge:
- Basic POSIX shell proficiency. Including environment variables.
- Basic GCC arguments. Including
-c
,-o
, and-W
. - Basic C, knows the difference between an executable, object files, source files, and header files.
The basics
It’s highly recommend for anyone reading this to follow along. Get started by cloning this repo:
git clone https://noahloomans.com/tutorials/makefile
Getting a "certificate has expired" error?
42 Network iMacs currently ship with an outdated list of certificate authorities, causing the clone to fail.
You can clone from this alternative URL instead for the time being:
git clone https://gitlab.com/nloomans/noahloomans.com-tutorials-makefile
This repository includes the following Makefile
:
foo: main.o greeter.o
gcc -o foo main.o greeter.o
main.o: main.c
gcc -c -Wall -Wextra -Werror -o main.o main.c
greeter.o: greeter.c
gcc -c -Wall -Wextra -Werror -o greeter.o greeter.c
This Makefile consists of 3 rules. A rule consists of a target, any amount of prerequisites, and finally a recipe. The syntax is as follows:
target: prerequisites
recipe
A rule describes how make
can make something. The first rule with a target
of foo
describes how we can link together our executable. The second & third
rules describe how to compile the individual object files.
When you run make foo
, make will look for a rule which can compile foo
, in
this case the first rule. Before executing the recipe, make will first figure
out if the recipe needs to be run at all, and if any other rules need to be
executed first. Make first checks if every individual prerequisite exists and
is up-to-date.
The main.o
prerequisite will not exist on the first run. Make will look
through the rules to find one with a compatible target. In this case this is
the main.o
rule. For this rule, the same process will start over again, as if
you ran make main.o
. Make sees that main.c
exists and main.o
does not. It
will therefor compile main.o
by executing the recipe. If however, main.o
were to already be present, then make would compare the edit dates. If main.c
is more recent than main.o
, make will act as if main.o
does not exist, and
recompile it.
On the first run, the same thing would happen for greeter.o
.
Exercise: Compile the
foo
binary using make. Then compile it again. Now, changemain.c
a bit and compilefoo
again. Take note of what commands make executes.
Question: You ran
make foo
3 times, during which instances didgreeter.o
get compiled?
Variables and pattern matching
Writing a new rule for every object file is cumbersome and error-prone. One
can write the previous Makefile
much cleaner like this:
foo: main.o greeter.o
gcc -o $@ $^
%.o: %.c
gcc -c -Wall -Wextra -Werror -o $@ $^
This Makefile functions exactly1 the same as the first Makefile.
The %.o
rule will now handle the compilation of all object files. The %
is
a pattern match. This is what the GNU Makefile manual says about pattern
matching, in 10.5.4 How Patterns Match:
A target pattern is composed of a ‘%’ between a prefix and a suffix, either or both of which may be empty. The pattern matches a file name only if the file name starts with the prefix and ends with the suffix, without overlap. The text between the prefix and the suffix is called the stem. Thus, when the pattern ‘%.o’ matches the file name test.o, the stem is ‘test’. The pattern rule prerequisites are turned into actual file names by substituting the stem for the character ‘%’. Thus, if in the same example one of the prerequisites is written as ‘%.c’, it expands to ‘test.c’.
That was quite a blob of text, let’s give an example. When make tries to
search for a rule to compile main.o
with, it will match the pattern. The
stem, main
, is then injected in %.c
to become main.c
. Effectively, make
will pretend that the target is main.o
and the prerequisite is main.c
.
The same is true for greeter.o
, and any other file ending in .o
. Note that
%
is not a preprocessor statement. This all happens during runtime, and
therefor also works on files which have never been referenced in the Makefile.
The new version of our Makefile also contains two so-called
automatic variables. Namely $@
and $^
. These variables allow us to write
generic recipes that work with pattern matched rules. $@
resolves to the
target and $^
resolves to all of the prerequisites.
Question: There is an additional
stray.c
included in the example repo. Using the above Makefile, what would happen if we were to executemake stray.o
?
Variables
Until now we wrote things like the list of object files and which flags gcc takes inline. This is fine for small Makefiles, but let’s clean it up a bit:
NAME := foo
OBJFILES := main.o greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $^
The variables set with :=
are always set unconditionally. Variables set with
?=
are only set if it hasn’t been set already. Make inherits variables from
environment variables. This allows us to easily run make with custom CFLAGS
by, for example, running the following in your shell:
# Compile the program in debug mode (-g), and don't stop compiling on
# warnings.
CFLAGS="-Wall -Wextra -g" make
The CC
variable is a bit special in that it is pre-defined to cc
. The idea
is simple, if you write standards-compliant code, why should you specify with
which compiler to compile your code with? cc
is the default C compiler on most
systems, and if the user wants your program to be compiled with a different
compiler, they can set the CC
variable:
# Use the clang compiler and compile with the clang specific
# -fsanitize=address option to detect memory corruption.
export CC=clang
export CFLAGS="-Wall -Wextra -g -fsanitize=address"
export LDFLAGS="-g -fsanitize=address"
make
Note: You may also see the
=
operator being used in Makefiles you find in the wilderness. This is also correct, but functions in a slightly different way from:=
. For the differences, see the official documentation.
Folders
With the Makefiles used until now, all source files and object files are littered in the root of the repository. One can make a repository a lot more organized by putting them in separate folders.
Let’s take a look at our object file rule again:
%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $^
The %
pattern doesn’t just work on suffixes, it works on prefixes as well. We
can use this to compile our object files to a different folder from our source
files.
# Compile object files in the obj folder from source files in the src
# folder.
obj/%.o: src/%.c
$(CC) -c $(CFLAGS) -o $@ $^
Note that make has no knowledge of directories here. All it does is look for
“obj/” at the start and “.o” at the end, and replaces those with “src/” at the
start and “.c” at the end. Because the $@
and $^
automatic variables
resolve to the processed forms, something like this will be executed:
cc -c -Wall -Wextra -Werror -o obj/example.o src/example.c
This won’t quite work however, as the compiler won’t create an obj
folder if
it doesn’t already exist. We can address this by running mkdir -p
before cc
,
which creates a directory if it does not already exist. To figure out which
directory to create, one can use the $(dir ...)
function.
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
The @
at the start is just a little aesthetic choice. It makes it so that all
these mkdir
calls won’t be printed. The output can be quite cluttered
otherwise.
Now to make this all work, we just need to make sure that anywhere we use an
object file as a prerequisite we reference them with their obj/
prefix,
otherwise make won’t be able to find them. Our Makefile
now looks like this:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
Further reading: You can avoid writing the
obj/
prefix every time using 6.3.1 Substitution References.
Question: Imagine that all of our
.c
files had anft_
prefix, and were in thesrc
folder. So our main source file would besrc/ft_main.c
. How can we make object files for them without theft_
prefix? So thatsrc/ft_main.c
would get compiled to theobj/main.o
object file?
Cleaning up
Until now, all of our Makefile rules actually make something, as defined by the
target. This is almost always what we want, but there are a few exceptions.
Take make clean
for example. In most Makefiles2, such a rule will cleanup
all the files the Makefile created. This is what a naïve implementation would
look like:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
clean:
rm -f $(NAME) $(OBJFILES)
The issue with this is that make will now think that our clean
rule has a
target file named clean
. Let me illustrate why is this a problem:
$ touch clean
$ make clean
make: 'clean' is up to date.
Make did not want to execute the clean
rule, because a file named clean
already exists. To fix this, we need to declare the clean
target to be a
phony. Make has a magic .PHONY
target, all the prerequisites of this
target are marked as phonies, and make will no longer check them against any
file with the same name. Instead make will assume phonies are always out of
date, and need to be recompiled. This gives us the following Makefile
:
NAME := foo
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $^
clean:
rm -f $(NAME) $(OBJFILES)
.PHONY: clean
Question: Imagine that our
.PHONY
rule was defined like this:.PHONY: clean $(NAME)
. We runmake
twice. What would happend the second time?
Finishing touches
There are two more things which are nice to have in our Makefile. First of,
right now recompiling our project using make
won’t quite work if you modified
a header file, as make is unaware of those.
This easiest way is to tell make that every object file also depends on every header file. That would look like this:
NAME := foo
HEADERFILES := src/greeter.h
OBJFILES := obj/main.o obj/greeter.o
LDFLAGS ?=
CFLAGS ?= -Wall -Wextra -Werror
$(NAME): $(OBJFILES)
$(CC) $(LDFLAGS) -o $@ $^
obj/%.o: src/%.c $(HEADERFILES)
@mkdir -p $(dir $@)
$(CC) -c $(CFLAGS) -o $@ $<
We made three changes. First we added a HEADERFILES
variable which contains a
list of header files, then we added it as a prerequisite to the obj/%.o
rule, and finally, we changed the second $^
to a $<
.
Our obj/%.o
rule now has multiple prerequisites: The corrosponding src/%.c
file and all header files defined in HEADERFILES
. Because make checks if any
prequisite is newer than the target, the target will be recompiled
whenever a header file updates. This same check will be executed for every
object file rule, causing all of them to be recompiled.
The $^
and $<
automatic variables are different in a subtile but
important way: $^
resolves to all of the prerequisites, while $<
just
resolves to the first one. In our case, that means that $<
ignores the header
files and just expands to our source file. This is exactly what we want since
the our compiler shouldn’t be told what the header files are directly.
Closing words
That was it, I hope you found my Makefile tutorial useful! Big thanks to Pepijn Holster for helping me write this, and thanks to everyone who helped me proof read.
Want to translate this tutorial? I will gladly accept translations! Shoot
me an email at inbox@noahloomans.com, or DM me on Slack at nloomans
if you
are a student who is part of the 42 Network. I’ll help you get started.