Worked Example: Hardening an Inherited Build¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive Make"]
section["Portability Hermeticity Failure Modes"]
page["Worked Example: Hardening an Inherited Build"]
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"]
This example hardens one build without inventing defects that the evidence does not show. It uses the course specimens for mechanisms, then proves each claim again in the inherited project.
Begin with two evidence bundles¶
Run:
gmake -C programs/reproducible-research/deep-dive-make \
capstone-environment-contract-audit
gmake -C programs/reproducible-research/deep-dive-make/capstone \
semantic-fault-audit
The environment bundle provides paired cases for changed non-file state, recursive planning visibility, and required-tool rejection. The semantic bundle provides the clean-workspace clock comparison and grouped-publication cases.
PASS means each specimen matched its expected observation, including broken specimens
whose defect was reproduced. Read the finding name, trace, and output state.
The inherited build¶
SHELL := /bin/sh
MODE ?= release
PYTHON ?= python3
ENV_MANIFEST := build/environment.manifest
.PHONY: all vendor
all: vendor build/app build/app.tar.gz
vendor:
gmake -C vendor/lib all
$(ENV_MANIFEST): | build/
@printf 'MODE=%s\nBUILD_TIME=%s\n' \
'$(MODE)' "$$(date +%s)" > $@
generated/api.h generated/api.json: schema.yml tools/gen-api.py
$(PYTHON) tools/gen-api.py
build/app: source.txt generated/api.h $(ENV_MANIFEST) | build/
$(PYTHON) tools/render-app.py \
--source source.txt --environment $(ENV_MANIFEST) --output $@
build/app.tar.gz: build/app
tar -czf $@ $<
The complaints are:
- an older GNU Make reports unclear syntax after grouped targets are proposed
gmake -n vendordoes not reveal the child plan- changing
MODEafter the first build leaves the existing manifest unchanged - independent clean workspaces produce different manifests because of
BUILD_TIME -jsometimes invokes the API generator twice- the team wants to replace Make because the build is "slow"
- release promotion is being added to the packaging recipe
Each complaint belongs to a different claim.
Record observations without overclaiming¶
Run file-target queries, not only the phony aggregate:
gmake all
gmake -q build/app build/app.tar.gz
gmake -n vendor
gmake -j4 --trace generated/api.h generated/api.json
all and vendor are phony, so gmake -q all returning 1 would be expected and would
not prove artifact non-convergence. Query the file targets whose settled state matters.
Create a ledger:
| Observation | Supported conclusion | Not yet proven |
|---|---|---|
child plan absent under -n |
recursion is undeclared to planning | jobserver was lost on this host |
manifest ignores later MODE |
no evaluation trigger for non-file state | every environment fact belongs in the manifest |
| clean-workspace manifest hashes differ | clock is hidden artifact input | no-op build necessarily rebuilds |
| generator trace contains two starts | output ownership is independent | Make cannot model grouped output |
| one full build is slow | a latency symptom exists | which layer dominates |
Declare runtime capabilities and tools¶
The repaired build requires grouped targets and secondary expansion. Gate advertised features rather than guessing version prefixes:
ifeq ($(origin MAKE_VERSION),undefined)
$(error GNU Make is required)
endif
REQUIRED_MAKE_FEATURES := grouped-target second-expansion
MISSING_MAKE_FEATURES := $(filter-out $(.FEATURES),$(REQUIRED_MAKE_FEATURES))
ifneq ($(strip $(MISSING_MAKE_FEATURES)),)
$(error GNU Make $(MAKE_VERSION) lacks: $(MISSING_MAKE_FEATURES))
endif
SHELL := /bin/sh
.SHELLFLAGS := -eu -c
export LC_ALL := C
PYTHON ?= python3
ifneq ($(words $(PYTHON)),1)
$(error PYTHON must name one executable path)
endif
.PHONY: contract-check
contract-check:
@command -v "$(PYTHON)" >/dev/null 2>&1 || { \
printf 'contract: missing %s\n' "$(PYTHON)" >&2; \
exit 2; \
}
Test missing and available tool cases. Finding an executable proves presence, not version compatibility; add a behavioral or numeric policy if the renderer needs a specific Python feature.
Declare the recursive boundary¶
Replace:
with:
Now prove two claims separately:
gmake -n vendorenters the child plan without creating child outputs- an actual
gmake -j2 vendorrecords a child exactly oneMAKELEVELdeeper and measures peak recipe overlap within the shared budget
Do not infer token conservation from one MAKEFLAGS string.
Replace event metadata with semantic state¶
BUILD_TIME describes when an event happened; it does not describe application meaning.
Keep it in a run log if operators need it. The artifact-driving manifest contains
validated semantic facts:
VALID_MODES := release debug
ifneq ($(words $(MODE)),1)
$(error MODE must be exactly one of: $(VALID_MODES))
endif
ifeq ($(filter $(MODE),$(VALID_MODES)),)
$(error MODE must be one of: $(VALID_MODES))
endif
MODE_ROOT := build/$(MODE)
ENV_MANIFEST := $(MODE_ROOT)/environment.manifest
APP := $(MODE_ROOT)/app
.PHONY: FORCE
FORCE:
$(ENV_MANIFEST): FORCE | $(MODE_ROOT)/
@set -eu; \
candidate="$@.candidate.$$$$"; \
trap 'rm -f "$$candidate"' EXIT HUP INT TERM; \
{ \
printf 'MODE=%s\n' '$(MODE)'; \
printf 'PYTHON=%s\n' "$$("$(PYTHON)" --version 2>&1)"; \
printf 'LC_ALL=%s\n' '$(LC_ALL)'; \
} > "$$candidate"; \
if test -r "$@" && cmp -s "$$candidate" "$@"; then \
:; \
else \
mv "$$candidate" "$@"; \
fi
This design:
- reevaluates non-file facts on every invocation
- keeps equal manifest identity stable
- publishes complete content after interruption-safe candidate generation
- gives debug and release different path identities
Two clean workspaces with the same declared facts must produce equal manifest hashes.
Repeated equal-state invocation must preserve hash and modification time. Changing MODE
must publish in a different namespace.
Give the API generator one owner¶
generated/api.h generated/api.json &: schema.yml tools/gen-api.py | contract-check
"$(PYTHON)" tools/gen-api.py
test -s generated/api.h
test -s generated/api.json
The grouped feature gate appears before this syntax is selected. Under -j4, trace and an
invocation log must show one generation event. Delete one group member and prove one
repairing invocation.
Measure before redesigning¶
Define three workloads:
| Workload | Goal state |
|---|---|
| clean | no governed build outputs |
| no-op | selected mode completely settled |
| changed input | one declared source changed reproducibly |
For each, run repeated measurements with the same job count, tools, locale, and output capture. Measure planning separately from real execution and keep trace capture out of the timing comparison.
If no-op gmake -n and no-op real build are both expensive, investigate parsing and graph
decision with one controlled experiment such as -rR in a copy. If planning is cheap and
the changed-input build is slow, profile the selected renderer or archive tool. Either
result is more useful than a rewrite based on one timing.
Keep packaging and promotion separate¶
Building build/release.tar.gz from verified files is a file transaction Make can own.
Promotion to a remote environment adds durable authorization, retry, status, and rollback
state.
The handoff is:
| Make publishes | Deployment system owns | Receipt returned |
|---|---|---|
| immutable archive digest, checksums, software bill of materials | environment selection, approval, retry, rollback | signed status and remote revision |
Do not represent remote success with a local deployed.stamp. A convenience target may
submit the immutable digest, but the deployment system remains authoritative.
Repaired build shape¶
SHELL := /bin/sh
.SHELLFLAGS := -eu -c
ifeq ($(origin MAKE_VERSION),undefined)
$(error GNU Make is required)
endif
REQUIRED_MAKE_FEATURES := grouped-target second-expansion
MISSING_MAKE_FEATURES := $(filter-out $(.FEATURES),$(REQUIRED_MAKE_FEATURES))
ifneq ($(strip $(MISSING_MAKE_FEATURES)),)
$(error GNU Make lacks: $(MISSING_MAKE_FEATURES))
endif
MODE ?= release
VALID_MODES := release debug
ifneq ($(words $(MODE)),1)
$(error MODE must be exactly one of: $(VALID_MODES))
endif
ifeq ($(filter $(MODE),$(VALID_MODES)),)
$(error MODE must be one of: $(VALID_MODES))
endif
PYTHON ?= python3
ifneq ($(words $(PYTHON)),1)
$(error PYTHON must name one executable path)
endif
export LC_ALL := C
MODE_ROOT := build/$(MODE)
ENV_MANIFEST := $(MODE_ROOT)/environment.manifest
APP := $(MODE_ROOT)/app
BUNDLE := $(MODE_ROOT)/app.tar.gz
.PHONY: all contract-check FORCE vendor
FORCE:
all: vendor $(BUNDLE)
contract-check:
@command -v "$(PYTHON)" >/dev/null 2>&1 || { \
printf 'contract: missing %s\n' "$(PYTHON)" >&2; exit 2; \
}
vendor: | contract-check
+$(MAKE) -C vendor/lib all
$(ENV_MANIFEST): FORCE | $(MODE_ROOT)/
@set -eu; \
candidate="$@.candidate.$$$$"; \
trap 'rm -f "$$candidate"' EXIT HUP INT TERM; \
printf 'MODE=%s\n' '$(MODE)' > "$$candidate"; \
if test -r "$@" && cmp -s "$$candidate" "$@"; then :; \
else mv "$$candidate" "$@"; fi
generated/api.h generated/api.json &: schema.yml tools/gen-api.py | contract-check generated/
"$(PYTHON)" tools/gen-api.py
test -s generated/api.h
test -s generated/api.json
$(APP): source.txt generated/api.h $(ENV_MANIFEST) | contract-check
"$(PYTHON)" tools/render-app.py \
--source source.txt --environment $(ENV_MANIFEST) --output $@
$(BUNDLE): $(APP) | contract-check
tar -czf $@ $<
$(MODE_ROOT)/:
mkdir -p $@
generated/:
mkdir -p $@
Archive metadata normalization is deliberately not claimed here. Module 08 defines that release contract. This sketch shows the corrected Module 05 boundaries without pretending to prove portable archive bytes.
Acceptance matrix¶
| Check | Required observation |
|---|---|
unavailable PYTHON |
deliberate failure before artifact publication |
available PYTHON |
same gate accepts and build proceeds |
gmake -n vendor |
child plan visible, no child output |
bounded gmake -j2 vendor workload |
child one level deeper; peak overlap no greater than two |
| two equal clean workspaces | equal environment-manifest hashes |
| repeated equal mode | manifest identity unchanged; file goals query current |
| release and debug in parallel | distinct namespaces with no shared final writer |
both API members requested under -j4 |
one generator invocation |
| clean/no-op/changed-input timings | repeated, labeled, and paired with correctness checks |
| promotion failure | no false local success stamp; durable external status remains queryable |
Incident conclusion¶
The evidence supports this account:
The build had an undeclared feature and tool contract, recursion hidden from planning, stale and clock-derived environment evidence, independent owners for coupled outputs, an unclassified performance complaint, and remote workflow state entering a file build. We gated advertised capabilities, declared recursion, made semantic manifests convergent and configuration-specific, grouped output ownership, measured distinct workloads, and defined an immutable deployment handoff.
Another learner should be able to reconstruct every sentence from the paired specimen bundle and the project-specific acceptance matrix without instructor narration.