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.

CommandMeaning
blueprint / blueprint genGenerate 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-envPrint 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 NAMEPrepare 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 NAMEExecute an ordered task/build workflow, stopping on failure
blueprint tasksList tasks and their environments
blueprint updateWrite 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 checkRun the compiler check and validate all shell/task/build environments and executor declarations without requiring a lock
blueprint spec / blueprint flakePrint the Spec or the generated flake
blueprint compiler-versionPrint the exact Roc release tag bundled with this Blueprint binary, for automation
blueprint --helpProject 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:

  1. Read Blueprint.roc with 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.

  2. Read Blueprint.lock and require that it matches Blueprint.roc.

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

  4. Look on PATH for every program the steps need of the machine: the tools their environments Expect, and nix when a step has a tool from a Nix source or is a build. Everything missing is named in one message.

  5. Make every fetched program ready in the store, downloading what is not there and checking each against the lock.

  6. 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§

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}]}
FieldMeaning
formatThe number of this shape, 1. Every field below is always present. A change a reader of format 1 could misread gets another number.
systemThe system the report is for.
providersThe 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.
expectsThe programs those tasks expect the machine to have already, each once.
tasksOne object for each task, in the order asked, or in the order of Blueprint.roc when none is named.
tasks[].providers, tasks[].toolsThe providers that task needs, by the same rule, and the tools they supply, as Blueprint.roc writes them.
tasks[].programs, tasks[].unavailableThe 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_packagesThe 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": []
}
FieldMeaning
sourcesObject keyed by the declared source name, covering package sources, inputs, overlays and build sources.
sources.NAME.name, provider, ref, rev, digestThe 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_identityPer-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, flatSha256digestKind 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.metadataAll 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.fetchTreeFor 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.
fetchedArray 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:

obtainedMeaning
Computedblueprint update downloaded the bytes and hashed them. Nothing else was to be had.
PublishedThe release that serves the download reports this digest, and the bytes were not downloaded. A run on that system checks its download against it.
BothIt 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§

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.