Worked examples — Phony targets, pattern rules, implicit rules
Before we start, three pieces of vocabulary we'll use constantly.
We'll also touch three flag variables that steer the compiler and linker (see Compiler flags CFLAGS LDFLAGS):
The scenario matrix
Every situation this topic can produce is one of these cells. The examples below each announce which cell(s) they cover, so together they fill the whole grid.
| Cell | Scenario class | The question it tests |
|---|---|---|
| C1 | Target missing | Does the recipe fire from nothing? |
| C2 | Target up to date (newer than all prereqs) | Does Make correctly skip? |
| C3 | One prereq newer → partial rebuild | Which recipes fire, which don't? |
| C4 | Cascade — a rebuilt file makes its dependents stale | Does the wave propagate up the graph? |
| C5 | Phony collision — a real file shares a phony name | Does .PHONY force the run? |
| C6 | Degenerate stem — weird/empty/dotted names | Does % match what you expect? |
| C7 | Precedence fight — explicit vs pattern vs implicit | Which rule wins? |
| C8 | Zero-prerequisite target (a real rule with no prereqs) | Out of date or not? |
| C9 | Real-world word problem — a small project from scratch | Put it all together |
| C10 | Exam twist — order of touch, -B, -t flags |
Do you predict Make's exact output? |
The figure below is the dependency graph we will keep pointing back to. Follow the arrows: an arrow from A to B means "A is a prerequisite of B", i.e. B depends on A.

Example 1 — C1: building from nothing
Steps.
- Make wants the default goal
hello(the first rule's target — see the definition above). Its prereqhello.odoesn't exist. Why this step? The rebuild rule (§1 of parent): missing target ⇒ run. So we must first makehello.o. - To make
hello.o, Make checks its prereqhello.c. It exists and has no rule of its own — it is a plain source file, not a target. Why does this matter? A source with no rule is just an input: if it exists, Make treats it as up to date and never tries to build it. (Contrast this with cell C8, a genuine rule whose target has zero prerequisites — see Example 4b.) hello.ois missing ⇒ its recipe runs:gcc -c hello.c -o hello.o. Why does it fire? The rebuild rule says a missing target is always out of date, so its recipe must run to create it. Nowhello.oexists with a fresh mtime.- Back to
hello: it's missing ⇒ its recipe runs:gcc hello.o -o hello. Why? Same reason — a missing target is out of date, so the recipe runs to produce it. Make could only reach this step after step 3 createdhello.o, sincehellodepends on it.
Order of recipes: hello.o first, then hello. This is topological order — you build a node only after everything it depends on.
Verify: run make twice. First run prints both gcc lines. Second run prints make: 'hello' is up to date. — because now both targets exist and no source changed (that's Example 2's cell).
Example 2 — C2: everything up to date
Steps.
- Check
hello: it exists. Is any prereq newer? Its prereq ishello.o, and . Why compare? The rebuild rule fires only if a prereq is strictly newer. It isn't. Sohellois up to date. - But Make still recurses into
hello.oto be sure it's current. Why recurse? An up-to-date-looking target could still be stale if its prereq changed; Make must verify the whole subtree.hello.oexists; its prereqhello.cis older. Sohello.ois up to date too. - Nothing is out of date ⇒ zero recipes run. Output:
make: 'hello' is up to date.
Verify: the answer is 0 recipes. This is the payoff of Incremental compilation — unchanged inputs cost only a couple of stat() calls.
Example 3 — C3 & C4: one edit, a cascade
Look at figure s01: editing one leaf sends a wave upward.
Steps.
touch util.cmakes the newest thing in the project. Why?touchbumps the clock reading without changing contents — a clean way to simulate an edit.- Check
app→ recurse to prereqsmain.o,util.o. Why recurse before deciding onapp? Make can't know whetherappis stale until it knows whether its prerequisites are stale (and possibly rebuilt) — so it resolves children first, exactly the topological order from Example 1. main.o: its prereqmain.cis not newer ⇒ skip (cell C3, the "not-affected" side). Recall$<here ismain.c.util.o: its prerequtil.cis now newer ⇒ out of date ⇒ rungcc -c util.c -o util.o(the pattern rule with$< = util.c,$@ = util.o). Now is fresh.- Back at
app: one prereq (util.o) is now newer thanapp⇒appis out of date ⇒ relinkgcc main.o util.o -o app. This is the cascade, cell C4.
Recipes that fire, in order: util.o, then app. main.o is untouched.
Verify: 2 recipes. The cascade count = (number of stale nodes on the path from the edited leaf up to the goal). Here the path is util.c → util.o → app; the two buildable nodes on it are util.o and app. ✓
Example 4a — C5: the phony collision bug, live
Steps.
- Make treats
cleanas a target. Does the filecleanexist? Yes (you justtouched it). - Does
cleanhave prerequisites? No. Why does this matter? Rebuild rule: run iff missing OR a prereq is newer. It's not missing, and there are zero prereqs, so no prereq can be newer. - Conclusion: Make declares
cleanup to date and printsmake: 'clean' is up to date.— thermnever runs. The bug bites. - Fix: add
.PHONY: clean. Now Make knowscleannames an action, ignores the on-disk file, and treats it as always out of date ⇒rmruns every time.
Verify: without .PHONY ⇒ 0 recipes run despite make clean. With .PHONY ⇒ 1 recipe runs. This exact behaviour is why phony targets exist.
Example 4b — C8: a real rule with zero prerequisites
Steps.
- First
make version.txt: the file is missing ⇒ out of date ⇒ recipe runs, creatingversion.txt. Why? Missing target is always out of date, regardless of having zero prereqs. - Second
make version.txt: the file now exists, and with zero prerequisites no prereq can be newer ⇒ Make declares it up to date ⇒ recipe does not run. Why? This is the same logic that caused thecleanbug in 4a — a zero-prereq real file is "sticky": once it exists, Make leaves it alone. - First and second
make greet:greetis phony, so it is treated as permanently out of date ⇒ recipe runs both times. Why? Phony strips away the timestamp reasoning entirely.
So C8 splits by phoniness: a real zero-prereq target runs once (then sticks); a phony zero-prereq target runs every time. This is exactly the distinction Example 1 avoided — there hello.c was a source with no rule at all, which is a third, separate case (never built).
Verify: version.txt ⇒ recipe runs 1 time over two invocations; greet ⇒ recipe runs 2 times over two invocations. ✓
Example 5 — C6: degenerate stems (what does % actually match?)
The % is greedy but literal-anchored. Let's push it on odd names.

Steps.
parser.o: strip the literal.o⇒ stemparser⇒ needsparser.c. Straightforward. Why? The rule's target part is%.o; matching it againstparser.oforces%to equal everything before the literal.o.a.b.o: the literal suffix is.o;%matches everything before it, dots included ⇒ stema.b⇒ needsa.b.c. Why?%is not "up to the first dot" — it grabs the longest string that leaves.oat the end. Extra dots are fine..hidden.o: stem.hidden⇒ needs.hidden.c. Leading dot is part of the stem. Why?%just requires a non-empty match; a leading dot is legal.o: to match%.othere must be a literal.oat the end.ohas no.osuffix (it's a single char) ⇒ no match, the pattern is skipped entirely. Why care? Degenerate names like this teach that%cannot match the empty stem and still supply the literal — the literal.omust physically appear.
Completed table:
| Requested target | Stem $* |
Prereq built |
|---|---|---|
parser.o |
parser |
parser.c |
a.b.o |
a.b |
a.b.c |
.hidden.o |
.hidden |
.hidden.c |
o |
— (no match) | rule skipped |
Verify: 3 of 4 rows match; the o row does not, because %.o requires an actual .o suffix. ✓
Example 6 — C7: precedence fight (explicit vs pattern vs implicit)
Steps.
parser.o: there is an explicit rule with a recipe (E). Why does it win? Parent §4 precedence: explicit ≻ your pattern ≻ built-in. So E fires:gcc -O2 -DDEBUG -c parser.c -o parser.o. Note$<=parser.c(first prereq only);parser.his listed so that editing the header forces a rebuild, but it is not passed togcc.util.o: no explicit rule ⇒ fall to your pattern (P) ⇒gcc -O2 -c util.c -o util.o. The built-in implicit rule is never reached because your P already matched. Why? Your pattern shadows the built-in.- If you deleted both E and P,
util.owould still build — the built-in implicit%.o:%.ccatches it. That built-in recipe is roughly$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@: it splices in the preprocessor flagsCPPFLAGS(header paths,-Dmacros) and the compiler flagsCFLAGS(see the flag-variable definition above). Why mention CPPFLAGS here? It's the hook the built-in gives you to add-Iincludewithout writing any rule.
Verify: parser.o ⇒ rule E (has -DDEBUG); util.o ⇒ rule P (no -DDEBUG, has -O2). Precedence order confirmed. ✓ See also Compiler flags CFLAGS LDFLAGS for how these flags flow in.
Example 7 — C9: a real-world project from scratch
The Makefile (using Makefile variables and expansion):
CC := gcc
CFLAGS := -Wall -O2 # compiler flags: warnings on, optimise
LDFLAGS := # linker flags — empty here; e.g. put -lm to link libm
OBJ := main.o geometry.o
.PHONY: all clean
all: shapes
shapes: $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@ # $^ = main.o geometry.o ; LDFLAGS steers the linker
main.o: main.c geometry.h # header dep so edits to the .h rebuild main.o
$(CC) $(CFLAGS) -c $< -o $@ # $< = main.c only ; $@ = main.o
geometry.o: geometry.c geometry.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJ) shapesWhy the empty LDFLAGS? We define it explicitly (even as blank) so the link line has a documented place to add linker options later — e.g. LDFLAGS := -lm to pull in the math library. An undefined LDFLAGS would expand to nothing anyway, but naming it makes the intent clear and keeps the build reproducible.
Steps (first make, nothing built yet).
- You type
make⇒ default goal isall(first non-dot rule), andallis phony, so it's always out of date and Make proceeds to its single prereqshapes. Why start here? The default-goal definition picks the first rule's target;allexists only to point at the real deliverableshapes. shapesis missing ⇒ out of date. But to build it Make must first resolve its prereqsmain.oandgeometry.o. Why resolve prereqs first? Topological order: you cannot link objects that don't exist yet.main.ois missing ⇒ its recipe runs (recipe #1):gcc -Wall -O2 -c main.c -o main.o. Why does it fire? Missing target ⇒ out of date. Here$<=main.c(only the source is compiled), whilegeometry.his listed purely to trigger future rebuilds.geometry.ois missing ⇒ its recipe runs (recipe #2):gcc -Wall -O2 -c geometry.c -o geometry.o. Why? Same missing-target rule; its$<isgeometry.c.- Now both objects exist and
shapesis still missing ⇒ its recipe runs (recipe #3):gcc $^ -o shapeswith$^ = main.o geometry.oand the (empty)LDFLAGS. Why last? The link step is the top of the graph — it can only run once every object beneath it is present.
Total: 2 object files, 3 recipe runs on a clean first build.
Now edit only the header: touch geometry.h. Both main.o and geometry.o list geometry.h as a prereq, so both recompile, then shapes relinks ⇒ 3 recipes again. Why include the header on both? Because a struct change in geometry.h invalidates every translation unit that includes it — miss one and you get a silent ABI mismatch. This is the crux of correct Incremental compilation.
Verify: clean first build ⇒ 3 recipes; touch geometry.h ⇒ 3 recipes (both .o + relink); touch geometry.c only ⇒ 2 recipes (geometry.o + relink). ✓
Example 8 — C10: the exam twist (touch, -t, -B)
Steps.
- (a) Nothing changed ⇒ no target is missing and no prereq is newer than its target ⇒ 0 recipes, prints
make: 'shapes' is up to date.(this is cell C2). Why? Straight application of the rebuild rule to an all-current tree. - (b)
-Bmeans "unconditionally make all targets", ignoring timestamps entirely. Why does this flag exist? As a sledgehammer when you suspect stale or corrupt intermediate state (e.g. after switching compilers). ⇒ all 3 recipes run:main.o,geometry.o, then the relink ofshapes. - (c)
-tis "touch mode": Make does not run recipes; it just bumps every target's mtime so they look up to date. Why does it exist? To mark a tree as "already built" without spending compile time (e.g. after you know the outputs are fine). Somake -truns 0 real recipes (it only touchesmain.o,geometry.o,shapes); the following plainmakethen sees everything current ⇒ also 0 recipes. Danger:-tcan mask a genuinely-needed rebuild and silently break Build reproducibility — use sparingly. - (d)
touch geometry.omakes the object newer than its own source (sogeometry.ois itself up to date — no prereq is newer than it) but newer thanshapes. Make recurses:main.ocurrent,geometry.ocurrent, thenshapessees a prereq (geometry.o) newer than itself ⇒ out of date ⇒ relink only. ⇒ 1 recipe (the link step). Why no recompile? Nothing undergeometry.ochanged; only the link's freshness check failed — a neat illustration that staleness is checked per edge, not globally.
Verify: (a) 0, (b) 3, (c) 0 recipes (both the -t pass and the follow-up make), (d) 1. ✓
Recall
Recall Which cell does each behaviour belong to?
A phony action target runs every time regardless of on-disk files ::: C5 — .PHONY strips away timestamp reasoning.
A real target with its own rule but zero prerequisites builds once then sticks ::: C8 — once the file exists, no prereq can be newer, so it's declared up to date.
Editing one leaf triggers recompile of its object AND a relink ::: C4, the cascade.
make -B runs every recipe ignoring timestamps ::: C10, the flag twist.
%.o matches a.b.o with stem a.b ::: C6, degenerate stem — % grabs everything before the literal .o.
Explicit parser.o: beats the pattern %.o: ::: C7, precedence: explicit ≻ pattern ≻ implicit.