> Documentation version: main. [HTML page](https://lukewilliamboswell.github.io/roc-blueprint/builds.html)

<a id="build-artifacts-and-run-workflows"></a>

# Build artifacts and run workflows

A ` Task ` runs a repository-owned command in its environment. Use a ` Build `
when the command produces an artifact whose inputs, dependencies, execution
capabilities and output should be managed explicitly. Keep compilation and
packaging logic in the script; Blueprint supplies the declared inputs and asks
the executor to obtain the result. A ` Workflow ` orders tasks and builds.
The shipped sandboxed executor uses Nix on x86\_64 Linux.

<a id="declare-a-build"></a>

## Declare a build

Inside ` config `, with a Nix package source named ` nix `,
` scripts/build.roc ` in the project and the ` build-scripts ` environment of
[Lock the Roc packages scripts use](configuration.md#what-the-lock-pins):

``` roc
Source("assets", "path:./assets"),
Executor("native", Nix("nix")),
Build("app", [
	Execute("native"),
	Use("build-scripts"),
	Inputs(["assets"]),
	Run(["roc-stable", "scripts/build.roc"]),
	Output("dist/app"),
]),
```

` Executor("native", Nix("nix")) ` declares an executor named ` native ` that
builds with Nix and takes its bootstrap package set from the source ` nix `;
` Execute("native") ` selects it. A ` Source ` belongs to Nix without a
` Resolver `.

` Run ` is any argv; nothing about a build requires Roc. Roc takes the first
` -- ` of its arguments for itself, so pass a script its own arguments after
one: ` Run(["roc-stable", "scripts/build.roc", "--", "--release"]) `.

<a id="what-a-build-sees-and-produces"></a>

## What a build sees and produces

Run ` blueprint update `, then ` blueprint build app `. The printed store path is
resolved by Nix and contains exactly the declared file or directory. Missing
outputs fail. ` Needs(["library"]) ` adds build dependencies; cycles fail during
configuration validation. Sources live under ` $BLUEPRINT_INPUTS/<name> ` and
needed artifacts under ` $BLUEPRINT_ARTIFACTS/<name> `, both read-only and separate
from the writable project copy.

Each build snapshots current project files, including untracked/task-generated
files, excluding VCS metadata, caller-generated roots, authority and all local
input trees. Blueprint makes that copy itself, as
[Select a build’s project files](#select-a-builds-project-files)
describes, and a file is executable in a build only when its owner may execute
it. Changing a locked local source requires explicit update. Initial
source/output policy rejects symlinks and special files. Only local x86\_64 Linux
sandboxed execution is verified; tasks/config compilation are not sandboxed.
A build’s ` PATH ` holds exactly the tools its environment takes from its
package sources, and nothing of the machine: no ` sh `, ` cp `, ` mkdir ` or
` python3 ` unless the environment’s ` Tools ` name ` bash `, ` coreutils ` or
` python3 `. A command that is not there fails with ` build command not found `,
or inside the build’s own script when that script calls one. A build does not
see the variables Nix’s ` stdenv ` exports either. A tool may give what it runs
more than the build was given: the Roc compiler’s package puts coreutils and
a C compiler on the ` PATH ` of a script it runs. Expected host tools cannot be supplied inside this sandbox. Tool origins are
checked against the selected executor’s capabilities before effects; support
for a fetched program must be established by that executor rather than inferred
from a task’s ability to run it.
A versioned standalone runner executes builds using only the locked package
set. Its bytes are independent of the Blueprint CLI executable. Caller namespace
identities are mounted as an authorization witness at execution time, outside
the derivation; they do not change the derivation identity. Both mount and
network namespaces must differ from the caller before the runner creates a
workspace or starts the command. A missing mount, disabled sandbox, invalid
witness or shared namespace fails closed. Builds run locally on x86\_64 Linux;
remote Nix builders are disabled for this isolation contract.
See the [complete runnable example](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/examples/artifacts/README.md),
[real build fixture](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/fixtures/builds/README.md).

<a id="original-files-with-published-hashes"></a>

## Original files with published hashes

` SourceFile(name, url, sha256) ` declares an original upstream file, such as a
release archive whose publisher supplies a flat SHA-256. The hash is exactly
64 lowercase hexadecimal characters. The URL follows the same public HTTPS
rules as fetched programs. Use ` Inputs([name]) ` on a build: the unchanged file
is available at ` $BLUEPRINT_INPUTS/<name> `. An archive is never unpacked
implicitly; name the unpacker in the environment’s ` Tools ` and run it in the
producer script when needed. ` Source(name, ref) ` continues to supply a locked source
tree, including archives that Nix unpacks and pins by their NAR hash.

` blueprint update ` fetches and verifies the original bytes against the declared
published hash before committing a pin. ` blueprint update NAME ` can select the
file alone. The authority is a ` sources ` entry in ` Blueprint.lock ` with
` provider = "file" `, the original URL in ` ref `, and its flat
` sha256:<hex> ` in ` digest `. There is no additional source lock. URL, name,
hash, addition or removal changes require explicit update.

Ordinary builds and generation use the committed digest. They hash cached
bytes on every use, fetch a missing file from its locked URL, and repair a
corrupt cache by fetching and verifying it again. Wrong upstream bytes fail;
ordinary fetches never change the lock. A verified cached file works offline.
The source enters Nix as a locked path: Blueprint measures the verified
cached file with ` nix hash path `, and the flake reads it with
` builtins.fetchTree `, which checks that NAR hash, then checks the flat
SHA-256 again. Its store identity is independent of the caller’s cache
location, and the recipe sees the original archive bytes. Unrelated files
that no selected build uses are not fetched.

` blueprint lock show --json NAME ` reports ` digestKind: "file" ` and the original
` flatSha256 ` separately from tree identities, with empty ` metadata: {} `. A NAR
hash is not a publisher’s hash of an archive; do not compare them as if they
were the same digest. Build provenance includes the selected file source’s
name, original URL and flat digest alongside the other locked sources used.

<a id="order-tasks-and-builds-in-a-workflow"></a>

## Order tasks and builds in a workflow

Inside ` config `, referring to existing tasks and builds:

``` roc
Workflow("ci", [RunTask("check", ["--verbose"]), BuildArtifact("app"), RunWorkflow("verify")]),
Workflow("verify", [RunTask("version", [])]),
```

` blueprint workflow ci ` plans the complete dependency/capability/layout/lock
closure before executing any task. Steps run in order and stop on the first
failure. Repeated explicit tasks and builds repeat; every build snapshots current
project files and rebuilds its dependency graph from that snapshot. Nix may reuse
unchanged inputs, but Blueprint never caches artifact results across tasks.
Locked sources are verified again; task edits to them require explicit update.
Cycles and excessive depth/expansion fail during configuration validation.
See the [real workflow fixture](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/fixtures/workflows/README.md).

<a id="select-a-builds-project-files"></a>

## Select a build’s project files

` Files(["scripts/producer.sh", "dependencies/shared"]) ` on a ` Build ` selects
only those project files and directory trees for that build. Paths are relative
to the project root; absolute paths, ` . ` and ` .. ` components are refused.
A selected path must exist. The usual generated-root, authority, VCS and locked
local-source exclusions still apply. Omit ` Files ` to retain the whole-project
snapshot; ` Files([]) ` gives the command an empty writable workspace.

Blueprint applies the selection itself, once for each build it prepares. It
copies the selected files into ` snapshots/<build> ` in the workspace, emptied
first, keeping each name’s bytes and whether the owner may execute each file,
and refuses a symlink or special file among them; excluded entries are never
inspected. The Nix executor measures that copy with ` nix hash path ` and
records where it is and its NAR hash in ` project-<build>.json ` beside the
flake. The flake reads the copy with ` builtins.fetchTree `, locked to that
hash, so evaluation is pure and the flake never reads the project itself.
Nix has the copy in its store once the build is prepared, so Blueprint
removes it when the build ends, however it ends, and removes any copy an
interrupted run left before it stages anything.

Give each component its own single-output build with its own ` Files ` list.
The builds share their locked package sets and any identical selected trees in
the Nix store; an unrelated application edit does not change their snapshots.

<a id="reach-a-tools-libraries"></a>

## Reach a tool’s libraries

` BLUEPRINT_TOOLS ` is a read-only directory of complete package roots. A plain
or configured tool ` nix#glibc ` is at ` $BLUEPRINT_TOOLS/nix#glibc `, so a producer
can use ` $BLUEPRINT_TOOLS/nix#glibc/lib/ld-linux-x86-64.so.2 ` without guessing a
store path. A ` Command("cc", "nix#clang") ` exposes its underlying package root
at ` $BLUEPRINT_TOOLS/cc `. These paths do not add tools to ` PATH `, enable
network access or opt into ` stdenv ` setup. Quote paths as usual in a shell.

<a id="check-reproducibility-and-record-provenance"></a>

## Check reproducibility and record provenance

` blueprint build NAME --rebuild ` asks Nix to rebuild the existing derivation
and compare its output. Nix fails when the output differs. Build it once
before asking for this check; a missing prior output is not a reproducibility
comparison. Run this for each component in CI.

` blueprint build NAME --json ` prints one typed result as JSON and keeps progress
on stderr. Both flags may be combined. Build result schema version 2 contains:

- ` schema_version `: ` 2 `.


- ` name ` and ` system `: requested build and execution system.


- ` provider `: selected executor provider, currently ` "nix" `.


- ` build_identity `: opaque identity of the fully prepared build; compare it
  together with ` provider `.

- ` metadata `: typed provider-namespaced details. Nix exposes its exact
  derivation under ` metadata.nix.derivation `.

- ` outputs `: returned output names and locations.


- ` sources `: selected authoritative source entries for the build closure.


- ` snapshot_digest `: the digest of the captured input snapshot the build
  used, the same value ` plan ` and ` explain ` report. The result does not
  contain the snapshot itself; ` plan ` and ` explain ` print it with ` --json `,
  and the local build record keeps it.

Reject unsupported schema versions and tolerate additive fields. Output
locations are not general content checksums. The provider owns cache reuse;
a successful build alone does not prove it was a cache hit.

<a id="plan-a-build-and-explain-changes"></a>

## Plan a build and explain changes

` blueprint plan build NAME ` shows the selected files, dependencies, executor and
capabilities without preparing the provider recipe. Add ` --prepare ` to capture
selected inputs and obtain the exact ` build_identity `. Preparation may fetch
pinned inputs and write caches, but does not change pins or execute the requested
build or dependency builds. Nix preparation disables import-from-derivation.

` blueprint explain build NAME ` compares the declared/captured input evidence
with the last successful local build. Add ` --prepare ` to compare the exact
current and previous build identities as well. Missing or incompatible baseline
evidence is reported as unavailable, not unchanged. Relevant direct and
transitive changes are named. A changed build identity means the request
changed; it does not mean the output bytes differ.

Both commands first ask the executor’s provider to validate the executor
declaration, as ` blueprint check ` does.

An unprepared plan has ` prepared: false ` and no exact build identity. It cannot
establish that a cached artifact corresponds to the current request.

Use narrow ` Files ` selections to avoid unrelated project changes affecting a
build. The default whole-project snapshot intentionally includes every eligible
project file. Pinned dependencies, tools, options, commands and declared build
dependencies also participate in preparation. Scripts must declare what they
need: Blueprint cannot infer dependencies hidden in arbitrary commands.

<a id="the-plan-and-explain-json"></a>

### The plan and explain JSON

Both commands accept ` --json ` and print one JSON object, the same typed value
their human output renders. ` plan ` and ` explain ` compute the same object;
` explain ` differs only in its human output, which lists the changes. The
object’s ` schema_version ` is ` 1 `, independent of build result schema version 2.
Reject unsupported schema versions and tolerate additive fields.

| Field | Meaning |
| --- | --- |
| ` schema_version ` | ` 1 `. |
| ` name ` | The requested build. |
| ` system ` | The execution system. |
| ` provider ` | The selected executor’s provider, currently ` "nix" `. |
| ` prepared ` | ` true ` with ` --prepare `, otherwise ` false `. |
| ` build_identity ` | The exact prepared build identity, compared together with ` provider `. Empty when ` prepared ` is ` false `. |
| ` outputs ` | The prepared outputs, each ` { name, path } `. Empty when ` prepared ` is ` false `. |
| ` required_capabilities ` | What this request needs: ` builds `; ` build-files ` when a build in the closure selects ` Files `; ` dependency-builds ` when one has ` Needs `; ` prepare ` when prepared. |
| ` provider_capabilities ` | The capabilities the executor’s provider advertises; see [Capabilities and guarantees](#capabilities-and-guarantees). |
| ` supported_guarantees ` | The guarantees a successful build by that provider gives. |
| ` snapshot_digest ` | ` sha256:<hex> ` of the snapshot’s S-expression encoding. A build result reports the same value. |
| ` snapshot ` | The captured inputs: ` name `, ` system `, ` provider ` and ` nodes `, one node per build in the closure. |
| ` baseline ` | ` "available" ` when the last successful local build record was read and compared, otherwise ` "unavailable" `. |
| ` baseline_reason ` | Why the baseline is unavailable, such as ` no successful local build record `. Empty when it is available. |
| ` build_identity_status ` | ` "unchanged" ` or ` "changed" ` against the recorded build identity. ` "unavailable" ` when not prepared or when the baseline is unavailable. |
| ` changes ` | What differs from the baseline, each ` { build, dependency_path, kind, name } `. Empty when the baseline is unavailable. |

Each ` snapshot.nodes ` entry describes one build:

| Field | Meaning |
| --- | --- |
| ` name ` | The build. |
| ` selected_files ` | ` true ` when the build selects its project files with ` Files `, ` false ` for the whole-project snapshot. |
| ` command ` | Its exact argv. |
| ` environment ` | Its environment. |
| ` executor ` | The name of its executor. |
| ` output ` | Its declared output path. |
| ` dependencies ` | The builds it ` Needs `. |
| ` declarations ` | An S-expression of the declarations that shape it: the build; its environment with that environment’s tools, commands, configured tools, programs, expected tools and Roc packages; the package sources it uses; ` Raw ` data for its executor’s provider; its executor; and its ` Files ` selection. |
| ` files ` | Each captured project file and directory, ` { path, digest } `, sorted by path. The digest is Blueprint’s tree digest of that one entry: its kind, path, executable bit and, for a file, its contents. |
| ` inputs ` | Each locked input the build uses, ` { name, provider, content_identity } `, sorted by name. A fetched program has provider ` "blueprint" ` and its ` sha256:<hex> `. |

Each change names the build it concerns, the ` dependency_path ` from the
requested build to that build (such as ` ["app", "library"] `), its ` kind ` and
the ` name ` of what changed:

| ` kind ` | Meaning; ` name ` is |
| --- | --- |
| ` dependency-added ` | A build joined the closure; the build. |
| ` dependency-removed ` | A build left the closure; the build. Its path is the one it had in the baseline. |
| ` dependencies-changed ` | The build’s ` Needs ` changed; the build. |
| ` declaration-changed ` | Its command, environment, executor, output, file-selection mode or ` declarations ` changed; the build. |
| ` file-changed ` | A captured file or directory is new or its digest changed; its path. |
| ` file-removed ` | A captured file or directory is no longer captured; its path. |
| ` input-changed ` | A locked input is new, or its provider or content identity changed; the input. |
| ` input-removed ` | A locked input is no longer used; the input. |

<a id="capabilities-and-guarantees"></a>

### Capabilities and guarantees

A capability is an operation an executor supports. ` blueprint build ` requires
` builds ` and ` prepare `, and ` checked-rebuild ` with ` --rebuild `; an executor
without one is refused before any effect.

| Capability | Meaning |
| --- | --- |
| ` builds ` | Run managed builds. |
| ` build-files ` | Capture only the project files a build selects with ` Files `. |
| ` dependency-builds ` | Build what a build ` Needs ` and supply those artifacts to it. |
| ` prepare ` | Obtain the exact build identity without executing the build. |
| ` checked-rebuild ` | Rebuild an existing output and compare the result, for ` --rebuild `. |

A guarantee is what a successful build by that executor establishes. Listing
one in a plan does not mean that planning ran or checked a sandbox.

| Guarantee | Meaning |
| --- | --- |
| ` captured-declared-inputs ` | The command sees the captured project files and declared locked inputs, not the live working tree. |
| ` sandboxed-filesystem ` | The command runs in a mount namespace separate from the caller’s, with its inputs read-only. |
| ` sandboxed-network ` | The command runs in a network namespace separate from the caller’s. |
| ` checked-rebuild-comparison ` | A checked rebuild fails when the rebuilt output differs from the existing one. |

The Nix executor advertises all five capabilities and all four guarantees.

<a id="the-local-baseline"></a>

### The local baseline

A successful, validated build atomically advances its versioned local baseline
at ` .blueprint/build-records/<system>/<name>.scm `. Failed execution, planning and
explanation preserve that baseline. Saving the record is best-effort: when it
cannot be written, for example because it would exceed the 8 MiB bound, the
build still succeeds, a warning goes to stderr and the baseline does not
advance to that build. With no earlier record, ` explain ` reports the baseline
as unavailable.

These S-expression records are operational evidence, not executable plans or
dependency authority; do not commit them. Deleting them removes the comparison
baseline, not the provider’s cached artifacts. Arbitrary historical-record
comparison is not part of this interface.

<a id="capture-selected-reproduction-evidence"></a>

## Capture selected reproduction evidence

The Nix executor supplies a read-only JSON file at
` $BLUEPRINT_INPUTS/blueprint-input-evidence `. Its native format 1 records selected
package revisions and NAR hashes, original-file SHA-256 pins, other source
tree paths, dependency artifact paths, Roc package paths, the store path of
the build’s locked copy of its selected project files and tool roots. This is
provider-native reproduction material, not another provider-neutral report API
or a complete lock export.

A prepared ` plan `, ` explain ` or build measures each build’s selected files in
that same copy, after Blueprint hashed it for Nix, rather than in the project
again, so the evidence is exactly what Nix received even if the project
changes meanwhile. An unprepared ` plan ` makes no copy and measures the project.

A producer can ship selected files and its producer module alongside this
material. A dedicated configuration using the recorded package revision/NAR
hash can prepare the same build request without depending on unrelated project
pins. Mutable references, the whole Lock and the final build identity do not
participate in this evidence input. Final identity and artifact SHA-256 belong
in external result or release evidence, avoiding a build identity that depends
on itself.

<a id="override-packages-in-a-producer"></a>

## Override packages in a producer

` Input("producer-overrides", "path:./producer-overrides") ` may name a local
flake with an ` overlays.default ` output. Select it with
` Overlays(["producer-overrides"]) ` in the producer’s environment. For example,
that flake can define:

``` nix
{
  outputs = { self }: {
    overlays.default = final: prev: {
      meson = prev.meson.overrideAttrs (old: {
        version = "1.12.0";
        src = prev.fetchurl {
          url = "https://github.com/mesonbuild/meson/releases/download/1.12.0/meson-1.12.0.tar.gz";
          # Replace with the checksum published for this exact upstream archive.
          hash = "sha256-<published-base64-sha256>";
        };
        # Apply a specific patch here when the upstream test change needs one.
      });
    };
  };
}
```

The checksum is an intentional placeholder; fill it before building. Blueprint
locks the local flake’s tree and refuses edits until ` blueprint update `.
The override applies to builds whose environment selects that overlay. The
[artifact example](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/examples/artifacts/Blueprint.roc)
declares two single-output builds selecting their producer directory and sharing
the same pinned package environment.

