Project Structure with One DAG¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive Make"]
section["Parallel Safety Project Structure"]
page["Project Structure with One DAG"]
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"]
As builds grow, the temptation is to split the repository into little private worlds and let each subdirectory run its own Makefile. Module 02 argues for a different default:
keep one top-level DAG and layer the build so the graph stays visible.
A simple layering pattern¶
For this module, the healthy shape is:
Each layer has one job:
Makefileowns the public targetscommon.mkholds stable policy knobsobjects.mkmaps sources to outputsrules.mkpublishes artifacts
That is enough structure to scale without turning the build into a maze.
Why this layering helps humans¶
The layering is not only for the machine. It helps you answer different questions in different places:
- "What can I ask the build to do?" ->
Makefile - "Which knobs are stable policy?" ->
common.mk - "How do source files map to outputs?" ->
objects.mk - "Which recipes publish the artifacts?" ->
rules.mk
That separation lowers cognitive load without hiding the graph.
Why recursive make is not the default¶
If each directory runs its own private make process, you often lose global visibility:
- hidden cross-directory dependencies
- weaker scheduling decisions
- harder debugging because each sub-make sees only part of the truth
It also weakens review. If you look at one subdirectory Makefile in isolation, you may get the impression that the local file is the whole build story, when the real dependencies cross that boundary. One top-level DAG keeps those relationships explicit.
There are legitimate boundaries, but they should be treated as explicit tool invocations with named inputs and outputs, not as a casual way to avoid maintaining one graph.
Optional local overrides need discipline¶
Local overrides such as config.mk can be useful for ergonomics. They should not decide
whether the build is fundamentally correct.
If an override changes artifact meaning, that fact must still be modeled in the graph.
A healthy default for config.mk¶
Good uses:
- local tool paths
- debug convenience toggles
- machine-specific ergonomics
Bad uses:
- hidden semantic flags that change artifact meaning without graph evidence
- private source discovery rules that CI does not share
- local correctness patches that make one machine pass and another fail
The question is always the same: does this override change build meaning? If yes, the DAG must know.
A small architecture sketch¶
flowchart TD
makefile["Makefile"] --> common["mk/common.mk"]
makefile --> objects["mk/objects.mk"]
makefile --> rules["mk/rules.mk"]
objects --> rules
common --> rules
rules --> app["app and build artifacts"]
This picture is intentionally simple. It shows that layering can help review without splitting the build into separate invisible worlds.
End-of-page checkpoint¶
Before leaving this page, you should be able to explain:
- why one top-level DAG is the default architecture here
- what each
mk/*.mklayer is responsible for - why readability and correctness both get worse when sub-builds hide real dependencies
- what kinds of local overrides are acceptable and which ones must become graph evidence