GNU Make, at work
Twenty-eight lessons on GNU Make, one concept per page: a rule's anatomy of goal, prerequisites and recipe; how make decides what to rebuild from modification times and the dependency graph; variables, the two flavors of = and :=, ?= and +=, the environment and override; the automatic variables $@ $< $^, pattern rules, implicit rules and .PHONY; functions in the $(...) form, wildcard, patsubst, subst, filter and foreach; include and -include, recursive make with $(MAKE), MAKECMDGOALS and the @ - + prefixes; parallel builds with -j, order-only prerequisites, vpath and VPATH, double-colon rules; the missing separator and make's error grammar; and a first real Makefile for a small multi-file project, walked end to end.
One concept, one page, one real capture. That's the rhythm.
GNU Make, at work
Twenty-eight lessons on GNU Make, one concept per page: a rule's anatomy of goal, prerequisites and recipe; how make decides what to rebuild from modification times and the dependency graph; variables, the two flavors of = and :=, ?= and +=, the environment and override; the automatic variables $@ $< $^, pattern rules, implicit rules and .PHONY; functions in the $(...) form, wildcard, patsubst, subst, filter and foreach; include and -include, recursive make with $(MAKE), MAKECMDGOALS and the @ - + prefixes; parallel builds with -j, order-only prerequisites, vpath and VPATH, double-colon rules; the missing separator and make's error grammar; and a first real Makefile for a small multi-file project, walked end to end.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Every fact in this book is as the official source states it, fetched and read during this build: the complete GNU Make manual from gnu.org (the definition of truth for this book) and the Wikipedia article on Make (software). The Wikipedia Makefile article is a redirect stub, not a source. No facts are sourced from Reddit, Stack Overflow or vendor blogs. Every command and output pair on every page was run on this machine (GNU Make 4.3 at /usr/bin/make, Debian, cc 12.2.0) and confirmed before printing; every build ran in a throwaway directory under a scratch tree, never inside a project or a shared tree. Where this machine's GNU Make 4.3 prints older wording than the manual's newer-version examples, such as the missing-separator hint added after 4.3, the page says the version out loud. An independent guide, not affiliated with or endorsed by the Free Software Foundation, the GNU Make maintainers, or the Wikimedia Foundation.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
Per the GNU Make manual, a rule appears in the makefile and says when and how to remake certain files, listing the files that are the prerequisites and the recipe for updating; a rule tells make two things, when its goal is out of date and how to update it. This machine's capture: a three-rule makefile built a small C program with cc, printing each recipe as it ran.
A rule's anatomy
A rule says when and how to remake files: a goal, the prerequisites it needs, and a recipe of shell lines.
Our capture built a real program: cc -c main.c, then the link line. The recipe runs in a sub-shell, one line at a time.
Three parts, one page. Every later idea in this book is one of these three parts wearing a costume.
Write a two-rule makefile by hand and run it once; name the three parts out loud as make runs.
Per the GNU Make manual, variables in make can come from the environment in which make is run: every environment variable becomes a make variable, but an explicit assignment in the makefile or with a command argument overrides the environment, and the -e flag flips that precedence, which the manual does not recommend. This machine's captures: MODE=night make reached a recipe; a makefile MODE = day won over the environment; MODE=night make -e let the environment win.
The environment
Every environment variable becomes a make variable at startup. But an explicit makefile assignment overrides the environment.
Our captures: MODE=night make printed night; add MODE = day to the file and it printed day; only make -e handed the win back, a flag the manual calls not recommended.
Recipes also see your shell: HOME was visible in our capture without any makefile help.
Print one variable from a recipe with and without a makefile assignment for it, then once with -e.
Per the GNU Make manual, a phony goal is one that is not really the name of a file; the prerequisites of the special goal .PHONY are always remade, and a phony goal avoids trouble when a real file with that name exists, since the file would otherwise satisfy the goal. This machine's captures: with a file named clean present, make clean said 'clean' is up to date and did nothing; with .PHONY: clean the recipe ran.
.PHONY
A phony goal is not really the name of a file. Declare it under .PHONY and make stops asking the file system for permission.
Our captures: with a junk file named clean, make clean refused, calling it up to date. One line, .PHONY: clean, and the rm ran.
clean, all, install-style helper goals: if no file should ever carry the name, mark it phony.
Create a file named clean in a scratch project and feel the refusal, then add .PHONY: clean and rerun.
Per the GNU Make manual, $(patsubst pattern,replacement,text) finds whitespace-separated words in text that match pattern and replaces them with replacement, where % in the replacement stands for the matched text, and $(subst from,to,text) replaces every occurrence of from with to. This machine's captures: patsubst %.c,%.o over the source list produced the object list, and subst .c,.bak produced the backup list.
patsubst and subst
$(patsubst %.c,%.o,list) rewrites each word that matches; the % carries the stem across. $(subst from,to,text) swaps plain text anywhere.
Our captures: sources became extra.o main.o util.o, then .bak names, in two calls.
Sources to objects in one call is the load-bearing line of half the makefiles on earth.
Turn a SRCS list into OBJS with patsubst and echo both lists from one recipe line.
Per the GNU Make manual, recursive make commands should always use the variable $(MAKE), not the explicit command name, and -C changes to a directory before doing anything. This machine's capture: a recipe $(MAKE) -C sub echoed make -C sub, printed make[1]: Entering directory with the sub path, built in sub, then Leaving directory.
Recursive make
For subdirectories, the recipe is $(MAKE) -C sub. Never the plain word make: the variable carries your path and flags to the child.
Our capture shows the child's own voice: make[1]: Entering directory, its output, then Leaving.
The [1] is a depth counter; the child is a full make with its own first goal and its own graph.
Build a two-line sub makefile and count the [1] markers between Entering and Leaving directory.
Per the GNU Make manual, without -j make runs one recipe at a time, but the -j option tells make to execute many recipes simultaneously, and .NOTPARALLEL inhibits parallelism for some or all goals from within the makefile. This machine's captures: a slow and a quick recipe finished slow then quick serially; with -j2 the quick one finished first; .NOTPARALLEL under -j2 restored the slow-first order.
Parallel builds
-j tells make to run many recipes simultaneously. Independent branches of the graph overlap; the finish order changes with the clock.
Our captures: serial gave slow then quick; -j2 flipped the finish order; .NOTPARALLEL pinned it serial again. Danger: recipes sharing one file can race.
Keep every recipe writing only its own outputs and -j becomes free speed.
Give two independent recipes different sleeps and race them serially, with -j2, then with .NOTPARALLEL.
Per this book's plan, a small multi-file project walked end to end assembles the book's concepts: variables for the compiler and object list, one pattern rule with automatic variables, a clean goal marked phony. Every line in the diagram ran on this machine (GNU Make 4.3): the first make compiled both sources and linked, the second said 'app' is up to date, make clean removed the products, and a fourth make rebuilt everything.
A real Makefile
Eleven lines: CC and OBJS, a link rule with $@ $^, one %.o: %.c pattern, clean marked .PHONY.
Our captures, all four: the build, then 'app' is up to date, then clean's rm, then a full rebuild after clean.
Every concept in this book is on this page at once. If you can read it cold, the book worked.
Type those eleven lines for a two-file project of yours and run the four-make sequence yourself.