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.
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:
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"]).
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
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,
real build fixture.
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.
Order tasks and builds in a workflow§
Inside config, referring to existing tasks and builds:
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.
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.
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.
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.nameandsystem: requested build and execution system.provider: selected executor provider, currently"nix".build_identity: opaque identity of the fully prepared build; compare it together withprovider.metadata: typed provider-namespaced details. Nix exposes its exact derivation undermetadata.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 valueplanandexplainreport. The result does not contain the snapshot itself;planandexplainprint 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.
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.
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. |
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. |
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.
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.
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.
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:
{
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
declares two single-output builds selecting their producer directory and sharing
the same pinned package environment.