Project Structure with One DAG¶
Splitting Make syntax across files does not require splitting the dependency graph. Included fragments are parsed into one Make process, allowing global prerequisite reasoning and scheduling while giving human readers clear ownership boundaries.
Page maps¶
graph LR
course["Deep Dive Make"] --> module["Parallel Safety and Project Structure"]
module --> page["Project Structure with One DAG"]
page --> proof["Architecture and scheduler evidence"]
flowchart TD
entry["top-level Makefile"] --> policy["policy/config includes"]
entry --> discovery["source/output mapping"]
entry --> rules["producer rules"]
entry --> public["public targets"]
discovery --> rules
policy --> rules
rules --> graph["one evaluated DAG"]
public --> graph
File boundaries help readers. Target and prerequisite edges remain the scheduling truth.
Organize by responsibility¶
A small durable layout:
| Owner | Responsibility | Should not own |
|---|---|---|
top-level Makefile |
supported public targets, default goal, include assembly | domain recipe details |
policy.mk |
documented defaults and supported configuration | hidden local correctness patches |
discovery.mk |
rooted membership and source-to-output mapping | artifact publication |
compile.mk |
compile producers and dependency inclusion | report targets |
reports.mk |
report producers and their owned outputs | public CI orchestration |
verification.mk |
selftest and audit entry points | alternate product graph |
The names should describe stable responsibility, not the order in which files were created.
Includes assemble one evaluated graph¶
include mk/policy.mk
include mk/discovery.mk
include mk/compile.mk
include mk/reports.mk
include mk/verification.mk
After parsing, Make reasons about rules from all included files together. A report target can depend on an object target owned in another fragment without creating a scheduler boundary.
Include order can affect variable assignments, conditionals, and generated syntax. Make the order intentional:
- policy defaults before rules that consume them;
- discovery before mappings that derive outputs;
- generated dependency files after base rules are known;
- optional local overrides at one documented precedence point;
- no circular include ownership.
“One DAG” does not mean include order is semantically irrelevant.
Keep public requests at the entry¶
The top-level file should let a reader find supported requests quickly:
.DEFAULT_GOAL := all
.PHONY: help test selftest
all: build/app build/report.json
test: all
./scripts/test_product.sh
selftest:
./scripts/test_build.sh
include mk/policy.mk
include mk/discovery.mk
include mk/compile.mk
include mk/reports.mk
include mk/verification.mk
If included fragments contribute prerequisites to a public target, keep that extension easy to discover and avoid multiple fragments redefining its recipe.
Map ownership through the graph¶
For one public target, create:
| Target or path | Declared owner | Inputs | Writes | Consumers |
|---|---|---|---|---|
Then draw the cross-fragment edges:
flowchart LR
all["all (Makefile)"] --> app["build/app (compile.mk)"]
all --> report["build/report.json (reports.mk)"]
app --> objects["objects (compile.mk)"]
report --> data["data/clean.csv (reports.mk)"]
objects --> generated["generated header (compile.mk)"]
A path has one producing owner even when several fragments refer to it. Multiple textual mentions are not multiple ownership.
Avoid catch-all fragments¶
Files such as misc.mk, common-rules.mk, or extras.mk tend to collect unrelated
targets. Prefer a domain or boundary that tells a reader:
- which outputs are owned;
- which public requests are extended;
- which variables form the interface;
- which other layer may depend on it.
When one fragment grows several unrelated responsibilities, split by ownership rather than by arbitrary file size.
Why recursive Make hides scheduling truth¶
Suppose:
The parent sees two targets. It does not see internal file edges in the child graphs. If frontend consumes a generated backend schema, the parent needs an explicit cross-boundary artifact contract. Otherwise the dependency remains private knowledge.
Costs include:
- the global scheduler cannot coordinate internal file targets;
- cross-directory dependencies become wrapper-target edges or hidden recipe reads;
- incremental decisions are split across processes;
- traces and databases show only one graph at a time;
- separate child output roots may still collide in shared parent locations;
- configuration and jobserver propagation need explicit care.
Use $(MAKE), not plain make, for genuine recursive boundaries so GNU Make can
propagate invocation flags and coordinate job tokens. That improves coordination but
does not merge the graphs.
Decide whether recursion is a real boundary¶
| Question | Favor one DAG | Favor explicit sub-build boundary |
|---|---|---|
| do components share file-level prerequisites | yes | no |
| should one scheduler see all producers | yes | no |
| are artifacts consumed directly across components | yes | only through versioned interface |
| can component build independently from declared inputs | not yet | yes |
| does component use another build tool or release lifecycle | not necessarily | often |
| can parent name exact input/output handoff | required either way | essential |
Directory layout alone is not a reason for recursion.
Treat a sub-build like a tool¶
When recursion is justified, define:
input paths and versions:
output paths and schema:
supported variables:
environment policy:
parallel/jobserver behavior:
failure and cleanup:
consumer acceptance:
The parent target should depend on the inputs that determine the sub-build result and publish an output or manifest it can reason about. “Run make over there” is not a file contract.
Local overrides need an interface¶
An optional include:
raises several questions:
- Is absence supported?
- Which variables may it set?
- Can command-line values override it?
- Do its values affect artifact meaning?
- How are semantic changes represented in freshness?
- Does CI run without it?
- Can it add private sources or rules?
Good local uses are ergonomic tool paths or documented developer modes. If an override changes compiler flags, source membership, or output meaning, record that state through the graph and proof evidence. An untracked override must not be the only reason a build is correct.
Keep outputs partitioned by owner¶
Readable includes do not prevent output collisions. Define durable roots:
build/objects/ compile.mk
build/generated/ generator owner
build/reports/ reports.mk
artifacts/tests/ verification.mk
Separate roots make ownership review easier, but the actual recipes must still obey the
partition. A helper that writes a global build/output.tmp violates it.
Review the capstone assembly¶
From the capstone directory:
Read the top-level Makefile, then mk/contract.mk, mk/common.mk, mk/objects.mk,
mk/macros.mk, and mk/stamps.mk. For one public target, locate:
- public declaration;
- prerequisite expansion;
- producing rule;
- output root;
- configuration inputs;
- audit or selftest route.
The architecture audit can support an ownership claim. It does not replace reading the actual cross-layer graph.
Architecture warning signs¶
- several fragments define recipes for the same public target;
- include order silently changes output ownership;
- recursive components read one another’s private outputs;
- local overrides add correctness rules unavailable to CI;
- one helper writes across several domain roots;
- public targets are scattered and undocumented;
- child builds receive plain
makewithout jobserver or variable intent; - cleaning one layer deletes another layer’s outputs.
Architecture review record¶
Public request:
Entry declaration:
Included owners:
Cross-layer edges:
Output ownership:
Configuration boundary:
Recursive boundaries:
Parallel collision review:
Verification route:
Decision and limit:
End-of-page checkpoint¶
Before leaving this page, you should be able to:
- explain how included fragments form one evaluated DAG;
- assign public interface, policy, discovery, producer, and verification ownership;
- map a public request across fragment boundaries;
- identify when recursion hides a real file dependency;
- define a justified sub-build through inputs, outputs, environment, and failure;
- review local overrides and output roots for parallel correctness.