Question bank — Makefiles — targets, prerequisites, recipes, variables, automatic variables
Figure 1 (below) is the timestamp model — refer back to it for every True/False and Edge case about staleness (the "is an input newer?" traps). Figure 2 is the DAG walk order — refer to it for the post-order, cycle, and parallelism traps.
The picture below is the whole mental model in one frame: files as nodes, timestamps as a timeline, and the recipe firing only when an input arrow is newer than the target.

And here is the walk order Make takes over that graph — build the leaves first, the root last:

True or false — justify
Make rebuilds a target whenever its recipe is different from last time.
If a target file exists, Make will never run its recipe.
$^ and $? always expand to the same list.
$^ is all prerequisites; $? is only the ones newer than the target — the red-highlighted arrows in Figure 1. On a full rebuild they may match, but on an incremental build $? is usually a small subset.$^ and $+ are interchangeable.
$^ removes duplicate prerequisites; $+ keeps every duplicate in order. This only matters when a prereq is repeated (e.g. a library that must appear twice on a linker line) — then $+ preserves it and $^ silently drops it.A = 1 and A := 1 behave identically here.
$(A) equal 1. The difference only shows when the value contains a reference to another variable that changes later; = re-reads it, := snapshots it..PHONY: clean makes the clean target run faster.
clean is not a real file, so Make never does a timestamp check and always runs the recipe — even if a file literally named clean exists.A prerequisite must be a file that already exists on disk.
Listing a header like parser.h as a prerequisite is pointless because the compiler doesn't take .h files as arguments.
parser.h changes, Make must recompile parser.o. The recipe uses $< (the .c), but the .h still triggers the rebuild.$< is guaranteed to be the source file you want to compile.
$< is literally the first prerequisite, whatever it is. If you write foo.o: foo.h foo.c, then $< is foo.h and your compile breaks.An order-only prerequisite (after |) triggers a rebuild when it changes, just like a normal one.
A double-colon rule (::) is just cosmetic — same as :.
:: you may write several independent rules for one target, each with its own recipe, and Make runs each whose prerequisites are out of date. With single : a target may have only one recipe.Spot the error
all:
gcc main.c -o app(the recipe line is indented with spaces) — what fails and why?
*** missing separator. Stop. error. Make needs a literal TAB to mark a recipe line; spaces look identical but Make refuses to treat the line as a recipe.app: main.o utils.o
gcc $< -o appWhy does linking fail?
$< is only the first prerequisite (main.o), so utils.o is never linked → undefined-reference errors. The linker needs all objects, so use $^. See Linkers and Object Files.OBJS = $(SRCS:.c=.o)
SRCS := main.cWhy might $(OBJS) be surprising?
OBJS uses recursive (=) assignment, so it is expanded when used, re-reading SRCS at that moment. Combined with later reassignment this can shift unexpectedly; prefer := for derived lists to snapshot a stable value.%.o: %.c
gcc -c $@ -o $<What's swapped?
$@ (target, the .o) and $< (first prereq, the .c) are reversed. It tries to compile the object file into the source — nonsense. Correct: -c $< -o $@.clean:
rm -f *.o appA file named clean exists in the folder — what breaks?
clean as an up-to-date file (no prerequisites, already exists) and skips the recipe. Fix with .PHONY: clean so it always runs.CFLAGS = -Wall
build:
gcc $(CFLAGS) main.c
CFLAGS = -O2Which flags actually get used?
-O2. Because CFLAGS is recursive (=), $(CFLAGS) is expanded when the recipe runs, by which point the last assignment -O2 has overwritten -Wall.FLAGS := -Wall
FLAGS += $(EXTRA)
EXTRA := -O2What is in FLAGS, and why?
-Wall, with an empty append.== Step by step: (1) FLAGS := -Wall makes FLAGS a simple (already-expanded) variable. (2) The rule for += is: if the left side is simple, the appended text is expanded immediately at the += line; if it is recursive, the text is stored unexpanded. Since FLAGS is simple, $(EXTRA) is expanded right there — but EXTRA is still undefined at that point, so it expands to the empty string. (3) The later EXTRA := -O2 runs too late to affect the value already appended. Result: FLAGS is -Wall (trailing space, no -O2). Moral: with := + +=, define your dependency variables before you append them, or the append silently snapshots an empty value.%.o: %.c
gcc -c $< -o $@
build/main.o: main.cbuild/ does not exist — what fails?
gcc errors because it can't write into a missing directory. Fix with an order-only prerequisite: build/%.o: %.c | build and a rule build:\n\tmkdir -p build. The | makes build exist first without its timestamp forcing rebuilds.Why questions
Why does Make build prerequisites before checking the target's timestamp?
.c/.h), so its recipe must run before Make can even know the full input set. This is the post-order walk of the DAG.Why do automatic variables like $@ exist instead of just typing the filename?
target: prereqs line. Hard-coded names can drift apart when you edit one but not the other; $@/$</$^ always refer to this rule's actual files.Why is ?= useful for something like CC ?= gcc?
make CC=clang) without editing the Makefile. Good for overridable defaults.Why prefer $+ over $^ on a linker line sometimes?
$^ collapses duplicates and breaks that; $+ keeps every occurrence in order.Why does the graph of files have to be acyclic?
a depends on b depends on a) has no valid post-order build sequence — Make could never decide what to build first (there is no bottom leaf in Figure 2's walk). See Directed Acyclic Graphs (DAG).Why does $(SRCS:.c=.o) save you from bugs?
OBJS from SRCS automatically, so adding a source in one place updates the object list. No second list to forget to update.Why does incremental building make the recipe use $? sometimes instead of $^?
$? gives only the stale prerequisites, so tools that append or re-process only changed inputs do less work — the heart of Incremental Compilation.Edge cases
What does Make do when the target does not exist at all?
¬exists(T) is true and the recipe runs.What if a prerequisite and the target have the exact same timestamp?
t(P_i) > t(T)), so equal times count as "not stale."What happens if a rule has no prerequisites and its target file exists?
.PHONY, in which case it always runs.What does $* expand to for a non-pattern rule (no \%)?
$* is defined as the stem matched by \%; with no pattern there is no stem, so relying on it there is a bug.What if two prerequisites are identical, like app: a.o a.o?
$^ removes duplicates, so it expands to just a.o once; use $+ if you actually need both listed.What happens if a prerequisite has no rule and doesn't exist on disk?
No rule to make target '...'. Newcomers hit this when a filename is misspelled or a source directory isn't reachable by any pattern rule.When can a static pattern rule surprise you? Consider:
OBJS := main.o data.txt
$(OBJS): %.o: %.c
gcc -c $< -o $@::: The three colons read as: targets : target-pattern : prereq-pattern. So $(OBJS) is the target list, \%.o is the pattern each target must match, and \%.c builds the prereq. The stem \% is matched only against the listed targets, not every file. Here main.o matches \%.o (stem main, recipe compiles main.c), but data.txt does not match \%.o, so it silently gets no recipe from this rule — the classic "why isn't it building?" trap.
Under make -j (parallel builds), why can order-only directory prerequisites still race?
-j runs independent branches of Figure 2's DAG at the same time. If two targets each list | build and build's recipe is mkdir build (no -p), the second mkdir can fire while the first is mid-flight and crash with "File exists." Always use mkdir -p (idempotent), and give the directory its own order-only rule so Make builds it exactly once instead of racing.What else can -j expose that a serial build hides?
-j that ordering is gone, so the two rules run concurrently and the reader sees intermittent failures. The fix is to make the hidden dependency explicit as a real prerequisite so the DAG (Figure 2) forces the correct order.Connections
- Compilation Pipeline — where these rebuild decisions sit in the flow.
- CMake and Build Generators — tools that generate Makefiles for you.
- Shell Scripting Basics — recipes are shell commands, one process per line.