Exercise Answers¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive Make"]
section["Build Graph Foundations Truth"]
page["Exercise Answers"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
Use this file after you have written your own answers. The value is comparison, not copying.
How to use the answer page well¶
Do not read a model answer first and then reshape your work to sound similar.
A better rhythm is:
- finish the exercise with your own files, traces, and notes
- write one plain-language explanation of the graph
- compare that explanation with the model answer
- revise where the model answer exposes a missing edge, a weak proof route, or vague ownership language
The best Module 01 answers usually do four things:
- they name the target or prerequisite fact directly
- they explain why the current graph is truthful or weak
- they point to one evidence route
- they describe the repair in terms of ownership and observable behavior
The best self-study packets also leave behind reviewable artifacts:
- one corrected graph sketch
- an update-decision and ownership ledger
- one hidden-input repair with relevant and irrelevant mutations
- one Make-versus-shell evaluation note
- one failure-safe publication drill
- one stale-output diagnosis made without cleaning
- one final graph whose completeness, minimality, recovery, and convergence are evidenced
If your answers rely only on "Make knows what changed," the reasoning is still too thin.
Exercise 1: Draw the graph¶
A defensible graph labels roles as well as paths:
flowchart TD
all["all\nphony requested goal"] --> app["app\ngenerated file"]
app --> maino["build/main.o\ngenerated file"]
app --> utilo["build/util.o\ngenerated file"]
maino --> mainc["src/main.c\nsource leaf"]
utilo --> utilc["src/util.c\nsource leaf"]
maino --> header["include/util.h\nsource leaf"]
utilo --> header
maino --> stamp["build/flags.<id>.stamp\ngenerated semantic evidence"]
utilo --> stamp
maino -.order only.-> directory["build/\ndirectory target"]
utilo -.order only.-> directory
The order-only edges ensure the directory exists but do not make objects stale whenever the directory timestamp changes. Header edges are normal because header content can change object meaning.
An update-decision table should reason from the requested goal inward:
| Starting state | build/main.o |
build/util.o |
app |
|---|---|---|---|
| no generated files | missing; compile | missing; compile | missing; link |
only src/util.c is newer |
up-to-date | stale; compile | stale after util.o; link |
| every normal prerequisite is older | up-to-date | up-to-date | up-to-date |
all is always considered because it is phony, but that does not force its file
prerequisites to rebuild. The most dangerous missing edge is usually the shared header
or semantic flags stamp: omitting either permits a semantically stale object to appear
current.
A common weak drawing is main -> util -> app. That is execution storytelling, not a
dependency graph: the arrows point the wrong way and the published targets disappear.
Exercise 2: Find a hidden input¶
An incomplete rule can build successfully and still be false:
After make CFLAGS=-O2 build/alpha.o, requesting
make CFLAGS=-O0 build/alpha.o can report the object up to date. The object file and
source path have not changed, so the graph has no evidence that compiler policy changed.
One repair makes the semantic value select a prerequisite path:
FLAGS_LINE := CC=$(CC) CFLAGS=$(CFLAGS)
FLAGS_ID := $(shell printf '%s' "$(FLAGS_LINE)" | cksum | awk '{print $$1}')
FLAGS_STAMP := build/compile-flags.$(FLAGS_ID).stamp
$(FLAGS_STAMP): | build/
@candidate="$@.candidate.$$$$"; \
printf '%s\n' "$(FLAGS_LINE)" > "$$candidate" && \
rm -f build/compile-flags.*.stamp && \
mv -f "$$candidate" "$@" || { rm -f "$$candidate"; exit 1; }
build/%.o: src/%.c $(FLAGS_STAMP) | build/
$(CC) $(CFLAGS) -c $< -o $@
Changing CFLAGS now changes FLAGS_ID, selects a missing stamp, and makes that stamp a
prerequisite of the requested object. A later identical request selects the existing
stamp and converges. The stamp content gives a reviewer the human-readable policy; the
checksum only gives it a path-safe identity.
Removing the previously active stamp is essential in this single-configuration build
directory. After -O2 -> -O0, an old -O2 stamp must not remain available: returning
to -O2 has to create fresh evidence newer than the -O0 object. If configurations must
build concurrently, give each one a separate output directory rather than sharing object
paths and deleting each other's active stamp.
The rejection test matters. After convergence, changing notes/reading.md should leave
make -q build/alpha.o at status 0 because the note has no edge to the object. Test
both -O2 -> -O0 and -O0 -> -O2; a solution that proves only the first transition has
not established reversible semantic tracking. A solution that rebuilds on every file
change avoids staleness by giving up minimality.
Running make clean after flag changes does not repair the graph. It destroys the state
needed to test whether the graph recognized the semantic change.
Exercise 3: Review rule ownership¶
The explicit version names membership and ownership together:
build/alpha.o: src/alpha.c | build/
$(CC) -c $< -o $@
build/beta.o: src/beta.c | build/
$(CC) -c $< -o $@
app: build/alpha.o build/beta.o
$(CC) $^ -o $@
The generalized version separates the mapping from membership:
OBJS := build/alpha.o build/beta.o
build/%.o: src/%.c | build/
$(CC) -c $< -o $@
app: $(OBJS)
$(CC) $^ -o $@
The pattern says how a requested matching object can be produced. OBJS says which
objects belong in app. The pattern does not scan the filesystem and silently decide
application membership.
Adding a second recipe for build/alpha.o should produce an overriding-recipe warning.
By contrast, these two lines are compatible because there is still only one writer:
Make combines their prerequisites. Ownership becomes confused only when competing recipe
bodies claim the same path. The ledger should therefore name the pattern rule as the
sole object writer and the app rule as the sole executable writer.
Exercise 4: Explain evaluation timing¶
This example exposes immediate versus deferred expansion and dollar ownership:
name = early
captured := $(name)
deferred = $(name)
name = late
$(info captured=$(captured))
$(info deferred=$(deferred))
$(info flavor=$(flavor deferred))
$(info origin=$(origin deferred))
$(info raw=$(value deferred))
.PHONY: inspect
inspect:
label=shell; printf '%s %s %s\n' '$(captured)' '$(deferred)' "$$label"
Without an override, Make prints captured=early and deferred=late; the shell receives:
Make owns $(captured) and $(deferred), so it expands them before launching the shell.
The doubled dollar survives Make as one dollar for the shell. $(value deferred) reports
the stored text $(name), while normal expansion follows that reference to late.
With make name=command inspect, the command-line assignment wins over ordinary
Makefile assignments; origin reports command line. The already captured value also
becomes command because the override is in force when parsing begins.
For a graph-shaping source list, := is usually the safer default because the set is
fixed when Make parses the declaration. The claim is about stable meaning, not speed.
Exercise 5: Prove safe publication¶
A failure-safe link rule uses a candidate name specific to the target and shell process:
app: build/main.o build/util.o
candidate=$@.candidate.$$$$; \
$(CC) $^ -o "$$candidate" && mv -f "$$candidate" $@ || { \
status=$$?; rm -f "$$candidate"; exit $$status; \
}
The rename is the publication commit point. Before it, readers still see either the old
valid app or no app; after it, they see the complete new one. The candidate is in the
same directory so the final rename can use the filesystem's atomic rename behavior.
A valid drill is:
before=$(cksum app)
# Introduce a compile or link failure.
if make app; then exit 1; fi
after=$(cksum app)
test "$before" = "$after"
test -z "$(find . -name '*.candidate.*' -print -quit)"
If no old target existed, replace the checksum assertion with test ! -e app. After
repair, the target should rebuild and a safely captured make -q app status should be
0.
For an object plus depfile, two renames cannot be one atomic transaction. Publishing the depfile first and object last is recovery-biased: a published new object will not be paired with older dependency information. The object rename remains the commit point.
Writing directly to $@, using one shared candidate name, or relying only on
.DELETE_ON_ERROR does not prove the same invariant.
Exercise 6: Write the smallest useful Makefile¶
A complete answer can stay small:
.DEFAULT_GOAL := all
.PHONY: all
all: message.upper.txt
message.upper.txt: message.txt
tr '[:lower:]' '[:upper:]' < $< > $@
The mutation ledger should look like this:
| State or mutation | Classification before request | Expected recipe |
|---|---|---|
| target absent | missing | build message.upper.txt |
| no change | up-to-date | none |
edit message.txt |
stale | rebuild message.upper.txt |
| edit unrelated note | up-to-date | none |
After the unrelated change, capture the query result with an if statement rather than
make -q ...; echo $? in a shell that may use set -e. Status 0 is the rejection
evidence. The edge message.upper.txt -> message.txt explains both the warranted rebuild
and the ignored note.
Exercise 7: Predict automatic variables¶
For the rule shown:
$@expands tobuild/report.txt$<expands totitle.txt$^expands totitle.txt results.txt
The shell command is:
The duplicate title.txt appears only once in $^. The order-only build/ prerequisite
is absent because $^ contains normal prerequisites. After reversing the two normal
prerequisite names, $< becomes results.txt and $^ begins with results.txt.
That order is observable because cat is order-sensitive. $< would be wrong because
the report's meaning comes from both normal prerequisites, not only the first. Automatic
variables abbreviate graph facts; they do not decide which facts belong in the graph.
Exercise 8: Remove duplicated compile recipes¶
A reusable rule is:
Before membership changes:
make build/gamma.o can bind % to gamma and use src/gamma.c. Merely adding that
source does not cause make app to request it. If it belongs in the program, declare:
The rejection observation is that make app remains a no-op after an unrequested
matching source appears. This is correct for an explicit membership policy. The pattern
answers “how can this object be built?”; OBJS answers “which objects belong in this
goal?”
Exercise 9: Diagnose a stale header build¶
The broken shape is:
If both sources include config.h, the second rule lies. Changing the header can leave
build/beta.o stale because the graph has no evidence connecting that object to the
header.
Compiler-generated dependency files scale the repair:
DEPFLAGS := -MMD -MP
OBJS := build/alpha.o build/beta.o
DEPS := $(OBJS:.o=.d)
build/%.o: src/%.c | build/
$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -MF $(@:.o=.d) -MT $@ -c $< -o $@
-include $(DEPS)
One generated file might contain:
On the first parse, the .d file is absent and -include tolerates that absence. The
successful compile emits it. On later parses, Make reads the stored header edge. -MP
adds a phony header stub so a removed header can be diagnosed through compilation rather
than failing immediately with “No rule to make target.”
After the repair, changing config.h must rebuild both objects. Before the repair,
build/beta.o was the stale artifact and its missing config.h edge was the cause.
Cleaning would delete the evidence of that false incremental decision.
For production code, publish the depfile and object through candidates as taught in Exercise 5; the short fragment above isolates dependency discovery.
Exercise 10: Design and defend a small graph¶
One valid design publishes each file through an owning rule:
.PHONY: all
all: summary.txt chart.svg
cleaned.csv: raw.csv
candidate=$@.candidate.$$$$; \
cp $< "$$candidate" && mv -f "$$candidate" $@ || { rm -f "$$candidate"; exit 1; }
summary.txt: cleaned.csv
candidate=$@.candidate.$$$$; \
printf 'summary from %s\n' "$<" > "$$candidate" && mv -f "$$candidate" $@ || { rm -f "$$candidate"; exit 1; }
chart.svg: cleaned.csv
candidate=$@.candidate.$$$$; \
printf '<svg><!-- data from %s --></svg>\n' "$<" > "$$candidate" && mv -f "$$candidate" $@ || { rm -f "$$candidate"; exit 1; }
The node and ownership review is:
| Path | Role | Sole writer | Content-deciding input |
|---|---|---|---|
raw.csv |
source leaf | outside the build | none inside the build |
cleaned.csv |
intermediate file target | cleaned.csv rule |
raw.csv |
summary.txt |
final file target | summary.txt rule |
cleaned.csv |
chart.svg |
final file target | chart.svg rule |
cleaned.csv |
all |
phony requested goal | no file writer | final target membership |
Expected evidence:
| Change | Must rebuild | Must not rebuild |
|---|---|---|
edit raw.csv |
all three file targets | none |
delete summary.txt |
summary.txt |
cleaned.csv, chart.svg |
delete chart.svg |
chart.svg |
cleaned.csv, summary.txt |
| edit unrelated note | none | all file targets |
| unchanged rerun | none | all file targets |
During the chart failure drill, the old chart.svg checksum must remain unchanged, or
the file must remain absent if it had never been valid. No *.candidate.* file should
remain. After repair, only the warranted downstream work should run and make -q all
should return 0.
The reviewer guide maps claims to artifacts:
- completeness: the graph and relevant-change traces
- minimality: delete-one-output and unrelated-note traces
- failure safety: checksum or absence assertion plus candidate search
- convergence: unchanged trace plus query status
The fan-out is honest because cleaned.csv has one writer and both consumers name it as
their content-deciding input. Neither consumer writes the shared intermediate.
What a mastery-level answer set looks like¶
A mastery-level submission does not just contain the right snippets. It shows that you can move comfortably between three levels:
- graph language:
- target, prerequisite, convenience target, ownership, staleness
- command evidence:
--trace, failure drills, repeated runs, before-and-after comparisons- plain-language explanation:
- why the build is or is not telling the truth
If your answers can move between those three levels without slipping into folklore, you are learning Module 01 in the right direction.