Blueprint.roc declares every tool a contributor and CI need. Blueprint.lock
pins them. Both are committed. The blueprint command from
roc-blueprint realises that
environment, the same way on a contributor’s machine and on every CI runner.
The environment holds the tooling compiler, the tools the scripts call, and the native toolchains a platform host or a test host needs. It does not hold the package compiler, which is never pinned.
Roc packages can use Programs, Fetch and Expect with no Nix source.
Follow the complete Roc package adoption guide. Native
platforms can declare Nix sources for their host toolchains and sandboxed builds.
Risks and controls for tooling selection§
A contributor and CI can run different tool versions, or a new compiler can break the scripts needed to diagnose the product. Keep tooling identity explicit and separate from the compiler under test. Blueprint recipes and locks select the tooling bytes; local source scripts declare the corresponding compiler. Verify configuration and run the same named tasks locally and in CI. Changing a lock is an admission decision, not a harmless cache refresh. The lock establishes selected identities; it does not establish that the selected upstream code is free of defects.
Declare the environment§
app [config] { pf: platform "<roc-blueprint release URL>" }
config = [
Name("roc-example"),
Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable"))),
Input("roc", "github:roc-lang/roc-overlay"),
Environment(
"dev",
[
Overlays(["roc"]),
Command("roc-stable", "nix#rocpkgs.nightly-<date>-<commit>"),
RocPackages([
"https://github.com/roc-lang/basic-cli/releases/download/<version>/<hash>.tar.zst",
"https://github.com/roc-lang/http/releases/download/<version>/<hash>.tar.zst",
]),
Tools(["nix#coreutils", "nix#gh", "nix#git", "nix#unzip", "nix#zip"]),
],
),
Shell("default", [Use("dev")]),
Task("check", [Use("dev"), Run(["scripts/check_all.roc"])]),
Task("bundle", [Use("dev"), Run(["scripts/build_bundle.roc"])]),
Task("test-bundle", [Use("dev"), Run(["scripts/test_bundle.roc"])]),
Task("package-examples", [Use("dev"), Run(["scripts/package_examples.roc"])]),
Task("package-docs", [Use("dev"), Run(["scripts/package_docs.roc"])]),
Task("test-published", [Use("dev"), Run(["scripts/test_published.roc"])]),
]
In this example, Packages declares its Nix package source,
Input declares the flake input and Overlays selects its role in this environment.
Source-qualified names such as nix#gh select the declared package source.
Each setting has one job:
Command("roc-stable", ...)Names the exact nightly release used for tooling. The compiler comes from roc-overlay, which packages recorded nightlies under their release tags.
Commandexposes that one release asroc-stablewithout adding the tool’s own executables, so the environment provides no bareroc.RocPackagesDeclares the content-addressed Roc bundles to prepare before scripts start. For tasks and shells, Blueprint downloads missing bundles and uses
roc unbundleto verify their URL hashes before publishing them into Roc’s package cache; these task-only bundles have no entries inBlueprint.lock. Sandboxed builds instead take their bundles from provider-locked sources. List every bundle, including dependencies of dependencies;roc-stable deps scripts/check_all.rocprints them. Blueprint does not read package headers and cannot add a missing one.ToolsEvery program a script starts, and every toolchain a build needs: for example
pandoc,zig, a Rust toolchain orgo. A tool that a script calls and the environment does not declare is a defect, because CI and contributors then run different versions.TaskA named entry point. CI calls tasks, never script paths, so the workflow and a contributor run the same command.
The two files divide the work. Blueprint.roc states which release is the
tooling compiler and which tools are needed, where a reviewer can read it.
Blueprint.lock pins the overlay revision that supplies that release’s download
URL and hash, package sources, fetched programs and bundle sources used by
sandboxed builds. Task-only bundle identities remain in their committed URLs.
Commit the generated lock after review. Derived files and generated Nix stay in
.blueprint/, which the repository’s .gitignore lists.
Get blueprint§
Blueprint needs Nix only for Nix-sourced tools and sandboxed builds. For
those environments install Nix first. There are two
ways to get the blueprint command. Whichever you
use, take the same roc-blueprint release as the platform URL in the
repository’s Blueprint.roc header.
With Nix§
Nix can run or install blueprint straight from the release tag, with nothing
to download or verify by hand:
# run it once, without installing
nix run github:lukewilliamboswell/roc-blueprint/<version> -- run check
# or install it into your profile
nix profile install github:lukewilliamboswell/roc-blueprint/<version>
Nix builds the command from tagged source on first use.
As a release binary§
Each roc-blueprint release also attaches prebuilt blueprint-<system> binaries
and a blueprint.sha256 file. Download the one for your system, check it
against the published SHA-256, make it executable and put it on PATH.
CI always uses the release binary, through
actions/setup-blueprint,
which checks it against a reviewed SHA-256 on every run. Nobody builds the CLI
from source in CI.
What the command needs§
The binary needs neither Python nor Roc installed. It embeds its fixed
configuration compiler, the nightly its release notes record. On first use it
installs the embedded archive into its content-addressed store and verifies the
recorded hash before extraction. It reuses verified bytes on later runs and
repairs a damaged archive from its embedded copy. Bootstrapping this compiler
needs no GitHub request; project tools and packages can still need downloads.
A compatible explicit ROC override takes precedence, then a compatible host
roc. An override must match the compiler required by the Blueprint binary.
blueprint compiler-version reports the bundled compiler tag without evaluating
a project, installing a compiler or writing a cache. It describes Blueprint
itself even when a host override is selected. The report does not require the
repository’s roc-stable pin to match: update that pin separately after testing
the scripts that use it.
This compiler is separate from roc-stable for a reason: Blueprint is written
in Roc and must run before the environment it describes exists. See
Three compilers, three jobs.
Work locally§
A contributor needs blueprint, Nix when their selected tools/builds use it,
and a current nightly installed
as roc, the way Roc is normally installed. Nothing else has to be set up.
blueprint run check # run a declared task in the pinned environment
blueprint shell # or enter the environment and run scripts directly
blueprint tasks # list the declared tasks
blueprint is the outer command. Run it from an ordinary shell: there is no
nix develop step first and no nested shell.
Inside the environment roc-stable is the pinned compiler, and the
contributor’s own roc remains visible for package work.
Use the environment in CI§
Every job that runs repository scripts sets up Blueprint first, then installs the latest nightly, then calls a task:
Set up the verified
blueprintbinary, requesting Nix when needed, withactions/setup-blueprint.Install the latest nightly as
rocwith the officialroc-lang/setup-rocaction; see Install the nightly compiler.Run
blueprint run <task>.
Choose runners for which the selected release has reviewed binaries in
actions/setup-blueprint/releases.json, and verify that the declared tools also
support those systems. Cross-compiling a host in
the builder job does not establish that it
runs on the target.
The job needs no cache account and no token for this. A Nix cache is an
optimisation a repository may add; a cache that requires id-token: write
widens a test job’s token and must be an explicit choice.
A change to Blueprint.lock changes the tools every build uses. Where a
repository fingerprints native build inputs, Blueprint.lock is one of the
fingerprinted files; see
Keep rebuild selection narrow and complete.