Make Cheatsheet
Phony and Special Targets
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
.PHONY, and why clean needs it
A phony target is a name for a recipe, not a file. Declaring it tells make to run the recipe unconditionally and to skip implicit-rule search.
.PHONY: all build test clean install
The failure without it, reproduced exactly:
touch clean
make clean
# make: 'clean' is up to date.Any target that is not a real file needs .PHONY, and dist, docs, test,
and check collide with real paths often enough that people hit this in the
wild. What .PHONY buys you:
- The recipe always runs, no timestamp comparison.
- No implicit-rule search, which is a real speedup on large makefiles.
- Make knows the target is not a file, so
-tand-Bbehave sensibly.
Declare each one next to its rule so the list cannot rot:
.PHONY: test test: go test ./...
Two things phony targets must not do: appear as a prerequisite of a real file
target (the file rebuilds every single run), and be used as a pattern
(.PHONY: %.x does nothing).
Special targets worth knowing
Special targets are directives dressed as rules. They take no recipe, and with no prerequisites most of them apply globally.
| Target | Effect |
|---|---|
.PHONY | Listed targets are not files |
.DEFAULT_GOAL | Variable, not a target, sets what plain make builds |
.DELETE_ON_ERROR | Delete a target whose recipe failed |
.SILENT | Do not echo recipes, globally or for listed targets |
.IGNORE | Ignore recipe errors for listed targets |
.PRECIOUS | Never delete these on interrupt |
.SECONDARY | Keep intermediate files |
.INTERMEDIATE | Treat these as intermediate |
.NOTINTERMEDIATE | 4.4, opt out of intermediate deletion |
.ONESHELL | GNU only, one shell per recipe instead of per line |
.NOTPARALLEL | Run this makefile serially even under -j |
.EXPORT_ALL_VARIABLES | Export every variable to recipes |
.SUFFIXES | With no prerequisites, wipe the suffix list |
.DEFAULT | Recipe for any target with no rule |
.POSIX | POSIX-conforming behavior, must be first |
.SECONDEXPANSION | Expand prerequisite lists a second time |
.DEFAULT_GOAL
Without it, the default goal is the first non-special target in the file, which makes the order of your rules load-bearing. Set it explicitly.
.DEFAULT_GOAL := help
make # builds .DEFAULT_GOAL make -p -n | grep -m1 DEFAULT_GOAL # confirm what make chose
.DEFAULT_GOAL := help
Reading $(.DEFAULT_GOAL) before any rule is parsed gives an empty string.
$(MAKECMDGOALS) holds the goals actually named on the command line, and is
empty when the default goal is used.
ifeq ($(MAKECMDGOALS),clean) # skip the expensive -include of dependency files endif
.DELETE_ON_ERROR, the one everybody should set
A failed recipe leaves its half-written output on disk, and because that file
now has a fresh timestamp the next make treats it as up to date and ships
corrupt output.
out.txt:
echo partial > $@
falseecho partial > out.txt false make: *** [Makefile:3: out.txt] Error 1 # out.txt still exists, 8 bytes, and looks current
Add one line at the top of every makefile that writes files:
.DELETE_ON_ERROR:
make: *** [Makefile:4: out.txt] Error 1 make: *** Deleting file 'out.txt'
Make deletes a target on interrupt by default, but not on recipe failure,
which is the exact opposite of what you want. .PRECIOUS opts a specific
target back out.
.ONESHELL and the one-shell-per-line rule
Every recipe line normally gets its own shell, so cd and shell variables do
not survive to the next line.
bad: cd /tmp pwd # prints the makefile directory, not /tmp good: cd /tmp && pwd # one shell, one line
.ONESHELL (GNU make 3.82+, no BSD or POSIX equivalent) passes the whole recipe
to a single shell.
.ONESHELL:
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
release:
x=$$(git rev-parse --short HEAD)
echo "building $$x"
cd dist
tar czf ../rel-$$x.tgz .Two consequences to plan for:
- Line prefixes @, -, and + are only honored on the first line, so the
whole block is echoed or silenced together.
- Without -e in .SHELLFLAGS only the last command's exit status counts, so a
failing middle line is ignored. Set .SHELLFLAGS := -eu -o pipefail -c
whenever you enable .ONESHELL.
.SILENT, .IGNORE, and SHELL
.SILENT: # everything, equivalent to make -s .SILENT: version # just this target .IGNORE: lint # like prefixing every line with - SHELL := /bin/bash # default is /bin/sh, which is dash on Debian
Prefer the per-line @ and - prefixes over the global forms. A makefile that
prints nothing is very hard to debug, and MAKEFLAGS += --silent at least
leaves make --debug a way back in.
SHELL is not inherited from your environment. Make always uses /bin/sh
unless the makefile says otherwise, which is why [[, arrays, and
pipefail fail on Debian and Ubuntu where /bin/sh is dash.
A task-runner Makefile with a self-documenting help
Every recipe line below begins with a literal tab.
SHELL := /bin/bash .SHELLFLAGS := -eu -o pipefail -c .DEFAULT_GOAL := help .DELETE_ON_ERROR: MAKEFLAGS += --no-builtin-rules --warn-undefined-variables IMAGE ?= app TAG ?= $(shell git rev-parse --short HEAD) .PHONY: help help: ## Show this help @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \ | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n",$$1,$$2}' .PHONY: install install: ## Install dependencies npm ci .PHONY: test test: install ## Run the test suite npm test -- --run .PHONY: image image: ## Build the container image docker build -t $(IMAGE):$(TAG) . .PHONY: clean clean: ## Remove build output $(RM) -r dist coverage
$(MAKEFILE_LIST) makes the help target work unchanged when you split rules
across *.mk includes. The ## marker after the colon is the convention every
variation of this trick uses.