Exercise Answers¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive Make"]
section["Rule Semantics Precedence Edge Cases"]
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 these as reasoning models after completing your own evidence packet. Exact output varies with tool versions and file times; the semantic conclusions should not.
Exercise 1: Choose the right CLI probe¶
A useful harness has one parse effect and two recipe classes:
$(info parsed)
PARSE_RESULT := $(shell printf 'parse\n' >> parse-events.log)
.PHONY: inspect child
inspect:
@printf 'ordinary\n' >> recipe-events.log
+$(MAKE) --no-print-directory child
child:
@printf 'recursive\n' >> recipe-events.log
The expected boundary matrix is:
| Mode | parse log | ordinary recipe | recursive Make line | Primary meaning |
|---|---|---|---|---|
-n |
changes | printed, not run | runs under recursive option propagation | preview recipes, not parsing |
-q |
changes | not run | recursive line may still be processed as recursive Make | query freshness |
-t |
changes | replaced by target touching where applicable | runs under recursive option propagation | mark targets current |
The exact child outcome depends on whether the child goal is already current, so record its exit status and state instead of claiming every recursive recipe writes. The durable conclusion is that parse-time functions run before these modes suppress or replace ordinary recipes, and recursive Make recipe lines receive special treatment.
The repair removes the mutating top-level shell call. If the event log is a required artifact, give it an explicit file target with declared inputs. The acceptance assertion is that a before/after inventory around each diagnostic mode contains no unexpected file change.
Exercise 2: Prove where a variable value came from¶
A child probe should expose every channel:
.PHONY: show
show:
@printf 'level=%s origin=%s flavor=%s raw=%s value=%s env=%s overrides=%s\n' \
'$(MAKELEVEL)' '$(origin CFLAGS)' '$(flavor CFLAGS)' \
'$(value CFLAGS)' '$(CFLAGS)' "$${CFLAGS-unset}" '$(MAKEOVERRIDES)'
The supported strongest-to-weakest order is override, command line, ordinary file,
environment, built-in default. -e promotes the environment above an ordinary file
assignment, but not above command line or override.
| Parent channel | Child origin | Why |
|---|---|---|
| unexported file assignment | undefined |
Make state is not automatically process state |
| exported file assignment | environment |
child imports the recipe environment |
| parent command-line assignment | command line |
recursive Make forwards it through its protocol |
explicit $(MAKE) CFLAGS=value |
command line |
child invocation defines it directly |
parent override only |
undefined unless exported or forwarded |
precedence inside parent is not transmission |
This rejects the claim that "visible in the child" implies export. The origins distinguish environment import from recursive command-line forwarding.
Exercise 3: Make generated include publication recoverable¶
Direct redirection can leave a truncated file that Make will parse on the next invocation. A recoverable owner writes and validates a process-local candidate:
mk/generated-config.mk: config/mode.txt tools/render-config
@set -eu; \
candidate="$@.candidate.$$$$"; \
trap 'rm -f "$$candidate"' EXIT HUP INT TERM; \
tools/render-config $< > "$$candidate"; \
grep -Eq '^MODE := (release|debug)$$' "$$candidate"; \
if test -r "$@" && cmp -s "$$candidate" "$@"; then \
:; \
else \
mv "$$candidate" "$@"; \
fi
On bootstrap, the first parse reports restart 0; after the rule publishes the include,
the restarted parse reports 1. A separate settled invocation reports 0, because
MAKE_RESTARTS counts restarts in the current process rather than historical restarts.
The proof must also show that the settled digest and modification time remain unchanged.
Interruption leaves the last valid final file intact and the trap removes the candidate.
gmake -n must not create either path.
Exercise 4: Replace a platform branch with a capability gate¶
A complete answer separates three variables:
ARCHIVE_MODE ?= auto
VALID_ARCHIVE_MODES := auto enabled disabled
ifneq ($(words $(ARCHIVE_MODE)),1)
$(error ARCHIVE_MODE must be exactly one of: $(VALID_ARCHIVE_MODES))
endif
ifeq ($(filter $(ARCHIVE_MODE),$(VALID_ARCHIVE_MODES)),)
$(error ARCHIVE_MODE must be one of: $(VALID_ARCHIVE_MODES))
endif
ARCHIVE_PROBE ?= tar --help 2>/dev/null | grep -q -- '--sort'
HAVE_SORTED_ARCHIVE := $(if $(shell $(ARCHIVE_PROBE) && printf yes),yes,no)
ifeq ($(ARCHIVE_MODE),enabled)
ifneq ($(HAVE_SORTED_ARCHIVE),yes)
$(error deterministic archive ordering was requested but is unavailable)
endif
ARCHIVE_ENGINE := sorted
else ifeq ($(ARCHIVE_MODE),disabled)
ARCHIVE_ENGINE := unsorted
else ifeq ($(HAVE_SORTED_ARCHIVE),yes)
ARCHIVE_ENGINE := sorted
else
ARCHIVE_ENGINE := unsorted
endif
ARCHIVE_MODE is caller policy, HAVE_SORTED_ARCHIVE is discovery, and
ARCHIVE_ENGINE is the decision. Test ARCHIVE_PROBE=true and false against all three
valid modes. Test empty, maybe, and auto enabled as rejected inputs.
If sorted versus unsorted entry order changes artifact bytes, the two paths are not
equivalent. In that contract, auto must either select a canonical alternate
implementation or fail; quietly producing host-dependent archives is not a fallback.
Exercise 5: Repair a multi-output generator honestly¶
Grouped targets express the real owner:
ifeq ($(filter grouped-target,$(.FEATURES)),)
$(error grouped-target support is required)
endif
api.h api.json &: gen_api.py schema.yml
python3 gen_api.py
test -s api.h
test -s api.json
A parallel trace should show one invocation. After settling, gmake -q api.h api.json
returns 0; deleting either member triggers one grouped regeneration.
A stamp can model "the generator completed and both members passed validation," but it does not automatically repair a missing member while the stamp remains. A valid compatibility design must make consumers depend on the stamp as the publication event and define a missing-member rejection or recovery route. Listing missing output targets as recipe-less dependents of an existing stamp does not recreate them.
Exercise 6: Compare assignment flavors¶
One revealing harness is:
BASE = first
RECURSIVE = $(BASE)
SIMPLE := $(BASE)
BASE = second
OPTIONAL ?= fallback
EMPTY :=
EMPTY ?= ignored
LIST := alpha
LIST += beta
SHELL_VALUE != printf observed
RECURSIVE stores $(BASE) and expands to second; SIMPLE captured first.
EMPTY remains empty because ?= tests definedness, not non-emptiness. += preserves the
existing variable's flavor. != invokes the shell while Make parses the assignment, even
under -n.
Printing value distinguishes stored expression from eventual expansion. The repair for a
mutating or environment-sensitive != is an explicit file target whose output becomes a
declared input; changing != to := $(shell ...) does not move the observation out of
parsing.
Exercise 7: Prove target-specific scope propagation¶
With:
shared inherits the target-specific value from the top-level target that first causes it
to be built. Requesting debug release versus release debug can therefore change the
context of a once-built prerequisite.
private prevents inheritance:
It does not create a debug variant of shared. If the bytes genuinely depend on mode, the
graph needs distinct identities such as build/debug/shared and
build/release/shared. Export changes process visibility, and recursive forwarding
changes child-Make precedence; neither repairs one path representing two artifacts.
Exercise 8: Audit parse effects across include restarts¶
The inventory should classify:
| Construct | Evaluation boundary | Restart behavior |
|---|---|---|
top-level $(shell ...) inside := |
parse | repeats after a makefile remake |
top-level $(file ...) |
parse | repeats and mutates in diagnostic modes |
!= assignment |
parse | repeats after a makefile remake |
| ordinary recipe command | target update | does not run merely because parsing restarted |
| generated-include recipe | makefile-remake boundary | may cause one restart |
A sound repair removes every mutation from the first three rows. Guarding one with
MAKE_RESTARTS still permits mutation during the initial parse and diagnostic modes.
After repair, bootstrap has parse counts 0 then 1, a settled invocation has only 0,
and -n leaves the filesystem unchanged.
Exercise 9: Prove rule and prerequisite selection¶
The prediction and observation table should converge on:
| Model | Requested target | Selected prerequisite | Published content |
|---|---|---|---|
| generated rule first | build/a.choice |
generated/a.src |
selected=generated |
| root rule first | build/a.choice |
root/a.src |
selected=root |
| namespaced targets | two explicit paths | one prerequisite per namespace | both selections |
The two ambiguous Makefiles differ only in which of these candidate rules appears first:
build/%.choice: generated/%.src
printf 'selected=%s\n' "$$(cat $<)" > $@
build/%.choice: root/%.src
printf 'selected=%s\n' "$$(cat $<)" > $@
Reversing the rules reverses the content of build/a.choice. The trace should be used to
show both the selected recipe location and the prerequisite bound to $<. The output then
shows the semantic consequence of that selection.
The control separates the namespaces:
build/generated/%.choice: generated/%.src
printf 'selected=%s\n' "$$(cat $<)" > $@
build/root/%.choice: root/%.src
printf 'selected=%s\n' "$$(cat $<)" > $@
The resulting paths are:
They can coexist because each path has one reviewable source domain. An explicit rule would be better if only one exceptional target existed; a static pattern rule would be appropriate for a known finite target set.
The review rule should express the contract rather than outlawing syntax:
Reject overlapping patterns when a change in Makefile text order can change the meaning of one target path; keep patterns when the target namespace makes source ownership unambiguous.
Weak answer pattern:
- "The first rule wins, so put the preferred rule first"
That preserves hidden policy. It does not make a future edit safe or make target ownership visible.
The secondary-expansion half should show:
Without escaping, $@ is empty during initial expansion and gmake -np report shows the
computed inputs are absent. With escaping, target context exists during secondary
expansion and the database shows both prerequisites. Rule selection decides which recipe
owns a target; secondary expansion decides target-context prerequisite text. They are
different graph-construction boundaries.
Exercise 10: Diagnose a compound semantic incident¶
A strong report uses one evidence route per boundary:
| Defect | Falsifiable evidence | Repair |
|---|---|---|
| parse mutation | before/after inventory around -n |
move effect to explicit target |
| provenance disagreement | parent/child origin and forwarding state | choose export or command-line contract |
| target-context expansion loss | bounded database entry | escape for secondary expansion or write explicit edge |
| duplicate output ownership | parallel trace and invocation log | grouped publication owner |
Reject the decoy with evidence—for example, prove that shell choice does not explain a
prerequisite missing from Make's database. Then rerun the original probes, not only the
happy build. Completion requires: no dry-run mutation, one grouped generator invocation,
the intended child origin, restored prerequisite edges, and query status 0 after a
settled build.
Review standard¶
A complete model answer says when an observation occurred, what single controlled change distinguished the cause, which competing explanation was rejected, and how the same proof changed after repair. Feature names and successful commands without that reasoning are not enough.