Make Cheatsheet
Debugging
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Read the error message first
Make's five common errors each mean one specific thing.
| Message | Cause |
|---|---|
*** missing separator. Stop. | a recipe line indented with spaces instead of a TAB |
*** No rule to make target 'X', needed by 'Y'. | X does not exist and no rule builds it, often a typo or a stale .d file |
*** No targets specified and no makefile found. | wrong directory, or the file is named makefile.txt |
Nothing to be done for 'X'. | X exists as a file and is up to date, usually a missing .PHONY |
'X' is up to date. | same, for a target you named explicitly |
*** recipe commences before first target. | a tab-indented line above any rule |
warning: overriding recipe for target 'X' | two rules with recipes for one target, the later one wins |
sed -n 'l' Makefile | head -20 # tabs print as \t , portable grep -nE '^ +[^ ]' Makefile # recipe lines indented with spaces
Dry run and forced states
make -n # print recipes without running them make -n target # what would happen for one goal make --dry-run --trace # the same, annotated with why make -B # rebuild everything, ignore timestamps make -W src/main.c # pretend one file just changed make -o config.h # pretend one file is up to date make -t # touch targets instead of building make -q; echo $? # 0 up to date, 1 out of date, 2 error
make -n still executes any line containing $(MAKE), by design, so a recursive
build's dry run really does descend. It also cannot show recipes that depend on
files an earlier recipe would have created.
make -q is the scriptable form: it prints nothing and answers "would this
build do anything" with an exit status.
--trace, the first flag to reach for
--trace (GNU make 4.0+) prints each target, the line that defined it, and the
prerequisites that forced it, then the recipe with variables already expanded.
make -B --trace
Makefile:14: update target 'build' due to: unknown reasons mkdir -p build Makefile:11: update target 'build/main.o' due to: src/main.c include/u.h cc -std=c11 -Wall -Iinclude -MMD -MP -c -o build/main.o src/main.c
"due to: unknown reasons" means the target simply did not exist. Otherwise the list is exactly the prerequisites that were newer, which answers the single most common question, why is this rebuilding.
-p, the database dump
-p prints every variable with its origin, every rule, and every target's state.
Combine it with -n so it does not build anything first.
make -p -n | less make -p -n | grep -m1 DEFAULT_GOAL # what plain make would build make -p -f /dev/null # only the built-ins make -p -n | grep -B1 '^CFLAGS' # where CFLAGS came from
The origin comment above each variable is the useful part:
# makefile (from 'Makefile', line 2) CFLAGS := -std=c11 -Wall -Iinclude -MMD -MP
And each target carries its resolved state:
build/main.o: src/main.c include/u.h | build # Implicit/static pattern stem: 'main' # Last modified 2026-09-15 20:08:56 # File has been updated.
-d and the --debug categories
-d is everything, which is thousands of lines. Pick a category instead.
| Flag | Shows |
|---|---|
--debug=b | basic, which targets are out of date |
--debug=v | verbose, makefiles read and every target considered |
--debug=i | implicit-rule search, which patterns were tried |
--debug=j | job invocation and reaping, for -j problems |
--debug=m | remaking makefiles, for include restart loops |
--debug=n | none, cancels a -d set in MAKEFLAGS |
--debug=a | all, same as -d |
make --debug=v 2>&1 | grep -i 'considering target' make --debug=i 2>&1 | grep -i 'trying pattern' # why no rule matched make --debug=j 2>&1 | grep 'Putting child' # jobs actually launched make --debug=m # include restarts
An excerpt of --debug=v, past the version banner every debug mode prints:
Reading makefiles... Reading makefile 'Makefile'... Reading makefile 'build/main.d' (search path) (don't care) (no ~ expansion)... Updating goal targets.... Considering target file 'build/app'. Considering target file 'build/main.o'.
--debug=i is the answer to "make says no rule to make target but I wrote a
pattern rule". It lists every pattern tried and why each was rejected.
Printing from inside the makefile
| Function | Destination | Stops the build |
|---|---|---|
$(info msg) | stdout | no |
$(warning msg) | stderr, with file:line: | no |
$(error msg) | stderr, with file:line: | yes, exit status 2 |
$(info CC=$(CC) CFLAGS=$(CFLAGS)) $(warning PREFIX defaulted to $(PREFIX)) ifndef VERSION $(error VERSION is required, e.g. make release VERSION=1.2.3) endif
The timing trap: these expand while the makefile is read or while a recipe is expanded, not when the recipe runs. So this prints out of order:
all: @echo running $(info expanded before the shell runs)
expanded before the shell runs running
Use @echo for anything that should appear at recipe time, and $(info) for
parse-time state.
A print-any-variable target, worth keeping in every makefile:
print-%: @echo '$* = $($*)' @echo ' origin = $(origin $*)' @echo ' flavor = $(flavor $*)' @echo ' value = $(value $*)'
make print-CFLAGS
--warn-undefined-variables
Make treats an undefined variable as an empty string, silently, which turns a typo into a missing compiler flag.
X := $(NOPE) all: ; @echo [$(ALSO_NOPE)]
make --warn-undefined-variables
u.mk:1: warning: undefined variable 'NOPE' u.mk:2: warning: undefined variable 'ALSO_NOPE' []
Turn it on permanently, and disable the built-in variables at the same time so the built-in rules stop generating false positives.
MAKEFLAGS += --warn-undefined-variables --no-builtin-rulesExpect noise the first time on an old makefile. Most hits are real.
A debugging checklist
1. Rebuilds when nothing changed. make --trace and read the "due to" list.
Usual culprit is a directory used as a normal prerequisite instead of an
order-only one, or a phony target in a file target's prerequisites.
2. Does not rebuild when something did change. make -p -n | grep -A3
'^target:' to see the prerequisite list make actually has. Usual culprit is a
missing header edge, so add -MMD -MP.
3. missing separator. sed -n 'l' Makefile and look for a line that starts
with spaces where a \t belongs.
4. A variable is empty. make print-VAR, then $(origin VAR). Check whether
a := read it before it was assigned.
5. A recipe behaves differently than the same command in your shell. Make
uses /bin/sh, not your login shell, and does not source any profile. Set
SHELL := /bin/bash and remember $$ for shell variables.
6. Works serially, fails with -j. A missing dependency edge, every time.
Reproduce with make -j16 -B and read --debug=j.
7. Nothing at all happens. Check .PHONY, then check .DEFAULT_GOAL with
make -p -n | grep -m1 DEFAULT_GOAL.