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.

MessageCause
*** 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.

FlagShows
--debug=bbasic, which targets are out of date
--debug=vverbose, makefiles read and every target considered
--debug=iimplicit-rule search, which patterns were tried
--debug=jjob invocation and reaping, for -j problems
--debug=mremaking makefiles, for include restart loops
--debug=nnone, cancels a -d set in MAKEFLAGS
--debug=aall, 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

FunctionDestinationStops the build
$(info msg)stdoutno
$(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-rules

Expect 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.