This page is a workout . The parent note taught you the rules; here we run Make's brain by hand across every situation it can face. Before each answer you will forecast — guess out loud. That is how the rules stick.
Definition The three logic symbols we use (read them like English)
This page writes Make's decision as a tiny logic formula. Three symbols appear — each is just a plain word:
¬ X — "not X " . Flips true↔false. ¬ exists ( T ) = "T does not exist."
A ∨ B — "A OR B " . True if either side is true.
∃ i : … — "there exists some i such that ..." . True if at least one item in a list makes the "..." true. Over an empty list it is false (no item exists to make it true).
That's the entire alphabet. Everywhere below, when you see these symbols, just read the English.
Definition The five automatic variables (defined here so nothing is assumed)
Inside a recipe, Make fills these in per rule — we use all of them on this page, so here are their meanings up front:
$@ — the target name of this rule.
$< — the first prerequisite .
$^ — all prerequisites , space-separated, duplicates removed.
$? — only the prerequisites strictly newer than the target (the stale ones). Always a subset of $^.
$* — the stem : the exact text the % wildcard matched in a pattern rule.
Make's whole job is a single yes/no question per file: "is this target stale?" Everything below is just that question under different conditions. Here is every case-class we must cover.
#
Case class
The tricky part it tests
A
Fresh build — target does not exist
The "doesn't exist" branch of the rebuild rule
B
Target exists, all prereqs older
The "already up-to-date, do nothing" branch
C
Target exists, one prereq newer
Timestamp comparison picks the stale input
D
Chained / recursive dependency
Post-order DAG traversal — build deep before shallow
E
Automatic variables in a pattern rule
$@ $< $^ $? and the stem $* expanding per rule
F
Variable flavor trap (= vs :=)
Lazy vs eager expansion timing
G
Degenerate inputs — no prereqs, .PHONY, empty recipe
Edge behaviour, always-run targets
I
Exam twist — $? vs $^, ordering of prereqs
Subtle "only the newer ones" semantics
J
Pathological edges — equal timestamps, missing prereq, a cycle
What bites you at the boundaries
H
Word problem — incremental edit in a real project
Which minimal set rebuilds
Intuition The one comparison that drives every case
Every row reduces to Make asking t ( P i ) > t ( T ) ("is input P i newer than target T ?"). t ( X ) just means "the clock time when file X was last modified" — a plain number that grows as time passes. Bigger number = more recently touched = newer . Keep that picture and nothing below is mysterious.
We will use a timeline picture for the timestamp cases: time flows left→right, each file is a dot placed at its modification time.
The image below is the mental model for cases B, C, H and I. Walk it slowly:
The white arrow is time; further right means a newer modification time.
The yellow dot is the target T , placed at t = 100 .
The green dot (left, t = 90 ) is a prerequisite that is older than T — it sits to T 's left , so it is safe : it cannot make T stale.
The red dot (right, t = 150 ) is a prerequisite that is newer than T — it sits to T 's right . The red arrow from T to it is the trigger: any input to the right ⇒ rebuild.
So the whole page, visually, is one rule: "is any input-dot to the right of the target-dot?"
Worked example The target file doesn't exist yet
hello : hello.c
gcc hello.c -o hello
You just cloned the project. hello.c exists; hello does not . What does Make do?
Forecast: run the recipe, or skip it? Decide before reading.
Steps:
Make looks for the file hello. It is missing.
Why this step? The rebuild rule's first branch is ¬ exists ( T ) — "target not present." If a thing isn't there, there is nothing to be up-to-date, so no comparison is even needed.
That branch is true, so the whole condition ¬ exists ( T ) ∨ ( … ) is true.
Why this step? An OR is true if either side is; the missing-target side is already true, so we never even look at timestamps.
Make runs gcc hello.c -o hello, producing hello.
Why this step? When the rule evaluates to true, Make's response is to execute the recipe — that is the whole point of a rule, to produce the missing target.
Verify: After building, hello exists and its timestamp is now (newer than hello.c). Run make again → it prints make: 'hello' is up to date. Consistent: the file now exists and is newer than its input, so both stale-conditions are false.
Worked example Target exists, every prerequisite is older
Same rule as Case A. Now hello was built at clock-time t ( hello ) = 100 , and hello.c was last edited at t ( hello.c ) = 90 .
Forecast: rebuild or skip?
Steps:
hello exists → first branch ¬ exists ( T ) is false.
Why this step? The file is present on disk, so "doesn't exist" is false; we cannot stop here — we must check the second branch.
Check the only prerequisite: is t ( hello.c ) > t ( hello ) ? That is 90 > 100 → false.
Why this step? A target is stale only if some input is strictly newer . Here the input is older, so it cannot make the target stale.
Both branches false → condition false → do nothing .
Why this step? When the rebuild rule is false, Make deliberately skips the recipe — this "do nothing" is exactly the time-saving the whole tool exists for.
Verify: In figure s01, hello's dot (100, yellow) sits to the right of hello.c (90, green). No input dot is right of the target → up-to-date. Make prints "up to date."
Worked example You edited the source
Same rule. Now you save hello.c, so its timestamp jumps: t ( hello.c ) = 120 , while t ( hello ) = 100 (unchanged).
Forecast: which comparison flips, and does the recipe run?
Steps:
hello exists → existence branch ¬ exists ( T ) is false.
Why this step? We must first rule out the "missing" case before timestamps matter; the file is on disk, so this branch contributes nothing and we move to the comparison.
Compare: t ( hello.c ) > t ( hello ) → 120 > 100 → true .
Why this step? Editing the source moved its dot to the right of the target's dot — the input is now newer, so the output is stale. (In figure s01, imagine the green dot sliding to the right of the yellow one — it turns into the red case.)
The OR is true → run gcc hello.c -o hello. After building, t ( hello ) becomes ~121 (now).
Why this step? A true rebuild rule means execute the recipe; rebuilding stamps the new binary with the current time, which is now newer than the source.
Verify: Post-build, hello (121) is again right of hello.c (120). Re-running make skips — the edit is "absorbed." One edit → exactly one rebuild.
Definition Post-order DAG traversal (the algorithm, stated plainly)
A DAG (see Directed Acyclic Graphs (DAG) ) is a set of nodes (here: files) with directed edges (here: "target depends on prerequisite") and no cycles . Post-order traversal is this recipe for visiting nodes:
visit ( T ) : first visit ( P i ) for every prerequisite P i , then process T .
In words: fully finish all of a node's children before you process the node itself. For Make, "process" = "check the rebuild rule and run the recipe if stale." So the deepest files are judged first, roots last.
Worked example A depends on B depends on C
app : main.o
gcc main.o -o app
main.o : main.c
gcc -c main.c -o main.o
Dependency chain: app ← main.o ← main.c. Suppose main.c was just edited (newest), while main.o and app are old.
Forecast: in what order does Make touch these files?
Steps:
Make is asked for app. Before judging app, it calls visit ( main.o ) .
Why this step? Post-order says finish the children first: you cannot compare t ( main.o ) to t ( app ) until main.o itself is known-fresh, so Make descends before deciding.
To judge main.o, it calls visit ( main.c ) . main.c has no prerequisites → it's a leaf , treated as up-to-date, and processing returns immediately.
Why this step? A leaf has an empty prerequisite list, so its ∃-clause is vacuously false and (assuming the source file exists) there is nothing to build — the recursion bottoms out here.
Back at main.o: is main.c strictly newer than main.o? Yes (you edited it) → rebuild main.o. Its timestamp becomes now .
Why this step? Post-order has now finished the child, so the comparison t ( main.c ) > t ( main.o ) is finally meaningful; it is true, so we run the compile recipe.
Back at app: is main.o strictly newer than app? Yes — we just rebuilt it → rebuild app.
Why this step? Rebuilding main.o in step 3 stamped it with the current time, which is now newer than the old app; the freshly-updated child propagates staleness up to the root.
Order actually run: main.o first, then app. Deepest node's recipe fires first.
Verify: See figure s02 below. Post-order visits leaves before roots, so recipes fire bottom-up: main.c (nothing to do) → main.o (compile) → app (link). An edit at the bottom ripples up exactly once per level. See Directed Acyclic Graphs (DAG) for why "acyclic" guarantees this recursion terminates.
Walk the picture top-to-bottom: the green node main.c is the leaf you edited (newest) — nothing to compile there. The white arrows point up the dependency chain. Make descends to the bottom, then executes upward: the blue node main.o runs its recipe first (labelled "1st recipe: compile"), and only then the yellow node app runs "2nd recipe: link." The red arrow on the left reminds you the edit that started it all lives at the leaf.
Intuition The stem variable
$*, pictured
Think of % as a blank you fill in . In %.o matching parser.o, the % stood for parser. The stem variable $* simply hands you back whatever you wrote in that blank — here, parser. (% is a literal wildcard character; do not confuse it with a percent sign.)
Worked example The pattern rule expands for two different targets
CC := gcc
CFLAGS := -Wall
% .o : % .c
$( CC ) $( CFLAGS ) -c $< -o $@ # (stem available as $* )
Make wants parser.o, matched against parser.c. Then separately lexer.o against lexer.c.
Forecast: write out the exact gcc command Make executes for parser.o, and say what $* holds.
Steps:
% matches the stem parser. So $* = parser.
Why this step? The pattern %.o matched parser.o, forcing % = parser; that captured stem is exactly what $* reports.
$< = first prerequisite = parser.c.
Why this step? The compile flag -c takes exactly one source file, and $< gives precisely the first (here only) prerequisite — matching what -c expects.
$@ = target = parser.o.
Why this step? The output flag -o names the file we're building — which is the target — so $@ is the right variable to hand it.
Substitute all four into the recipe: gcc -Wall -c parser.c -o parser.o.
Why this step? Make textually replaces each automatic variable with the value computed above, yielding the real shell command it runs.
For lexer.o the same rule re-expands: gcc -Wall -c lexer.c -o lexer.o, with $* = lexer. One rule, many files — that's the whole point of automatic variables.
Verify: Read the produced command aloud: "compile-only (-c) the source parser.c, output to parser.o." Source and target agree with the rule's target: prereq line. And $* (parser) is exactly $@ with the .o suffix stripped — the invariant "stem + suffix == target" holds. No filename was typed twice — impossible for them to drift.
Worked example The classic snapshot trap
A = 1
B = $( A ) # recursive (lazy)
C := $( A ) # simple (eager) — snapshots A now (=1)
A = 2
D = $( A ) # recursive
A = 3
Forecast: at the very end, what are $(B), $(C), $(D)?
Steps:
C := $(A) expands immediately . At that moment A is 1, so C is frozen to 1.
Why this step? := is eager : it snapshots the value at definition time, so later changes to A cannot reach C.
B = $(A) is lazy : B stores the expression $(A), not a value. Its value is decided when used , at the end, where A = 3. So $(B) = 3.
Why this step? = (recursive) re-reads the latest A every time you reference B, and by the end A has become 3.
D = $(A) is also lazy → used at end → $(A) = 3 → $(D) = 3.
Why this step? Same lazy rule as B: the value is resolved at reference time, when A is 3.
Answers: $(B) = 3, $(C) = 1, $(D) = 3.
Verify: Only C used :=, so only C froze early (to 1). Both lazy variables see the final A=3. This is why the parent note insists on := for derived lists like OBJS — you want a stable snapshot, not a value that silently changes.
Worked example G1: A target with NO prerequisites
banner :
echo "=== BUILD START ==="
Forecast: with an empty prerequisite list, when does the recipe run?
Steps:
Prerequisite list is empty → there is no P i , so the clause ∃ i : t ( P i ) > t ( T ) is vacuously false.
Why this step? "There exists an i ..." over an empty set is always false — no such i exists to test.
So the only thing that can trigger a build is the existence branch ¬ exists ( banner ) .
Why this step? With the second branch permanently false, the rebuild rule collapses to "rebuild only if the target file is missing."
If a file named banner exists, Make thinks it's up-to-date forever and never echoes. If no such file exists, it echoes (and still never creates banner, so it echoes every run).
Why this step? The recipe echo never produces a file called banner, so the existence test decides everything — this is the bug .PHONY fixes next.
Verify: With a stray file named banner present, make banner prints "up to date" and skips — matching the rule collapsing to the existence test.
.PHONY fixes the degenerate case
.PHONY : banner
banner :
echo "=== BUILD START ==="
Steps:
.PHONY: banner declares banner is not a file .
Why this step? It removes the existence test entirely — Make stops looking for a file called banner on disk.
A phony target is treated as always out-of-date → recipe runs every invocation, even if a stray file banner exists.
Why this step? With no file to compare against, Make has no notion of "fresh," so it defaults to always running — exactly what a command-label like clean needs.
Verify: Without .PHONY, create an empty file banner → make banner prints "up to date," recipe skipped (the bug). With .PHONY, the same stray file is ignored → recipe always runs. This is exactly the clean target's need in the parent Makefile.
Worked example Only the newer prerequisites
archive.a : a.o b.o c.o
@ echo "all prereqs: $^ "
@ echo "newer only : $? "
Timestamps: archive.a = 100, a.o = 90 (old), b.o = 150 (newer!), c.o = 130 (newer!).
Forecast: what do $^ and $? each print?
Steps:
First, does the recipe even run? b.o (150) is strictly newer than archive.a (100) → stale → yes, run.
Why this step? Before any variable expands, Make applies the rebuild rule; one newer prerequisite is enough to make the whole ∃-clause true.
$^ = all prerequisites, order-preserved, duplicates removed → a.o b.o c.o.
Why this step? $^ ignores timestamps entirely; it's the complete input list, used when a step (like linking/archiving) needs every input.
$? = only prerequisites strictly newer than the target . Compare each to archive.a (100): a.o (90) → not newer (drop); b.o (150) → newer (keep); c.o (130) → newer (keep) → b.o c.o.
Why this step? For incremental updates you only want to re-process the changed inputs, and $? is precisely "the stale ones" — the subset that flipped the rebuild rule.
Answers: $^ = a.o b.o c.o, $? = b.o c.o.
Verify: $? is always a subset of $^. Here it drops exactly the one input (a.o) whose timestamp (90) is not greater than 100. Number in $? = count of prereqs with t ( P i ) > t ( T ) = 2. See figure s04: only dots to the right of the target dot land in $?.
The yellow dashed line marks the target archive.a at t = 100 . Each object is a dot on the timeline: a.o (green, t = 90 ) sits left of the line → not newer → dropped from $? ; b.o (t = 150 ) and c.o (t = 130 ) sit right of the line (red) → newer → kept in $? . The caption spells out the outcome: $^ = all three, $? = the two red ones. $? is literally "everything to the right of the yellow line."
Worked example J1: Exactly equal timestamps — the
> vs >= question
Suppose hello.c and hello have the identical modification time: t ( hello.c ) = t ( hello ) = 100 .
Forecast: rebuild or skip?
Steps:
Existence branch: hello exists → false.
Why this step? The file is present, so we move on to the timestamp comparison.
Test the stale clause: is t ( hello.c ) > t ( hello ) ? That is 100 > 100 → false .
Why this step? The rule uses strictly newer (> ), not "newer-or-equal" (≥ ). An equal timestamp is a tie , and a tie is resolved in favour of "up to date" — Make does not rebuild.
Both branches false → skip .
Why this step? Neither trigger fired, so the recipe does not run — the equal-time input is treated as harmless.
Verify: This is why a hurried touch hello.c hello (both stamped the same second) can fail to trigger a rebuild — the tie is resolved as "up to date." If you truly want a rebuild, make the source strictly newer (e.g. touch hello.c alone). The check 100 > 100 = false confirms the tie is a skip.
Worked example J2: A missing (dangling) prerequisite with no rule
app : main.o missing.o
gcc $^ -o $@
Here missing.o does not exist and there is no rule that says how to build it.
Forecast: does Make build app, skip it, or error out?
Steps:
To judge app, Make must first make missing.o up-to-date (post-order, Case D).
Why this step? Prerequisites are processed before their target, so Make treats missing.o as a target it must produce first.
missing.o does not exist, and there is no rule and no source file that produces it.
Why this step? Make can only create a file if some rule's recipe makes it (or it already exists on disk); here neither holds, so the dependency is dangling .
Make aborts with: make: *** No rule to make target 'missing.o', needed by 'app'. Stop.
Why this step? With nothing to run and nothing to read, Make cannot satisfy the dependency, so it fails loudly rather than silently producing a broken app.
Verify: The error names both the impossible target (missing.o) and who needed it (app) — that pairing is your fix-map. Compare with a file that exists but has no rule (like a .h header): that's fine, Make just treats it as a leaf. The error only fires when the prereq is both absent and unbuildable.
Worked example J3: A dependency
cycle
a : b
touch a
b : a
touch b
a depends on b, and b depends on a — a loop, i.e. not a DAG.
Forecast: infinite loop, or does Make notice?
Steps:
Make tries to build a → needs b → needs a → … it revisits a node already on the current build path.
Why this step? Post-order descends into prerequisites; with a loop, that descent circles back to a node it is already in the middle of processing.
Make does not loop forever. It prints make: Circular a <- b dependency dropped. and drops one edge to break the loop, then proceeds as best it can.
Why this step? Make tracks the chain it is descending, so meeting a node twice on the same path is a detectable cycle; it breaks the loop to guarantee termination.
Verify: The Directed Acyclic Graphs (DAG) "acyclic" requirement is exactly what forbids this. Make's post-order traversal (Case D) is only guaranteed to terminate on an acyclic graph — the moment you introduce a cycle, Make must break it, and the "dependency dropped" warning is your signal that your graph is malformed.
Worked example Which files rebuild after one edit?
Project:
app : main.o utils.o math.o
gcc $^ -o $@
% .o : % .c
gcc -c $< -o $@
Files main.c, utils.c, math.c each have their own .o. Everything is freshly built (timestamps: all .c = 100, all .o = 101, app = 102). Now you edit only utils.c, so t ( utils.c ) = 200 .
Forecast: how many recipes run, and which files change?
Steps:
Make descends to build app's prerequisites first (post-order).
Why this step? The final binary can only be judged once its object files are known-fresh, so Make visits the objects before app.
main.o: is main.c (100) strictly newer than main.o (101)? 100 > 101 → false → skip . Same test for math.o → skip .
Why this step? Their sources are untouched, so timestamps are unchanged and neither object is stale — Make reuses them.
utils.o: is utils.c (200) strictly newer than utils.o (101)? 200 > 101 → true → rebuild utils.o. Now t ( utils.o ) ≈ 201 .
Why this step? The edit moved utils.c far to the right of its object, so the object is stale; recompiling stamps the new object with the current time.
Back at app: is any of main.o (101), utils.o (201), math.o (101) strictly newer than app (102)? utils.o at 201 > 102 → true → relink app.
Why this step? Rebuilding utils.o in step 3 made it newer than app, so the ∃-clause fires and the binary must be relinked to include the fresh object.
Answer: exactly 2 recipes run — recompile utils.o, relink app. main.o and math.o are reused. That reuse is the whole payoff; see Incremental Compilation .
Verify: Count of rebuilt targets = 1 (the edited object) + 1 (the final binary that depends on it) = 2. Untouched sources ⇒ untouched objects. This is Make computing the minimal stale set , exactly as the parent's "50 .c files" motivation promised.
Read the three columns as three source→object chains all feeding the single binary app at the bottom. Only the middle column is red : you edited utils.c (red source), so its utils.o turns red (recompiled) and app turns red (relinked). The green sources main.c, math.c and their blue objects main.o, math.o stay cool — reused untouched . The red text on the sides names the two recipes that actually run; everything green/blue is the work Make skipped .
Recall Which case did each example cover? (reveal to self-check closure)
A fresh build ::: existence branch true — target absent, so rebuild with no timestamp check
B skip ::: exists AND all prereqs older — nothing to the right of the target, do nothing
C single edit ::: one prereq newer flips the comparison to true — exactly one rebuild
D chain ::: post-order DAG traversal — deepest recipe (compile) fires before the root (link)
E pattern rule ::: $@ $< $^ and the stem $* expand per matched target
F flavors ::: := snapshots early (C=1), = re-reads latest (B=D=3)
G degenerate ::: empty prereqs make the ∃ clause vacuously false; .PHONY forces always-run
I twist ::: $? is the subset of $^ strictly newer than the target (b.o c.o)
J1 equal times ::: strict > means a tie (100 = 100) counts as up-to-date — no rebuild
J2 missing prereq ::: absent AND unbuildable ⇒ fatal "No rule to make target" error
J3 cycle ::: not a DAG ⇒ Make warns "Circular dependency dropped" and breaks the loop
H word problem ::: exactly 2 recipes rebuild (utils.o + app) — the minimal stale set
Mnemonic The stale test in one line
"Absent, or any input strictly to the right of me → rebuild." Every case A–J is that sentence applied to a different timeline — with J reminding you that ties don't count , dangling inputs are fatal , and loops get broken .
Hinglish version of the parent note
Directed Acyclic Graphs (DAG) — why post-order (Case D) terminates and cycles (Case J3) are forbidden
Incremental Compilation — the minimal-rebuild payoff of Case H
Compilation Pipeline · Linkers and Object Files — what the recipes in E/H actually invoke
CMake and Build Generators — tools that generate these Makefiles for you
Shell Scripting Basics — recipes are shell commands