Automation
Makefiles: targets, variables, and recipes
How GNU Make decides what to rebuild: targets, prerequisites, variables, and the tab, phony, and timestamp mistakes that leave the stale file in place.
9 min read
GNU Make reads a file of rules and runs the recipes whose targets are out of date. It does not remember the text of a recipe. A target is rebuilt when it is missing, or when a prerequisite is strictly newer than it. Equal timestamps count as up to date. Change the command and leave the files alone, and the next make does nothing.
The make on Debian, Ubuntu, Fedora, and RHEL is GNU Make. make --version prints GNU Make and a version. The examples below were checked on GNU Make 4.3. macOS ships BSD make under the name make. BSD make shares targets and recipes, and it disagrees on assignment, on $(wildcard), and on several special targets. On a Mac these examples want GNU Make, often installed as gmake.
Make is not the shell. Each recipe line is handed to /bin/sh -c. On Debian and Ubuntu that is dash, not Bash, so arrays and [[ fail even when they work interactively. Quoting, once that shell starts, is covered in Shell quoting, pipelines, and exit codes. Make does not watch the clock either. That is Cron jobs. A crontab entry that runs make is how a schedule uses a makefile.
Rules and the default target
A rule names a target, lists prerequisites, and gives the recipe. Recipe lines begin with a tab. Spaces in that column are not an indent Make understands.
dist/bundle.js: src/a.js src/b.js
cat src/a.js src/b.js > dist/bundle.jsMake runs that recipe when dist/bundle.js does not exist, or when either source is strictly newer than the bundle. touch -r src/a.js dist/bundle.js copies the source timestamp onto the bundle, and the next make has nothing to do until the source is touched again.
With no goal on the command line, Make builds the first target whose name does not start with a dot. Assignments, comments, and special targets such as .PHONY do not count. Put the real build first, or call it all and list it first.
Make looks for GNUmakefile, then makefile, then Makefile, and uses the first it finds. A forgotten GNUmakefile silently wins over the Makefile you just edited. make -f build.mk names the file and skips that search.
A file that builds something
all below is phony, so a stray file named all cannot mark the project finished. dist is an order-only prerequisite, written after |. Make creates the directory before the files that need it, and a later change to the directory's timestamp does not rebuild those files. $(wildcard) returns names in sorted order. The substitution rewrites each page path into the output path, so all knows the text files before they exist.
.DELETE_ON_ERROR:
.PHONY: all clean
SRC := $(wildcard src/*.js)
PAGES := $(wildcard pages/*.md)
OUT := $(PAGES:pages/%.md=dist/%.txt)
all: dist/bundle.js $(OUT)
dist/bundle.js: $(SRC) | dist
cat $(SRC) > $@
dist/%.txt: pages/%.md | dist
sed 's/^/> /' $< > $@
dist:
mkdir -p $@
clean:
rm -rf dist$@ is the target of the rule that is running. $< is the first prerequisite. In the pattern, dist/intro.txt is built from pages/intro.md, and the stem that matched % is intro.
With src/a.js, src/b.js, pages/intro.md containing Hello, and pages/body.md containing Body, the first make prints:
mkdir -p dist
cat src/a.js src/b.js > dist/bundle.js
sed 's/^/> /' pages/body.md > dist/body.txt
sed 's/^/> /' pages/intro.md > dist/intro.txtMake expanded $(SRC) before the shell started, which is why the echoed line names the two files rather than the wildcard. The bundle is first because it is the first prerequisite of all. In a serial run the pages follow in sorted order, body then intro. dist/body.txt contains > Body. A second make prints make: Nothing to be done for 'all'. Touch src/a.js and only the bundle recipe runs. make clean removes dist, because clean is phony and has a recipe.
Variables
Make expands $ before the shell sees the line. One dollar sign is a Make variable. A dollar sign for the shell is written $$.
| Form | When the right-hand side is expanded |
|---|---|
name = value |
Each time name is used. $(shell ...) runs again at every use. |
name := value |
Once, while Make reads the line. ::= is the POSIX spelling. |
name ?= value |
Only if name is unset, including unset in the environment. |
name += value |
Appends, with a space. If the name was unset, this behaves as =. |
counted = $(shell echo rec >> log; echo R)
frozen := $(shell echo imm >> log; echo I)
show:
echo $(counted)
echo $(counted)
echo $(frozen)Reading that file appends imm to log once. Running show appends rec twice, and the values are R, R, and I. := freezes the list while Make reads the file. = expands when a later recipe is about to run, so it can see files that recipe's prerequisites just created.
?= steps aside for anything already set. The environment is imported before the makefile is read, so it counts.
prefix ?= /usr/local
show:
echo $(prefix)make show runs echo /usr/local. Both prefix=/opt make show and make show prefix=/opt run echo /opt.
A plain := does not step aside for the environment. The command line still wins, and -e hands that same power to the environment.
prefix := /usr/local
show:
echo $(prefix)prefix=/opt make show still runs echo /usr/local. make show prefix=/opt runs echo /opt. prefix=/opt make -e show runs echo /opt. override forces the file over the command line. Callers rarely want that.
In a recipe, $$ becomes one $ for the shell. A single $ would make Make expand $((n+1)) itself.
SRC := src/a.js src/b.js
count:
n=0; \
for f in $(SRC); do n=$$((n + 1)); done; \
echo $$nThe backslashes join the physical lines into one recipe line, so there is one shell and n survives. The value printed is 2.
One shell per line
Make starts a new /bin/sh for every recipe line. It does not read .bashrc. A variable set on one line is gone on the next, and cd does not stick. The lines below succeed, and file.txt lands where make started, not in out.
build:
mkdir -p out
cd out
echo hi > file.txtJoin the steps with &&. A normal recipe line does not enable the shell's -e. && is what stops the line when cd fails, and what makes the redirect run in out.
build:
mkdir -p out && cd out && echo hi > file.txt.ONESHELL runs the whole recipe in one shell. Make then sees only the last line's status. false followed by echo still prints still and Make exits zero. Turn on the shell's own check if you use it:
.ONESHELL:
.SHELLFLAGS = -ec
build:
false
echo still-e stops that shell at false, so still is not printed, and Make reports Error 1. -c is how the script is passed. Replacing .SHELLFLAGS has to keep it. For two or three commands, && is the smaller change.
make -j4 runs up to four recipes at once. Every file a recipe reads has to be a prerequisite, or one job reads a file another has not finished. A plain make hides that race.
@echo hi prints hi and not the command. A leading - ignores a failure: Make prints Error 1 (ignored) and continues. Fine on a clean-up line, a bad habit on a compiler.
Phony targets and directories
A target that names an action needs .PHONY. Otherwise an existing file of that name is the finished product.
clean:
rm -rf distIf a file called clean exists and the rule has no prerequisites, make clean prints make: 'clean' is up to date. and leaves dist alone. all and test do the same. .PHONY: all clean marks them always out of date. A phony target with a recipe runs it every time. One that only lists prerequisites, as all does above, walks those and stops. That is why a second make can report nothing to do.
A directory's timestamp changes when a file inside it is created, deleted, or renamed. As a normal prerequisite, that rebuilds the bundle whenever anything lands in dist:
dist/bundle.js: src/a.js dist
cat $< > $@After the bundle is current, echo extra > dist/other.txt makes dist newer, and the next make runs cat again. Order-only prerequisites, to the right of |, are built when missing and ignored in the timestamp check.
dist/bundle.js: src/a.js | dist
cat $< > $@
dist:
mkdir -p $@dist/other.txt no longer rebuilds the bundle. $^ leaves order-only names out, so cat $^ will not try to concatenate the directory. $| is that list on its own.
Patterns and automatic variables
% is the stem, the part that differed.
dist/%.txt: pages/%.md | dist
mkdir -p $(dir $@)
sed 's/^/> /' $< > $@make dist/intro.txt sets the stem to intro, $< to pages/intro.md, and $(dir $@) to dist/ including the slash.
| Variable | In the recipe |
|---|---|
$@ |
The target |
$< |
The first prerequisite |
$^ |
Normal prerequisites, duplicates removed |
$? |
Prerequisites strictly newer than the target |
$* |
The stem, in a pattern rule |
| `$ | ` |
$? is the list for a copy or archive rule that should mention only what changed. In an explicit rule with no pattern, $* is empty.
A * in a recipe is the shell's. Make echoes *.js unchanged, and the shell globs it. A * in a prerequisite is expanded by Make. An assignment is not. Use $(wildcard src/*.js).
A missing file with no rule stops before any empty output is written:
make: *** No rule to make target 'missing.txt', needed by 'out.txt'. Stop.Failures that leave the old file
A recipe line that does not start with a tab is not a recipe. Make stops, and the error contains missing separator and the line number. cat -A Makefile shows a tab as ^I and the end of a line as $. If the recipe line does not start with ^I, that is the character it wanted. Spaces copied from a page that ate the tab are the usual reason.
GNU Make 4.3 can use another prefix. It applies from the assignment downward. The recipe line here begins with >, not a tab.
.RECIPEPREFIX := >
all:
> echo prefixedChange the prefix only in a file you control. A tab is what every other makefile is using.
A recipe that fails after creating its target leaves the file behind. The next make sees a target that is not older than its prerequisites.
bad.txt:
echo partial > $@
falseThe first run exits non-zero and leaves bad.txt containing partial. The second prints make: 'bad.txt' is up to date. .DELETE_ON_ERROR, anywhere in the file, makes Make delete a target whose recipe changed it and then failed. The trace includes Deleting file 'bad.txt'. Files from an earlier successful run stay.
Make does not notice that a recipe changed. This target was built with cp. Adding the echo does not rebuild, because in.txt is not newer than out.txt.
out.txt: in.txt
cp $< $@
echo extra >> $@make -B treats targets as out of date, so extra is appended. Touching in.txt rebuilds this target alone, and a later plain make still understands why. Saving the makefile does not.
Checking the decision
make -n prints recipes and does not run them. Before the first build it lists the mkdir, the cat, and both sed lines, and dist is still absent. An up-to-date tree prints Nothing to be done. make -d prints every comparison. An unexpected prerequisite in that trace is why a target rebuilds on every run. A missing one is why it never does.
- Spaces instead of a tab. The error contains
missing separator.cat -Ashows a tab as^I. - A file named
clean,all, ortest. Mark the action.PHONY. - A directory as a normal prerequisite. It goes stale when a file inside it changes. Put the directory after
|. - A new shell on every line.
cdand shell variables stop there. Use&&, or.ONESHELLwith.SHELLFLAGS = -ec. - One
$means Make. A shell variable needs$$. Make's$@is the target. - A failed recipe that already created its target. The next run looks finished.
.DELETE_ON_ERRORremoves the partial file. - A changed recipe and untouched files.
make -Bforces the recipes. GNUmakefileis preferred toMakefile, including one you forgot was there.- The environment does not override
:=unless you pass-e. It already counts as set, so?=keeps it. The command line overrides both. $(wildcard)assigned with:=, read too early to see files a recipe has not created.make -jacross a missing edge. A serial run will not warn you.
The target is a file, or a phony name for an action. The recipe runs when that target is missing or older than a prerequisite, and only then.