The Dependency Graph โ How Make Decides What to Rebuild
Lesson 4: The Dependency Graph โ How Make Decides What to Rebuild
Make's superpower is that it remembers what needs doing. Every prerequisite edge in your Makefile builds a dependency graph โ a directed acyclic graph (DAG) of files โ and Make's job is to walk that graph and do the minimum work required to bring a target up to date.
The One Rule That Drives Everything
That's it. No content hashing, no change detection โ just timestamps compared recursively. Simple, fast, and exactly right for most build pipelines. (It's also why Make can be fooled by clock skew or by touching files โ more on that below.)
Walking the Graph: A Worked Example
Consider this Makefile:
app: main.o util.o
cc -o app main.o util.o
main.o: main.c main.h
cc -c main.c -o main.o
util.o: util.c util.h
cc -c util.c -o util.o
You edit util.c and run make. Here's the decision process, step by step:
- Default goal is
app. Isappmissing or older than a prerequisite? Check its prerequisites first. main.oโ is it older thanmain.cormain.h? No. Up to date, skip.util.oโ is it older thanutil.c? Yes (you just edited it). Rebuildutil.o.- Now back to
app:util.ois now newer thanapp. Relinkapp.
Only util.o is recompiled; main.o is untouched; app is relinked once. On a huge project this is the difference between a 2-second build and a 30-minute one.
Depth-First, Prerequisites First
Make always builds prerequisites before targets, recursively. This means:
- You never need to order rules in a Makefile for execution โ Make figures out the order from the graph.
- Each prerequisite is built at most once per invocation, even if several targets share it.
- If a prerequisite can't be built (rule missing, command fails), Make stops and reports the error.
Why Header Dependencies Matter
Notice in the example that main.o depends on main.h and main.c. If you change a header that many files include, you must list it as a prerequisite of each object file, or Make won't know to recompile them. Hand-maintaining these lists is miserable โ Lesson 5 shows how compilers generate dependency lists automatically.
foo.c includes foo.h, but the Makefile only lists foo.c. Edit foo.h, run make โ nothing rebuilds, and you link stale objects. Symptom: weird runtime behavior after a header edit. Cure: automatic dependency generation (Lesson 5).
Phony Targets Revisited โ and .DEFAULT_GOAL
.PHONY targets never correspond to files, so Make always runs them. Two more graph tools you'll use:
.DEFAULT_GOAL := all # choose which target plain `make` builds
.PHONY: all clean test
all: app
clean:
rm -f app *.o
test: app
./app --test
Note that test lists app as a prerequisite โ so make test builds the binary first, then runs it. That's the idiom for "action that requires the build."
Debugging the Graph
Make gives you great tools for inspecting its decisions:
| Command | What it shows |
|---|---|
make -n | Print the commands it would run, without running them. |
make -d | Massive debug output โ every file considered, every timestamp compared. |
make --debug=b | Basic debugging: which targets are considered out of date and why. |
make -W file | Pretend file was just modified โ useful to see what a change would trigger. |
touch file; make | Actually bump a file's timestamp to force a rebuild chain. |
Limitations of the Timestamp Model
Timestamps are fast but dumb. Three situations to know about:
- Clock skew: if a source file's timestamp is in the future (bad NTP, timezone mishap,
git checkoutquirks), Make may never rebuild it. Fix:make cleanortouchthe sources. - Same-second edits: if you edit a source within the same second as its object was built, Make may see equal timestamps and skip the rebuild. Rare, but
make -Bforces the issue. - Content changes with old timestamps:
git checkout old-branchcan leave files with identical mtimes. Amake cleanis the reliable reset.
make clean and rebuild. It's the universal reset button โ costs a full build, but eliminates the entire class of timestamp confusion.
๐ง Knowledge Check
1. Make rebuilds a target when...
2. If app: main.o util.o, you edit main.c only, and both objects already exist โ what does make do?
3. You change util.h, which util.c includes โ but util.o's rule only lists util.c as a prerequisite. What happens?