5.3.7 · D2Build Systems & Toolchain

Visual walkthrough — Phony targets, pattern rules, implicit rules

1,925 words9 min readBack to topic

Step 1 — A file has a birthday: the timestamp

WHAT. Every file on disk carries a number: the moment it was last written, called its modification time (we write it ). Think of it as a clock stamp glued to the file. When you edit main.c and save, the operating system re-stamps it with "now".

WHY this and not file contents? Make could read both files and compare them byte-by-byte, but that means reading the whole file — slow for big projects. Asking the OS "when was this touched?" is a single instant lookup (one stat() call). Make chooses speed: compare two clock numbers, not two whole files.

PICTURE. Two files sit on a timeline. main.c was saved at time 10. main.o (the compiled result) was built at time 12. Later than its source — good.

Figure — Phony targets, pattern rules, implicit rules

Step 2 — The rebuild question, drawn as an arrow

WHAT. A rule ties a target to the things it is built from:

The colon : reads as "depends on". We draw it as an arrow from prerequisite to target.

WHY an arrow? Because the freshness flows one way. If the source changes, the target becomes stale — never the reverse. The arrow points in the direction "must be rebuilt after".

PICTURE. main.cmain.o. Under each file, its . Make's whole job is: compare the two numbers along that arrow.

Figure — Phony targets, pattern rules, implicit rules

Step 3 — Case walk: the three things that can happen on one arrow

WHAT. With one target and one prerequisite there are exactly three situations. We must cover all of them — a reader should never meet a case we skipped.

WHY enumerate? The formula in Step 2 is one line, but a beginner can't feel it until they see each branch resolve. So we split vs into: target absent, target older, target newer.

PICTURE. Three little timelines stacked. Green = recipe runs, grey = skipped.

Figure — Phony targets, pattern rules, implicit rules
Case Situation check Verdict
(a) main.o missing target run (build it)
(b) main.c newer than main.o run (stale)
(c) main.o newer than main.c skip (up to date)
Recall Quick check

If you touch main.c (re-stamp it to "now") but change nothing inside it, does main.o rebuild? Yes ::: Make only reads timestamps, not contents. main.c is now newer, so case (b) fires — even though the text is identical.


Step 4 — The degenerate case that births phony targets

WHAT. Now a target with no prerequisites at all — like the clean job:

clean:
	rm -f *.o program

WHY it breaks. Feed this into the Step 2 formula. Is clean missing? Suppose a file literally named clean sits in the folder — then no, it exists. Is any prerequisite newer? There are no prerequisites, so the part is vacuously false. Both branches false → skip. Make prints 'clean' is up to date and your rm never runs. That's the bug.

PICTURE. The formula's two boxes both light up "false", so the whole OR collapses to "skip" — and we watch the recipe die.

Figure — Phony targets, pattern rules, implicit rules

Step 5 — .PHONY = "pretend this target is eternally missing"

WHAT. Declaring the target phony:

.PHONY: clean

tells Make: this name is never a real file — do not stat it, treat it as forever out of date.

WHY it fixes Step 4. .PHONY short-circuits the whole formula to run = true, always. Equivalently: a phony target behaves as if from the "is it missing?" side, so the first OR-branch is permanently true.

PICTURE. The same formula, but the phony flag forces the left box to true — the OR lights up green no matter what's on disk.

Figure — Phony targets, pattern rules, implicit rules

Step 6 — One arrow becomes many: the pattern rule %

WHAT. You have main.c, math.c, parser.c, … all compiling the same way. Instead of writing one rule per file, write one template:

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

The % is the stem — a wildcard that matches any non-empty string, and the same match is reused on both sides.

WHY the % must match on both sides. That identical stem is exactly how Make links a target back to its source. When it wants math.o, it peels off the literal .o, so the stem is math, then re-glues that stem onto %.cmath.c. Same stem = the two files belong together.

PICTURE. Watch the stem math get extracted from the target and injected into the prerequisite; the automatic variables $@, $<, $* are labelled right where they land.

Figure — Phony targets, pattern rules, implicit rules

Each symbol: $@ = the target Make is currently building, $< = its first prerequisite, $* = the bare stem. Make fills these in at match time because the template never knew the concrete names.


Step 7 — When you write no rule at all: implicit rules & precedence

WHAT. GNU Make already ships a %.o: %.c template internally. So even an empty-of-patterns Makefile compiles your .o files. When several rules could apply to one target, Make picks by a fixed priority.

WHY a priority order? Because more than one template can match parser.o (your explicit rule, your pattern rule, and the built-in one). Make resolves the tie by specificity: the most specific, most deliberate rule wins.

PICTURE. Three candidate rules queue up for parser.o; the explicit one wins, the built-in is the last resort.

Figure — Phony targets, pattern rules, implicit rules

Step 8 — The cascade: how one edit ripples through the graph

WHAT. Real projects chain the arrows: sources → objects → program. Editing one source must ripple up the chain. This is the dependency graph in action.

WHY it cascades. Rebuilding math.o re-stamps it to "now". Now program, which lists math.o as a prerequisite, sees a newer prereq → case (b) of Step 3 → program relinks. The freshness propagates one arrow at a time. (This is exactly what makes Incremental compilation fast: only the touched sub-tree rebuilds.)

PICTURE. Touch math.c. Follow the orange "stale" wave: math.cmath.oprogram. main.o and main.c stay grey — untouched.

Figure — Phony targets, pattern rules, implicit rules

The one-picture summary

Everything above is a single flow: stamp → compare → decide → (maybe) run → re-stamp → cascade. Phony bypasses the compare; pattern/implicit rules supply the recipe when no explicit one exists.

Figure — Phony targets, pattern rules, implicit rules

yes

no

yes

no

yes

no

yes

no

Make wants target

Explicit rule with recipe?

use it

Your pattern rule matches?

built-in implicit rule

Phony target?

always run

Target missing OR prereq newer?

skip, up to date

re-stamp target, cascade to dependents

Recall Feynman retelling — say it in plain words

Make is a lazy but careful assistant. Every file wears a little clock showing when it was last touched. You hand the assistant a list of "this is made from that" arrows. When you ask for something, it walks each arrow and asks one question: "Is the thing I want missing, or is any of its ingredients fresher than it?" If yes, it cooks; if no, it saves its energy and skips. Some jobs — like clean — aren't files at all, so its clock-comparison makes no sense; you tape a .PHONY sign on them meaning "always cook this, don't check the clock." Writing a rule for every file would be exhausting, so you give it a template with a % blank: it peels the name apart, keeps the blank part (the stem), and reuses it to find the source. And Make already carries a stack of these templates in its pocket, so often you write nothing. Finally, when it does cook something, that file's clock resets to now — which can make its dependents look stale, and the freshness ripples up the chain until everything that needed rebuilding is rebuilt, and nothing that didn't, wasn't.