Status: Accepted
Planning ends with an action graph: a list of process launches with their inputs, outputs, and ordering. Everything after that is a separate concern — who runs those processes.
That separation was not expressed anywhere. exec::run matched on an
Executor enum with two arms; one arm wrote build.ninja and started ninja,
the other walked the actions in process. Both reached into Plan directly,
so every fact a runner needed was reachable whether or not it was part of any
stated contract, and adding a third way to run a build meant editing the
match rather than adding a file.
Two things were wanted, and neither had a place to go.
make invocation, a distributed
execution service, or a tool of its own has no way indowel graph --kind=action
--format=json already printed an action graph, but it was a dump: an
unversioned convenience for reading, with no promise that it contained
enough to actually run a build, and a second JSON shape to keep in step
with whatever the executors happened to useThe second is the one that decides the design. A format that merely describes what a build would do rots, because nothing fails when a fact stops being written into it. A format that a real backend consumes cannot rot, because the build stops working.
The output stage is a layer. Between the planner and any runner sits one
neutral value, BuildGraph, and every backend consumes only that.
BuildGraph holds the build directory, an ordered list of steps, the
artifact of each target, and the default outputs. A step is one process
launch: program, arguments, inputs, outputs, an optional depfile, and the
steps that must complete firstTargetId, no Session, and no planner state. Targets
appear as their display labels — the same strings diagnostics useBackend is a trait: emit writes that backend’s own input files, run
executes them, available reports whether the environment has it, and
builds says whether it produces artifacts at all. Backends are listed in
one table; adding one is a new file and one rowFour backends ship:
| Name | What it does |
|---|---|
ninja |
writes build.ninja and runs ninja. The default where ninja exists |
direct |
runs the steps in process, sequentially, comparing mtimes and reading depfiles |
make |
writes Makefile and runs make |
graph |
writes build-graph.json and stops |
graph is the connection point for a backend that is not in this
repository. Its output is the serialization of BuildGraph itself, with a
format name and an integer version, and it can be parsed back into an
equal BuildGraph.
dowel graph --kind=action --format=json prints that same document. There is
one JSON description of an action graph, not two, and the one that exists is
the one the backends run on.
--executor is renamed to --backend. The old spelling is refused with a
message naming the new one rather than silently accepted, because the set of
values it takes has changed.
ninja, direct, and
make read BuildGraph and nothing else, so a fact missing from it is a
broken build, caught by the existing end-to-end tests, not a documentation
bug found later by whoever tried to write a backendbuild-graph.json (14-build-graph.md) and needs no
Rust, no linking against dowel, and no knowledge of manifestsmake is a real second generator, not a demonstration: it is what proves
the layer is not shaped around ninja. It also has limits ninja does not —
make cannot express a path containing whitespace, :, #, or % — and
the backend refuses such a build with a diagnostic naming the path instead
of writing a Makefile that silently builds the wrong thinggraph) is a state the commands must
handle. dowel build --backend=graph reports the file it wrote instead of
claiming artifacts exist, and dowel test refuses it outrightninja and direct and now holds for
make for freeversion is how it changes; a
reader that does not recognize the version is expected to refuse rather
than guess