Tooling for C: Make, Part 2

Home · Blog

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:

Functions

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

Phony targets

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.

Generating the “depend” file

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.

Overriding variables

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.

Putting it all together

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.