5.3.7 · D5Build Systems & Toolchain
Question bank — Phony targets, pattern rules, implicit rules
Reminder of the one rule that decides everything:
True or false — justify
A phony target is a target that has no prerequisites.
False — "phony" means it does not correspond to a file on disk; it may still have prerequisites (e.g.
all: program or test: program). The defining property is being declared in .PHONY, not being childless.If you never create a file named clean, the .PHONY: clean line is pure dead weight.
False — a later recipe, tool, or redirection like
make clean > clean.log can create the colliding file, silently breaking the build; .PHONY is cheap insurance and also lets Make skip a pointless stat on it..PHONY makes the recipe run faster.
False — it doesn't speed up the recipe itself; it forces the recipe to run at all (target treated as always out of date) and lets Make skip filesystem checks on that target, which is a scheduling saving, not a recipe speed-up.
In %.o: %.c, the % can match an empty string.
False — the
% (the stem) must match a non-empty string, so .o alone would never trigger this pattern; there has to be at least one character forming the stem.The same % can capture different text on the target side and the prerequisite side.
False — the stem matched on the target (
%.o) is substituted identically into every prerequisite (%.c); that shared stem is precisely the link between an object file and its source.An implicit rule and a pattern rule are two names for the same thing.
False in origin — both are pattern rules mechanically, but an implicit rule is one Make ships built-in (e.g.
%.o: %.c), while a pattern rule is one you write. Your written pattern shadows the built-in one.If a target has an explicit rule and matches your pattern rule, Make runs both recipes.
False — precedence is winner-take-all: the explicit rule with a recipe wins, and the pattern rule is not consulted for that target. Only one recipe runs.
Listing a header in parser.o: parser.c parser.h means the header gets passed to the compiler.
False — only
$< (the first prerequisite, parser.c) reaches the compile line; the header is listed purely so that editing it makes parser.o out of date and triggers a rebuild.Make detects changes by comparing file contents.
False — Make compares timestamps (
mtime) because a stat() is while hashing contents is ; it deliberately trades exact-content correctness for speed.Spot the error
clean:
rm -f *.oWhat's wrong here (assuming the recipe line uses spaces)?
The recipe must begin with a real TAB, not spaces — Make will report a "missing separator" error. (Also
clean should be declared .PHONY, but the fatal syntax error is the tab.)%.o: %.c
gcc -c $@ -o $<Why does this compile the wrong file?
$@ and $< are swapped: $@ is the target (foo.o) and $< is the first prerequisite (foo.c). The correct line is gcc -c $< -o $@ — compile the source into the object.build:
gcc main.c -o build
.PHONY: buildWhy is marking build phony a mistake here?
build is a real output file, so declaring it phony forces a relink on every invocation and destroys incremental behaviour — phony is only for non-file action names like clean/test/all.%.o : %.c
$(CC) $(CFLAGS) -c foo.c -o $@Why does this "pattern" secretly build only one thing correctly?
The recipe hard-codes
foo.c instead of $<, so every stem compiles foo.c; the pattern generalises the names but the recipe throws that away. Use $< so each stem compiles its own source.all clean:
rm -f *.oWhat's the trap in giving all and clean the same recipe on one line?
This makes both
all and clean run rm -f *.o, so make all deletes objects it was supposed to build; targets sharing a line share the recipe, which is almost never what you want here.Why questions
Why does a missing .PHONY on clean cause make: 'clean' is up to date.?
A file named
clean exists, has no prerequisites, so the rebuild formula finds it neither missing nor older than any prereq — Make concludes it's up to date and skips the recipe.Why do automatic variables ($@, $<, $^, $*) even exist?
A pattern rule is written before the concrete filenames are known, so Make injects them at match time; without these variables you couldn't write one generic recipe that adapts to
main.o, util.o, etc.Why does editing a .c file cascade all the way up to relinking the final program?
Each edit bumps the source's
mtime, making its .o out of date; rebuilding the .o bumps its mtime, which makes the linked program out of date — the dependency graph propagates staleness upward one edge at a time.Why does make CC=clang change which compiler runs even with no %.o rule written?
The built-in implicit rule uses the
CC variable (default cc), so overriding CC on the command line redirects the implicit recipe — the compiler isn't auto-detected, it's a variable lookup. See Makefile variables and expansion.Why does listing every action target as .PHONY help with parallel builds (-j)?
Phony targets are known never to be files, so Make can skip
stat checks on them and schedule them without worrying about spurious timestamp comparisons, keeping the incremental logic clean under -j.Why does a hand-written %.o: %.c pattern override GNU Make's built-in one?
Because precedence is explicit > your pattern > built-in implicit: your own pattern is more trusted than the shipped default, so writing one shadows Make's version — this is the sanctioned way to change how objects compile.
Edge cases
If a target has no prerequisites and the file exists, does its recipe run?
No — with nothing newer to compare against and the file present, the formula finds it up to date. That's exactly the situation that makes a non-phony
clean silently do nothing.What happens if parser.c doesn't exist but parser.o is requested via %.o: %.c?
Make tries to make the prerequisite
parser.c first (via any rule that produces it); if none exists and the file is absent, the pattern rule doesn't apply and Make reports it doesn't know how to build parser.o.Two pattern rules both match foo.o (say %.o: %.c and %.o: %.cpp). Which wins?
Make chooses the pattern whose prerequisite actually exists (or can be made); if
foo.c exists it uses the .c rule, if foo.cpp exists it uses that one — existence of the source disambiguates.A prerequisite has the exact same mtime as the target (tie). Does the recipe run?
No — the condition is strictly greater (), so an equal timestamp counts as up to date; this is why sub-second clock resolution can occasionally miss a rebuild.
Does .PHONY: all still let all's prerequisites use timestamp logic normally?
Yes —
all itself always "runs" (it's phony), but its prerequisites like program are ordinary files judged by the normal rebuild formula, so real work is still skipped when nothing changed.What if CC is left unset entirely — does the implicit rule break?
No —
CC has a built-in default of cc, so the implicit rule still functions; but for reproducible builds you should pin it explicitly (CC := gcc) rather than trust the environment's cc.Can a phony target ever be skipped?
Effectively no from the timestamp side (it's always out of date), but Make still won't run it unless it's the goal or a prerequisite of something being built — "always out of date" means if reached, it runs.
Recall One-line summary of every trap here
Every miss above traces to one of three roots: (1) confusing file existence/timestamp with action, (2) confusing which automatic variable is the source vs the target, or (3) forgetting that precedence and stem-matching pick exactly one recipe. Keep the rebuild formula in front of you and each trap dissolves.
Related: CMake and Ninja solve the same staleness problem with content/command hashing instead of raw timestamps — worth contrasting once these traps feel obvious. See also Compiler flags CFLAGS LDFLAGS for what actually flows into $(CC).