Make and Makefiles

What this module covers

Installing and invoking Make First Makefile and default target Targets, prerequisites, and recipes Tabs, shell commands, and exit codes

Variables, assignment operators, and expansion Built-in and automatic variables Pattern rules and suffix rules Wildcards, functions, and substitutions Phony targets and force rebuilds

Header dependencies and generated dependency files Recursive Make versus non-recursive Make

Parallel builds, jobserver, and race conditions Includes, conditional logic, and environment variables

Debugging Make with dry-run and database output Makefile best practices and common mistakes

4.1 A minimal Makefile

Makefile (recipe lines must begin with a tab):

Terminal session (GNU Make 4.4.1, GCC 11.2, Ubuntu 22.04 on WSL2) with the Makefile above and a three-file project in which math.h defines SCALE:

make builds the default target (often the first ordinary target).

make clean removes generated outputs.

make -n prints commands without executing them.

make -j4 allows up to four jobs, subject to Make's scheduling and dependencies.

The rm command is Unix-like. For cross-platform projects, use an appropriate shell or a portable cleanup tool.

project/
├── Makefile
├── main.c
├── math.c
└── math.h
CC := cc
CFLAGS := -Wall -Wextra -Wpedantic -g

.PHONY: all clean

all: app

app: main.o math.o
	$(CC) $(CFLAGS) $^ -o $@

main.o: main.c math.h
	$(CC) $(CFLAGS) -c $< -o $@

math.o: math.c math.h
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f app main.o math.o
make
make clean
make -n
make -j4
$ make
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
$ ./app
scale 10, result 50
$ make
make: Nothing to be done for 'all'.
$ touch math.c
$ make
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
$ touch math.h
$ make
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
$ touch main.c
$ make -n
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
$ make clean
rm -f app main.o math.o
$ make -j4
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
$ ./app
scale 10, result 50

4.2 Target syntax

Make compares prerequisites with target state and decides whether the recipe should run. A target may be a file or a logical action.

target: prerequisite1 prerequisite2
<TAB>command

4.3 Automatic variables

Common variables:

$@ — target name.

$< — first prerequisite.

$^ — all prerequisites, with duplicates removed in common GNU Make behavior.

$? — prerequisites newer than the target.

$* — stem matched by an applicable pattern rule.

Exact details can vary between rule forms and Make implementations; consult GNU Make documentation for advanced edge cases.

app: main.o math.o
	$(CC) $^ -o $@

4.4 Variables and expansion

Terminal session (GNU Make 4.4.1) — the block above plus a show target that echoes both variables:

= uses recursive expansion: the right side is expanded when the variable is used.

:= uses simple expansion: the right side is expanded when assigned.

?= assigns only if the variable is not already defined.

+= appends according to the variable's assignment flavor.

Terminal session (GNU Make 4.4.1), then the same file with FLAGS :=:

With recursive expansion, FLAGS sees the later BASE value. := would capture the value at assignment time.

CC = cc
CFLAGS = -Wall
CFLAGS += -Wextra
CC := clang
$ make -s show
CC=clang CFLAGS=-Wall -Wextra
$ make -s show CC=gcc
CC=gcc CFLAGS=-Wall -Wextra
BASE = -Wall
FLAGS = $(BASE)
BASE = -Wextra

.PHONY: show-flags
show-flags:
	@echo $(FLAGS)
$ make -s show-flags
-Wextra
$ sed -i 's/^FLAGS = /FLAGS := /' Makefile && grep -n FLAGS Makefile
2:FLAGS := $(BASE)
7:	@echo $(FLAGS)
$ make -s show-flags
-Wall

4.5 Pattern rules

Terminal session (GNU Make 4.4.1): the pattern rule plus an explicit header line, since the pattern alone cannot know about math.h:

This rule describes how to make any matching .o file from a corresponding .c file. Add explicit header dependencies or generated dependency files; the pattern rule alone does not infer arbitrary C header relationships.

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
$ cat Makefile
CC := cc
CFLAGS := -Wall -Wextra

app: main.o math.o
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

main.o math.o: math.h
$ make
cc -Wall -Wextra -c main.c -o main.o
cc -Wall -Wextra -c math.c -o math.o
cc main.o math.o -o app
$ touch math.h && make
cc -Wall -Wextra -c main.c -o main.o
cc -Wall -Wextra -c math.c -o math.o
cc main.o math.o -o app

4.6 Phony targets

Terminal session (GNU Make 4.4.1): first without, then with the .PHONY line:

A phony target represents an action rather than a file. If a real file named clean exists, declaring clean phony prevents Make from treating that file as proof the action is up to date.

In the session above, a stray file named clean made make clean report “up to date” and skip the recipe; after declaring .PHONY: test clean format, the same command ran.

.PHONY: test clean format
$ touch clean main.o
$ make clean
make: 'clean' is up to date.
$ ls
Makefile
clean
main.o
$ make clean
rm -f *.o app
$ ls
Makefile
clean

4.7 Order-only prerequisites

Terminal session (GNU Make 4.4.1), finally replacing | build with an ordinary prerequisite for comparison:

The build directory must exist before the recipe, but its timestamp should not by itself make the object file stale. The syntax uses | to separate order-only prerequisites in GNU Make.

build/output.o: source.c | build
	$(CC) -c source.c -o $@

build:
	mkdir -p build
$ make build/output.o
mkdir -p build
cc -c source.c -o build/output.o
$ touch build/unrelated.log
$ make build/output.o
make: 'build/output.o' is up to date.
$ sed -i 's/source.c | build/source.c build/' Makefile && make build/output.o
cc -c source.c -o build/output.o

4.8 Automatic header dependency generation

With GCC/Clang-style compilers, a common approach is:

Terminal session (GNU Make 4.4.1, GCC 11.2): the three lines in a complete Makefile, with -include placed after the rules:

-MMD generates dependency information for user headers; -MP adds empty rules for headers to help avoid certain deletion-related errors. Compiler flags vary by toolchain. Ensure the output paths and dependency-file naming match your project.

Where the -include line goes The output above places -include $(DEPS) after the first rule. Placed at the very top, the first rule Make sees comes from an included .d file — so that rule becomes the default goal:

Terminal session (GNU Make 4.4.1), same project, with the course's three lines at the top of the Makefile:

Plain make rebuilt only main.o; the program kept printing the old value until make app was requested. Keep an explicit all: (or the main target) as the first rule, or put the include at the end.

CFLAGS += -MMD -MP
DEPS := $(OBJS:.o=.d)
-include $(DEPS)
$ cat Makefile
CC := cc
CFLAGS := -Wall -Wextra
OBJS := main.o math.o
CFLAGS += -MMD -MP

app: $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

DEPS := $(OBJS:.o=.d)
-include $(DEPS)
$ make -s && ./app
scale 10, result 50
$ cat main.d
main.o: main.c math.h
math.h:
$ sed -i 's/#define SCALE 10/#define SCALE 100/' math.h
$ make
cc -Wall -Wextra -MMD -MP -c main.c -o main.o
cc -Wall -Wextra -MMD -MP -c math.c -o math.o
cc main.o math.o -o app
$ ./app
scale 100, result 500
$ head -6 Makefile
CC := cc
CFLAGS := -Wall -Wextra
OBJS := main.o math.o
CFLAGS += -MMD -MP
DEPS := $(OBJS:.o=.d)
-include $(DEPS)
$ make -s && ./app
scale 10, result 50
$ sed -i 's/#define SCALE 10/#define SCALE 100/' math.h
$ make
cc -Wall -Wextra -MMD -MP -c main.c -o main.o
$ ./app
scale 10, result 50
$ make app && ./app
cc -Wall -Wextra -MMD -MP -c math.c -o math.o
cc main.o math.o -o app
scale 100, result 500

4.9 Functions and wildcards

Terminal session (GNU Make 4.4.1) with a print target added:

wildcard expands matching paths; patsubst substitutes matching patterns. Be cautious with automatically discovering every source file: explicit source lists can make code inclusion and build behavior easier to review.

SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))
$ ls src
can.c
main.c
notes.txt
sensor.c
$ make -s print
SOURCES = src/can.c src/main.c src/sensor.c
OBJECTS = build/can.o build/main.o build/sensor.o

4.10 Parallel builds and race conditions

make -j can run independent recipes simultaneously. If two recipes write the same generated file or a recipe uses an undeclared prerequisite, parallel execution may expose races.

Bad design: multiple targets each generate the same header independently. Better: model one generation target and make all consumers depend on it.

4.11 Debugging Make

Terminal session (GNU Make 4.4.1) on the 4.1 project with a deliberate typo ($(LDFLGAS)) in the link recipe:

GNU Make options may not exist in other implementations. Read the emitted command carefully and test from a clean state.

Why did Make rebuild that?

--debug=b prints Make's reasoning for each target:

Terminal session (GNU Make 4.4.1) after touch math.h, using the Makefile from 4.1:

make -n target       # dry-run
make --debug=b       # basic rebuild reasoning (GNU Make)
make -p              # print database
make --warn-undefined-variables
$ make -n app
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
cc -Wall -Wextra -Wpedantic -g  main.o math.o -o app
$ make --warn-undefined-variables -n app 2>&1 | grep -i warning
Makefile:9: warning: undefined variable 'LDFLGAS'
$ make -p -n | grep -E '^(CFLAGS|CC) :='
CFLAGS := -Wall -Wextra -Wpedantic -g
CC := cc
$ touch math.h
$ make --debug=b | sed -n '/Updating goal/,$p'
Updating goal targets....
 File 'all' does not exist.
   Prerequisite 'math.h' is newer than target 'main.o'.
  Must remake target 'main.o'.
cc -Wall -Wextra -Wpedantic -g -c main.c -o main.o
  Successfully remade target file 'main.o'.
   Prerequisite 'math.h' is newer than target 'math.o'.
  Must remake target 'math.o'.
cc -Wall -Wextra -Wpedantic -g -c math.c -o math.o
  Successfully remade target file 'math.o'.
  Prerequisite 'main.o' is newer than target 'app'.
  Prerequisite 'math.o' is newer than target 'app'.
 Must remake target 'app'.
cc -Wall -Wextra -Wpedantic -g main.o math.o -o app
 Successfully remade target file 'app'.
Must remake target 'all'.
Successfully remade target file 'all'.

4.12 Makefile checklist

Use tabs for recipe indentation in conventional Makefiles.

Declare action targets .PHONY.

Model all relevant file dependencies.

Avoid two jobs writing the same output.

Quote paths where shell whitespace may occur.

Separate generated outputs from source files.

Provide all, test, and clean targets where appropriate.

Keep compiler flags and tools configurable.

Test both clean and incremental builds.

Document platform-specific assumptions.

Quiz

? With `FLAGS = $(BASE)` and `BASE` reassigned later, what did `make show-flags` print in the real run?
- `-Wall` :: That was the result with `:=`.
+ `-Wextra` :: `=` expands when used, so it saw the later value.
- Nothing :: The variable was defined.
- `-Wall -Wextra` :: `BASE` was replaced, not appended.

? Why did `make build/output.o` stay “up to date” after a file was added to `build/`?
+ `build` was an order-only prerequisite: it must exist, but its timestamp does not make the object stale. :: The `|` separates order-only prerequisites.
- Make ignores directories. :: With a normal prerequisite, the same change triggered a rebuild.
- The object was newer than `source.c` only by accident. :: The behaviour is deliberate.
- `mkdir -p` failed. :: It succeeded on the first run.

? Which practices make a Makefile safer for `make -j`? (select all that apply)
+ Model every real dependency, including generated headers :: Missing edges become races in parallel builds.
+ Generate each shared file from exactly one rule :: Two recipes writing one file can collide.
+ Test both clean and incremental builds :: Each exercises different paths through the graph.
- Rely on the order prerequisites
…