Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/
  1. Loading — the Starlark front-end (src/starlark/) lexes, parses, and evaluates BUILD/.bzl files. load() pulls in .bzl modules (including the @builtin// prelude). The result is the unconfigured target graph: which targets exist and how they refer to each other by label.

  2. Configuration — applies the build configuration (platform, flags, select()) to produce the configured graph. Toolchains are resolved here.

  3. Analysis — runs each rule’s implementation function. This is where ctx.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.

  4. 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.

  5. 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/.