A package provider is one place a project’s tools can come from, and
blueprint starts one only for what a project takes from it. Nix is the
provider that ships today; the core supplies the rest itself: programs
fetched by digest, Roc packages, tools the machine is expected to have, and
the tasks, shells and activations that use them. A project that fetches its
programs, expects the rest of the machine and declares no build runs every
task with no provider installed. This chapter says what such a project looks
like, what each command then does, and where the downloads are kept.
A project that never starts a provider§
app [config] { pf: platform "<the platform URL of a release>" }
import pf.Recipes
config = [
Name("my-package"),
Environment("dev", [
Programs([Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b"))]),
Expect(["git"]),
RocPackages([
"https://github.com/roc-lang/basic-cli/releases/download/0.26.0-rc2/Gj3zVDr3qrQa6aTh7Z1zgUfe9NeBE6R76T5ho3xrc8af.tar.zst",
"https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst",
]),
]),
Shell("default", [Use("dev")]),
Task("check", [Use("dev"), Run(["scripts/check.roc"])]),
]
Nothing in this file belongs to a provider: there is no Packages, Input,
Source or Build, and no tool written "<source>#<name>". The compiler
comes from a release by a digest in Blueprint.lock, git is the machine’s,
and the Roc packages are downloaded by the URLs that name their hashes.
What starts a provider is decided by Blueprint.roc alone, never by a
variable, a flag or what happens to be installed:
| Request | Starts a provider when |
|---|---|
blueprint run TASK, blueprint shell, blueprint env NAME | The environment has, on this system, a tool from a package source: Tools, ToolsFor this system, Command or ToolWith. An overlay the environment selects does not start a provider by itself. |
blueprint workflow NAME | One of its tasks does, or it has a build. |
blueprint build NAME, plan build NAME --prepare | Always. A build runs through the executor it selects. |
blueprint update | Blueprint.roc declares something a provider resolves: a package source, an Input, a Source, or a Roc package that a build’s environment has. An update that names only fetched programs never does. |
blueprint check, spec, tasks, requirements, gen, flake, plan build NAME, lock show, store verify | Never. |
blueprint requirements gives the first row’s answer for a project’s tasks
without running anything; see
What tasks need. Where a provider is
needed and its program, nix for the Nix provider, is not on PATH, the
command stops before it downloads or runs anything and names the tool that
asked for it:
blueprint: this machine lacks what the task needs, and blueprint installs none of it:
nix: environment host takes nix#zigpkgs."0.16.0" from the Nix source nix
What each command does without a provider§
| Command | What happens |
|---|---|
blueprint update | Records in Blueprint.lock the SHA-256 and length of every fetched program on every declared system. It downloads this machine’s programs and keeps them in the store; see What update needs and records. |
blueprint run TASK | Looks on PATH for every program the environment expects, and stops with all that are missing in one message. Then makes each fetched program ready in the store, puts the environment’s Roc packages where Roc looks for them, and runs the task’s command from the project root with the fetched programs first on PATH. |
blueprint workflow NAME | The same for each task in order. Everything every step needs is checked before the first step runs. |
blueprint shell [NAME] | Does the same and then starts the caller’s own shell, the program SHELL names, or /bin/sh when it is unset, with the fetched programs first on PATH. Leaving that shell ends the command. |
blueprint / blueprint gen, blueprint flake | Generate nothing: the project has no flake. blueprint flake prints nothing on standard output and says so on standard error. |
blueprint check, spec, tasks, requirements | Work as everywhere. They read Blueprint.roc and nothing else. |
A task still needs a Blueprint.lock that matches Blueprint.roc: the
digests are in it. Evaluating Blueprint.roc needs the Roc compiler
blueprint was built for, which it embeds and installs into the same store when
ROC is unset and no matching roc is on PATH.
The store of downloads§
Everything blueprint downloads for itself is kept in one per-user
directory, named by content:
<store>/v1/artifacts/sha256/<hex> the downloaded bytes
<store>/v1/trees/sha256/<hex>.<kind>/ those bytes, made usable
<store>/v1/trees/sha256/<hex>.<kind>.digest what that tree held when made
<store> is blueprint under XDG_CACHE_HOME, or under .cache in HOME
when that is unset; a relative value is refused. <kind> is file,
tar.gz, tar.xz or zip: a single file made executable, or an archive
unpacked whole, because a program may need what lies beside it. On macOS
roc links with the darwin directory of its archive.
- An artifact is never trusted
It is hashed again before every use, wherever it came from: a download, an earlier run, a restored cache or a hand that put it there. One whose bytes are not those its name says is removed, with every tree made from it, and downloaded again. A download that does not have the length and the SHA-256 in
Blueprint.lockis not kept and nothing is unpacked from it.- A tree is never trusted either
When a tree is made from an artifact that has just been verified, its digest is recorded beside it as S-expression text, never inside it: the content digest
blueprintuses for local sources, over every file, directory and executable bit, with where each symbolic link leads. Before every use, after its artifact, the tree is hashed again and compared with that record, which only reads it. A tree that differs, or has no record, is removed and made again from the verified artifact, with no download. In CI, cachev1/artifactsalone, keyed onBlueprint.lock: trees are rebuilt from it, and a cached artifact is re-hashed like any other. This check cannot see a tree and its record changed together;blueprint store verifycompares both with a fresh extraction of the archive, and--repairmakes them again. See Verify the store.- An archive is listed before it is unpacked
An archive that holds an absolute name, a
..segment, a name that is not plain text, a link that leaves the tree, or anything that is not a file, a directory or a link, is refused whole, and so is one that lacks the program the recipe names.- Two runs at once are safe
Each file and tree is written under a fresh name beside its final one and renamed into place, so no half-written file is ever seen and two processes that fetch the same thing both succeed. There is one gap: a run that finds an artifact or a tree that is not what was recorded removes the tree, and another process that is running a program from it at that moment loses it.
The store may be deleted at any time; the next run fetches what it needs
again. The files of a tree are read-only and its directories are not, so an
ordinary rm -r removes it. A Roc nightly is about 67 MB as an artifact and
about 220 MB unpacked, and both are hashed on every use.
How a download is made§
blueprint downloads over HTTPS itself. It sends no token, cookie or other
credential, and follows at most five redirects, each of which must be HTTPS.
A request that fails in transit or is answered 408, 429 or a 5xx status is
tried three times; a 404 is final at once. When a download has alternate
URLs (Mirrored), they are tried in order, and the message for a program no
URL served lists what each one answered. A whole download is held in memory
while it is hashed.
Roc packages are not kept in this store. They go into Roc’s own package cache; see Lock the Roc packages scripts use.
What is on PATH§
For each step, blueprint makes a directory run/run-<random>/bin under the
generated directory (.blueprint/ unless you moved it), holding one symbolic
link for each fetched program, named as Fetch names it and pointing at the
program inside its tree. On Windows, and wherever a symbolic link cannot be
made, the program is copied there with its mode instead. Nothing is exposed
through a hard link, which would be the store’s own file under a second name
that a step could write through. That directory is put first on the step’s
PATH, ahead of everything the caller had, and is removed when the step
ends, however it ends. A task whose command is the name of a fetched program
is started from that link whatever else is on PATH.
Expected programs are not put anywhere: they are found on the caller’s
PATH, which the step inherits after the links. In an environment that also
has a provider’s tools, the provider’s shell is started with that PATH and
puts its own tools ahead of it; for Nix that is nix develop.
Programs blueprint still starts§
Three programs of the machine are used on this path, besides the compiler and whatever the project expects:
| Program | Used for |
|---|---|
tar | Listing and unpacking an archive. A Zip unpacks only where tar is bsdtar, as on macOS; GNU tar does not read a zip. |
ln | The symbolic links of a step’s bin directory on Linux and macOS. |
chmod | Making a downloaded File executable and a tree’s files read-only on Linux and macOS. |
What still needs a provider§
A build, always: it runs through the executor it selects, and the Nix executor builds sandboxed on x86_64 Linux only. That sandbox holds nothing of the machine, so a build’s environment may not have fetched programs or expected tools.
Any tool from a package source, in the environments that have one. Other tasks of the same project run without the provider.
blueprint updatein a project that declares anything a provider resolves. Adding the first package source to a project therefore makes its lock stale for every task until someone with that provider runsblueprint updateand commits the result; after that, the tasks that use no provider tool again run without it.
Intel macOS§
The release includes blueprint-x86_64-darwin, built for the platform’s
x64mac target. Current nixpkgs no longer builds for Intel macOS, so a
project’s tasks run there without a provider: from fetched programs, expected
tools and Roc packages. Recipes.roc_nightly selects the nightly’s
macos_x86_64 archive. The no-nix CI job and the release’s tests run the
binary on macos-15-intel: installing the embedded compiler, writing a lock,
reporting requirements, and building and running a basic-cli program.
x86_64-darwin is in the default Systems, so fetched programs are locked
for it unless a project lists its systems without it. Provider tools and
sandboxed builds remain unavailable.
Windows§
The release includes blueprint-x86_64-windows.exe and the platform’s
x64win target. The no-nix CI job checks installing the embedded compiler, writing a
lock, reporting requirements, and building and running a basic-cli program.
The store uses LOCALAPPDATA when XDG_CACHE_HOME is unset.
Recipes.roc_nightly selects the nightly’s zip and roc.exe; the Windows
tar reads zip archives. Fetched programs are copied into a step’s bin
directory with their mode, never hard-linked, and need no ln or chmod.
Include "x86_64-windows" in the project’s Systems: the default list
contains Linux and macOS only, and fetched programs are locked for the
declared systems.
Windows does not execute a script’s shebang. Name its interpreter in the task:
Run(["roc-stable", "--no-cache", "hello.roc"]). The --no-cache flag
works around Roc issue #12429:
the October 18 nightly reports duplicate symbols when linking cached Windows
objects, including on the first run with an empty cache. CI uses this
workaround on Windows and still builds, links and runs the program. It
disables Roc’s compilation cache for that invocation; Blueprint’s download
store and lock verification still apply.
Provider tools and sandboxed builds remain unavailable. CI checks Windows Server; Windows 10 and 11 desktop installations have not been verified here.