Noah Loomans

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, change main.c a bit and compile foo again. Take note of what commands make executes.

Question: You ran make foo 3 times, during which instances did greeter.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 execute make 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 an ft_ prefix, and were in the src folder. So our main source file would be src/ft_main.c. How can we make object files for them without the ft_ prefix? So that src/ft_main.c would get compiled to the obj/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 run make 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.


  1. One minor difference, the second rule will now match any .o file, not just main.o and greeter.o

  2. Note that the clean rule for the Makefiles we write at 42 schools work slightly differently.