10 June 2025
Part of a series: Tooling for C
In Make, Part 1, I showed how to write a Makefile for a sample C project with source, header, and unit test files. Here’s the Makefile again:
CFLAGS = -Wall -Wextra
build/%.o: src/%.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
build/main: build/main.o build/io.o build/math.o
$(CC) $(CFLAGS) $(CPPFLAGS) -o build/main build/main.o build/io.o build/math.o
build/math_test: build/math_test.o build/math.o
$(CC) $(CFLAGS) $(CPPFLAGS) -o build/math_test build/math_test.o build/math.o
include depend
In this article, we’ll make some improvements:
make test
to run all unit tests.depend
file automatically.
The most annoying thing about the old Makefile is that we need to list all the .o files explicitly:
build/main: build/main.o build/io.o build/math.o
$(CC) $(CFLAGS) $(CPPFLAGS) -o build/main build/main.o build/io.o build/math.o
The first step to improve that is to use the automatic variable $^
, which expands to the list of all dependencies:
build/main: build/main.o build/io.o build/math.o
$(CC) $(CFLAGS) $(CPPFLAGS) -o build/main $^
Recall that $<
is the first dependency; $^
is all of them.
Now, on the first line we still have a .o file in build
for each .c file in src
. We’ll generate that list step by step. Fist, collect all .c files and store them in a variable:
sources = $(wildcard src/*.c)
wildcard
is a function that finds files matching a pattern, similar to running ls src/*.c
in the shell. Next, we’ll use the filter-out
function to remove the ones for unit tests:
non_test_sources = $(filter-out src/%_test.c,$(sources))
filter-out
removes filenames that match a pattern. Actually, let’s also remove main.c
; then we can use the same list later when building unit tests:
lib_sources = $(filter-out src/%_test.c src/main.c,$(sources))
To get the matching object files, we need to substitute “src” with “build” and “.c” with “.o”. The patsubst
function is perfect for that:
objects = $(patsubst src/%.c,build/%.o,$(lib_sources))
Now the rule for build/main
looks like this:
build/main: build/main.o $(objects)
$(CC) $(CFLAGS) -lm -o $@ $^
We could do the same thing for the build/math_test
rule, but let’s go one step further and create a pattern rule that will also work for any new test files we might add in the future:
build/%_test: build/%_test.o $(objects)
$(CC) $(CFLAGS) -lm -o $@ $^
We can still build and run tests as before:
$ make build/math_test && build/math_test
clang -Wall -Wextra -c -o build/math_test.o src/math_test.c
clang -Wall -Wextra -c -o build/io.o src/io.c
clang -Wall -Wextra -c -o build/math.o src/math.c
clang -Wall -Wextra -lm -o build/math_test build/math_test.o build/io.o build/math.o
It would be neat if we could just run
make test-math
to build and run the “math” unit tests in one go. We’ll need to add a target test-math
to the Makefile -- but notice that this is different from the rules we’ve seen so far in that test-math
is not the name of a file. It’s just the name of the rule. In Make this is called a phony target and it should be defined like this:
.PHONY: test-math
test-math: build/math_test
build/math_test
We can use a pattern so it’ll work for any test file:
.PHONY: test-%
test-%: build/%_test
$<
That last line may look odd, but remember that $<
will be replaced with the first dependency, so when we run make run-math
, it becomes
build/math_test
which is the executable we want to run.
When we get some more tests, it’ll be nice if we could run all of them with one command, let’s say make test
. To make that work, we first figure out the names of all test targets from the *_test.c files:
test_sources = $(filter src/%_test.c,$(sources))
test_targets = $(patsubst src/%_test.c,test-%,$(test_sources))
Now we can define a test
rule that depends on these:
.PHONY: test
test: $(test_targets)
The test
rule has no commands -- it only exists to depend on those other rules.
The depend
file needs to be re-generated whenever the dependencies between source and header files change (in other words, whenever we change a #include "….h"
line). It would be nice to put that command in the Makefile. There’s more than one way to do that; what I’ve settled on for now is to just include it in a clean
target that also removes generated files:
.PHONY:
clean:
rm -f build/*
$(CC) $(CPPFLAGS) -MM $(sources) | sed -E -e 's,^(.+)\.o:,build/\1.o:,' > depend
This way you can run make clean
and you’ll be sure everything will be up-to-date after the next build.
The GNU Make manual shows a different setup where dependencies will automatically be re-generated when a source file changes (Generating Prerequisites Automatically). It’s neat, but it does adds some complexity.
At the top of our Makefile we’re setting these compiler flags:
CFLAGS = -Wall -Wextra
That’s a good default but sometimes we want different flags, for example to enable a sanitizer. We can override any variable in the Makefile by setting it on the command line:
make -s clean test-math CFLAGS="-Wall -Wextra -fsanitize=address"
I added the clean
target here to make sure all sources are compiled with the same flags.
We can also allow the user to override variables with environment variables by defining them with the ?= operator:
CC ?= clang
CFLAGS ?= -Wall -Wextra
CPPFLAGS ?=
I think this makes sense for implicit variables like $(CC). Some C programmers will set these in their shell environment to match their local setup.
Here’s the complete Makefile:
CC ?= clang
CFLAGS ?= -Wall -Wextra
CPPFLAGS ?=
sources = $(wildcard src/*.c)
test_sources = $(filter src/%_test.c,$(sources))
test_targets = $(patsubst src/%_test.c,test-%,$(test_sources))
lib_sources = $(filter-out src/%_test.c src/main.c,$(sources))
objects = $(patsubst src/%.c,build/%.o,$(lib_sources))
build/main: build/main.o $(objects)
$(CC) $(CFLAGS) -lm -o $@ $^
build/%.o: src/%.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
build/%_test: build/%_test.o $(objects)
$(CC) $(CFLAGS) -lm -o $@ $^
.PHONY: test-%
test-%: build/%_test
$<
.PHONY: test
test: $(test_targets)
.PHONY:
clean:
rm -f build/*
$(CC) $(CPPFLAGS) -MM $(sources) | sed -E -e 's,^(.+)\.o:,build/\1.o:,' > depend
include depend
You can find the complete sample code here: sample-code/c/make/v2. To learn more, check out the excellent GNU Make manual.