Prerequisite ideas you may want open in another tab: Compilation Pipeline, Incremental Compilation, Directed Acyclic Graphs (DAG), Shell Scripting Basics.
Target ::: report.pdf — the file we want to produce.
Prerequisites ::: report.tex figures.png — the inputs it depends on.
Recipe ::: pdflatex report.tex — the shell command that builds it.
The recipe line (pdflatex ...) must begin with a literal TAB. The target: prerequisites line begins in column 0 with no TAB.
Recall Solution L1.2
$^ — all prerequisites.
$@ — the target.
$< — first prerequisite.
$? — the stale (newer) prerequisites.
$* — the pattern stem.
Why does $^ remove duplicates, and in what order? A file can legitimately be listed as a prerequisite twice — for example a shared header named in two different rule fragments that Make merges. When you feed prerequisites to a linker or compiler via $^, passing the same file twice can cause errors (e.g. "duplicate symbol") or simply waste work. So Make keeps only the first occurrence of each file and drops later repeats, which means it preserves the original left-to-right order of first appearances. (If you genuinely need the duplicates, $+ is the variant that keeps them — but $^ is the safe default.)
$^ = parser.c common.h (all prerequisites, in order, duplicates removed).
$* = parser (the stem the % matched).
The actual command that runs is: gcc -c parser.c -o parser.o. Note common.h never appears in the command even though it is a prerequisite — it forces a rebuild if the header changes, but the compile line only feeds the .c.
Recall Solution L2.2
Why does this operate on each word? Make treats a variable's value as a list of words separated by whitespace — that is Make's fundamental data model (spaces, not commas, are the delimiter). Make first tokenizesSRCS into three words main.c, parser.c, io.c, then applies the suffix rewrite to each token independently, then rejoins them with single spaces. That is why the rule "replace the trailing .c with .o" acts per file and not on the string as a whole:
OBJS = main.o parser.o io.o
Similarly $(SRCS:.c=.d) = main.d parser.d io.d. The pattern is $(VAR:suffix=replacement) — it only rewrites the trailing suffix, per word.
Recall Solution L2.3
CFLAGS := -Wall%.o: %.c gcc $(CFLAGS) -c $< -o $@
The % matches the stem (a, b, c); $< becomes each .c; $@ becomes each .o. One rule now covers all three files and any future ones.
Here we reason about timestamps — the heart of Make, using the rebuild law defined at the top of the page. Study the figure below: it is a small dependency DAG with timestamps attached.
Figure s01 — DAG for L3.1. Three layers, arrows point from a prerequisite to the thing built from it ("built from"). Bottom layer = source files: main.c (t=10, teal), utils.h (t=12, plum), utils.c (t=5, teal). Middle layer = objects: main.o (t=8, orange) fed by main.c and utils.h; utils.o (t=9, orange) fed by utils.c and utils.h. Top = app (t=11, ink) fed by both objects. The shared utils.h (t=12) is the newest source, so it is the trouble-maker: it sits below both objects, meaning both objects have a prerequisite newer than themselves.
Recall Solution L3.1
Work bottom-up (post-order: prerequisites before target, exactly the walk in Figure s01). Recall from the "how we model now" definition that each rebuilt file is stamped one greater than the current maximum timestamp, so it lands strictly above everything built so far. The starting maximum is 12 (utils.h).
main.o (t=8): prereqs main.c (t=10) and utils.h (t=12). Is any newer than 8? Yes, both. → rebuild. Current max is 12, so the new stamp is 12+1=13: t(main.o) becomes 13.
utils.o (t=9): prereqs utils.c (t=5), utils.h (t=12). Is any newer than 9? utils.h (12) > 9 → rebuild. Current max is now 13, so the new stamp is 13+1=14: t(utils.o) becomes 14.
app (t=11): prereqs main.o (now 13) and utils.o (now 14) — both just got newer than 11. → rebuild. New stamp 14+1=15.
Final: all three rebuild. The key insight: even though app was newer than the old object times, rebuilding the objects made them newer, which cascades up. (The exact numbers 13, 14, 15 are just bookkeeping; the only fact that matters is "each rebuilt file is newer than everything before it.")
Recall Solution L3.2
main.o (5): prereqs at t=1, all older → up to date, skip.
utils.o (5): prereqs at t=1 → skip.
app (9): prereqs main.o(5), utils.o(5), both older than 9 → skip.
Nothing is stale. Make prints:
make: 'app' is up to date.
Recall Solution L3.3
$? = prerequisites newer than the target (t=20). Compare each: a.h(25>20 ✓), b.h(10>20 ✗), c.h(30>20 ✓).
$? = a.h c.h
Command run: cp a.h c.h /usr/include/. Only the two changed headers are copied — that's the whole point of $?.
Key choices: := snapshots OBJS = main.o stack.o eval.o. The pattern rule lists defs.h as a prerequisite so touching the header rebuilds every object, but uses $< so only the .c is compiled. Linking uses $^ (all objects) and $@ (calc).
Recall Solution L4.2
Add run to the single existing.PHONY line (do not write a second .PHONY), then add the rule:
.PHONY: all clean runrun: $(TARGET) ./$(TARGET) 2+2
Best practice: group all phony targets on one .PHONY declaration rather than sprinkling several. run depends on $(TARGET), so Make builds calc first (post-order), then runs ./calc 2+2. Because run is phony, it executes every time even if a file named run exists.
B = $(A) is recursive: B stores the expression$(A), re-read each use.
C := $(A) is simple: at that line A=1, so C snapshots 1.
When the $(info ...) lines run, A has been reassigned:
B is 2 — because at that point A=2 and B re-reads it.
C is 1 — snapshot never changes.
B again 3 — A is now 3, B re-reads again.
C again 1 — still the frozen snapshot.
Figure s02 — Build trace for L5.2. Same three-layer shape. Bottom: foo.c (t=100), foo.h (t=200, plum), bar.c (t=100). Middle: foo.o (t=150) built from foo.c and foo.h; bar.o (t=150) built from bar.c. Top: prog (t=250). The plum tag near foo.o reads "200>150 STALE" — its prerequisite foo.h is newer, so foo.o must rebuild. The teal tag near bar.o reads "100<150 fresh" — bar.c is older, so bar.o is skipped. Following the post-order walk (children before parent), you settle the bottom sources, then foo.o (rebuild) and bar.o (skip), then prog.
Recall Solution L5.2
Read the tags in Figure s02 first: the plum "200>150 STALE" tag on foo.o and the teal "100<150 fresh" tag on bar.o already hand you the two verdicts. Now confirm them with the post-order walk from prog (children before parent). The starting maximum timestamp is 250 (prog).
foo.o (150): prereqs foo.c(100), foo.h(200). foo.h 200>150 → rebuild (matches the plum tag). Runs cc -c foo.c -o foo.o. New stamp = current max + 1 = 250+1=251.
bar.o (150): prereq bar.c(100). 100 < 150 → up to date, skip (matches the teal tag). No command.
prog (250): prereqs foo.o(now 251) and bar.o(150). foo.o 251>250 → rebuild. Runs ld foo.o bar.o -o prog.
Commands that actually run, in order:
cc -c foo.c -o foo.o
ld foo.o bar.o -o prog
bar.o is not recompiled — this is exactly the incremental win Make gives you, and Figure s02's teal "fresh" tag is the visual reason.
Recall Solution L5.3
Each edited .c is newer than its .o → 2 compiles run.
The other 4 objects are up to date → 0 of those compiles run.
The 2 rebuilt objects become newer than the binary → the link rule runs → 1 link.
recipes run=2+1=3
Out of a possible 7 recipes, only 3 fire — Make skipped 4 unnecessary compiles.
Recall Solution L5.4
The | splits the prerequisite list into two kinds:
Normal prerequisites (left of |, here %.c): a change in these — newer timestamp — triggers a rebuild.
Order-only prerequisites (right of |, here build): Make guarantees these are made first, but their timestamp is never compared to the target. They enforce ordering only.
Why plain build (no |) is a bug: a directory's modification time bumps every time a file is added to it. If build were a normal prerequisite, then compiling the first object updates build's timestamp, making it newer than the other objects — so every subsequent make would needlessly recompile them all. The | says "just make sure the folder exists first; don't watch its clock." Order-only prerequisites are also excluded from $^ and $< — only normal prerequisites appear there, so $< here is still just %.c.
Recall Solution L5.5
(a) A static pattern rule has the form targets: target-pattern: prereq-pattern. Here the target list is restricted to exactly main.o and util.o — the %.o: %.c part only applies to those two names, matching each against its .c. Why safer than plain %.o: %.c? A bare pattern rule would fire for anyX.o Make ever wants, possibly catching files you never intended (e.g. a .o that should be built a different way). The static form scopes the pattern to a known, explicit list, so it can never fire on a surprise target. $< is main.c / util.c; $@ is main.o / util.o respectively.
(b) For an ordinary rule with multiple targets (single colon), Make treats it as shorthand for one separate rule per target, each sharing the same prerequisites and recipe. So:
When Make needs a.out, $@ = a.out; the recipe runs as gcc shared.c -o a.out.
When Make needs b.out, $@ = b.out; the recipe runs again as gcc shared.c -o b.out.
If both are stale, the recipe runs twice, once per target — because each target got its own copy of the rule. (This is a classic gotcha: people expect one invocation. If you truly want a single command to produce both outputs at once, you need a grouped target with &:, i.e. a.out b.out &: shared.c.)