Run project commands in the directory containing Blueprint.roc, or set
BLUEPRINT_ROOT. compiler-version and store verify work in any directory.
lock show needs only a committed lock, and works even when Blueprint.roc is
absent or invalid.
| Command | Meaning |
|---|---|
blueprint / blueprint gen | Generate the Nix files under .blueprint/ from the lock. A project with no Nix package source has none, and nothing is generated |
blueprint shell [NAME] | Enter the environment of the shell alias NAME (default default): through nix develop when it has a tool from a Nix source, and otherwise in the caller’s own shell |
blueprint run TASK [-- ARGS...] | Run the task’s argv in its environment, with the extra arguments appended |
blueprint env NAME, with --json, --github-path or --github-env | Print what entering the shell NAME exports, as shell lines to eval, a JSON report, or the lines a GitHub Actions step appends; nothing is entered |
blueprint build NAME | Prepare and obtain an artifact plus declared dependencies through its selected executor; print its output path |
blueprint plan build NAME [--prepare] [--json] | Inspect the build request; optionally prepare its exact provider build identity without executing builds |
blueprint explain build NAME [--prepare] [--json] | Explain changes against the last successful local build; optionally compare exact prepared build identities |
blueprint workflow NAME | Execute an ordered task/build workflow, stopping on failure |
blueprint tasks | List tasks and their environments |
blueprint update | Write Blueprint.lock: the digest of every fetched program on every declared system, and the pins of package sources, inputs and build sources. See What update needs and records |
blueprint update NAME... | Update only the named package sources, inputs, build sources and fetched programs, keeping everything else of a lock that is current for Blueprint.roc. Local path: inputs are read again either way. A named program is resolved again whether or not its URLs changed |
blueprint requirements [--json] [TASK...] | Say what the named tasks need, or every task: a provider, fetched programs, tools of the machine. See What tasks need |
blueprint lock show [--json] [NAME...] | Read committed pins and per-entry content identities without evaluating the project. See Inspect committed pins |
blueprint store verify [--repair] [--json] | Unpack every archive in the store of downloads again and compare each tree and its record with it, without a project or a download; exit 1 when anything differs. --repair makes what differs again. See Verify the store |
blueprint check | Run the compiler check and validate all shell/task/build environments and executor declarations without requiring a lock |
blueprint spec / blueprint flake | Print the Spec or the generated flake |
blueprint compiler-version | Print the exact Roc release tag bundled with this Blueprint binary, for automation |
blueprint --help | Project help; subcommand help lists tasks, aliases and builds |
Align an automation compiler§
blueprint compiler-version prints one release tag and a newline, with no
labels or colour. It reports this binary’s pinned compiler even when ROC
or PATH selects another executable. It works offline without a project,
configuration evaluation, compiler installation or cache writes. Native smoke
tests run the actual bundled roc version and require it to match this tag
on every released system.
A CI check or commit hook can compare an installed automation compiler:
test "$(roc-stable version)" = \
"Roc compiler version $(blueprint compiler-version)"
A project can also compare its committed compiler pin with the reported tag before installing tools. Keep upgrades explicit: align that pin when the project’s automation is ready for the new compiler.
When a project fetches the same nightly with Recipes.roc_nightly, it can
reuse the compiler archive Blueprint installed in its store. Matching tags
does not make an independently installed roc-stable, such as a Nix package,
reuse Blueprint’s cached executable.
Use an environment from another program§
blueprint env NAME describes the shell NAME as data instead of entering
it: the directory of fetched programs the core puts first on PATH, and what
the provider’s shell would export, obtained by asking the provider for its
shell as data (Nix answers with nix print-dev-env). Nothing runs in the
environment. The directory of programs is kept at
.blueprint/env/NAME/bin, rebuilt on every call, so what was printed stays
valid until the next blueprint update.
eval "$(blueprint env dev)" # this shell
blueprint env dev --github-path >> "$GITHUB_PATH"
blueprint env dev --github-env >> "$GITHUB_ENV"
blueprint env dev --json # for an editor or agent
The shell rendering single-quotes every value and prepends to the caller’s
own PATH. The JSON report has format 1, path entries { directory, origin } first on the search path first, and variables { name, value, origin }; origin names what supplies each, such as blueprint for fetched
programs or nix. A provider’s shell variables that describe its own process,
such as HOME and PWD, are not exported.
What a run does, in order§
blueprint run, shell, workflow and build share the following
validation and execution boundary:
Read
Blueprint.rocwith the Roc compiler, and refuse what the configuration alone rules out, such as a task that does not exist or a program its recipe says is unavailable on this system.Read
Blueprint.lockand require that it matchesBlueprint.roc.Plan every step of the request. All problems are reported together: a program the lock does not hold, one whose URLs changed, a build this machine cannot run.
Look on
PATHfor every program the steps need of the machine: the tools their environmentsExpect, andnixwhen a step has a tool from a Nix source or is a build. Everything missing is named in one message.Make every fetched program ready in the store, downloading what is not there and checking each against the lock.
Run the steps. Each gets its Roc packages published and its fetched programs first on
PATH.
Nothing is downloaded, generated or run for a request that fails an earlier
check. Work without a provider
describes the store and the PATH of a step.
What each command validates§
Provider capability checks follow the requested environment closure; unrelated
valid provider declarations do not block selected operations. Whole-project
structural validation and required-feature checks still apply. Full rendering
(gen, update, check, flake) checks every shell, task and build
environment that Nix builds; one with no tool from a Nix source is entered
without Nix and is left out of the flake.
The lock and generated files§
Commit
Blueprint.rocand the authority (defaultBlueprint.lock, a versioned S-expression listing each fetched program with its digest and each pinned source with its revision and digest); generated state (default.blueprint/) is not committed:blueprintwrites a.gitignorethere that ignores everything in it.Normal
gen,shell,run,buildandworkflowrequire a lock that matchesBlueprint.rocand never rewrite it. A lock that does not match says what changed, and one in a format thisblueprintdoes not read is refused; both nameblueprint update.A fetched program is stale by itself. When its URLs in
Blueprint.rocare not those in the lock, only requests that use it fail, andblueprint update <name>resolves it alone.nix developtakes the bash it runs a shell or task with from a flake input namednixpkgs, so the generated flake gives that name to the first Nix package source declared and that bash is pinned byBlueprint.locklike every tool, unless the project declares an input namednixpkgsitself.
What tasks need§
blueprint requirements reads Blueprint.roc and nothing else: not PATH,
not the store and not Blueprint.lock. It answers before anything is
installed or resolved, and gives the same answer on every machine. The system
is BLUEPRINT_TARGET, or the one it runs on.
$ blueprint requirements
system x86_64-linux
providers: nix
task check [dev]: programs roc-stable; expects gh, git; roc packages 2
task host [zig]: provider nix (nix#zigpkgs."0.16.0"); programs roc-stable; expects gh, git; roc packages 2
With --json the report is one JSON object for a program to read:
{"format":1,"system":"x86_64-linux","providers":["nix"],"expects":["gh","git"],"tasks":[
{"name":"check","environment":"dev","providers":[],"tools":[],"programs":["roc-stable"],"unavailable":[],"expects":["gh","git"],"roc_packages":2},
{"name":"host","environment":"zig","providers":["nix"],"tools":["nix#zigpkgs.\"0.16.0\""],"programs":["roc-stable"],"unavailable":[],"expects":["gh","git"],"roc_packages":2}]}
| Field | Meaning |
|---|---|
format | The number of this shape, 1. Every field below is always present. A change a reader of format 1 could misread gets another number. |
system | The system the report is for. |
providers | The providers that must be on the machine to run the reported tasks on that system, each once. "nix" is there exactly when one of those tasks has, in its environment on that system, a tool from a Nix package source. A Nix source that is declared and that no reported task uses does not put it there. Neither does a shell, a build or blueprint update: they are not tasks. Build execution requirements follow the selected executor, and an update needs Nix only for declarations it owns. |
expects | The programs those tasks expect the machine to have already, each once. |
tasks | One object for each task, in the order asked, or in the order of Blueprint.roc when none is named. |
tasks[].providers, tasks[].tools | The providers that task needs, by the same rule, and the tools they supply, as Blueprint.roc writes them. |
tasks[].programs, tasks[].unavailable | The programs its environment fetches on that system, and those it lists that do not exist there. A task with an unavailable program cannot run on that system. |
tasks[].expects, tasks[].roc_packages | The programs the task expects of the machine, and how many Roc packages its environment has. |
A task that is not declared is an error, and nothing is printed for the
others. To install Nix only where it is needed, test whether providers
holds "nix" for the tasks a job runs:
blueprint requirements --json check | jq -e '.providers | index("nix")'
Inspect committed pins§
blueprint lock show reads only Blueprint.lock. It does not evaluate
Blueprint.roc, check freshness against declarations, invoke Roc or Nix,
access the network, or write generated files or caches. BLUEPRINT_ROOT and
BLUEPRINT_LOCK select the project directory and lock path as usual; other
workspace, target and compiler settings have no effect. It reports committed
authority even when a working configuration has changed. Run blueprint gen
when you need the ordinary freshness checks.
The stable S-expression lock remains the authority and is directly readable
through core.Lock. The optional JSON projection gives Python, jq, Nix and
other consumers public provider-qualified identities, digest kinds and native
metadata without decoding private provider hints. See
Choose the authority or its public projection
for when to use each interface and the projection’s compatibility boundary.
Names select package sources, inputs, overlays, build sources, or fetched programs. With no names, every entry is included. A program name selects all its systems. Unknown names, unreadable locks, unsupported lock major versions and inconsistent provider pins fail with nonzero status and no report on stdout. Diagnostics go to stderr. Output order follows the lock; use the per-entry identity when fingerprinting a dependency, rather than the output bytes or whole lock.
--json emits one JSON object followed by a newline. Its format is 2,
independent of the lock’s own format. Every field listed here is present; a
change a reader of format 2 could misread gets another number. Readers should
check format, accept extra fields and treat ordering as insignificant.
{
"format": 2,
"sources": {
"nix": {
"name": "nix",
"provider": "nix",
"ref": "github:NixOS/nixpkgs/nixos-unstable",
"rev": "4975466d324710c576dc11ad614684e6bd8cad8e",
"digest": "sha256:c49f97e2106d39c00518139ee6700cc8c51e17d7e82419883aedcd8c1a81c9c5",
"digestKind": "tree",
"flatSha256": null,
"content_identity": "sha256:c49f97e2106d39c00518139ee6700cc8c51e17d7e82419883aedcd8c1a81c9c5",
"metadata": {
"nix": {
"fetchTree": {
"type": "github",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4975466d324710c576dc11ad614684e6bd8cad8e",
"narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=",
"lastModified": 1790185690
}
}
}
}
},
"fetched": []
}
| Field | Meaning |
|---|---|
sources | Object keyed by the declared source name, covering package sources, inputs, overlays and build sources. |
sources.NAME.name, provider, ref, rev, digest | The source fields recorded in the lock. rev is empty when absent. digest is the existing provider content digest; prefer content_identity for fingerprinting because aliases may have an empty recorded digest. |
sources.NAME.content_identity | Per-entry pinned content identity, opaque outside the named provider. Compare identities together with provider; unknown providers retain their recorded digest. For Nix and core file sources the identity is sha256:<hex>. Nix remote entries use the decoded NAR hash, local entries the recorded Blueprint tree digest, and follows entries their resolved target’s identity. It is independent of names, mutable reference spelling, tasks, other entries, provider graph node names, and lock format minor versions. Unrelated updates and adding/removing tasks cannot change it. For trees it identifies the full source tree, including when a flake selects a subdirectory within that tree. For SourceFile, it identifies the unchanged original file bytes. |
sources.NAME.digestKind, flatSha256 | digestKind is "file", "tree", or "unknown". Providers establish the kind; an unrecognized provider is never assumed to describe a tree. flatSha256 is the original file hash as 64 lowercase hexadecimal characters for core SourceFile entries, and null for all other sources. Core file sources have provider: "file", their URL in ref, an empty rev, and empty metadata; their content identity is the original-file digest. |
sources.NAME.metadata | All adapter-supplied typed metadata, namespaced by provider name. Consumers may use only fields whose provider-specific contract they understand. Unknown provider entries retain their recorded identity and have empty metadata. Duplicate metadata keys are refused, including nested keys. |
sources.NAME.metadata.nix.fetchTree | For a remote Nix tree source, the exact locked fetch attributes, with string type, owner/repo or url, optional rev and numeric lastModified, narHash, and any applicable locked fetch flags or host metadata. No registry resolution or defaults are inserted. The flake-only dir selection is omitted because it is not a builtins.fetchTree argument. It is omitted for local paths; other providers define their own metadata. Local paths require a project location and Blueprint’s tree verification. |
fetched | Array of program entries. Each has string name, system, sha256, content_identity, array of string urls, integer size (download bytes), string obtained (computed, published, both, or unstated), array of string served and string witness. served lists the hosts that served the bytes blueprint update hashed, after any redirect, sorted; it is empty when nothing was downloaded, as for a digest that was only published. witness is the address a published digest was read from, such as the GitHub releases API address of the release, and is empty when the digest was only computed. content_identity is sha256: followed by the download hash. URL or provenance changes do not change byte identity. |
An external producer can save a named pin without parsing Blueprint’s lock:
blueprint lock show --json nix > locked-input.json
A Nix expression can consume that JSON directly:
let
locked = builtins.fromJSON (builtins.readFile ./locked-input.json);
pin = assert locked.format == 2; locked.sources.nix;
nixpkgs = builtins.fetchTree pin.metadata.nix.fetchTree;
in import nixpkgs.outPath { }
The inspector itself is offline; evaluating builtins.fetchTree may fetch the
source if Nix does not already have it. Use sources.nix.content_identity as the
producer’s input fingerprint so an unrelated roc-stable update does not
invalidate the producer’s artifacts.
Verify the store§
Every use of a fetched program checks the store of downloads first: its archive is hashed again and compared with the SHA-256 it is named by, and its tree is hashed again and compared with the record written beside it when it was unpacked. An archive that differs is downloaded again, and a tree that differs is made again from its archive. That check guarantees a tree is what its record says. It cannot see a tree and its record changed together, so that they agree: nothing in it compares the tree with what its archive unpacks to.
blueprint store verify makes that comparison, at the cost of unpacking every
archive again. It hashes each archive in the store; for each tree with a
record, it unpacks the archive afresh into a scratch directory beside the
trees, never inside one, compares the tree and its record with that
extraction, and removes the scratch directory. It reads the store and nothing
else: not Blueprint.roc, the lock, a compiler or the network. It changes
nothing.
$ blueprint store verify
intact /home/me/.cache/blueprint/v1/artifacts/sha256/<a>
tree differs /home/me/.cache/blueprint/v1/trees/sha256/<a>.tar.gz: it and its record agree, but it holds sha256:<x>, and its archive unpacks to sha256:<y>
intact /home/me/.cache/blueprint/v1/artifacts/sha256/<b>
intact /home/me/.cache/blueprint/v1/trees/sha256/<b>.file
store /home/me/.cache/blueprint: 4 checked, 3 intact, 1 tree differs
blueprint: the store is not intact; `blueprint store verify --repair` makes what differs again from its archive
Each archive is followed by its trees, in the order of their digests. A
finding is intact; archive damaged, when an archive is not its SHA-256;
tree differs or record differs, from the fresh extraction; no record,
for a tree with none; or no archive, for a tree whose archive is missing or
damaged, which nothing can verify without a download. The status is 0 when
every finding is intact, and 1 otherwise.
--repair then makes the store what it should be, and prints a repaired
line for each change. A tree that differs, or has no record, is replaced by
its fresh extraction, with a record; a record that differs is written again.
A damaged archive is removed, and so is a tree with no archive, for the next
use to download the archive and make the tree from it. Nothing is downloaded,
and the status still says what was found: run verify again to see a clean
store.
--json prints one JSON object for a program to read: format 1, store,
findings as { item, verdict, detail } with verdict one of intact,
archive_damaged, tree_differs, record_differs, no_record and
no_archive, counts of each verdict, and repaired as { item, action }.
The JSON is presentation: the records in the store remain S-expressions.
What update needs and records§
blueprint update is the command that explicitly selects dependency pins. It
writes the whole lock or nothing.
Pure Roc consumers can import the official core.LockSnapshot module from
this release’s existing core bundle. See
Consume lock inspection in pure Roc
for typed parsing, lookups, metadata and the offline schema/profile.
Without Nix, and with it§
blueprint update starts Nix only when Blueprint.roc declares something Nix
resolves: a Nix package source, an input or overlay, or a build source,
including the bundle of a Roc package that a build uses. It looks for nix
before doing anything else and says why it needs it:
blueprint: this machine lacks what blueprint update needs, and blueprint installs none of it:
nix: Blueprint.roc declares the Nix source nix, which Nix resolves
A project that declares none of these is updated with no Nix on the machine.
In a project that does, the pins Nix resolves and the digests of fetched
programs share the one lock, and blueprint update PROGRAM..., naming
fetched programs alone, asks Nix for nothing: it needs a lock that is already
current for everything else. A name that is neither an input nor a program is
an error that lists both kinds.
The digests of fetched programs§
For each fetched program and each declared system, the lock holds the URLs the program is declared with, the SHA-256 and the length of the download, and how the digest was obtained:
obtained | Meaning |
|---|---|
Computed | blueprint update downloaded the bytes and hashed them. Nothing else was to be had. |
Published | The release that serves the download reports this digest, and the bytes were not downloaded. A run on that system checks its download against it. |
Both | It hashed the bytes, and the release reports the same digest and length. |
A reviewer of a lock change reads these: Computed alone rests on the one
download blueprint update made, and Published on what the publisher’s
release says.
An entry whose URLs are unchanged is kept, and nothing is asked or downloaded
for it; a program named on the command line is resolved again regardless.
Otherwise the download for the system the update runs on is fetched and
hashed, and is kept in the store for the run that follows. A program that is
the very download blueprint fetched as its own compiler, as a
Recipes.roc_nightly of that nightly is, is hashed from the store and not
fetched a second time.
When the first URL is a GitHub release asset,
https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>, the
release is asked once what it publishes. A digest computed here that differs
from the published one stops the update, and nothing is written. For another
system the published digest is recorded and nothing is downloaded; an asset
with no published digest, or a URL that is not a release asset, is downloaded
and hashed instead.
When the release cannot be asked§
The question to GitHub carries no token, and GitHub answers 60 such requests
an hour from one address. A question that is refused, that cannot be put, or
whose answer does not describe a release does not stop the update:
blueprint update says so once for the release, downloads and hashes that
release’s assets for every system, and records each as Computed. A digest
that is published and differs from the computed one always stops it. A run
never asks GitHub anything.
blueprint: could not ask for the digests roc-lang/nightlies publishes for its release nightly-2026-10-28-bad769b: HTTP 403 from https://api.github.com/repos/roc-lang/nightlies/releases/tags/nightly-2026-10-28-bad769b (1 attempt). GitHub answers 60 such requests an hour from one address when no token is sent, and blueprint sends none. The digests of its downloads are computed by downloading each of them instead, and Blueprint.lock records them as computed
Such an update downloads the program for every declared system, where one that is answered downloads it for this machine alone.
New download hosts§
For each download it hashed, the lock records the hosts that served the
bytes, after any redirect, beside the URLs Blueprint.roc declares. Before
it writes the lock, blueprint update prints every download host the new
lock names and the lock before it did not, one to a line, and says whether a
URL in Blueprint.roc declares it or a redirect led there:
Blueprint.lock will name download hosts it did not name before:
github.com, declared in Blueprint.roc
release-assets.githubusercontent.com, reached by a redirect
It prints nothing when there is none. A new host in a lock update is worth a
reviewer’s look, and one reached by a redirect is named by no line of
Blueprint.roc. The list comes before the lock is written, so an update that
then fails to write it has still said what it would have named.
Environment variables§
BLUEPRINT_ROOTnames the directory containingBlueprint.roc(default: the current directory).BLUEPRINT_WORKSPACE,BLUEPRINT_GENERATED_ROOT,BLUEPRINT_LOCKchoose caller paths; relative values resolve against the selected project root, not the invocation directory. Out-of-tree generated roots are supported.BLUEPRINT_TARGETselects the declared execution system (default: this machine’s System), not a separate artifact target.ROCselects the compiler that readsBlueprint.roc, which must be the nightlyblueprintwas built for; unset, a matchingroconPATHis used, or the nightly is downloaded into the store.XDG_CACHE_HOMEnames the directory that holds the store of downloads,blueprint/, and Roc’s package cache,roc/packages(default:.cacheinHOME).SHELLnames the programblueprint shellstarts for an environment with no tool from a Nix source (default:/bin/sh).PATHis where expected tools,nix,tar,lnandchmodare found.
No variable selects a provider or turns Nix on or off: Blueprint.roc alone
decides what a request needs. No request blueprint makes carries a token or
any other credential, and it reads none from the environment.