Make Cheatsheet

Rules and Targets

Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Recipe lines must start with a literal TAB

This is the number one make error. Every command line in a recipe begins with a real tab character, not spaces. Editors that expand tabs will silently break the file.

Makefile:2: *** missing separator.  Stop.

That message almost always means line 2 started with spaces. Spot it without guessing:

cat -A Makefile | head        # GNU: tabs show as ^I
sed -n 'l' Makefile | head    # portable: tabs show as \t
grep -nE '^ +[^ ]' Makefile    # recipe lines indented with spaces

Tell your editor to leave Makefiles alone:

# .editorconfig
[Makefile]
indent_style = tab
[{Makefile,*.mk,makefile,GNUmakefile}]
indent_style = tab

GNU make 3.82 and newer can change the character with .RECIPEPREFIX, but nothing else expects it, so keep tabs.

.RECIPEPREFIX := >

Anatomy of a rule

A rule is a target, its prerequisites, and a recipe. Variables and functions are expanded by make, then each recipe line is handed to /bin/sh as its own process.

app: main.o util.o        # target: prerequisites
	cc -o app main.o util.o    # recipe line, starts with a TAB

- Target: usually a file make creates. If it is not a file, mark it .PHONY. - Prerequisites: files that must exist and be up to date first. - Recipe: shell commands. Each line is a separate shell, so cd does not persist to the next line unless you join with && or enable .ONESHELL.

Recipe line prefixes, which may be combined:

PrefixEffect
@Do not echo the line before running it
-Ignore a non-zero exit status
+Always run, even under make -n
clean:
	@echo "cleaning"
	-rm -f app *.o

The staleness model

Make compares modification times, nothing else. A target is rebuilt when it does not exist, or when any prerequisite is newer than it. There is no content hash and no build cache.

Consequences worth remembering:

- Touching a file forces a rebuild. touch src/main.c then make proves a dependency edge exists. - Clock skew across a network mount or a container bind mount causes both spurious rebuilds and, worse, skipped ones. - A missing prerequisite edge is invisible until a stale artifact ships. Headers are the classic hole, which is why auto-generated dependencies exist. - Make 4.3 and newer compare with sub-second precision where the filesystem provides it, older versions used whole seconds.

Force the issue when you need to:

make -B          # unconditionally rebuild everything
make -W src/x.c  # pretend src/x.c was just modified
make -o config.h # pretend config.h is up to date, never rebuild it
make -t          # touch targets instead of running recipes

Order-only prerequisites

Prerequisites after a | must exist, but their timestamps never trigger a rebuild. This is the correct way to depend on an output directory, whose mtime changes every time a file lands in it.

build/%.o: src/%.c | build
	cc -c -o $@ $<

build:
	mkdir -p $@

Without the |, every new object file updates build/'s mtime and make rebuilds everything on the next run. Order-only prerequisites are also excluded from $^ and $?, so cc -o $@ $^ stays correct.

Several targets, and rules with no recipe

A rule with multiple targets and a single-colon : defines the same recipe once per target, and the recipe runs once for each.

a.out b.out: input.txt
	process $< $@          # runs twice, $@ differs each time

GNU make 4.3 added &: for a recipe that produces all the targets in one invocation.

parser.c parser.h &: parser.y
	bison -d -o parser.c $<

A rule with prerequisites and no recipe just adds edges to an existing target. That is how you attach header dependencies without repeating the compile line.

main.o: config.h version.h

Double-colon rules :: are independent, each with its own recipe, and all of them run. They are rare outside legacy makefiles.

A complete minimal Makefile

CC     := cc
CFLAGS := -std=c11 -Wall -Wextra -O2

.PHONY: all clean
all: app

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

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

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

clean:
	$(RM) app main.o util.o

The first non-special target in the file is the default goal, so plain make builds all. Everything after this point is about not writing that middle section by hand.