The five layers
A build flows through five layers. Each is (conceptually) a pure function of the previous one, which is what makes the whole pipeline memoizable.
BUILD / .bzl files
│ Starlark evaluation
▼
Unconfigured target graph nodes = (rule, attrs, dep labels) — "what exists"
│ apply configuration (platform, flags, select())
▼
Configured target graph select() resolved, toolchains chosen
│ analysis: run each rule's implementation
▼
Action graph commands + declared inputs/outputs (the cache unit)
│ execution through the action cache + sandbox
▼
Outputs content-addressed files, materialized into zut-out/
-
Loading — the Starlark front-end (
src/starlark/) lexes, parses, and evaluatesBUILD/.bzlfiles.load()pulls in.bzlmodules (including the@builtin//prelude). The result is the unconfigured target graph: which targets exist and how they refer to each other by label. -
Configuration — applies the build configuration (platform, flags,
select()) to produce the configured graph. Toolchains are resolved here. -
Analysis — runs each rule’s
implementationfunction. This is wherectx.actions.run(...)registers actions and rules return providers. The output is the action graph: a DAG of commands, each with a declared input set and declared outputs. -
Execution — the executor runs actions whose results aren’t already cached, in parallel up to
--jobs, each inside the chosen sandbox tier. The result of each action is stored by content. -
Materialization — the requested target’s declared outputs are copied out of the content-addressed store into
./zut-out/.
Two kinds of memoization
zut layers two distinct caches, and the distinction matters:
-
DICE keys memoize loading, configuration, and analysis — the pure, in-process computation graph. These are versioned: when an input changes, DICE invalidates exactly the dependent keys (with early cutoff — if a recomputation produces the same value, dependents don’t recompute).
-
The action cache memoizes execution — keyed by the content hash of an action’s inputs, not by a DICE version. This is what survives across runs and across machines (via the remote cache), because a content hash is machine-independent.
The incremental engine lives in src/dice/; the executor and action-cache
orchestration in src/exec/.