> Documentation version: main. [HTML page](https://lukewilliamboswell.github.io/roc-blueprint/configuration.html)

<a id="write-blueprint-roc"></a>

# Write Blueprint.roc

` Blueprint.roc ` is a Roc application on the roc-blueprint platform. It
provides ` config `, a list of settings:

``` roc
app [config] { pf: platform "<the platform URL of a release>" }

import pf.Recipes

config = [
	Name("my-project"),
	Environment("dev", [
		Programs([Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b"))]),
		Expect(["git"]),
	]),
	Shell("default", [Use("dev")]),
	Task("check", [Use("dev"), Run(["roc-stable", "check", "main.roc"])]),
]
```

An environment gets what it has from three places, and each is written
differently: a program that is downloaded ([Fetch a program](#fetch-a-program)), a tool the
machine already has ([Expect a tool on the machine](#expect-a-tool-on-the-machine)), and a tool a package
source installs ([Choose where tools come from](#choose-where-tools-come-from)).

[Get started](getting-started.md#write-a-first-blueprint-roc) has a
complete file with the current URL.

<a id="top-level-settings"></a>

## Top-level settings

| Setting | Meaning |
| --- | --- |
| ` Name(Str) ` | Project name. Required, once. |
| ` Systems(List(System)) ` | The systems the project is for, each ` <arch>-<os> ` of a known architecture (` x86_64 `, ` aarch64 `, ` riscv64 `, ` i686 `, ` armv7l `, ` powerpc64le `) and operating system (` linux `, ` darwin `, ` windows `, ` freebsd `). Defaults to x86\_64 and aarch64 on Linux and macOS; an empty list is invalid. Every fetched program is resolved for each of them. |
| ` Packages(InputName, From(Provider)) ` | A package source and the set it is: ` NixPackages(FlakeRef) ` or ` GuixPackages(Str) `. Every source is declared; none is implied, and a project that takes no tool from a package source declares none. |
| ` Input(InputName, FlakeRef) ` | A flake input the Nix resolver fetches: an overlay when an environment lists it in ` Overlays `, otherwise a plain input for ` Raw ` data. It is not a tool source. |
| ` Resolver(Str, List(InputName)) ` | Assigns sources, inputs and overlays that are not package sources to a provider. ` Input `, ` Overlay ` and ` Source ` declarations, and the Roc bundle sources ` RocPackages ` adds (each named ` roc-<bundle file name without .tar.zst> `, see [Lock the Roc packages scripts use](#roc-packages)), belong to Nix without one; write ` Resolver ` only to assign one of them to another provider. Package sources already identify their provider; core ` SourceFile ` needs no resolver. |
| ` Executor(InputName, ExecutorProvider) ` | Named build executor that builds select with ` Execute `. ` Nix(source) ` builds with Nix, taking its bootstrap package set from the declared ` Packages ` source ` source `. ` Adapter(provider, source) ` names another provider’s executor; ` source ` is its bootstrap package source, or ` "" ` when it needs none. ` blueprint check ` asks the provider to validate each executor and refuses a Nix executor with an empty bootstrap source. |
| ` Environment(EnvName, List(EnvironmentSetting)) ` | Named tools and scoped overlays, optionally inherited. |
| ` Shell(EnvName, List(ShellSetting)) ` | Named alias with exactly one ` Use(environment) `. ` blueprint shell ` selects alias ` "default" `. |
| ` Task(TaskName, List(TaskSetting)) ` | Named argv command with exactly one ` Use(environment) ` and one ` Run(argv) `. No shell alias is required. |
| ` Source(InputName, FlakeRef) ` | Locked non-flake input, e.g. ` Source("assets", "path:./assets") `. |
| ` SourceFile(InputName, Url, Str) ` | Original file pinned by its published SHA-256 (64 lowercase hexadecimal characters). ` Inputs ` exposes the unchanged file. See [original file sources](builds.md#original-files-with-published-hashes). |
| ` Build(InputName, List(BuildSetting)) ` | Managed artifact with required ` Execute(executor) `, ` Use `, exact argv ` Run ` and relative ` Output `; optional ` Files `, ` Inputs ` and ` Needs `. |
| ` Workflow(WorkflowName, List(WorkflowStep)) ` | Ordered ` RunTask(name, extra_argv) `, ` BuildArtifact(name) ` and ` RunWorkflow(name) ` steps. |
| ` Raw(backend, target, Val) ` | Provider-specific data; see [Pass data to the provider](#pass-data-to-the-provider). |
| ` Custom(kind, name, Val) ` | Extension data. The current CLI rejects unsupported extensions. |

<a id="environment-settings"></a>

## Environment settings

Inside an ` Environment `, ` Tools ` and ` Overlays ` occur at most once, ` ToolsFor `
occurs at most once per System, ` Command ` occurs at most once per command
name, ` ToolWith ` occurs at most once per tool, and ` RocPackages `, ` Programs `
and ` Expect ` occur at most once each:

| Setting | Meaning |
| --- | --- |
| ` Tools(List(Tool)) ` | Tools a package source installs, each written ` "<source>#<name>" `: ` "nix#git" ` is ` git ` from the source declared as ` nix `. A Nix name is an attribute path; quote a segment that contains dots, as in ` "nix#zigpkgs.\"0.16.0\"" `. |
| ` ToolsFor(System, List(Tool)) ` | Add tools only for a declared target System. Each System occurs at most once per Environment. |
| ` Command(Str, Tool) ` | Expose one tool’s main program under another command name, without adding the tool’s own executables. |
| ` ToolWith(Tool, List((Str, Val))) ` | A tool built with options its package takes; see [Configure a tool](#configure-a-tool). |
| ` RocPackages(List(Str)) ` | Released Roc bundle URLs to have in place before anything runs, so Roc programs in the environment resolve them without downloading; see [Lock the Roc packages scripts use](#roc-packages). |
| ` Programs(List(Program)) ` | The programs the environment has on ` PATH ` that are downloaded, each a ` Fetch(ProgramName, recipe) `; see [Fetch a program](#fetch-a-program). |
| ` Expect(List(ProgramName)) ` | Programs the machine must already have on ` PATH `; see [Expect a tool on the machine](#expect-a-tool-on-the-machine). |
| ` Overlays(List(InputName)) ` | Ordered inputs applied to this environment’s package set as overlays. |
| ` Extend(EnvName) ` | Inherit one environment’s tools, overlays, commands, configured tools, Roc packages, programs and expected tools before appending this environment’s selections. |

Inheritance deduplicates by first occurrence, parent first. Omitted or empty
` Tools `/` ToolsFor `/` Overlays ` lists do not clear inherited values. A standalone environment
has no overlays unless selected. ` Run ` must contain a nonempty executable;
arguments remain separate strings, including extra CLI arguments after ` -- `.
There is no ` In ` setting or implicit task environment. One name is only one
of a program, an expected tool and a ` Command ` in an environment, including
what it inherits.

<a id="fetch-a-program"></a>

## Fetch a program

A program that is published as a file or an archive is **fetched**: downloaded
from a URL ` Blueprint.roc ` gives, checked against a digest in
` Blueprint.lock `, and put on the ` PATH ` of the environments that list it. No
package source is involved and Nix is not started.

``` roc
Environment("dev", [
	Programs([
		Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b")),
		Fetch("blueprint", Recipes.file("https://github.com/lukewilliamboswell/roc-blueprint/releases/download/0.5.0/blueprint-{system}")),
	]),
]),
```

` Fetch(name, recipe) ` is a command name and a **recipe**: a function from a
system to where the program is downloaded from on that system. The
environment gains the command ` name `. A recipe downloads nothing. It is
asked once for each of the project’s ` Systems ` while ` Blueprint.roc ` is
checked, and its answers are what ` blueprint ` works from. The ` Recipes `
module is the platform’s; import it with ` import pf.Recipes `.

<a id="what-a-recipe-answers"></a>

### What a recipe answers

| Answer | Meaning |
| --- | --- |
| ` File(url) ` | The program is that one file. It is made executable. |
| ` Archive(url, format, member) ` | An archive in the format ` TarGz `, ` TarXz ` or ` Zip `, with the program at the path ` member ` inside it. The archive is unpacked whole, and the command is a link to the program where it lies, so it finds what the archive put beside it. |
| ` Mirrored(download, urls) ` | A ` File ` or an ` Archive ` with alternate URLs for the same bytes, tried in order after its own. ` Blueprint.lock ` holds one digest for all of them, so an alternate need not be trusted. |
| ` Unavailable ` | The program does not exist for that system. This is an error only where a task on that system needs it; a program unavailable on every declared system is refused when ` Blueprint.roc ` is checked. |

A recipe that cannot answer at all, for a version it does not know, stops
with ` crash ` and a message; the compiler reports it with the recipe’s file
and line.

<a id="the-recipes-the-platform-provides"></a>

### The recipes the platform provides

- **` Recipes.file(template) `**: A program that is one file at a URL which names the system. ` {system} ` in the
  template stands for the system asked about, such as ` x86_64-linux `, and
  ` {arch} ` and ` {os} ` for its two parts. No other placeholder is accepted, and
  none may stand in the host name. The program is taken to exist for every
  declared system: when its publisher has no file for one of them,
  ` blueprint update ` fails on that file and says so. State it by listing the
  systems the project supports in ` Systems `, or write a recipe that answers
  ` Unavailable `.

- **` Recipes.roc_nightly(tag) `**: The Roc compiler of one release of
  [roc-lang/nightlies](https://github.com/roc-lang/nightlies/releases), by its
  tag, ` nightly-YYYY-MM-DD-<commit> `. It answers for x86\_64 and arm64 Linux,
  Apple Silicon and Intel macOS and x86\_64 Windows, and ` Unavailable ` for any
  other system. Fetched as ` roc-stable `, it is the compiler a script that
  starts ` #!/usr/bin/env roc-stable ` runs on.

<a id="write-a-recipe"></a>

### Write a recipe

A recipe is an ordinary function, so a project writes its own for a program
whose downloads do not follow one pattern:

``` roc
app [config] { pf: platform "<the platform URL of a release>" }

import pf.Config
import pf.Recipes
import pf.System

config = [
	Name("my-project"),
	Systems(["x86_64-linux", "aarch64-linux", "aarch64-darwin"]),
	Environment("dev", [
		Programs([
			Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b")),
			Fetch("wasm-tools", wasm_tools),
		]),
	]),
	Task("validate", [Use("dev"), Run(["wasm-tools", "validate", "app.wasm"])]),
]

wasm_tools : System -> Config.Download
wasm_tools = |system| {
	base = "https://github.com/bytecodealliance/wasm-tools/releases/download/v1.240.0"
	match system {
		"x86_64-linux" => Archive("${base}/wasm-tools-1.240.0-x86_64-linux.tar.gz", TarGz, "wasm-tools-1.240.0-x86_64-linux/wasm-tools")
		"aarch64-linux" => Archive("${base}/wasm-tools-1.240.0-aarch64-linux.tar.gz", TarGz, "wasm-tools-1.240.0-aarch64-linux/wasm-tools")
		"aarch64-darwin" =>
			Mirrored(
				Archive("${base}/wasm-tools-1.240.0-aarch64-macos.tar.gz", TarGz, "wasm-tools-1.240.0-aarch64-macos/wasm-tools"),
				["https://mirror.example.net/wasm-tools-1.240.0-aarch64-macos.tar.gz"],
			)

		_ => Unavailable
	}
}
```

Give a recipe of more than a line its type, ` System -> Config.Download `, as
here: a mistake in it is then reported on its own line. A recipe written
inline, as ` Fetch("tool", |system| ...) `, is checked just the same, and the
compiler may report a mistake in it against the platform’s file with the
line and column of the literal in words.

A ` System ` is not text. To write it into a URL, convert it:

``` roc
Environment("dev", [
	Programs([Fetch("tool", |system| File("https://example.org/releases/1.0/tool-${system.to_str()}"))]),
]),
```

With ` ${system} ` alone the compiler reports a type mismatch of
` from_interpolation ` on ` Url `, at the first line of the file and not at the
recipe. ` Recipes.file ` avoids the interpolation altogether:

``` roc
Environment("dev", [
	Programs([Fetch("tool", |system| File("https://example.org/releases/1.0/tool-${system}"))]),
]),
```

<a id="share-a-program-between-environments"></a>

### Share a program between environments

` Fetch ` is written inside ` Programs `, so a program cannot be listed without
being declared. An environment that extends another inherits its programs.
Two environments that do not extend one another share a program through an
ordinary binding:

``` roc
app [config] { pf: platform "<the platform URL of a release>" }

import pf.Recipes

roc_stable = Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b"))

config = [
	Name("my-project"),
	Environment("scripts", [Programs([roc_stable]), Expect(["git"])]),
	Environment("release", [Programs([roc_stable]), Expect(["gh"])]),
	Environment("ci", [Extend("scripts")]),
]
```

One name always means the same downloads. Two ` Fetch ` values with one name
whose recipes answer differently for a declared system are refused as ` two different programs are named roc-stable `.

<a id="what-is-checked-and-when"></a>

### What is checked, and when

**` roc check `, in an editor or on the command line:** Each text, as what it is where it is written: a system is ` <arch>-<os> ` of a known architecture and operating system, in a ` match ` pattern and beside ` == ` too; a URL is ` https://<host>/<path> ` with no credentials, query or fragment; the path of a program in an archive is relative and not a URL, so the two cannot change places; a nightly tag has its form; a template has only its three placeholders. Then the whole configuration: a program name is a plain file name, one name is only one of a program, an expected tool and a ` Command ` in an environment, no URL is listed twice for one download, and every program exists for at least one declared system.

**` blueprint update `:** That every download exists, by downloading it or by taking the digest its release publishes; and, where both are to be had, that they agree. See [What update needs and records](commands.md#what-update-needs-and-records).

**` blueprint run `, ` shell ` and ` workflow `:** That ` Blueprint.lock ` holds the program for this system under the URLs ` Blueprint.roc ` now gives; that every expected tool is on ` PATH `; then that the download has the recorded length and SHA-256, on every use; that an archive holds nothing that leaves the directory it is unpacked in; and that the program is in it.

A program whose URLs changed since the lock was written is stale by itself:
the message names it, and ` blueprint update <name> ` resolves it alone.

<a id="expect-a-tool-on-the-machine"></a>

## Expect a tool on the machine

Some tools are better taken from the machine than installed: ` git `, which
every developer and CI runner has, or ` gh `, which must be the one that is
logged in. ` Expect ` declares them:

``` roc
Environment("dev", [
	Expect(["git", "gh"]),
]),
```

Before a task or shell of that environment starts, and before anything is
downloaded for it, ` blueprint ` looks for each name on ` PATH `. One message
names everything that is missing:

```
blueprint: this machine lacks what the task needs, and blueprint installs none of it:
  gh: environment dev expects it on PATH (Expect)
```

An expected tool is not installed, not put on ` PATH ` and not pinned, and no
version is asked of it: the environment runs with whichever one the machine
has. Where a version matters, fetch the program or take it from a package
source instead, or have a task check ` tool --version `. ` blueprint requirements ` lists what a project’s tasks expect, so a CI job can install
it first.

A sandboxed build has nothing of the machine, so an environment a build uses
may not have ` Expect ` or ` Programs `.

<a id="choose-where-tools-come-from"></a>

## Choose where tools come from

A tool that a package source installs is written ` "<source>#<name>" `, for a
source declared with ` Packages `:

``` roc
Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable"))),
Packages("stable", From(NixPackages("github:NixOS/nixpkgs/nixos-24.05"))),
Environment("dev", [Tools(["nix#git", "stable#jq"])]),
```

The source’s name is the project’s choice; ` nix ` and ` stable ` mean nothing to
` blueprint `. The source is always written, in ` Tools `, ` ToolsFor `, ` Command `
and ` ToolWith ` alike, and there is no default. ` "git" ` alone is refused
where it is written:

```
"git" names no package source. Write "<source>#git" for a declared source, as
in Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable")))
and "nix#git"; or, if git is already on the machine, list it in Expect.
```

A tool that names a source the project does not declare is refused with the
sources it does declare, as ` unknown source: nix; Blueprint.roc declares stable `. A project whose environments have no such tool declares no source,
and Nix is not started for it.

Nix is started only for an environment that has a tool from a Nix source.
An environment Nix must build although it has no tool of its own, such as
the sandbox of a build whose environment is empty, is built with the
bootstrap source of the Nix executor that the project’s builds select (the
first by name when they select several). When no build selects one, it is
built with the first Nix source declared, in the order of ` Blueprint.roc `.
The bash that ` nix develop ` runs a shell or task with comes from the first
Nix source the generated flake declares.

The core does not autodetect a provider or translate package names. Guix
source intent and pure capability validation exist, but there is **no Guix
renderer or executor**.
An incompatible requested environment fails; it never retries another provider.
Missing native packages and unavailable target packages fail in Nix rather than
being silently filtered out. Nix tools applicable to unsupported targets fail
before file writes or provider execution. ` Systems ` remains authoritative for
fetched programs: a mixed project can include Windows alongside Unix when its
Windows environments need no Nix tools. Scope Unix tools with ` ToolsFor `;
unscoped tools, commands and configured tools apply to every declared system.
Inherited system scopes are preserved. An unused Nix source does not require
Nix outputs for Windows.

Full rendering checks all used environments and their applicable systems.
Selected tasks and shells check the requested system and environment closure;
unrelated systems do not invalidate a supported Unix operation. Sandboxed
build outputs cover only ` x86_64-linux `, the existing executor target. Explicit
build requests elsewhere fail before provisioning or execution.

Create and update locks requiring Nix resolution on a supported Unix host.
Windows consumes that same committed lock for provider-free tasks without Nix;
Windows updates requiring Nix remain unsupported.

<a id="add-tools-for-one-system"></a>

## Add tools for one system

For a shell shared by Linux and macOS, put common tools in ` Tools ` and declare
platform libraries explicitly:

``` roc
Environment("dev", [
	Tools(["nix#git", "nix#python3"]),
	ToolsFor("x86_64-linux", ["nix#wayland", "nix#alsa-lib"]),
]),
```

` ToolsFor ` is provider-neutral Spec intent. It applies to inherited environments,
shells, tasks and builds on that System; undeclared systems and duplicate
declarations are rejected during evaluation.

<a id="give-a-tool-another-command-name"></a>

## Give a tool another command name

` Command ` exposes one tool of a package source under another command name,
without adding the tool’s own executables. It keeps a pinned tool beside
another of the same name: a second Python, say, for scripts that must not
follow the project’s own:

``` roc
Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable"))),
Packages("stable", From(NixPackages("github:NixOS/nixpkgs/nixos-24.05"))),
Environment("dev", [
	Tools(["nix#python3"]),
	Command("python-stable", "stable#python3"),
]),
```

The environment gains ` python-stable ` beside the ` python3 ` of ` nix `. The
command runs the tool’s main program as its package declares it; a package
that declares none fails in Nix. Command names are plain file names. An
extending environment inherits commands and replaces one by declaring the
same name.

A ` Command ` is a tool of a package source, so its environment starts Nix. A
program that is published as a download needs neither: ` Fetch ` names its
command itself. A Roc compiler for scripts is written
` Fetch("roc-stable", Recipes.roc_nightly(tag)) `, as in [Fetch a program](#fetch-a-program);
` Command("roc-stable", "nix#rocpkgs.<tag>") ` with the roc-overlay selects
the same compiler through Nix, for an environment a sandboxed build uses.

<a id="configure-a-tool"></a>

## Configure a tool

Some packages are built to order. A Rust toolchain from rust-overlay comes
with the host’s standard library only; the targets it can compile for and
its extra components are options of the package. ` ToolWith ` names the tool
and those options:

``` roc
Input("rust", "github:oxalica/rust-overlay"),
Input("zig", "github:mitchellh/zig-overlay"),
Environment("host", [
	Overlays(["rust", "zig"]),
	Tools(["nix#zigpkgs.\"0.16.0\""]),
	ToolWith("nix#rust-bin.stable.\"1.95.0\".default", [
		("targets", List([Str("x86_64-unknown-linux-musl"), Str("aarch64-unknown-linux-musl")])),
		("extensions", List([Str("rust-src")])),
	]),
]),
```

The environment gains ` cargo ` and ` rustc ` from Rust 1.95.0 with both musl
targets and the standard library’s source. Zig needs no options: zig-overlay
names each release as an attribute, which ` Tools ` selects.

The options are the names and values the package’s own ` override ` takes,
written as ` Val ` data: strings, integers, booleans, lists and attribute
sets. They are passed as data and are never an expression, so they select
among what the package offers and cannot say how to build it. A package
that takes no options, or not the ones named, fails in Nix.

A tool is either plain or configured in an environment, not both. An
extending environment inherits configured tools; configuring an inherited
tool replaces all of its options. Three things are not supported:

- options on a ` ToolsFor ` or ` Command ` tool;


- rust-overlay’s ` fromRustupToolchainFile `, which reads a file of the
  project: the generated flake does not see the project’s files;

- configured tools from a Guix source.


<a id="pin-an-exact-version-of-a-tool"></a>

## Pin an exact version of a tool

A tool’s version is whatever its source holds at the pinned revision.
` Blueprint.roc ` names the tool, not its version, so to hold one tool at an
exact version, give it a package source of its own, pinned to a nixpkgs
revision that ships that version. For CPython 3.14.3:

``` roc
Packages("py", From(NixPackages("github:NixOS/nixpkgs/<revision>"))),
Environment("dev", [Tools(["nix#git", "py#python314"])]),
```

` <revision> ` is a full nixpkgs commit whose ` python314 ` is 3.14.3. To find
one, look at the history of the package’s file in nixpkgs, or evaluate a
candidate:

``` sh
nix eval --raw github:NixOS/nixpkgs/<revision>#python314.version
```

How the source is written decides whether the version can move:

- A source that names a **revision**, as above, never moves. ` blueprint update `
  resolves it to the same commit every time. Change the version by editing
  the revision.

- A source that names a **branch**, such as ` github:NixOS/nixpkgs/nixos-25.05 `,
  is moved by ` blueprint update ` and by ` blueprint update py `, and takes
  whatever version the branch then has. ` blueprint update nix ` leaves it
  alone.

Nothing in ` Blueprint.roc ` states the version, so nothing fails if it changes.
If a version matters, assert it where the tool is used, for example by having
the project’s check task run ` python3.14 --version ` and compare.

An overlay that publishes each version under its own attribute needs no second
source: select the attribute, quoting a segment that contains dots.

``` roc
Input("zig", "github:mitchellh/zig-overlay"),
Environment("dev", [Overlays(["zig"]), Tools(["nix#zigpkgs.\"0.16.0\""])]),
```

<a id="roc-packages"></a>

## Lock the Roc packages scripts use

A Roc script names its platform and packages by release URL, and Roc downloads
them on first use. ` RocPackages ` names those bundles for an environment
instead, so Blueprint has them in place before its scripts start, and they
run at all in a sandboxed build, which has no network:

``` roc
Environment("scripts", [
	Programs([Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-28-bad769b"))]),
	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",
	]),
]),
Task("release", [Use("scripts"), Run(["scripts/release.roc"])]),
```

Each URL must be ` https:// ` and end in ` <hash>.tar.zst `. The hash is that of
the bundle’s bytes, so the URL itself says what the download must be.

**List every bundle, including dependencies of dependencies.** basic-cli above
depends on ` http `, so both are listed. ` roc deps <file> ` prints the URLs a
program depends on. Blueprint does not read package headers, so it cannot add a
missing one for you. A bundle that is not listed is downloaded by Roc as usual
in a shell or task, and fails to resolve in a sandboxed build.

<a id="what-the-lock-pins"></a>

### What the lock pins

For a shell or a task, nothing: ` blueprint ` downloads a missing bundle itself
and has the compiler unpack it with ` roc unbundle `, which fails unless the
bytes are those the name is the hash of. That check is the pin, and it needs
no Nix and no entry in ` Blueprint.lock `. A project whose Roc packages are
used by tasks and shells alone has none of them in its lock.

A sandboxed build cannot download. The bundles of an environment that a build
runs in, or that it extends, are therefore fetched by Nix as well:
` blueprint update ` records those in ` Blueprint.lock ` like any other source,
each named ` roc-<hash> ` and owned by Nix without a ` Resolver `, and Nix checks
the bytes against that pin. An environment a build uses takes
its compiler from a Nix source, since the Nix executor does not support downloaded programs in managed builds:

``` roc
Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable"))),
Executor("native", Nix("nix")),
Input("roc", "github:roc-lang/roc-overlay/a9b461cd4df73ccb69a79a2bc7f58ab60e715619"),
Environment("build-scripts", [
	Overlays(["roc"]),
	Command("roc-stable", "nix#rocpkgs.nightly-2026-10-27-de79a0f"),
	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",
	]),
]),
Build("notes", [Execute("native"), Use("build-scripts"), Run(["roc-stable", "scripts/notes.roc"]), Output("notes.txt")]),
```

The overlay is pinned to a revision that records this nightly. Keep the
overlay revision and compiler version in step when changing the compiler.

An extending environment inherits its parent’s bundles.

<a id="what-is-written-and-where"></a>

### What is written, and where

- **` blueprint shell ` and ` blueprint run `** publish the environment’s bundles
  into Roc’s own package cache before entering it: the directory
  ` roc/packages ` under ` $XDG_CACHE_HOME `, or under ` ~/.cache ` when that is
  unset. Each bundle becomes the directory ` <hash> ` there, the files the
  compiler unpacked from the download, which is where Roc looks before
  downloading. A ` <hash> ` directory that
  already has a ` main.roc ` is never touched, whether Roc downloaded it or an
  earlier run published it; one without a ` main.roc ` is incomplete and is
  replaced, as Roc itself would. The copy is made in a temporary directory
  beside it, named ` blueprint-<hash>.<random>.tmp `, and renamed into place, so
  another Roc process never sees half a package. Nothing else is written, and
  nothing is downloaded or written when every bundle is already there or the
  environment has no Roc packages. If the cache is deleted, the next run
  downloads and publishes again.

- **` blueprint build `** does not use that cache. The build gets a package cache
  of its own inside its build directory, holding only its environment’s
  bundles, and ` XDG_CACHE_HOME ` points the build’s command at it.

A build sees only the tools its environment declares, so a build that runs Roc
needs a Roc tool, such as the ` roc-stable ` command above.

<a id="compose-settings-from-modules"></a>

## Compose settings from modules

Use ordinary Roc lists and functions for composition, not a plugin registry.
[ProjectTasks.roc](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/examples/composition/ProjectTasks.roc) returns
` List(Config.Setting) `; its [app](https://github.com/lukewilliamboswell/roc-blueprint/blob/main/examples/composition/Blueprint.roc)
concatenates those settings into ` config `. Imports do not install tools or add
runtime operations to the CLI.

<a id="pass-data-to-the-provider"></a>

## Pass data to the provider

The Nix provider accepts attribute data at two targets:

``` roc
Raw("nix", "shell:default", Attrs([
	("shellHook", Str("echo ready")),
	("RUST_LOG", Str("debug")),
])),
Raw("nix", "flake", Attrs([("formatter", Str("nixpkgs-fmt"))])),
```

- ` shell:<alias> ` adds attributes to that alias’s ` mkShell ` call, not other
  aliases or tasks using the same environment.

- ` flake ` adds attributes to flake outputs.


Values are ` Str `, ` Int `, ` Bool `, ` List([...]) ` and ` Attrs([(name, value), ...]) `.
They are data, not Nix expressions. Duplicate attributes and overrides of
managed ` packages `/` devShells ` are rejected. Raw for other providers is ignored.

