# pekit

> The recipe-driven build and packaging tool — declarative recipes in; built, tested, packaged software out.

---

# What is pekit

_pekit / Getting started_

> Pekit is the recipe-driven build and packaging tool for Peios. It reads declarative recipes, plans the work, then builds, tests, packages, signs, and publishes.

**Pekit** is the tool that turns source into packages. You describe a piece of software once — where its source comes from, how to build it, and how to split the result into `.peipkg` files — in a set of declarative **recipe** files, and pekit does the rest: fetches the source, selects versions, runs the build, stages the output, and writes packages you can install or publish.

Everything in the Peios tree is built with pekit. The kernel, the registry store, the init system, the package manager itself — each is a recipe, and `pekit package` produces the `.peipkg` files that [peipkg-compose](/peios/using-peios/package-management/composing-a-root.md) and [peiso](/peios/peiso/building-images/overview.md) assemble into a running system.

## The one idea: plan before execute

Pekit's guiding principle is **predictability**. Every command declares exactly which flags it accepts, every source declares what it can do, and every build is planned before any side effect happens. Before pekit clones a repository, runs a shell command, or writes a package, it has already resolved which recipe is selected, which source tree is needed, which versions apply, which build targets will run, and which artifacts will be produced.

That plan is inspectable. Add [`--dry-run`](/pekit/running/invocation.md) to any command and pekit builds the same plan it would execute, prints it, and stops — no clones, no downloads, no writes. Strictness is the other half of the same idea: unknown flags, unknown recipe keys, and flags a command does not support are **errors**, caught up front, not halfway through a build.

## The pipeline

A single invocation runs through a fixed sequence:

```text
argv
  → invocation        parse and validate flags against the command
  → recipe            locate and load the recipe (walking up from the cwd)
  → versions          resolve the version selector into concrete versions
  → source            materialise the source tree for each version
  → delegation        merge any behaviour borrowed from the source tree
  → execute           run targets, or stage builds and write packages
  → artifacts         staged output and .peipkg files under out_dir
```

`build`, `package`, and `publish` may run this for **several versions** at once; `test` and `install` require the selector to resolve to exactly one. `clean`, `gen`, and `verify` skip the version and source stages entirely — they operate on the recipe directly.

## The shape of a recipe

A recipe is a directory. At minimum it holds one file:

```text
mypkg/
├── pekit.toml                 # the recipe: source, build/test/install/clean targets, env
├── package.pekit.toml         # what to package: files, symlinks, dependencies, metadata
├── packages.pekit/            # (optional) extra package members for multi-package recipes
├── env.pekit.toml             # (optional) reusable environment / wrapper definitions
└── prod.keyring.pekit.toml    # (optional) named keyring for signing and secrets
```

`pekit.toml` is the recipe proper — it declares the [source](/pekit/recipes/sources.md), the [build/test/install/clean targets](/pekit/running/commands-and-targets.md), and shared [environment](/pekit/recipes/environments-and-keyrings.md). The [package files](/pekit/recipes/packages.md) describe how staged build output is mapped into one or more `.peipkg` payloads. A [`workspace.pekit.toml`](/pekit/running/workspaces.md) one level up turns a directory of recipes into a **workspace** that pekit can drive as a unit.

The [Anatomy of a recipe](/pekit/recipes/anatomy.md) page walks through every file; the [recipe format reference](/pekit/reference/recipe-format.md) is the exhaustive schema.

## The commands

Ten commands, each with a defined job:

| Command | What it does |
|---|---|
| `build` | Run build targets and their build dependencies. |
| `test` | Stage the builds a test needs, then run test targets. |
| `install` | Stage the builds an install needs, then run install targets. |
| `package` | Stage builds and write `.peipkg` package artifacts. |
| `publish` | Package, then publish the artifacts to a configured destination. |
| `clean` | Run a clean target and/or remove pekit's managed output directory. |
| `gen` | Run source-generating targets that write generated source into the tree. |
| `verify` | Run gen targets' drift checks without writing anything. |
| `lock` | Fetch and pin source inputs in `pekit.lock` without building. |
| `workspace` | Run any of the above across every member of a workspace. |

The [Commands and targets](/pekit/running/commands-and-targets.md) page covers what each command selects, its version behaviour, and how target selection works.

## Where pekit fits

Pekit is the **producer-side build tool**. It stops at the `.peipkg` file. What happens next is other tools' work:

- [**peipkg**](/peios/using-peios/package-management/overview.md) installs `.peipkg` files onto a live system.
- [**peipkg-compose**](/peios/using-peios/package-management/composing-a-root.md) assembles a set of packages into an offline package root.
- [**peiso**](/peios/peiso/building-images/overview.md) turns a composed root into a bootable image.

Pekit produces packages in the [peipkg format](/peios/using-peios/package-management/overview.md) and understands peipkg concepts directly: recipes declare package **dependencies**, automatic **ELF dependencies** are scanned from built binaries, and packages can declare the shared-name **claims** they participate in. See [Dependencies and claims](/pekit/recipes/dependencies-and-claims.md).

Pekit also carries the supply-chain half of producing packages: fetched sources are pinned trust-on-first-use in a lockfile, upstream release signatures can be verified against committed keys, artifacts are Ed25519-signed, binaries can be PIP-signed for the kernel, and every manifest records the provenance of its inputs, recipe, and builder. See [Signing and provenance](/pekit/running/signing-and-provenance.md).

> [!IMPORTANT]
> **Pekit supersedes the old `peipkg-build` recipe system.** Earlier Peios used a `peipkg.toml` + `build.sh` recipe built by a separate `peipkg-build` binary. Pekit replaces that entirely; a pekit recipe (`pekit.toml` + package files) is the current and only supported way to build a package. Documentation describing `peipkg-build` and its `build.sh` recipes is legacy.

## What to know before you start

A few facts about the tool as it ships today:

- **No `--help` yet.** Pekit does not currently print usage text. This page and the [invocation reference](/pekit/running/invocation.md) are the manual; the complete flag surface is tabulated in the [command-line reference](/pekit/reference/cli.md).
- **Exit status is 0 or 1.** Pekit exits `0` on success and `1` on any error. It does not use finer exit codes.
- **Events go to stdout.** Pekit's own events print as timestamped lines on stdout (structured JSON with [`--json`](/pekit/running/invocation.md)); a target's output streams through on its original stdout and stderr, and fatal errors go to stderr.

## Where to start

To learn the recipe format, begin with [Anatomy of a recipe](/pekit/recipes/anatomy.md) and follow the recipes section in order.

To use pekit against an existing recipe, read [Commands and targets](/pekit/running/commands-and-targets.md) and [Invocation and flags](/pekit/running/invocation.md).

For the exhaustive schema and CLI surface, the [reference section](/pekit/reference/recipe-format.md) has one page per file format plus the full command-line reference.

---

# Quick start

_pekit / Getting started_

> Build and package a tiny "hello" program with pekit from an empty directory — a minimal pekit.toml, one pekit build, and a .peipkg you can inspect.

This page takes you from an empty directory to a working `.peipkg` in about five minutes. You will write a two-file recipe, run `pekit build`, inspect the staged output, then run `pekit package` and look inside the artifact it writes. At the end you have a complete, minimal recipe you can grow into a real one.

## Prerequisites

- **A `pekit` binary on your `PATH`.** Pekit is a single binary built from its source repository with `go build ./cmd/pekit`; there is no separate installer. Note that pekit has [no `--help` or version banner](/pekit/running/invocation.md) — the docs are the manual.
- **A POSIX shell** (`sh`). Target commands run under `sh -euc`; any Linux system has this.
- **`zstd` and `tar`**, for the final inspection step only.

## Create the recipe

A recipe is a directory, and only one file in it is required: `pekit.toml`. Make a directory and write the recipe:

```console
$ mkdir hello && cd hello
```

```toml
# hello/pekit.toml
out_dir = "out"

[build]
command = '''
mkdir -p "$PEKIT_OUT/bin"
printf '#!/bin/sh\necho "Hello from Peios"\n' > "$PEKIT_OUT/bin/hello"
chmod +x "$PEKIT_OUT/bin/hello"
'''
```

Two things are going on here:

- **`out_dir`** names the directory pekit manages, relative to the recipe. Everything pekit stages or writes lands under it, and `pekit clean` may remove it wholesale — keep nothing precious there.
- **`[build]`** in this bare form defines a single build target named `main`. Its `command` is an ordinary shell script; pekit runs it with a set of `PEKIT_*` environment variables exported, of which the important one is `$PEKIT_OUT` — the target's own staging directory. **A build's job is to put its artifacts in `$PEKIT_OUT`.** Here the "build" just writes a shell script; a real recipe would run `make`, `cargo build`, or similar and copy the results in.

This recipe has no `[source]` section, so it builds against its own directory. Fetching real source trees is covered in [Sources](/pekit/recipes/sources.md).

## Run the build

From `hello/` (or any directory beneath it — pekit walks upward to find the nearest `pekit.toml`):

```console
$ pekit build
```

You should see three timestamped lines, ending in success:

```text
[14:30:01] [build] loaded recipe
[14:30:01] [main] running target
[14:30:01] [main] target succeeded
```

Nothing else prints because the build command itself is silent; anything your build writes to stdout or stderr is streamed through with a `[build:main]` prefix. Pekit exits `0` on success and `1` on any error.

## Inspect the staged output

Each build target stages into `out/build/<target>/`. Look at what the run produced:

```console
$ find out -type f
```

```text
out/.pekit/dependencies/build.main.json
out/build/main/bin/hello
```

`out/build/main/` is target `main`'s `$PEKIT_OUT`, and it now holds exactly what the command wrote (the `out/.pekit/` entry is pekit's own bookkeeping). This staged tree is what packaging draws from. The script even runs:

```console
$ out/build/main/bin/hello
Hello from Peios
```

## Describe the package

Packaging is declared in a second file beside the recipe. Write `hello/package.pekit.toml`:

```toml
# hello/package.pekit.toml
format = "peipkg"

[package]
name = "hello"
version = "0.1.0-1"
architecture = "noarch"
description = "A friendly greeting."
license = "MIT"

[files]
":bin/hello" = "usr/bin/hello"
```

The `peipkg` format requires `[package].version`, `[package].architecture`, and `[package].license`; the rest of the metadata is optional. The `[files]` table maps sources to destinations: the left side `":bin/hello"` is a build ref meaning "`bin/hello` from build target `main`'s staged output", and the right side is where the file lands inside the package. Destinations must sit under the peipkg layout's permitted top levels (`usr/bin/`, `usr/share/`, `etc/`, and so on) — see [Packaging files](/pekit/recipes/packages.md).

## Write the package

```console
$ pekit package
```

`package` stages the builds the package needs (re-running `build.main`), then writes the artifact:

```text
[14:31:12] [package] loaded recipe
[14:31:12] [main] running target
[14:31:12] [main] target succeeded
[14:31:12] [main] wrote package: /home/you/hello/out/package/main-e33807f0ae9abdc7/hello_0.1.0-1_noarch.peipkg
```

The artifact is named `<name>_<version>_<architecture>.peipkg` and lands in a per-package stage directory under `out_dir` (the hash in the directory name is derived from the package's identity). Add `--dry-run` to any command to see this plan without writing anything.

## Look inside

A `.peipkg` is a Zstandard-compressed tar archive: reserved `.peipkg/` metadata entries followed by the payload as it will be installed.

```console
$ zstd -d --stdout out/package/main-*/hello_0.1.0-1_noarch.peipkg | tar tf -
```

```text
.peipkg/manifest.json
.peipkg/files.json
usr/
usr/bin/
usr/bin/hello
```

That file is a real package: [peipkg](/peios/using-peios/package-management/overview.md) can install it, and `pekit publish` can copy it to a configured destination. To start over, `pekit clean` removes the whole `out/` directory.

## Where to go next

- [Anatomy of a recipe](/pekit/recipes/anatomy.md) — every file in the recipe directory, all of `pekit.toml`'s sections, and the full `PEKIT_*` environment contract.
- [Commands and targets](/pekit/running/commands-and-targets.md) — the commands, named targets, `needs` ordering, target selection, and the `gen`/`verify` source-generation pair.
- [Invocation and flags](/pekit/running/invocation.md) — the command line: `--dry-run`, `--json`, and how recipes are located.
- [Recipe format reference](/pekit/reference/recipe-format.md) — the exhaustive schema for `pekit.toml` and package files.

---

# Anatomy of a recipe

_pekit / Writing recipes_

> The files that make up a recipe, pekit.toml section by section, and the PEKIT_* environment contract every target command runs against.

A **recipe** is not a single file — it is a directory. One file, `pekit.toml`, is the recipe proper; the others beside it describe what to package, which environment to build in, and which secrets to sign with. This page walks through that file set, then focuses on `pekit.toml` itself and the one interface every recipe author leans on: the `PEKIT_*` environment variables that pekit hands to each target command.

## The recipe directory

Everything pekit needs to build one piece of software lives in a single directory. A rich recipe looks like this:

```text
mypkg/
├── pekit.toml                 # the recipe: source, targets, env, wrap, delegate
├── package.pekit.toml         # base package: files, symlinks, dependencies, metadata
├── packages.pekit/            # (optional) extra package members, one file each
│   ├── docs.package.pekit.toml
│   └── dev.package.pekit.toml
├── env.pekit.toml             # (optional) reusable environment / wrapper
├── prod.keyring.pekit.toml    # (optional) named keyring for signing and secrets
└── ...
workspace.pekit.toml           # (optional) one level up — groups recipes into a workspace
```

Only `pekit.toml` is required. The rest are opt-in and each has its own page:

| File | Purpose | Page |
|---|---|---|
| `pekit.toml` | The recipe: where source comes from, how to build/test/install/clean it, shared env. | this page |
| `package.pekit.toml` | The base package: how staged output maps into a `.peipkg`, plus dependencies and metadata. | [Package files](/pekit/recipes/packages.md) |
| `*.package.pekit.toml`, `packages.pekit/` | Additional package **members** for a recipe that produces more than one package. | [Multi-package recipes](/pekit/recipes/multi-package.md) |
| `env.pekit.toml`, `*.env.pekit.toml` | Reusable environment variables and a shell **wrapper** to run commands under. | [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md) |
| `*.keyring.pekit.toml` | Named collections of secrets exported into the build as `PEKIT_KEYRING_*`. | [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md) |
| `workspace.pekit.toml` | Sits **one level up** and groups a directory of recipes into a workspace pekit can drive as a unit. | [Workspaces](/pekit/running/workspaces.md) |

pekit discovers the package, env, and keyring files by their fixed names in the recipe directory (`packages.pekit/` is searched as an alternative location for package files). The base package file may live at `package.pekit.toml` **or** `packages.pekit/package.pekit.toml`, but not both. The supporting-files reference lists every name and location: [Supporting files](/pekit/reference/supporting-files.md).

## `pekit.toml`

The recipe file is a TOML document with a small, fixed set of top-level keys. pekit is strict: any key it does not recognise is an error, caught when the recipe loads. The recognised keys are exactly:

`out_dir`, `env`, `wrap`, `source`, `delegate`, `source_package`, the four target sections `build`, `test`, `install`, `clean`, and the source-generation section `gen`.

The one pekit itself ships with is deliberately minimal:

```toml
out_dir = "out"

[build]
command = "go build -trimpath -o \"$PEKIT_OUT/pekit\" ./cmd/pekit"

[test]
command = "go test ./..."

[install]
command = "go install ./cmd/pekit"

[clean]
command = "go clean ./..."
```

A fuller recipe that fetches an external source and uses more of the surface:

```toml
# Where pekit stages its output. Managed by pekit; removable by `clean`.
out_dir = "out"

# Environment variables layered onto every target command.
[env]
CARGO_TERM_COLOR = "never"

# Wrapper: every target command runs inside this. {{command}} is substituted
# with the actual build script exactly once.
[wrap]
command = "nix develop --command sh -c {{command}}"

# Where the source tree comes from, and which versions exist.
[source.git]
url = "https://github.com/BurntSushi/ripgrep.git"
ref = "{{version}}"
versions = ">=13.0.0, <=14.1.1"
tag_regex = '^(\d+\.\d+\.\d+)$'

# Borrow selected behaviour from a pekit.toml inside the fetched source.
[delegate]
env = true

# Named build targets. `needs` names other build targets to stage first.
[build.main]
command = "cargo build --release && cp target/release/rg \"$PEKIT_OUT/rg\""

[build.completions]
command = "cp complete/_rg \"$PEKIT_OUT/_rg\""
needs = ["main"]

[test]
command = "cargo test --release"
needs = ["main"]

[install]
command = "cargo install --path . --root \"$PEKIT_OUT\""

[clean]
command = "cargo clean"
```

The top-level keys break down as follows:

- **`out_dir`** — the directory pekit stages everything under, relative to the recipe unless absolute. Defaults to `"out"`. This is pekit's **managed** output: `pekit clean` may remove it wholesale, so keep nothing precious there. It holds fetched sources, per-target staging directories, and written `.peipkg` files.
- **`[env]`** — a table of `NAME = "value"` pairs exported into every target command. Names must be valid shell identifiers, and pekit reserves the `PEKIT_` prefix — you cannot set a `PEKIT_*` variable here. See [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md).
- **`[wrap]`** — a single `command` that every target runs *inside*. The string must contain exactly one `{{command}}` placeholder, which pekit replaces with the target's script. Use it to enter a toolchain shell, a container, or a sandbox.
- **`[source]`** — declares where the source tree comes from: `[source.git]`, `[source.url]`, or `[source.local]`. Absent, the recipe builds against its own directory. Covered in [Sources](/pekit/recipes/sources.md).
- **`[delegate]`** — opt in to borrowing `build`, `env`, `wrap`, or `packages` behaviour from a `pekit.toml` found inside the fetched source tree (or `delegate = true` for all of it).
- **`[source_package]`** — opt out of, or rename, the corresponding-source package a recipe with a reproducible source emits alongside its binary packages. See [Signing and provenance](/pekit/running/signing-and-provenance.md).
- **`[build]` / `[test]` / `[install]` / `[clean]`** — the target sections. See below.

## Targets are shell commands

Each of `build`, `test`, `install`, and `clean` defines one or more **targets**. A target is fundamentally a shell command plus a little scheduling metadata:

- **`command`** *(required)* — the command to run. A **string** is executed with `sh -euc` (POSIX shell, with `-e` abort-on-error and `-u` unset-variable-is-error). An **array of strings** is executed directly as a program and arguments, without a shell, unless a wrapper is in play.
- **`needs`** — a list of other target names in the same section to stage first. Each named build a target needs is exposed to it as `PEKIT_<NAME>_OUT` (see below).
- **`clear_out`** — whether pekit wipes this target's staging directory before running (default `true`).
- **`dependencies`** — *(build targets only)* package dependencies to resolve and expose to the build. See [Dependencies and claims](/pekit/recipes/dependencies-and-claims.md).

A section can be written in **bare** form, with the keys directly under it (an implicit target named `main`):

```toml
[build]
command = "make"
```

…or in **named** form, one subtable per target:

```toml
[build.main]
command = "make"

[build.docs]
command = "make man"
needs = ["main"]
```

The two forms cannot be mixed within one section: a bare `[build]` with a `command` may not also carry a `[build.docs]` subtable.

### The `PEKIT_*` environment contract

This is the key interface between pekit and your build scripts. Before running a target, pekit stages a directory for it and exports a set of `PEKIT_*` variables describing where everything is. Your `command` reads them as ordinary shell variables — `$PEKIT_OUT`, `$PEKIT_VERSION`, and so on. **The build writes its artifacts into `$PEKIT_OUT`; that staged directory is what the package files later draw from.**

The target command runs with its working directory set to the source root (`$PEKIT_SOURCE_ROOT`) — except `clean`, which runs in the recipe directory.

The variables a recipe author reaches for most are these — a subset of the full contract:

| Variable | Meaning | When set |
|---|---|---|
| `PEKIT_OUT` | This target's own staging directory. Write your build output here. | Every target |
| `PEKIT_RECIPE_ROOT` | Directory containing `pekit.toml`. | Every target |
| `PEKIT_SOURCE_ROOT` | Root of the source tree the command runs in (its working directory). Equals the recipe root when there is no external source. | Every target |
| `PEKIT_VERSION` (and `_MAJOR`, `_MINOR`, `_PATCH`, `_PRERELEASE`, `_BUILDMETA`) | The resolved version string being built, and its parsed components. | When a version is resolved (absent for `clean`, `gen`, and `verify`, which never resolve one) |
| `PEKIT_<NAME>_OUT` | The staging directory of build target `<NAME>` named in this target's `needs`. `<NAME>` is upper-cased with `-` and `.` turned into `_`. | Per entry in `needs` |

The full contract — every managed variable, its exact value, and when it is set — is the [environment contract in the command-line reference](/pekit/reference/cli.md). It also covers the timestamp variables, the other root and output paths, and the `PEKIT_DEPENDENCIES*` variables that carry [build dependencies](/pekit/recipes/dependencies-and-claims.md) into the command.

`env.pekit.toml` and keyring files layer more variables on top (`PEKIT_KEYRING_*`, plus any user env). User-set `[env]` values never override a `PEKIT_*` variable — that prefix is reserved.

### Shell variables, not templates

Inside a target `command` you use **shell** variables: `$PEKIT_OUT`, `$PEKIT_VERSION`. Pekit does **not** run its own templating over commands — they are handed to the shell verbatim (after the `PEKIT_*` exports are prepended). The `{{...}}` syntax you see in fields like `source.git.ref = "{{version}}"` is a separate, **declarative** template mechanism that applies to specific recipe *fields*, not to shell commands. Keep the two straight: declarative fields template with `{{...}}`; shell commands read `$PEKIT_*`. See [Versions](/pekit/recipes/versions.md) for the declarative side.

## How pekit finds the recipe

By default pekit walks **up** from the current working directory looking for `pekit.toml`, stopping at the first one it finds (the same upward search then looks for a `workspace.pekit.toml` above it). So running a pekit command anywhere inside a recipe tree finds the right recipe.

You can override this:

- `--recipe <path>` points at a specific `pekit.toml` (or a directory containing one).
- A **remote locator** — a `github.com/...` path, a `.git` URL, or a `scheme://...` URL, optionally with a `@ref` and a `//subdir` — makes pekit clone the recipe from a git remote before running it.

The full flag and locator surface is on the [invocation](/pekit/running/invocation.md) page.

## Where to go next

- [Sources](/pekit/recipes/sources.md) — the `[source]` section in depth.
- [Package files](/pekit/recipes/packages.md) — turning `$PEKIT_OUT` into `.peipkg` payloads.
- [Recipe format reference](/pekit/reference/recipe-format.md) — the exhaustive `pekit.toml` schema.

---

# Sources

_pekit / Writing recipes_

> How a recipe gets its source tree: the git, url, and local kinds, materialisation and caching, the lockfile, patches, enumeration, and delegation.

A **source** tells pekit where a recipe's code comes from. Before any build
target runs, pekit resolves the source into a concrete **source tree** on
disk — the directory that targets execute in and that [`@source:` package
references](/pekit/recipes/packages.md) read from. This page covers the source
kinds, how each is materialised, and how a recipe can borrow behaviour from its
source tree.

Sources are declared under `[source.*]` in the recipe. A recipe may have:

- **no external source** (sourceless) — the recipe directory itself is the tree;
- **one reproducible source** — either `[source.git]` **or** `[source.url]`,
  never both;
- **optionally a local override** — `[source.local]`, on its own or alongside a
  reproducible source.

A source is **reproducible** when it is git or url — those pin to exact
bytes. Declaring two reproducible tables is an error (`mixed_source`:
"exactly one reproducible source table is allowed").

## The three source kinds

### `[source.git]`

Clones a git repository and checks out a ref. It takes exactly four fields —
a required `url` (passed to `git clone --mirror`), a `ref` to check out
(templated with the [selected version](/pekit/recipes/versions.md); defaults to
`{{version}}` and must render non-empty), a `versions` **cap** that filters
enumerated or requested versions, and a `tag_regex` used when enumerating tags
(see [Enumeration](#enumeration)). The field-by-field schema is in the
[recipe format reference](/pekit/reference/recipe-format.md).

```toml
[source.git]
url = "https://github.com/example/widget.git"
ref = "v{{version}}"
versions = ">=2.0.0"
tag_regex = "^v([0-9]+\\.[0-9]+\\.[0-9]+)$"
```

If `ref` renders empty (for example, a bare `{{version}}` with no version
selected) pekit fails with `missing_version` — pass `--version` or set a
non-templated `ref`.

> [!NOTE]
> There is **no** submodule option. `[source.git]` accepts
> only the four fields above; a `submodules` key is an `unknown_key` error.

### `[source.url]`

Downloads a file over HTTP(S), optionally extracting it as an archive. The
required `url` is templated with the selected version; `extract` (default
`false`) treats the download as an archive, with `root` naming the subdirectory
**inside the archive** to promote as the source tree; `versions` is a version
cap (as for git); `file_regex` is used when enumerating a URL listing; and
`checksum` pins the download to an expected digest — a single string or a
per-version table (see below). The full schema is in the
[recipe format reference](/pekit/reference/recipe-format.md).

```toml
[source.url]
url = "https://ftp.example.org/widget/widget-{{version}}.tar.gz"
extract = true
root = "widget-{{version}}"
checksum = "sha256:0f1e2d..."
```

`checksum` is a `sha256:<hex>` string, and it is the **only** algorithm the
verifier accepts — any other prefix fails with "unsupported checksum spec". It
may instead be a table keyed by version:

```toml
[source.url.checksum]
"1.2.0" = "sha256:aaaa..."
"1.3.0" = "sha256:bbbb..."
```

When the table form is used and the selected version has no entry, pekit fails
with `missing_checksum`. You rarely need `checksum`, though: every fetched
version is pinned automatically in the recipe's
[lockfile](#the-lockfile), and a URL source is only materialised
**unanchored** — not pinned to a digest — when it has neither a checksum nor a
lock entry, which matters for
[reproducible packaging](/pekit/recipes/packages.md).

A URL source can also carry a `[source.url.signature]` table pinning the
upstream maintainer's release-signing key: pekit fetches the detached
signature beside the artifact and verifies it before anything is locked or
built, closing the trust-on-first-use gap for brand-new versions. The full
schema is in the [recipe format reference](/pekit/reference/recipe-format.md).

### `[source.local]`

Points at a directory already on disk. Its single field is a required `path` —
the directory to use as the source tree, resolved relative to the recipe
directory.

```toml
[source.local]
path = "../widget-checkout"
```

`[source.local]` is an **override**, not a reproducible source: a recipe
with only `[source.local]` cannot be pinned or enumerated. The local tree is
only used when a local flag selects it — see below.

## Selecting the local override

Two invocation flags choose the local override; they resolve differently and are
mutually exclusive (passing both is an error).

- **`--local`** (strict): require a local source. If neither `[source.local]`
  nor `--local=<path>` supplies a directory, pekit fails with
  `missing_local_source`. Use this to force building from a working checkout.
- **`--prefer-local`**: use the local source **if it is usable**, otherwise fall
  back to the reproducible source. If the local tree is missing and the recipe
  has no reproducible source, the fallback fails.

Both accept an inline path:

- `--local` / `--prefer-local` with **no** `=<path>` uses the directory from
  `[source.local]` (resolved relative to the recipe directory).
- `--local=<path>` / `--prefer-local=<path>` uses `<path>` resolved relative to
  the **invocation's working directory** (`cwd`), ignoring `[source.local]`.

The chosen directory must exist and be a readable directory, or pekit errors.
Local sources require an **exact** `--version` — `--latest`, `--all-versions`,
and constraint selectors are rejected (`unsupported_version_mode`); with no
version, a local build uses the placeholder `0.0.0-localdev`.

For a **sourceless** recipe, passing a local flag is an `unsupported_flag`
error — unless `--allow-unused` is set, in which case the flag is ignored with a
warning and the recipe stays sourceless.

## Materialisation

Resolving a source produces a source state: its kind, the source root
(where targets run), a provenance ref, and a timestamp. What pekit does to
produce the tree depends on the kind.

**Sourceless** (`recipe`). No work: the source root **is** the recipe
directory. Provenance is `recipe:<recipe-root>`.

**Local** (`local`). No copy or fetch: the source root **is** the local
directory itself; targets run in place. Provenance is `local:<path>`. Because
nothing is fetched, local sources are not cached and `--refresh-source` has no
effect on them.

**Git** (`git`). pekit keeps a bare **mirror** clone under
`<out_dir>/_source_cache/git/<hash>/repo.git`:

1. If the mirror does not exist, `git clone --mirror <url>`; otherwise
   `git fetch --prune --tags` to update it. A **failed refresh** is fatal
   only when nothing pins what the ref means: if the selected version is
   locked, the entry matches the rendered ref, and the pinned commit is
   already in the mirror, pekit warns and builds from the locked commit
   instead — so a rate-limited or unreachable upstream cannot block a
   locked build. Unlocked resolves still fail hard.
2. Resolve `ref` to an immutable commit (`git rev-parse <ref>^{commit}`).
3. When a version is selected, verify the resolved commit against the
   version's [lockfile entry](#the-lockfile) — or create the entry on first
   resolve.
4. Clone the mirror into `<out_dir>/git-<hash>/source`, then
   `git reset --hard <commit>` and `git clean -fdx` to pin the checkout.
5. Apply the recipe's [patch series](#patches), when one is declared. Reset
   and clean restored the pristine tree, so the series re-applies on every
   resolve.
6. Write a `source.pekit.json` manifest recording the URL, ref, resolved
   commit, and provenance.

Provenance is `git:<url>@<commit>` and the timestamp is the commit's committer
date, so a git build is anchored to a specific commit even when `ref` was a
branch or tag.

**URL** (`url`). pekit caches the downloaded artifact under
`<out_dir>/_source_cache/url/<hash>/`:

1. If the artifact is not already cached, download it (with a `pekit/2`
   user-agent).
2. If a `checksum` is set, verify it; on mismatch, re-download once and verify
   again before failing.
3. Verify the artifact against the version's [lockfile entry](#the-lockfile),
   cache hits included — or create the entry on first resolve, verifying the
   upstream signature first when `[source.url.signature]` is configured.
4. Materialise the tree at `<out_dir>/url-<hash>/source`: if `extract` is set,
   extract the archive to a temporary directory, then promote the `root`
   subdirectory (which must exist, else `missing_source_root`); otherwise copy
   the raw artifact into the source directory. Extraction preserves each
   regular file's archived mtime — release tarballs encode "generated
   outputs are newer than their inputs" in timestamps, and losing that
   fires autotools maintainer rebuild rules in environments without the
   maintainer tools.
5. Apply the recipe's [patch series](#patches), when one is declared. The
   series' content hash joins the materialisation scope, so an edited patch
   lands in a fresh extraction.
6. Write a `source.pekit.json` manifest.

The **downloaded artifact** is cached regardless. The **extracted tree** is
re-materialised from that cached artifact on each run when a checksum is set
(the pinned content is deterministic anyway); without a checksum the tree is
reused as long as the recorded manifest still matches.

### Caching and `--refresh-source`

Pekit does not expose configurable cache policies. Caching is exactly the
behaviour above — a persistent raw cache (mirror repo / downloaded artifact)
plus a materialised tree — and the one knob is **`--refresh-source`**, which
discards the cache and rebuilds from scratch:

- git: removes the mirror repo **and** the checkout scope, forcing a fresh
  `clone --mirror` and checkout.
- url: removes the cached artifact **and** the materialised scope, forcing a
  re-download and re-extract.
- local / sourceless: nothing to refresh.

A refresh does not relax the lock: the fresh download is still verified
against the lockfile, so `--refresh-source` is how you *check* upstream and
`pekit lock --repin` is how you *accept* a change.

### The lockfile

Fetched inputs are pinned **trust-on-first-use** in a machine-written
`pekit.lock` beside `pekit.toml`. The first time a source resolves for a
version, pekit records what it fetched — the artifact's SHA-256 for a url
source, the resolved commit for a git source — and every later resolve
verifies against that entry instead, cache hits included. A mismatch is a hard
`lock_mismatch` stop: upstream's published bytes (or a tag) changed under a
version that was already pinned. Accepting such a change is an explicit
ceremony, never automatic:

```text
pekit lock --repin --version 1.2.0
```

Commit the lockfile with the recipe. Local sources, dry runs, and git sources
without a selected version (a bare branch ref is a deliberately moving target)
are never locked. The file's exact schema is in
[Supporting files](/pekit/reference/supporting-files.md#pekitlock); the `lock`
command — including pre-locking versions without building — is in the
[command-line reference](/pekit/reference/cli.md).

### Source packages

A recipe with a reproducible source automatically emits a
**corresponding-source package** alongside its `peipkg`-format members — the
pristine upstream artifact, the patch series, and the recipe files, verifiable
against the committed `pekit.lock` — and every emitted manifest records the
provenance of its inputs, recipe, and builder. Both are covered in
[Signing and provenance](/pekit/running/signing-and-provenance.md); opt out or
rename with the recipe's
[`[source_package]`](/pekit/reference/recipe-format.md#source_package) table.

### Archive extraction

Extraction is chosen by the artifact's **filename suffix**. Only these formats
are handled; anything else is an `unsupported_archive` error:

| Suffix | Handler |
| --- | --- |
| `.zip` | native zip reader |
| `.tar` | uncompressed tar |
| `.tar.gz`, `.tgz`, `.crate` | gzip + tar (`.crate` is cargo's publish format — a plain gzipped tarball) |
| `.tar.bz2`, `.tbz2` | bzip2 + tar |
| `.tar.zst` | zstd + tar |
| `.tar.xz`, `.txz` | tar piped through the external `xz -dc` (requires `xz` on `PATH`) |

Extraction is hardened: entries that escape the extraction root (absolute
paths, `..`, or traversal through a symlinked parent), unsafe symlink targets,
NUL bytes, unsupported entry types, and colliding entries are all rejected as
`unsafe_archive`.

## Patches

A recipe can carry a patch series that pekit applies to the materialised
tree, declared as a directory name under `[source]`:

```toml
[source]
patches = "patches"
```

The directory's `series` file lists patch files in apply order (one relative
path per line; `#` starts a comment). pekit applies them with `git apply` —
strictly, with no fuzz — immediately after the pristine tree is materialised
and before any target runs, so targets always see the patched tree and
`@source:` mappings package patched files. How that interacts with caching
follows the source kind:

- **git** sources re-apply the series on every resolve: the checkout is
  reset to the pinned commit and cleaned first, so an edited patch takes
  effect on the next run.
- **url** sources materialise once per patch-set content: the series' hash
  joins the materialisation scope, so an edited patch extracts and patches a
  fresh tree.
- **local** sources are never patched — a local tree is your own working
  state, often with the series already applied or mid-rework. pekit emits a
  warning and continues.

A failed hunk is `patch_apply`; a patch that matches nothing is
`patch_skipped`; either aborts materialisation and leaves no half-patched
tree behind. The series grammar and its validation errors are in
[Supporting files](/pekit/reference/supporting-files.md#the-patches-directory).

Patches are committed recipe content, so the [lockfile](#the-lockfile) is
uninvolved: it pins fetched bytes, and the patch series is already under
version control. The whole directory ships in the recipe's
[source package](#source-packages) under `patches/` — the shipped series is
the applied series by construction.

Rebasing on a version bump is deliberately manual: a stale patch fails
loudly, so materialise the new tree (`pekit lock --version <v>` is enough),
fix the patch, and run again.

## Enumeration

Reproducible sources can list the versions they offer upstream; this feeds
`--latest`, `--all-versions`, and constraint selectors (see
[Versions](/pekit/recipes/versions.md)).

- **git**: `git ls-remote --tags <url>`, with tags optionally filtered by
  `tag_regex`. If the regex has a capture group, group 1 is the version;
  otherwise an embedded `MAJOR.MINOR.PATCH` is extracted.
- **url**: fetch the directory listing derived from the `url` template (the part
  before the first `{{…}}`, up to the last `/`), filter entries by `file_regex`,
  and extract versions from the matches.

Local and sourceless recipes cannot enumerate
(`version_enumeration_unavailable`) — they require an exact version. The
`versions` field on a git or url source acts as a **cap** applied to the
enumerated (or requested) set; version selection itself is documented on the
[Versions](/pekit/recipes/versions.md) page.

## Source roots and provenance

The **source root** is the directory targets execute in and the base that
[`@source:` file references](/pekit/recipes/packages.md) resolve against. Every
resolution records a **provenance ref** identifying exactly what was
materialised:

| Kind | Provenance ref |
| --- | --- |
| sourceless | `recipe:<recipe-root>` |
| local | `local:<path>` |
| git | `git:<url>@<commit>` |
| url (checksummed) | `url:<url>#<checksum>` |
| url (locked, no checksum) | `url:<url>#sha256:<hash>` |
| url (no checksum, no lock entry) | `url:<url>` (marked **unanchored** — reachable only in a dry run, since a real resolve locks) |

For git and url sources the provenance is also written to a `source.pekit.json`
manifest beside the materialised tree, and pekit emits it as a `source` event
under `--verbose`.

## Delegation

A recipe can **borrow** behaviour from a `pekit.toml` that lives inside its
materialised source tree, using the `[delegate]` table (or the shorthand
`delegate = true`, which enables everything):

```toml
delegate = true

# or, selectively:
[delegate]
build    = true
env      = true
wrap     = true
packages = true
```

`[delegate]` has four surfaces plus `all`: `build` borrows the
build/test/install/clean **targets** from the source's recipe, `env` its
environment variables, `wrap` its command wrapper, and `packages` the package
definitions discovered in the source tree; `all` enables all four. (The key
table is in the [recipe format reference](/pekit/reference/recipe-format.md).)

Delegation only happens when the source tree is a **different** directory from
the recipe (so a sourceless or in-place local recipe never delegates), and, for
build targets, only when a `pekit.toml` exists at the source root. The merge is **additive with the base recipe winning**: a
delegated target is adopted only if the base recipe does not already define a
target of the same kind and name, and adopted targets are tagged as owned by the
`source`. Env, wrap, and package delegation are likewise gated on their
corresponding `[delegate]` flags and only apply when the source tree differs
from the recipe root.

This lets a thin Peios recipe wrap upstream software that already ships its own
pekit recipe: point `[source.git]` at the upstream repo, set `delegate = true`,
and reuse its build and packaging while overriding only what you need.

## Where to go next

For how enumerated versions are selected and rendered, read [Versions](/pekit/recipes/versions.md).

For turning the materialised tree into package payloads, read [Packaging files](/pekit/recipes/packages.md).

For the exhaustive `[source]` schema, read [Recipe format reference](/pekit/reference/recipe-format.md).

---

# Versions

_pekit / Writing recipes_

> The version grammar, the --version/--latest/--all-versions selectors, the trailing-zero ladder, source version caps, and the version template variables.

A pekit build is almost always **about a version** of some upstream software. The
recipe declares where source of a given version comes from and how to name the
packages it produces; the invocation decides *which* versions are in scope. This
page covers the version grammar pekit accepts, the `{{...}}` variables that carry
version components into your recipe, and how the `--version`, `--latest`, and
`--all-versions` selectors resolve against a source.

## Version forms

A version is written as:

```text
MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILDMETA]
```

Only `MAJOR` is mandatory. Minor and patch are each optional but ordered — you
cannot write a patch without a minor. The optional `-PRERELEASE` and `+BUILDMETA`
tails may each contain digits, ASCII letters, dots, and hyphens.

| Written version    | major | minor | patch | prerelease | buildmeta |
| ------------------ | ----- | ----- | ----- | ---------- | --------- |
| `2`                | `2`   |       |       |            |           |
| `2.43`             | `2`   | `43`  |       |            |           |
| `2.43.1`           | `2`   | `43`  | `1`   |            |           |
| `1.21.0-rc.1`      | `1`   | `21`  | `0`   | `rc.1`     |           |
| `1.21.0+build.5`   | `1`   | `21`  | `0`   |            | `build.5` |
| `1.21.0-rc.1+bld`  | `1`   | `21`  | `0`   | `rc.1`     | `bld`     |

Anything that does not match this grammar is rejected with an `invalid_version`
error. Comparison and ordering are numeric on major/minor/patch (a missing
component counts as `0`), with the prerelease string compared lexically to break
ties.

## Version template variables

Six variables expose the parsed components of the selected version. They render
anywhere pekit expands `{{...}}` templates in a recipe — most importantly in
[source](/pekit/recipes/sources.md) refs and URLs, and in
[package](/pekit/recipes/packages.md) version and metadata fields. (These six are
the version-derived subset of the template engine; the complete variable table,
including the multi-package-only `{{multipack}}`, is in
[Supporting files](/pekit/reference/supporting-files.md).)

| Variable          | Expands to                       |
| ----------------- | -------------------------------- |
| `{{version}}`     | the version exactly as written   |
| `{{major}}`       | the major component              |
| `{{minor}}`       | the minor component              |
| `{{patch}}`       | the patch component              |
| `{{prerelease}}`  | the prerelease tail, or empty    |
| `{{buildmeta}}`   | the build-metadata tail, or empty |

```toml
[source.git]
url = "https://github.com/example/tool.git"
ref = "v{{version}}"
```

Two rules govern rendering:

- **Referencing a component the version does not have is an error.** `{{version}}`
  fails when no version is selected at all; `{{minor}}` and `{{patch}}` fail when
  the selected version stops short of that component (for example `{{patch}}`
  against `2.43`). `{{prerelease}}` and `{{buildmeta}}` are the exception — they
  render as empty text when absent rather than erroring.
- **Unknown variables are an error.** Any `{{name}}` that is not one of the six
  above (or the multi-package `{{multipack}}` token) is rejected. There is no
  silent pass-through.

> [!IMPORTANT]
> Version variables do **not** apply to shell target commands. A `command`
> in a build/test/install target receives version data through the
> `PEKIT_VERSION`, `PEKIT_VERSION_MAJOR`, `PEKIT_VERSION_MINOR`,
> `PEKIT_VERSION_PATCH`, `PEKIT_VERSION_PRERELEASE`, and
> `PEKIT_VERSION_BUILDMETA` environment variables instead — see
> [recipe anatomy](/pekit/recipes/anatomy.md).

## Selecting versions

Three mutually exclusive flags choose the version set. Supplying more than one is
rejected with `--version, --latest, and --all-versions are mutually exclusive`.

| Flag                    | Selects                                                        | Needs an enumerable source? |
| ----------------------- | ------------------------------------------------------------- | --------------------------- |
| `--version <selector>`  | exact version(s), or a semver constraint                      | only for constraints        |
| `--latest`              | the single highest available version                          | yes                         |
| `--all-versions`        | every available version                                       | yes                         |
| *(none)*                | an unversioned build (no `{{version}}` available)             | no                          |

**Enumerable sources** are those pekit can list versions for: a `[source.git]`
source (via `git ls-remote --tags`) or a `[source.url]` source (by fetching the
listing directory). `--latest`, `--all-versions`, and any constraint require one;
against a non-enumerable source they fail with `selected source cannot enumerate
versions`.

### `--version` as an exact selector

The simplest form is one or more exact versions, comma-separated:

```text
pekit build tool --version 2.43.1
pekit build tool --version 2.43.1,2.42.0
```

A comma-separated list without spaces is treated as a set of exact versions. When
the source is reproducible and enumerable, each written version is matched
against the versions the source actually publishes using the trailing-zero ladder
described below.

### `--version` as a constraint

A `--version` value that contains a space or a comparison operator
(`<`, `>`, `=`, `<=`, `>=`) is treated as a **constraint** and takes the
enumeration path. Constraints filter the enumerated set:

```text
pekit build tool --version ">= 2.40"
pekit build tool --version ">= 2.40, < 3.0"
```

Multiple constraints (comma- or space-separated) are ANDed together. The `~` and
`^` operators are **not** supported and produce
`unsupported version constraint operator`. A bare `*` matches everything.

### `--latest` and `--all-versions`

```text
pekit build tool --latest          # highest available version only
pekit package tool --all-versions  # every available version
```

`--latest` picks the single greatest version after enumeration and filtering.
`--all-versions` selects the whole set.

## The trailing-zero ladder

Upstreams are inconsistent about trailing zeros: one project tags `v2.43.0`,
another tags `v2.43`, a third tags `v2`. So when you ask for an exact version
against a reproducible, enumerable source, pekit does not demand a character-exact
tag. It builds a ladder of candidates from your written version, most specific
first, and picks the first candidate the source actually offers:

| You write | Candidates tried, in order |
| --------- | -------------------------- |
| `2.43.0`  | `2.43.0`, then `2.43`      |
| `2.0.0`   | `2.0.0`, then `2.0`, then `2` |
| `2.0`     | `2.0`, then `2`            |
| `2.43.1`  | `2.43.1` only              |

Only trailing **zero** components are dropped — a non-zero patch or minor is never
elided. The ladder is also **skipped entirely** when the version carries a
prerelease or build-metadata tail: `1.21.0-rc.1` is only ever matched as written.
If no candidate is available (or the source is not reproducible/enumerable), the
version you wrote is used verbatim.

The ladder applies to the exact-selector path only. `--latest`,
`--all-versions`, and constraints work on the enumerated set directly and do not
ladder.

## Source version caps

A source can declare a hard ceiling (or floor) on the versions a recipe will ever
build, independent of what the invocation asks for. Set `versions` under the
source table:

```toml
[source.git]
url = "https://github.com/example/tool.git"
ref = "v{{version}}"
versions = ">= 1.0, < 3.0"
```

The cap uses the same constraint grammar as `--version`. It is applied on both
paths:

- On the enumeration path (`--latest`, `--all-versions`, constraints) the cap
  filters the available set *before* the selector runs, so `--latest` returns the
  highest version *within* the cap.
- On the exact-selector path the cap filters the versions you named. If every
  named version falls outside the cap the build fails with `selected exact
  versions were filtered out by source version cap`.

An empty result after capping is always an error — a cap that excludes everything
is treated as a misconfiguration, not a no-op.

## Local builds

When the [source](/pekit/recipes/sources.md) resolves to a local directory, there is
nothing to enumerate. Local sources therefore require an **exact** `--version`;
`--latest`, `--all-versions`, and constraints fail with `local sources require an
exact --version`.

If you build locally without giving a `--version` at all, pekit stamps the build
with the sentinel version **`0.0.0-localdev`** so that `{{version}}` and the
`PEKIT_VERSION*` variables still resolve. This makes local development builds work
without inventing a version number, while keeping them clearly distinguishable
from real releases.

## Single-version commands

`build`, `package`, and `publish` operate on many versions at once — pass
`--all-versions` and pekit plans one build per version.

`test` and `install` do not. They require the selector to resolve to **exactly
one** version; if it resolves to more, the command fails with `test requires a
single resolved version` (or `install requires ...`). Use an exact `--version` or
`--latest` for these commands.

## Where to go next

For where a selected version's source tree comes from, read [Sources](/pekit/recipes/sources.md).

For how version components render into package names and metadata, read [Packaging files](/pekit/recipes/packages.md).

For the full flag surface behind `--version`, `--latest`, and `--all-versions`, read [Invocation and flags](/pekit/running/invocation.md).

---

# Packaging files

_pekit / Writing recipes_

> The [files] and [symlinks] tables, source-ref syntax, globs, excludes, and the destination and collision rules that turn staged output into a package payload.

A package definition answers one question: **which files, from where, go where in
the package?** Everything else — metadata, dependencies, claims, multi-package
fan-out — hangs off that payload. This page is about the payload itself: the
`[files]` and `[symlinks]` tables, how source references resolve, and the rules
that decide the final layout of the `.peipkg`.

Package definitions live in their own files, not in the recipe:
`package.pekit.toml` (the base definition) and `<name>.package.pekit.toml`
(named members). How those layer together, and how `[multipack]` expands one
definition into many packages, is covered in
[Multi-package recipes](/pekit/recipes/multi-package.md). Manifest metadata,
dependencies, and claims are covered in
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md). This page
assumes a single definition and concentrates on the file mapping.

## The `[files]` table

`[files]` maps sources to package destinations. The mapping direction is
**source on the left (the key), package destination on the right (the value)**:

```toml
[files]
":bin/hello"        = "usr/bin/hello"
"@recipe:default.conf" = "etc/hello/hello.conf"
```

The left side is a **source ref** naming what to copy; the right side is the
path the file takes **inside** the package. Destinations are always relative
(see [Destination rules](#destination-rules) below).

Each value may also be a table, which adds an `override` flag:

```toml
[files]
"@recipe:layout.json" = { path = "usr/share/hello/layout.json", override = true }
```

`path` is the package destination (required; an empty destination is an error),
and `override` — default `false` — exempts the entry from the package format's
layout policy (see [`override`](#override)). The bare-string form
`"src" = "dest"` is exactly `"src" = { path = "dest" }`. The field-by-field
schema is in the [recipe format reference](/pekit/reference/recipe-format.md).

## Source refs

A source ref names *where a file comes from*. There are three families.

### Build-output refs — `target:path`

A ref containing a colon is a **build ref**. The part before the colon is a
build target name; the part after is a path inside that target's staged output:

```toml
[files]
"tools:bin/hc" = "usr/bin/hc"   # from build target "tools"
":bin/hello"   = "usr/bin/hello" # empty target = "main"
```

`:path` is shorthand for `main:path` — an empty target name resolves to the
target `main`. Referencing a build target in `[files]` automatically pulls that
build into the package's build plan, so you do not have to list it separately.

### Rooted refs — `@source:`, `@recipe:`, `@workspace:`

A ref beginning with `@<root>:` is anchored to a named root rather than a build
output:

| Prefix | Root |
|---|---|
| `@source:` | The materialised source tree (the checked-out / extracted source). |
| `@recipe:` | The directory containing the recipe (and its package files). |
| `@workspace:` | The workspace root. Used outside a workspace, this is an error (`missing_workspace`). |

```toml
[files]
"@source:LICENSE"      = "usr/share/licenses/hello/LICENSE"
"@recipe:hello.service" = "usr/lib/systemd/system/hello.service"
```

The [reference table of source-ref roots](/pekit/reference/supporting-files.md)
gathers every ref form — build refs, the rooted prefixes, and bare paths — in
one place.

### Plain paths are owner-relative

A ref with **no** prefix and **no** colon is a plain path — and its meaning is
set by the file that *declared* it, not by any global default. At decode time,
pekit rewrites each plain path to a rooted ref based on the file's **owner**:

| The ref was declared in… | A plain path becomes… |
|---|---|
| a recipe-side package file | `@recipe:` |
| a delegated source-side package file | `@source:` |
| a workspace-side package file | `@workspace:` |

So in a recipe's own `package.pekit.toml`:

```toml
[files]
"README.md" = "usr/share/doc/hello/README.md"
```

`"README.md"` is normalised to `"@recipe:README.md"` — it resolves against the
recipe directory. The identical line in a source-delegated package file would
normalise to `"@source:README.md"` and resolve against the source tree instead.
This normalisation is what lets the same package file mean the right thing
whether it ships beside the recipe or is discovered inside a delegated source.

Refs that are already rooted (`@…:`) or already build refs (contain a `:`) are
left untouched by this rewrite.

> [!NOTE]
> The owner-relative rule also applies to the source path of `[multipack]` file
> enumeration and to every entry in `excludes` — both are normalised the same
> way.

### Path cleaning

The path portion of every rooted ref is cleaned: `.` and duplicated slashes are
collapsed, and a path that escapes its root with `..` (or is absolute) is
rejected:

```text
invalid_ref: "@recipe:../secrets" escapes its root
```

## Globs

A source ref's path may contain glob metacharacters — `*`, `?`, or a `[...]`
character class — and `**` for recursive matching:

```toml
[files]
"@source:share/icons/*.png" = "usr/share/hello/icons/"
"tools:lib/**/*.so"         = "usr/lib/"
```

Rules:

- Matching happens **only within the ref's source root**. A glob never reaches
  outside the root it is anchored to.
- Matches are **sorted**, and a match already covered by a matched parent
  directory is dropped (the directory carries its contents).
- By default a glob that matches **nothing** is an error:

  ```text
  missing_payload: file source /…/share/icons has no matches
  ```

- Placement is **relative to the non-glob prefix**. Everything up to the first
  segment containing a glob metacharacter is the base; matched paths are re-rooted under
  the destination from there. So `share/icons/*.png` → `usr/share/hello/icons/`
  places `foo.png` at `usr/share/hello/icons/foo.png`, and
  `lib/**/*.so` preserves the sub-directory structure below `lib/` under the
  destination.

A plain (non-glob) ref that names a **directory** copies the whole tree; empty
directories are emitted as explicit entries (so a pure directory skeleton is not
lost).

## Excludes

`excludes` prunes files after expansion. Each entry uses the **same source-ref
syntax** as `[files]` (and is owner-normalised the same way), so an exclude is
scoped to a root:

```toml
[files]
"@source:share/**" = "usr/share/hello/"

excludes = [
  "@source:share/**/*.tmp",
  "@source:share/internal/**",
]
```

An exclude is applied only to expansions that share its root; it is matched
against each candidate with the same glob engine. Unlike `[files]`, an exclude
that matches **nothing is allowed** — it is a filter, not a required input.

## Destination rules

The right-hand side of a `[files]` entry (and every symlink destination) is a
path **inside** the package. It must be relative and stay in-bounds:

- **No leading `/`** and **no `..`** — an absolute or escaping destination is
  rejected.
- **No NUL** bytes; an empty destination is an error.
- `.` segments and duplicate slashes are normalised away.

### Directory vs single-file destinations

For a **single, non-glob** match, the trailing slash on the destination decides
the shape:

| Ref | Destination | Result |
|---|---|---|
| `":bin/hello"` | `"usr/bin/hello"` | Renamed to `usr/bin/hello`. |
| `":bin/hello"` | `"usr/bin/"` | Placed as `usr/bin/hello` (basename kept). |

For a **glob** or a **multi-match** source, the destination is always treated as
a directory prefix and matched paths are re-rooted beneath it.

### Collisions are errors

Two payload inputs that resolve to the **same** package path is a hard error —
pekit will not silently let one input overwrite another:

```text
payload_collision: multiple package inputs map to usr/bin/hello: …
```

(Sibling checks catch two package instances writing the same artifact file, or
emitting the same package name.)

### `override`

`override = true` exempts an entry from the **package format's layout policy**
only. The peipkg format validates that non-override payload paths obey its
layout rules; an override entry skips that validation, letting you place a file
somewhere the policy would otherwise reject.

Override does **not** relax the safety rules above: destinations are still
cleaned, still may not escape the package with `..`, and collisions are still
errors. It only opts out of format-level layout policy.

## `[symlinks]`

`[symlinks]` adds symbolic links to the payload. Here the **key is the package
destination** (where the link lives) and the **value is the link's target text**:

```toml
[symlinks]
"usr/bin/hello-latest" = "hello"
"usr/lib/libhello.so"  = { target = "libhello.so.1", override = true }
```

The target text is stored **literally** — it is the exact string the symlink
points at and is *not* resolved through any source root. It may be relative or
absolute as a link target; pekit only checks that it is non-empty and contains
no NUL. The bare-string form is `"dest" = "target"`; the table form adds
`override`, which behaves as for files.

The destination key follows the same [destination rules](#destination-rules) as
`[files]`.

## The result

Packaging stages the resolved payload and writes one package artifact per
package instance under the recipe's output directory (`out_dir`). The format is
chosen by the package's `format` (defaulting to `tar`); a `peipkg` package emits
a file named `<name>_<version>_<architecture>.peipkg`, with the name,
architecture, and other manifest fields coming from `[package]`. A `peipkg`
package must set `[package].version`, `[package].architecture`, and
`[package].license`; a `tar` artifact carries payload only and is normalised
for determinism (entry timestamps zeroed, ownership dropped).

The `.peipkg` on-disk format itself is documented in
[Package management](/peios/using-peios/package-management/overview.md).

## Where to go next

For the rest of the manifest — dependencies, provides, claims — read
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md).

For splitting one recipe into several packages, read
[Multi-package recipes](/pekit/recipes/multi-package.md).

For the exhaustive key-by-key schema, read the
[recipe format reference](/pekit/reference/recipe-format.md).

---

# Dependencies and claims

_pekit / Writing recipes_

> How a package declares what it needs, what pekit derives from the built binaries, and how packages take part in shared filesystem names through claims.

A finished `.peipkg` carries a manifest that says what the package **needs** (its
dependencies), what shared names it **fills**, and how it plugs into names other
packages share. Pekit builds that manifest from three sources, in order of
increasing automation:

1. **Declared package dependencies** — `[dependencies]` and
   `[optional_dependencies]` you write in a package file.
2. **Derived dependencies** — sonames and pkgconfig capabilities pekit reads out
   of the staged binaries at pack time, added on top of what you declared.
3. **Claims** — the `[claims]` table, wiring a package's provides and
   dependencies into the peipkg "exactly one holder of a shared name" mechanism.

A separate, unrelated mechanism — **build dependencies** exported to the build
command — is also covered here because it shares the word "dependency". It never
touches the package manifest.

## Declared package dependencies

Package dependencies live in a **package file** (`package.pekit.toml` or a
`<name>.package.pekit.toml` member), not in the recipe. Two tables produce them:
`[dependencies]` for hard requirements and `[optional_dependencies]` for soft
ones. Both parse identically.

```toml
[dependencies]
libssl              = ">= 3.0"
"libc.so.6"         = "*"
"pkgconfig(zlib)"   = ">= 1.2.11"

[optional_dependencies]
bash-completion = "*"
```

Each entry maps a **name** to a **version constraint** string. The name is a
peipkg capability name — either a real package name or a virtual capability such
as a soname (`libc.so.6`) or a `pkgconfig(...)` token. The constraint is any
peipkg constraint expression (`">= 1.2"`, `"< 2"`, and so on). Both `"*"` and the
long form below with the constraint omitted mean **any version**.

### The table form: placing a dependency in a named root

An entry may instead be a table, which is the only way to attach a **placement
root** — the named root the dependency should be installed into, when it differs
from the depender's own root:

```toml
[dependencies]
# short form: constraint only, depender's own root
zlib = ">= 1.2"

# table form: constraint + explicit root
[dependencies.libfoo]
constraint = ">= 3"
root       = "system.lib"
```

A dependency table accepts exactly two keys, `constraint` and `root`; any other
key is an error. Omitting `constraint` matches any version. The `root` is a
**named** root reference (dotted lowercase segments, e.g. `system.lib`), never a
filesystem path — a `/` in it is rejected at build time. Roots are not carried
for `conflicts`, which stay root-local. There is no in-string `IN root` sugar;
the table is the only spelling.

These entries become the manifest's `Dependencies` and `OptionalDependencies`.
How they are matched to concrete packages at install time — capability lookup,
version satisfaction, root placement — is
[dependency resolution](/peios/using-peios/package-management/dependency-resolution.md) in
peipkg; pekit only records the requirement.

## Derived dependencies: ELF and pkgconfig scanning

At **pack time**, after staging the payload and before writing the archive,
pekit scans the files it is about to pack and **adds** derived provides and
dependencies on top of whatever the package declared by hand. Two derivations
run, both operating purely over the staged files:

- **ELF derivation** — reads the built binaries and shared objects.
- **pkgconfig derivation** — emits `pkgconfig(<name>)` capabilities from any
  `.pc` files in the payload.

The results are merged additively: a derived provide or dependency whose name
already appears in the hand-declared set is skipped, so declaring something by
hand always wins. Non-fatal problems (see below) are printed to stderr as
`warning:` lines; they do not stop the build.

### What ELF derivation detects

For every ELF object in the payload, pekit reads two dynamic-section fields:

| ELF field | Becomes | Meaning |
| --- | --- | --- |
| `DT_SONAME` | a **provides** entry | the object's own ABI name, e.g. `libfoo.so.3` |
| `DT_NEEDED` | a **dependency** entry | each shared library the object loads |

The **whole soname** is the capability name. ABI-version identity lives in the
string and is matched by equality, so a dependency on `libfoo.so.3` is never
satisfied by `libfoo.so.4` — a soname bump is a different capability.

Two refinements keep the result accurate:

- **Self-provided sonames are subtracted from the needs.** A package that ships
  both a library and something that links against it does not end up depending
  on itself.
- **Symlinks are skipped.** A `-devel` package's `libfoo.so -> libfoo.so.3`
  development symlink is not opened; following it would read the target's
  `DT_SONAME` and mis-attribute the provide to the package shipping the alias.
  The real object is derived where it actually lives.

If a file whose name looks like a shared library (`lib*.so`, `lib*.so.N…`)
carries no `DT_SONAME`, pekit warns that "nothing can depend on it" — such a
library is unusable as a dependency target.

Derivation is entirely automatic and takes **no configuration** beyond the
symbol-version policy below. In particular it has nothing to do with the
`dependency_provider` / `PEKIT_DEPENDENCIES*` mechanism described later — that is
a different feature that shares a word.

### Symbol version policy

By default a soname dependency is derived at **soname granularity only**: the
name must match exactly, with no version constraint. That is always safe but
coarse — it cannot express "needs at least the glibc that introduced
`GLIBC_2.34`".

A **workspace** may opt specific sonames into finer, version-aware derivation
through the `[policy.symbol_versions]` table in the workspace file. It maps a
soname to the **symbol-version token prefix** whose tokens are numbered on the
same scale as the providing package's version:

```toml
# in the workspace file
[policy.symbol_versions]
"libc.so.6"   = "GLIBC_"
"libfoo.so.1" = "FOO_"
```

For a soname listed here, ELF derivation is refined on both sides:

- **Consumer side** — pekit reads the object's versioned symbol needs, takes the
  highest token carrying the prefix (e.g. `GLIBC_2.34`), and turns it into a
  `>= 2.34` constraint on that dependency. Reserved, non-numeric tokens such as
  `GLIBC_PRIVATE` are ignored.
- **Provider side** — the soname's own provides entry is stamped with the
  building package's own version, so consumers have something to match against.

A soname **absent** from the policy is derived at soname granularity only, as
above. The policy is a distro-wide assertion and lives **only** in the workspace
file — `[policy]` currently accepts just the one sub-table, and any other key
under it is an error. Recipes and package files cannot set it. When no workspace
policy is configured, every soname is derived at name granularity.

## Build dependencies exported to the build command

Distinct from everything above, a **build** target in a recipe may declare
`[build.<target>.dependencies.<provider>]` — a set of dependencies grouped under
one or more **provider** names. These never enter the package manifest. Instead,
pekit renders them and hands them to the build command through the environment,
so a build script can fetch or pin its build-time inputs.

```toml
[build.main]
command = "make"

[build.main.dependencies.peipkg]
gcc  = ">= 13"
zlib = "*"        # "*" means any version; a build dep constraint may not be empty

[build.main.dependencies.system]
gcc = "*"
```

`dependencies` is only valid on **build** targets. Each provider block maps a
capability name to a non-empty constraint (use `"*"` for any). Names and
constraints are templated, so `{{version}}` and the other version variables are
available.

**Which provider is active** is chosen by the `dependency_provider` key in the
selected [environment file](/pekit/recipes/environments-and-keyrings.md):

```toml
# in env.pekit.toml (or a named <env>.env.pekit.toml)
dependency_provider = "peipkg"
```

For every target run, pekit writes a JSON payload to
`$PEKIT_ROOT/.pekit/dependencies/<command>.<target>.json` and exports three
variables into the build environment (with no provider selected,
`PEKIT_DEPENDENCY_PROVIDER` and `PEKIT_DEPENDENCIES` are empty strings and the
payload still carries `all_providers`):

| Variable | Contents |
| --- | --- |
| `PEKIT_DEPENDENCY_PROVIDER` | the selected provider name |
| `PEKIT_DEPENDENCIES_FILE` | absolute path to the JSON payload |
| `PEKIT_DEPENDENCIES` | the selected provider's deps, one `name constraint` per line |

These three are part of pekit's managed environment; the full
[environment contract](/pekit/reference/cli.md) lists every exported variable.

The JSON payload carries the `command`, `target`, `provider`, the resolved
`version`, the selected provider's `dependencies`, and `all_providers` (every
declared provider block, so a script can inspect alternatives). If
`dependency_provider` names a provider that a target with dependency blocks
does not define, that target's run fails (`missing_dependency_provider`); a
target with no dependency blocks at all ignores the selection. If no provider
is selected, the exported dependency list is empty but the payload still
lists `all_providers`.

Pekit does not resolve or install these — it only renders the declared list and
passes it through. Consuming it is the build script's job. `dependency_provider`
is a **name selector**, not a command or hook.

## Claims

A **claim** is how a package plugs into a shared filesystem name that at most one
installed package may hold at a time — the peipkg mechanism where several
packages can supply a name like `/usr/bin/cc`, but exactly one **holder** owns
the real file and the rest defer. See
[claims](/peios/using-peios/package-management/claims.md) for what this means at install time.

> [!IMPORTANT]
> Terminology: these are **claims**. Do not call them "roles" — in Peios
> operator documentation "role" is a different, reserved concept.

A package declares claims in the top-level `[claims]` table of a package file.
It has two sides:

- **`[claims.provides]`** — the provider side: slots a `[provides]` entry fills.
- **`[claims.dependencies]`** — the consumer side: slots a dependency expects.

Under each side, the next key is the **name of the provides or dependency entry
the claim attaches to**, and under that is one or more freely-named **slots**.
Each slot is a table with just two possible fields:

| Field | Set by | Meaning |
| --- | --- | --- |
| `target` | provider | the holder file this package ships that the shared name resolves to |
| `path` | consumer (or a provider default) | the shared filesystem location — where the claim symlink is materialised |

A worked example — a compiler package that offers to hold `/usr/bin/cc`, and a
consumer that expects `cc` to exist:

```toml
# provider: gcc.package.pekit.toml
[provides]
cc = "1"

[claims.provides.cc.default]
target = "/usr/bin/gcc"   # a file this package ships
path   = "/usr/bin/cc"    # provider default for the shared name

# consumer: some-toolchain.package.pekit.toml
[dependencies]
cc = "*"

[claims.dependencies.cc.default]
path = "/usr/bin/cc"      # the shared name this package needs present
```

Claim `path` and `target` values are run through the template engine, so a
recipe may parameterise them (an arch triplet in a library path, for instance)
like any other manifest value.

At pack time pekit validates the provider side: **every provider slot's `target`
must name a file the package itself ships.** A target pointing outside the
package's own payload is a packaging error — the materialised symlink would
dangle or point at another package's file — and all offenders are reported
together. Consumer-side slots and provider slots that set only a `path` own no
target and are exempt from this check.

## Where to go next

For the package files these tables live in, read [Packaging files](/pekit/recipes/packages.md).

For the environment file that selects a build dependency provider, read [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md).

For the workspace `[policy]` table that refines soname derivation, read [Workspaces](/pekit/running/workspaces.md).

---

# Multi-package recipes

_pekit / Writing recipes_

> Base and member package files, layer merge and precedence, selecting members with positional selectors and --all, and the [multipack] instance fan-out.

A recipe rarely produces exactly one artifact. The same build tree is often
split into a runtime package, a `-doc` package, and a `-dev` package; a single
build sometimes fans out into one package per locale, per font family, or per
plugin. pekit expresses both shapes without repeating the build: **member**
package files split one definition into several named packages, and the
`[multipack]` block expands a single definition into many **instances**.

This page covers where package definitions live, how their layers merge, how
you select which packages a run emits, and how multipack fan-out works.

## Package files and layers

pekit discovers package definitions from files whose names follow a fixed
convention, relative to a **root** (the recipe directory, and — under a
workspace or a delegated source — the workspace and source directories too).
Discovery recognises two kinds of file:

| Kind | Location | Selector |
|------|----------|----------|
| **Base** | `package.pekit.toml`, or `packages.pekit/package.pekit.toml` | `main` |
| **Member** | `<name>.package.pekit.toml`, or `packages.pekit/<name>.package.pekit.toml` | `<name>` |

Both the root itself and a `packages.pekit/` subdirectory are searched, so you
can keep package files beside the recipe or gather them in one folder.

- **At most one base** may exist. Defining `package.pekit.toml` in both the
  root and `packages.pekit/` is an error (`duplicate_package_base`).
- A member's selector is its filename with the `.package.pekit.toml` suffix
  removed. A file literally named `package.pekit.toml` is the base, never a
  member; an empty member name is skipped. Each member name must be a valid
  selector, and a name may not be defined twice across the two search
  locations (`duplicate_package`).

Every file becomes a **layer**. Each layer records its **owner** — `recipe`,
`workspace`, or `source` — which fixes how bare payload references in that file
are qualified: an unqualified file source or exclude in a recipe-owned layer is
read as `@recipe:`, in a workspace-owned layer as `@workspace:`, and in a
source-owned layer as `@source:` (already-qualified refs and `target:path`
build refs are left untouched). See [Package sources and
refs](/pekit/recipes/sources.md) for the ref grammar.

### How many packages a recipe emits

The base file is a shared foundation, not a package of its own once members
exist. The rule is:

- **No member files** — the base alone produces a **single** package with
  selector `main`. (With no base and no members at all, there is nothing to
  build: `missing_package`.)
- **One or more member files** — the recipe emits **one package per member**.
  The base is merged underneath every member as shared defaults; it does *not*
  additionally produce a standalone `main` package.

So to emit a runtime package plus a `-doc` package you write two member files;
the base carries what they share.

### Merge and precedence

For each emitted package, pekit stacks the applicable layers and merges them in
order, later layers overriding earlier ones. The order is:

1. workspace base
2. source base (only when the recipe [delegates](/pekit/running/workspaces.md)
   packages to a materialised source and the source root differs from the
   recipe root)
3. recipe base
4. source member(s) with this selector
5. recipe member(s) with this selector

Merge is **field-level, not deep**, and two fields behave differently:

- Scalars and metadata (`format`, `clear_out`, `[package]` fields such as
  `version`, `architecture`, `license`, `homepage`, `default_root`) override
  only when the later layer sets them; an unset field leaves the earlier value
  in place.
- `builds`, `[files]`, `[symlinks]`, `excludes`, `[multipack]`, `[publish]`,
  and the constraint maps (`dependencies`, `provides`, …) are **replaced
  wholesale** when the later layer defines them. In particular a member's
  `[files]` table does not merge key-by-key with the base's — if the member
  declares `[files]` at all, it replaces the base's `[files]` entirely. Put
  shared payload in whichever single layer owns it.

One special case governs names: a member that does **not** set its own
`[package].name` does not inherit the base's name. Its name is cleared, which
lets it fall back to the selector-derived default (see
[Instance naming](#instance-naming-and-artifacts) below). The base's
`[package].name` therefore only ever names the lone `main` package.

## Selecting packages on the CLI

Two commands select packages: **`package`** (stages the builds a package needs,
then writes its artifacts under `out_dir`) and **`publish`** (does the same,
then copies each artifact to a configured `localdir`). See
[Commands and targets](/pekit/running/commands-and-targets.md).

Selection works like target selection:

- **Positional selectors** name members: `pekit package hello hello-doc`.
- **No selector, one package** — that package is used.
- **No selector, several members** — the run is ambiguous and pekit lists the
  members (`ambiguous_package`).
- **`--all`** selects every member.

`--all` cannot be combined with positional selectors; doing so is rejected
before any work runs:

```text
invalid_flags: --all cannot be combined with package selectors
```

An unknown selector reports the available members (`missing_package`). A
selector may carry an `:instance` suffix to pick a single multipack instance —
covered next.

## Multipack: one definition, many instances

A single package definition fans out into several packages with the
`[multipack]` block. Each expansion is an **instance**, identified by a
**multipack value**; the value is available to the definition through the
`{{multipack}}` template variable. There are two enumeration modes.

### Static enumeration

List the values inline. `[multipack]` accepts exactly one key, `enum`; as an
array it is a fixed list of values (each a valid selector, no duplicates, and
the list may not be empty):

```toml
[package]
name = "fonts-{{multipack}}"
version = "{{version}}"
architecture = "any"

[multipack]
enum = ["serif", "sans", "mono"]

[files]
"build:share/fonts/{{multipack}}/**" = "usr/share/fonts/{{multipack}}/"
```

This one definition emits three packages: `fonts-serif`, `fonts-sans`, and
`fonts-mono`, each packing its own subtree.

### Enumeration from files

To derive the value set from what the build actually produced, give `enum` a
`files` sub-table with a `path` glob and a `regex` (both required):

```toml
[package]
name = "locale-{{multipack}}"
version = "{{version}}"
architecture = "any"

[multipack.enum.files]
path  = "build:usr/share/locale/*"
regex = '^(?P<value>[a-z]{2})$'

[files]
"build:usr/share/locale/{{multipack}}/**" = "usr/share/locale/{{multipack}}/"
```

pekit globs `path`, applies `regex` to each match's **basename**, and takes the
captured text as the multipack value. The regex must expose the value through
either a named `(?P<value>…)` group or a single unnamed capture group; anything
else is rejected (`invalid_regex`). Values are de-duplicated and each must be a
valid selector. If the glob resolves but nothing matches, that is
`empty_multipack`.

Note the phasing: the `path` template is rendered with `{{version}}` only —
`{{multipack}}` is **not** available while enumerating, because that is the very
value being discovered.

### The `{{multipack}}` variable

`{{multipack}}` is available **only in the package-instance phase** — once a
concrete instance and its value exist. That covers the rendered manifest
metadata (`[package]` name, version, description, dependencies, provides,
claims, …), the `[files]` source and destination templates, `[symlinks]`, and
the `[publish]` destination. Using `{{multipack}}` anywhere it is not bound —
including a non-multipack definition or the `enum.files` `path` — fails:

```text
{{multipack}} is not available in this context
```

`{{version}}` and its parts are available throughout; see
[Versions](/pekit/recipes/versions.md).

### Selecting one instance

A positional selector may target a single instance with `member:instance`:

```text
pekit package fonts:serif      # only fonts-serif
pekit package fonts            # all instances of fonts
```

The `:instance` suffix is valid only on a multipack definition; applying it to
an ordinary package is an error, as is naming an instance the enumeration never
produces (`missing_package`).

### Instance naming and artifacts

Each instance is named from its rendered `[package].name`. When a definition
has no name, the name defaults to the member selector, and for a multipack
instance the value is appended: a nameless multipack member `plugins` with value
`png` becomes `plugins-png`. The artifact filename follows the format:
`<name>_<version>_<arch>.peipkg` for `format = "peipkg"`, or `<name>.tar` for a
plain `tar` package.

Across everything a single run emits, pekit checks that no two instances write
the same artifact path (`artifact_collision`) or the same
name/version/architecture (`package_name_collision`).

## Dry runs and unresolved instances

`--dry-run` plans without executing. Because a dry run does not actually run the
build targets, a multipack that enumerates **from build output** (an
`enum.files` `path` pointing at a `build:` ref) has nothing to glob yet: its
instances cannot be resolved before the build exists. Rather than fail, pekit
reports each such definition as `unresolved`:

```text
unresolved  fonts  multipack enumeration found no instances for build:...
```

A dry run whose only shortfall is unresolved instances still succeeds — the
enumeration will resolve for real once the build runs. (Similarly, when package
discovery is delegated to a source that has not been materialised, a dry run
reports the packages as unresolved instead of erroring.) A real run performs the
builds first, so enumeration sees the produced files.

## Worked example: runtime plus docs

A recipe that emits two packages from one build, sharing metadata through the
base:

`packages.pekit/package.pekit.toml` — the shared base:

```toml
format = "peipkg"
builds = ["main"]

[package]
version      = "{{version}}"
architecture = "x86_64"
license      = "MIT"
homepage     = "https://example.org/hello"
```

`packages.pekit/hello.package.pekit.toml` — the runtime member:

```toml
[package]
name        = "hello"
description = "The hello program"

[files]
"build:usr/bin/hello" = "usr/bin/hello"
```

`packages.pekit/hello-doc.package.pekit.toml` — the docs member:

```toml
[package]
name        = "hello-doc"
description = "Documentation for hello"

[files]
"build:usr/share/doc/**" = "usr/share/doc/"
```

`pekit package --all` writes both `hello_<version>_x86_64.peipkg` and
`hello-doc_<version>_x86_64.peipkg`, each inheriting `format`, `version`,
`architecture`, `license`, and `homepage` from the base. `pekit package hello`
writes only the runtime package.

## Where to go next

For the `[files]`, `[symlinks]`, and metadata fields each member defines, read [Packaging files](/pekit/recipes/packages.md).

For the `package` and `publish` commands and their selectors, read [Commands and targets](/pekit/running/commands-and-targets.md).

For driving the packages of many recipes at once, read [Workspaces](/pekit/running/workspaces.md).

---

# Environments and keyrings

_pekit / Writing recipes_

> The [env] layers, env files and --env, [wrap] wrappers, dependency_provider, and how keyrings inject secrets as PEKIT_KEYRING_* variables.

Every target pekit runs — a `build`, `test`, `install`, or `clean` command —
executes inside an environment pekit assembles for it. That environment has three
distinct layers: the **managed** `PEKIT_*` variables pekit sets automatically, the
**keyring** values that carry secrets and keys, and the **user** variables you
declare in `[env]` and env files. This page describes how those layers are built,
how they compose and override, how `[wrap]` wrappers change the way the command is
launched, and how keyrings resolve and inject values.

For the exhaustive list of managed `PEKIT_*` variables and what each one means, see
the [environment contract in the command-line reference](/pekit/reference/cli.md);
[Recipe anatomy](/pekit/recipes/anatomy.md) walks through the ones you will use most.
This page covers everything you add on top of that contract.

## The three layers

When a target runs, pekit composes three maps and overlays them in a fixed order:

| Layer | Source | Naming |
| --- | --- | --- |
| Managed | Set by pekit (recipe/source roots, output paths, version parts, dependency outputs) | `PEKIT_*` |
| Keyring | Keyring files and `--keyring.x.y=` literals | `PEKIT_KEYRING_*` |
| User | `[env]` in the workspace, recipe, and selected env file | Any valid shell name |

The overlay order is **managed, then keyring, then user** — so in principle a later
layer overwrites an earlier one on a key collision. In practice the three
namespaces are kept disjoint by hard rules:

- User `[env]` **may not** set any variable beginning with `PEKIT_`. Doing so is a
  `reserved_env` error.
- A keyring export that collides with a user `[env]` variable or with a managed
  variable is an `env_collision` error, not a silent override.

So the layers never actually collide: managed owns `PEKIT_*`, keyrings own
`PEKIT_KEYRING_*`, and the user layer owns everything else.

## The `[env]` block

`[env]` declares plain environment variables as a TOML table. Each key is the
variable name and each value is a string:

```toml
[env]
CC = "clang"
CFLAGS = "-O2 -pipe"
PREFIX = "/usr"
```

Keys must be valid environment-variable names — they match
`^[A-Za-z_][A-Za-z0-9_]*$`. A non-string value, or a name outside that grammar, is
a parse error. Declaration order is preserved from the file.

`[env]` may appear in three places, each contributing to the user layer:

- the **workspace** file (applies to every member),
- the **recipe** (`pekit.toml`),
- the **selected env file** (see below).

### Composition and override

Within the user layer the sources are applied in this order, and a later source
overrides an earlier one on the same variable name:

1. workspace `[env]`
2. delegated source recipe `[env]`, then that source's env file (only when the
   recipe delegates env — see [Recipe anatomy](/pekit/recipes/anatomy.md))
3. recipe `[env]`
4. the selected env file's `[env]`

So a variable set in the recipe overrides the same variable inherited from the
workspace, and the selected env file has the final say.

### Shell expansion of `[env]` values

For the common case — a shell target, or any target run under a `[wrap]` wrapper —
pekit emits the user variables as `export` lines in a shell script that runs before
your command, and it does **not** quote them. That means `[env]` values are
expanded by the shell and may reference other variables:

```toml
[env]
PATH = "$PEKIT_OUT/bin:$PATH"
LD_LIBRARY_PATH = "$PEKIT_OUT/lib"
```

Managed `PEKIT_*` and keyring `PEKIT_KEYRING_*` values are exported *before* the
user block, so `[env]` can build on them. (A bare argv command — an array-form
command with no wrapper — receives the composed environment directly from the
process, where these values are literal rather than shell-expanded. Keep `[env]`
values literal if your target uses the array form without a wrapper.)

## Env files and `--env`

An **env file** is a supporting file that lives next to the recipe and carries an
`[env]` block, a `[wrap]` wrapper, a `dependency_provider`, or any combination of
the three. It lets you keep environment- or profile-specific settings out of the
recipe proper. An env file must declare at least one of those keys; the only
recognised top-level keys are `env`, `wrap`, and `dependency_provider`.

Which env file is loaded is controlled by `--env <name>`. The default is `main`,
so `env.pekit.toml` is picked up automatically when present and silently skipped
when absent. Passing `--env none` disables env-file loading entirely. Any other
name selects a named profile such as `release.env.pekit.toml` (`--env release`),
which **must** exist — a missing named file is an error.

```toml
# release.env.pekit.toml
dependency_provider = "peipkg"

[env]
CFLAGS = "-O3 -DNDEBUG"
```

For the exact env-file schema and the selection table, see
[Supporting files](/pekit/reference/supporting-files.md).

## `[wrap]` wrappers

A **wrapper** changes *how* the target command is launched: instead of running your
command directly, pekit substitutes it into a wrapper command at a `{{command}}`
placeholder. This is how you run a build inside a sandbox, a container shim, a
`nice`/`taskset` prefix, or any other launcher.

`[wrap]` takes a single `command`, written either as a shell string or as an argv
array. Exactly one `{{command}}` placeholder is required:

```toml
# String form — {{command}} appears once in the shell string
[wrap]
command = "firejail --quiet -- {{command}}"
```

```toml
# Array form — {{command}} must be a complete argument (never argv[0])
[wrap]
command = ["nice", "-n", "10", "sh", "-euc", "{{command}}"]
```

At run time pekit assembles the export prelude plus your target command into a
script, and:

- **String wrapper:** the script is shell-quoted and spliced in at `{{command}}`,
  then executed with `sh -euc`.
- **Array wrapper:** `{{command}}` is replaced by the script; the wrapper's first
  element is the program that is executed.

Only one wrapper is active per run. Like `[env]`, `[wrap]` may be declared in
several places, and the **last non-empty one wins**, in this order (lowest to
highest precedence):

1. workspace `[wrap]`
2. delegated source env file `[wrap]` (only when the recipe delegates wrap)
3. recipe `[wrap]`
4. the selected env file's `[wrap]`

So a recipe wrapper overrides the workspace's, and the selected env file's wrapper
overrides the recipe's.

## `dependency_provider`

Env files may also set `dependency_provider`, a single string naming which of a
target's declared `[build.<target>.dependencies.<provider>]` blocks is exported
to the build environment (`PEKIT_DEPENDENCIES`, `PEKIT_DEPENDENCY_PROVIDER`,
`PEKIT_DEPENDENCIES_FILE`). It is not an environment variable; it selects a
declared provider block by name. When both a delegated source env file and the
recipe's own env file set it, the recipe's env file wins. See
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md) for what the
selection exports.

## Keyrings

Keyrings supply secrets and keys to targets. Every keyring value is exported to the
target as a `PEKIT_KEYRING_*` environment variable. There are two ways to provide
them: keyring **files** and inline **literals**.

### `--keyring=<value>` (keyring files)

`--keyring` names a keyring file and is repeatable. The value is resolved like
this:

1. If the value is **path-like**, it is treated as a path immediately. A value is
   path-like when it is absolute, begins with `.`, begins with `~`, or contains a
   `/`. The path is resolved relative to the current directory; if the file does
   not exist, that is a `missing_keyring` error.
2. Otherwise the value is treated as a **name**: pekit looks for
   `<value>.keyring.pekit.toml` in each search root in turn. For a plain recipe the
   search root is the recipe directory; inside a workspace the workspace root is
   searched first, then the recipe directory.
3. If no named file is found in any search root, pekit falls back to treating the
   value as a path relative to the current directory; if that does not exist
   either, it is a `missing_keyring` error.

In a `pekit workspace` run, named keyrings are resolved once, against the
workspace root, and the resulting values are shared with every member.

```bash
# Resolved by name → looks for prod.keyring.pekit.toml in the search roots
pekit build --keyring=prod

# Path-like → loaded directly
pekit build --keyring=./secrets/prod.keyring.pekit.toml
```

When several `--keyring` files are given, they are loaded in command-line order and
each overlays the previous — so a **later keyring file overrides an earlier one** on
the same key.

### `--keyring.<dotted.path>=<value>` (literals)

To inject a single value without a file, use `--keyring.<dotted.path>=<value>`. The
dotted path is sanitised into an exported variable name — upper-cased, prefixed
with `PEKIT_KEYRING_`; the exact sanitisation rules are in
[Supporting files](/pekit/reference/supporting-files.md).

```bash
pekit build --keyring.tcb.priv=<value>
# exported to the target as PEKIT_KEYRING_TCB_PRIV
```

Literals are keyed by their dotted path, so repeating the same path replaces the
earlier value with the later one.

### Precedence

Keyring sources combine in this order, lowest to highest precedence:

```text
earlier --keyring file  <  later --keyring file  <  --keyring.x.y= literal
```

That is: keyring files overlay in command-line order, and all inline literals are
applied last, so a `--keyring.x.y=` value always overrides whatever a keyring file
provided for the same exported name. Any resulting `PEKIT_KEYRING_*` name that
would collide with a managed `PEKIT_*` variable or with a user `[env]` variable is
an `env_collision` error.

### Package signing

One keyring entry is read by pekit itself rather than merely exported:
`signing.package_key` names the Ed25519 private key that signs every
peipkg-format artifact a `package` or `publish` run produces. Because it rides
the keyring mechanism, the key path stays in a per-developer, gitignored file —
or arrives inline as `--keyring.signing.package_key=<path>` — and private key
material never enters a recipe. Without a key, `publish` refuses peipkg
packages unless you pass `--allow-unsigned`. The exact behaviour and the
accepted key encodings are in
[Supporting files](/pekit/reference/supporting-files.md#well-known-entries).

### Binary signing

A build target's [`sign` table](/pekit/reference/recipe-format.md#target-sign)
also reads the keyring, but through entries the recipe names rather than a
fixed one: `"bin/peinit" = "tcb.priv"` signs that output file with the key
whose path the `tcb.priv` leaf holds. Any leaf will do; the convention in
Peios' own recipes is `[tcb] priv = "<path>"`. See
[Signing binaries for PIP](/pekit/running/signing-and-provenance.md#signing-binaries-for-pip).

### Keyring file format

A `*.keyring.pekit.toml` file is a TOML document whose leaves are strings. Each
string leaf becomes one `PEKIT_KEYRING_*` variable, named from its full dotted key
path using the same sanitisation as inline literals. Nested tables are flattened:

```toml
# prod.keyring.pekit.toml
token = "<value>"          # -> PEKIT_KEYRING_TOKEN

[tcb]
priv = "<value>"           # -> PEKIT_KEYRING_TCB_PRIV
pub  = "<value>"           # -> PEKIT_KEYRING_TCB_PUB
```

A leaf that is not a string is rejected, and typed entries — a sub-table carrying a
`path` or `content` key — are reserved and currently rejected as
`unsupported_keyring_entry`. The full schema is documented in
[Supporting files](/pekit/reference/supporting-files.md).

### A note on secrets in output

Keyring values are exported to the target like any other environment variable.
pekit does **not** redact them from a target's stdout/stderr — if your command
prints a secret, pekit streams it verbatim. pekit's own diagnostics and its
`--verbose` environment summary log variable **names** only, never values, but you
are responsible for not echoing secrets from inside your commands. Prefer writing
secrets to files or consuming them directly rather than printing them.

## Where to go next

For the managed `PEKIT_*` variables these layers sit on top of, read the
[environment contract in the command-line reference](/pekit/reference/cli.md).

For the exact env-file and keyring schemas, read
[Supporting files](/pekit/reference/supporting-files.md).

For the signing key that rides the keyring mechanism, read
[Signing and provenance](/pekit/running/signing-and-provenance.md).

---

# Commands and targets

_pekit / Running pekit_

> The commands, what each selects and its version behaviour, target naming and needs ordering, and the gen/verify source-generation pair.

Every pekit invocation names exactly one **command**. The command decides three
things up front, before any side effect: which flags are legal, how many
versions the run may cover, and which **targets** in the recipe are selected and
in what order. This page is the reference for that surface.

For the flags themselves (how they parse, global flags like `--dry-run`), see
[Invocation and flags](/pekit/running/invocation.md). For version selectors
(`--version`, `--latest`, `--all-versions`), see [Versions](/pekit/recipes/versions.md).

## The commands

| Command | Selects | Versions | `--all` | Side effects |
|---|---|---|---|---|
| `build` | `build` targets + their `needs` | multiple | no | Runs build targets; stages their output under `out_dir`. |
| `test` | `test` targets + the builds they need | **single resolved** | no | Stages needed builds, then runs the selected test targets. |
| `install` | `install` targets + the builds they need | **single resolved** | no | Stages needed builds, then runs the selected install targets. |
| `package` | package members + the builds they list | multiple | yes | Stages builds and writes `.peipkg` artifacts under `out_dir`. Recipes with a reproducible source also emit a [corresponding-source package](/pekit/recipes/sources.md#source-packages). |
| `publish` | package members (as `package`) | multiple | yes | Packages, then publishes to a configured `localdir` destination. |
| `clean` | one optional `clean` target | none | no | Runs a clean target and/or removes the managed output directory. |
| `gen` | `gen` targets | none | yes | Runs gen commands; writes generated source **into the tree**. |
| `verify` | `gen` targets | none | yes | Runs gen `verify_command`s; a read-only drift check (writes nothing). |
| `lock` | the recipe's source | multiple | no | Fetches and pins the selected versions in [`pekit.lock`](/pekit/recipes/sources.md#the-lockfile) without building. With no version flags, reports the current lock instead. |
| `workspace` | a delegated command across members | (delegated) | (delegated) | Runs one of the above across every workspace member. |

`gen` and `verify` are the source-generating pair — see
[Generating source](#generating-source-gen-and-verify) below. The rest of this
page's target model applies to `build`/`test`/`install`/`clean`; `gen` targets
have their own shape.

`workspace` is a wrapper: it parses a delegated command after its own flags and
runs it for each member. Its capabilities are exactly the delegated command's.
See [Workspaces](/pekit/running/workspaces.md).

### Version behaviour

`build`, `package`, `publish`, and `lock` iterate over **every** version the
selector resolves to, running the whole plan once per version. `test` and `install`
require the selector to resolve to a **single** version; if more than one
resolves, pekit stops before doing any work:

```text
invalid_version_selector: test requires a single resolved version
```

`clean`, `gen`, and `verify` never resolve versions or materialise a source
tree — they operate on the recipe directly (the output directory, or the
committed tree at the recipe root), so version-selection flags are not accepted
(see the capability matrix below).

### Command / capability matrix

Each command accepts a fixed set of flag groups. Passing a flag the command does
not support is an **error** up front (`unsupported_flag`), unless you pass
`--allow-unused`, which downgrades it to a suppressed warning. Broadly:
`build`, `test`, `install`, `package`, and `publish` share the version-selection,
local-source, `--no-build`, `--no-verify`, and `--refresh-source` groups; `--all`
is `package`/`publish`/`gen`/`verify` only; `--allow-unanchored` and `--allow-unsigned` are `publish`
only; `clean` takes only `--env`, `--keyring`, and its own mode flags
(`--output-only` / `--target-only`); `gen` and `verify` take only `--env`,
`--keyring`, and `--all`; `lock` takes version selection, `--refresh-source`,
and its own `--repin`. The full command-by-flag matrix is in the
[command-line reference](/pekit/reference/cli.md).

Global flags (`--dry-run`, `--quiet`, `--verbose`, `--json`, `--recipe`,
`--allow-unused`) are accepted by every command and are covered in the
[invocation reference](/pekit/running/invocation.md).

## The target model

`build`, `test`, `install`, and `clean` each read a same-named section from the
recipe. A section holds one or more **targets**, and a target is a shell command
plus optional metadata. (`gen` targets live in a `[gen.*]` section too, but have
a different shape — [see below](#generating-source-gen-and-verify).)

### Bare (`main`) targets

A bare section — with a `command` key directly on it — defines a single target
named `main`:

```toml
[build]
command = "make -j$(nproc)"

[test]
command = "make check"
```

`pekit build` with no selector runs `build.main`; `pekit test` runs `test.main`.
The keys allowed directly on a bare section are `command`, `needs`, `clear_out`,
and (for `[build]` only) `dependencies`.

### Named targets

Give a section sub-tables to define **named** targets, `[<command>.<name>]`:

```toml
[build.lib]
command = "make lib"

[build.tools]
command = "make tools"
needs    = ["lib"]
```

A target name must be a canonical selector: one or more of the characters
`A-Z a-z 0-9 _ . + -`, and it may not start with `-` or contain `/` or `:`. An
invalid name is rejected at load time (`invalid_selector`).

### Bare and named cannot mix

Within one section you use **either** the bare form **or** named sub-tables,
never both. Putting a `command` key on the section and also adding a sub-table
that is not a recognised target key is an error:

```text
mixed_targets: cannot mix bare [build] target with named [build.foo]
```

## Target selection

When you name selectors (positional arguments, or arguments after `--`), pekit
selects exactly those targets; each must exist in the section or the run fails:

```bash
pekit build lib tools
```

```text
missing_target: unknown build target "libz"; available targets: lib, tools
```

With **no** selector, selection follows these rules for the command's section:

| Situation | Result |
|---|---|
| Section has a `main` target | `main` is selected. |
| No `main`, command is `build`, exactly one target exists | that lone target is selected. |
| No `main`, and none of the above | error, listing the available targets. |
| Section is absent / empty | `missing_target: recipe has no <command> targets`. |

The single-target fallback is **build-only**. For `test` and `install`, a bare
`pekit test` with no `test.main` and multiple test targets is ambiguous:

```text
ambiguous_target: no test.main target; available targets: fast, full
```

## Build dependencies (`needs`)

A target's `needs` lists **build** target names that must be staged first.

- For `build`, the selected targets and everything reachable through `needs` are
  gathered and run in **dependency order** (a target runs after everything it
  needs). Each build runs at most once per invocation.
- For `test`, `install`, and `package`, pekit pulls the build targets named in
  the selected target's `needs` (for `package`, the `builds` list on the package
  member), stages those builds first in dependency order, then runs the selected
  test/install targets (or writes packages).

A `needs` entry that does not name an existing build target is an error:

```text
missing_target: test.full needs missing build target "core"
```

Dependency edges form a DAG. A cycle is detected and reported with the path:

```text
target_cycle: build dependency cycle: a -> b -> a
```

`--no-build` may name already-staged build targets to skip re-running them;
naming a build target that does not exist is likewise a `missing_target` error.

## `clean`

`clean` has two independent effects, gated by two mutually exclusive mode flags:

| Invocation | Runs clean target | Removes output dir |
|---|:---:|:---:|
| `pekit clean` (default) | yes, if `clean.main` (or the named selector) exists | yes |
| `pekit clean --output-only` | no | yes |
| `pekit clean --target-only` | yes | no |

Details:

- **Default.** Runs `clean.main` if present (if there is no `clean.main` and no
  selector, the target step is simply skipped — not an error), then removes the
  recipe's managed output directory (`out_dir`).
- **`--output-only`.** Removes the output directory only; no target runs. Because
  no target runs, `--env` and `--keyring` are unused and rejected
  (`unsupported_flag`), unless `--allow-unused` downgrades them to warnings.
- **`--target-only`.** Runs the clean target only; the output directory is left
  in place. With no `clean.main` and no explicit selector this is an error:
  `missing_target: --target-only requires clean.main or an explicit clean target`.

`clean` accepts **at most one** target selector; more than one is rejected:

```text
invalid_selector: clean accepts at most one target selector
```

`--output-only` and `--target-only` cannot be combined.

## `--all` for `package` and `publish`

A multi-package recipe has several package **members**. `package` and `publish`
select members the same way targets are selected — by naming them, with an
optional `:instance` suffix for [multi-package](/pekit/recipes/multi-package.md)
enum instances — with two conveniences:

- With no selector and a single member, that member is used; with no selector and
  multiple members, the run is ambiguous and lists the members.
- `--all` selects **every** member.

`--all` is exclusive with explicit selectors:

```text
invalid_flags: --all cannot be combined with package selectors
```

On any command other than `package` or `publish`, `--all` is an unsupported flag.

## Generating source (`gen` and `verify`)

Some recipes have source that is **generated** and committed to the tree —
language bindings from a canonical header, a table baked from a schema, a file
derived from another. That is not a build: a build reads source and writes
artifacts under `out_dir`; a generator reads canonical inputs and writes
generated source *back into the tree*. pekit models it as a separate command,
`gen`, with a matching drift check, `verify`.

A `gen` target runs at the recipe root and its product is the files it writes
in-tree. Its `$PEKIT_OUT` is a pekit-managed **scratch** directory
(`<out_dir>/.scratch/gen/<name>`, cleaned on success, kept on failure) that sits
outside the artifact area — packaging and `--no-build` reuse never see it. It is
a handy place for a `verify_command` to regenerate into.

```toml
[gen.bindings]
# Runs at the recipe root; writes generated source in place.
command = "./tools/gen-bindings.sh"

# Optional drift gate. Exit 0 = in sync, non-zero = stale. It owns its own
# comparison — pekit never diffs for you and never assumes the generator is
# deterministic. A common shape regenerates into the scratch $PEKIT_OUT and
# diffs against the committed tree:
verify_command = """
./tools/gen-bindings.sh --out "$PEKIT_OUT/fresh"
diff -ru generated "$PEKIT_OUT/fresh"
"""

# Scope of the pre-flight (see below). Omit to gate every build/test.
verify_on_build = ["thing-that-embeds-bindings"]
verify_on_test  = []

[gen.bindings.dependencies.apt]
python3 = "*"
```

The keys allowed on a `[gen.<name>]` target are `command` (required),
`verify_command`, `verify_on_build`, `verify_on_test`, `dependencies`, and
`verify_dependencies`. Gen targets have **no** `needs` and no build DAG — they
are independent of the build chain. Bare `[gen]` means `gen.main`, exactly like
the other sections; `pekit gen --all` / `pekit verify --all` operate on every
gen target.

- **`pekit gen <name>`** runs `command` — regenerate in place.
- **`pekit verify <name>`** runs `verify_command` — check only, never writes the
  tree. This is the on-demand / CI drift gate. Naming a gen target that has no
  `verify_command` is an error. A bare `pekit verify` selects `gen.main`,
  exactly like the other commands; `pekit verify --all` runs every gen target
  that has a `verify_command`, and errors only when none does.

### Pre-flight drift checking

The point of `verify_command` is that a consuming command never proceeds from a
stale tree by accident. Before `build`, `test`, `install`, `package`, or
`publish` runs, pekit runs the drift gates **scoped to what that command will
do**, and fails fast if any is stale:

```text
gen_out_of_date: gen target "bindings" is out of date: ...
  fix:  pekit gen bindings
  skip: pekit build thing --no-verify=bindings
```

Scope is per namespace, and the two keys are separate because a `[build.x]` and
a `[test.x]` can share the bare name `x`:

| `verify_on_build` / `verify_on_test` | Meaning |
|---|---|
| **absent** | Gate **every** target in that namespace (the safe default). |
| **`[]`** (empty) | Gate **nothing** — the drift check runs only via `pekit verify`. |
| **`["a", "b"]`** | Gate only when target `a` or `b` in that namespace runs. |

A gen target's gate fires when its `verify_on_build` gates a build the command
will run, **or** its `verify_on_test` gates a test it will run. `package` and
`publish` gate conservatively against every build target (they run builds under
the hood, and the exact set depends on package resolution). Multiple stale gates
are collected and reported together, not one at a time.

Set `verify_on_build = []` for a generator whose output nothing builds (a
downstream SDK, say): leaving it absent would drag the generator's toolchain
into every unrelated build's pre-flight for no benefit. Use the scoped list
form when only some targets actually consume the generated output.

### `--no-verify` and `verify_dependencies`

`--no-verify` opts out of the pre-flight for one invocation:

- `pekit build --no-verify` skips **all** drift gates.
- `pekit build --no-verify=bindings,other` skips only the named gens (an unknown
  name is an error).
- On `package` / `publish`, `--no-verify` skips the same pre-flight — those
  commands gate conservatively against every build target, as above.

By default `verify_command` runs with the gen target's `dependencies`. If it
needs a *different* (usually smaller) toolchain — a cheap hash check rather than
a full regen — declare `[gen.<name>.verify_dependencies.*]`; when present it
**fully replaces** `dependencies` for the verify run.

## Selector rules

Selectors are the command's positional arguments plus everything after a `--`
separator. Two rules apply to all of them:

- A selector may **not** start with `-`. A leading dash makes pekit treat the
  value as a (probably mistyped) flag; put it after `--` instead:

  ```text
  invalid_selector: selector "-lib" starts with '-'; use -- before selector-like values
  ```

  ```bash
  pekit build -- -lib
  ```

- Target selectors must be canonical (`A-Z a-z 0-9 _ . + -`, no leading `-`, no
  `/` or `:`). Package selectors follow the same grammar but allow a single `:`
  to separate a member from an enum instance.

## See also

- [Invocation and flags](/pekit/running/invocation.md) — the grammar, flag value forms, and global flags.
- [Command-line reference](/pekit/reference/cli.md) — the full command-by-flag capability matrix.
- [Versions](/pekit/recipes/versions.md) — the version selectors these commands accept.
- [Workspaces](/pekit/running/workspaces.md) — running a command across every member.

---

# Invocation and flags

_pekit / Running pekit_

> The command-line grammar, flag value forms, the global flags and their exclusion rules, output renderers, exit codes, and how a recipe is located.

This page is the reference for pekit's command line: the grammar, how flag values
are written, the flags every command accepts, and how pekit decides which recipe
to run. For what each command does and how it selects targets, see
[Commands and targets](/pekit/running/commands-and-targets.md).

## Grammar

```text
pekit [global flags] <command> [args and flags]
pekit [global flags] workspace [workspace flags] <command> [args and flags]
```

There is exactly one command per invocation, drawn from `build`, `test`,
`install`, `package`, `publish`, `clean`, `gen`, `verify`, `lock`, and
`workspace`. Anything before the
command that is not a recognised flag is an error (`unknown_command` /
`unknown_flag`); a missing command is `missing_command`.

**Flags may appear before or after the command.** Pekit parses flags the same way
on both sides of the command word, so these are equivalent:

```console
$ pekit --dry-run build
$ pekit build --dry-run
```

The one structural exception is `workspace`, whose tail is parsed specially: the
`workspace`-only flags (`--fail-fast`, `--jobs`) must come *before* the delegated
command, and everything after the delegated command is parsed as an ordinary
invocation of that command. See [Workspaces](/pekit/running/workspaces.md).

### `--` stops flag parsing

A bare `--` ends flag parsing. Every token after it is captured verbatim and
treated as a positional selector:

```console
$ pekit build -- ./tools ./libs
```

Positional selectors may not begin with `-`. A leading-`-` token before `--` is
read as a flag (and rejected as unknown if it is not one); after `--` it is still
rejected as a selector with the diagnostic *"use `--` before selector-like
values"*. Use `--` for selectors that merely *look* option-like (for example a
path), not to pass a selector that begins with a dash.

## Flag value forms

Flags fall into three shapes — **no-value**, **required-value**, and
**optional-value** — and the shape decides how a value may be written. Which
shape each flag takes is tabulated per flag in the
[command-line reference](/pekit/reference/cli.md); the rules are as follows.

**No-value flags** (`--dry-run`, `--json`, and the like) never take a value:
`--flag=x` is an error (`unexpected_flag_value`).

**Required-value flags** accept either the separated form (`--recipe path`) or the
`=` form (`--recipe=path`). With the separated form pekit consumes the next token
as the value — unless that token is missing, is `--`, or itself begins with `-`,
in which case the flag errors with `missing_flag_value`. With the `=` form an
empty value (`--recipe=`) is rejected the same way.

**Optional-value flags** take a value **only** through `=`. They never consume the
following token. So:

```console
$ pekit build --local              # --local with an empty value
$ pekit build --local=/src/mypkg   # --local with a value
$ pekit build --local /src/mypkg   # --local (empty) + /src/mypkg as a selector
```

`-V` is an alias for `--version` and behaves identically (it is a required-value
flag; it is *not* a version-print flag — pekit has no version banner and no
`--help`).

Per-key keyring values use a dotted-path form, `--keyring.<path>=<value>`, which
always requires the `=` and a non-empty path.

## Global flags

Seven flags are parsed in any position regardless of command: `--recipe`,
`--workspace`, `--allow-unused`, `--dry-run`, `--quiet`, `--verbose`, and
`--json`. Six are accepted by every command; the seventh, `--workspace`, is only
valid with the `workspace` command. Each is covered in its own section below,
and the [command-line reference](/pekit/reference/cli.md) tabulates all seven with
their value forms. The rest of pekit's flags (`--version`/`-V`, `--latest`,
`--local`, `--env`, `--keyring`, `--all`, and so on) are command-specific and
are covered on [Commands and targets](/pekit/running/commands-and-targets.md).

`--recipe <path|dir>` selects the recipe directly instead of searching upward
from the cwd (see [How a recipe is located](#how-a-recipe-is-located));
`--workspace <path|dir>` does the same for the workspace file.

### Mutual-exclusion rules

Validation rejects contradictory combinations up front, before any work runs:
`--quiet` cannot be combined with `--verbose` or `--json`; `--recipe` cannot be
combined with `--workspace` (and `--workspace` is rejected outside the
`workspace` command); the version selectors (`--version` / `--latest` /
`--all-versions`), the local-source flags (`--local` / `--prefer-local`), and
the clean modes (`--output-only` / `--target-only`) are each mutually exclusive
within their group. The complete validation table, with each diagnostic, is in
the [command-line reference](/pekit/reference/cli.md).

## `--allow-unused`

Every command declares which flags it accepts. Passing a **recognised** flag that
the effective command does not support is normally an error
(`unsupported_flag`), for example version selection on `clean`, or `--all` on
`build`. `--allow-unused` turns each such case into a non-fatal notice: pekit
records it and emits an `unused_suppressed` event at the start of the run instead
of aborting.

`--allow-unused` only relaxes the "recognised-but-unused" check. It does **not**
excuse:

- unknown flags (`unknown_flag`) or unknown commands (`unknown_command`);
- malformed flag values — missing values, empty `=` values, or `=value` on a
  no-value flag;
- the mutual-exclusion rules above;
- bad selectors (a selector beginning with `-`, or more selectors than the
  command accepts).

This is what lets you fan a shared set of flags across a whole
[workspace](/pekit/running/workspaces.md) even when some members' commands ignore
some of the flags.

## `--dry-run`

`--dry-run` runs the same planning pipeline as a real invocation — it locates and
loads the recipe, resolves the version selector, and prepares source state — but
stops at every side-effecting boundary instead of crossing it. In place of the
action pekit emits a plan event:

- a target that would run emits `target_plan` ("would run target") and does not
  execute the command or touch the stage directory;
- `clean` that would delete managed output emits `clean_plan` ("would remove
  managed output") and removes nothing;
- `package` / `publish` emit their plan events rather than writing or shipping
  artifacts.

Use `--dry-run` to inspect exactly what a command would do. Combine it with
`--json` to get the plan as a single structured object (below).

## Output renderers

The renderer is chosen from the flags. `--json` selects the JSON renderer;
otherwise the human renderer is used, and `--quiet` puts it in quiet mode.

### Human (default)

Pekit's own events print as timestamped lines, `[15:04:05] message`, optionally
prefixed with a context tag (member, command, target, package, version) when the
event carries one. Live output from the targets pekit runs is streamed through
with a `[member target version]` prefix, preserving each line's original stream —
stdout stays on stdout, stderr stays on stderr. Fatal errors are written to
stderr.

### `--quiet`

Quiet mode keeps the human format but drops routine progress, emitting only
`warning`, `artifact`, `publish`, `workspace_summary`, `lock`, and `sign`
events. Target output
is unaffected. `--quiet` cannot be combined with `--verbose` or `--json`.

### `--verbose`

Verbose mode adds extra events (such as per-target stage preparation) to the human
log. It is otherwise the human renderer.

### `--json`

`--json` emits newline-delimited JSON — one event object per line — to stdout, so
the run can be consumed by another program. Each object has a `type` and a
timestamp plus whatever context fields apply; target output arrives as
`target_output` events carrying the `stream` and `text`. Errors are emitted as an
`error` event.

`--dry-run --json` is special-cased: instead of streaming events, pekit buffers
them and, at the end, prints a **single** `plan` object:

```json
{"type":"plan","time":"...","command":"build","workspace":false,"events":[ ... ]}
```

The `events` array holds the buffered plan events; `command` is the effective
command and `workspace` is whether the run went through the `workspace` command.

## Exit codes

Pekit uses two exit codes only: `0` on success, `1` on any error — bad
invocation, missing recipe, failed build, and so on. There are no finer-grained
codes, and there is **no `--help` or `help` command**: this page,
[Commands and targets](/pekit/running/commands-and-targets.md), and the
[command-line reference](/pekit/reference/cli.md) are the manual.

## How a recipe is located

Pekit resolves exactly one recipe per (non-workspace) run:

1. **`--recipe <path|dir>`** — if given, this wins. A directory means
   `<dir>/pekit.toml`; a file path is used as-is. A missing target is
   `missing_recipe`.
2. **A remote positional** — if `--recipe` was not given and the first positional
   argument looks like a remote locator, it is taken as the recipe (and dropped
   from the selector list).
3. **Walking up** — otherwise pekit searches from the current directory upward
   for the nearest `pekit.toml`, stopping at the filesystem root
   (`not_found` if none is found).

Once the recipe root is known, and unless `--workspace` was supplied, pekit also
walks upward from that root for a `workspace.pekit.toml`; if one is found the
recipe is treated as a member of that workspace.

### Remote recipe locators

A **remote locator** is a recipe reference that points at a git repository rather
than a local path. A value is treated as remote when it does *not* start with `.`
or `/` and it either begins with `github.com/`, contains `://`, ends with `.git`,
or contains a `//` subdirectory separator. Examples:

```console
$ pekit build github.com/acme/tools
$ pekit build github.com/acme/tools//subdir@v1.2.0
$ pekit build https://git.example.org/acme/tools.git//pkg@main
```

The locator may carry a `//subdir` to select a directory inside the repository and
an `@ref` (branch, tag, or commit) to pin a revision. When pekit sees a remote
locator — whether as the positional or as the value of `--recipe` — it
**materialises** it: it mirror-clones the repository into a cache under the
system temp directory (fetching updates if already cached), checks out the
resolved commit into a clean working tree, and then treats the checkout (plus any
`//subdir`) as the recipe root. From there the run proceeds exactly as it would
for a local recipe.

## See also

- [Commands and targets](/pekit/running/commands-and-targets.md) — what each command does and selects.
- [Command-line reference](/pekit/reference/cli.md) — every flag, its value form, and the validation table.
- [Workspaces](/pekit/running/workspaces.md) — the workspace tail and its flag placement.
- [Sources](/pekit/recipes/sources.md) — what materialising a remote recipe's source involves.

---

# Workspaces

_pekit / Running pekit_

> The workspace.pekit.toml file, member discovery, the workspace command and its flag placement, --jobs and --fail-fast, and cross-member publish checks.

A **workspace** lets you drive a whole collection of recipes as one unit. Instead
of running `pekit build` in each recipe directory by hand, you declare which
recipes belong together in a `workspace.pekit.toml` file and run a single
`pekit workspace build`, which delegates that command to every member. This is
how a distro-scale tree of packages — dozens or hundreds of recipes — is built,
tested, packaged, and published in one go.

A workspace does three things:

1. **Discovers members** from `include`/`exclude` globs (each member is a
   directory containing a `pekit.toml`).
2. **Shares configuration** — `[env]`, `[wrap]`, and a distro-wide `[policy]` —
   down to every member.
3. **Schedules and delegates** a single command across all members, with
   optional concurrency, fail-fast, and cross-member safety checks.

## The `workspace.pekit.toml` file

The workspace file is TOML and lives at the root of the tree it governs. Its
schema is small — exactly five top-level keys are accepted, and anything else
is rejected with `unknown_key`: `include` and `exclude`
(globs selecting and removing member directories, relative to the workspace
root), `[env]` and `[wrap]` (shared configuration contributed to every member),
and `[policy]` (distro-wide derivation policy — see
[Policy](#policy-symbol-version-floors)). The key-by-key schema is in
[Supporting files](/pekit/reference/supporting-files.md).

`include` is mandatory: an omitted or empty `include` fails with `missing_key`
("workspace include is required"). Everything else is optional.

```toml
# workspace.pekit.toml at the distro root
include = ["core/*", "extra/**"]
exclude = ["extra/experimental/*"]

[env]
SOURCE_DATE_EPOCH = "1700000000"

[wrap]
command = ["nice", "-n", "10", "{{command}}"]

[policy.symbol_versions]
"libc.so.6" = "GLIBC_"
```

### Member discovery

`include` and `exclude` are glob patterns matched against the directory tree
under the workspace root. Matching uses **doublestar** globbing, so ordinary
wildcards (`*`, `?`, character classes) work and `**` matches across directory
boundaries. Patterns are always workspace-root-relative.

Discovery proceeds as:

1. Every `exclude` pattern is expanded and its matches recorded.
2. Every `include` pattern is expanded. A match is kept only if it was **not**
   excluded **and** the matched directory contains a `pekit.toml`.

A matched directory with no `pekit.toml` is silently ignored — it is simply not a
recipe, so it is not a member. This means you can point `include` at a broad glob
(`extra/**`) and only the directories that actually hold recipes become members.

If discovery yields no members at all, the run fails with `empty_workspace`
("workspace has no members").

> [!NOTE]
> Members are recipe **directories**, identified by the presence of a
> `pekit.toml`. The workspace file itself is named `workspace.pekit.toml`; it is
> a different file from a recipe.

### Locating the workspace

When you run `pekit workspace`, pekit finds the workspace file by walking upward
from the current directory looking for `workspace.pekit.toml` (the same
upward search recipes use). You can override this with `--workspace <path>`,
which accepts a path to a `workspace.pekit.toml` file, a directory containing
one, or a remote locator (for example a `github.com/...` reference).

## Member identity

Each member has a stable **id**: its workspace-relative directory path, written
with forward slashes (for example `core/zlib` or `extra/tools/ripgrep`). This id
is the member's name everywhere it appears:

- in diagnostics and error messages (`member_failed`, `member_skipped`);
- in structured JSON events and in output-line prefixes;
- in the publish-collision owner labels (see below);
- in the end-of-run summary.

Members are processed in a deterministic **order**: ids are sorted
lexicographically. With the default single-worker execution this is the exact
order members run in; with concurrency it is the order in which work is
dispatched.

## Invocation

```text
pekit [global flags] workspace [workspace flags] <command> [args and flags]
```

`workspace` is itself a command, but it does no building of its own — it
**delegates** another command — any of `build`, `test`, `install`, `package`,
`publish`, `clean`, `gen`, `verify`, or `lock` — to every member. That delegated command and its arguments
come after the workspace flags.

### Strict flag placement

The tail after `workspace` is parsed specially, and the
placement rules are strict:

- **Workspace flags — `--jobs N` and `--fail-fast` — must appear after
  `workspace` and before the delegated command.** They are recognised only in
  this slot.
- **Command-level flags** (version selection, `--local`, `--env`, `--keyring`,
  `--refresh-source`, `--all`, …) belong to the delegated command and must come
  **after** it. Placing one before the delegated command is an error with a
  hint: *"workspace command flag `<name>` must appear after the delegated
  command."*
- **Global flags** (`--dry-run`, `--quiet`, `--verbose`, `--json`,
  `--allow-unused`, `--workspace`) may appear either before `workspace` or in
  the tail before the delegated command.

```console
# correct: workspace flags before the command, command flags after
$ pekit workspace --jobs 4 --fail-fast build --version 1.2.3

# also correct: a global flag may precede workspace
$ pekit --dry-run workspace build

# error: --version is a command flag placed before the delegated command
$ pekit workspace --version 1.2.3 build
#   workspace command flag version selection must appear after the delegated command
```

Because `--jobs`/`--fail-fast` are recognised *only* in the workspace slot,
writing them after the delegated command hands them to the delegated command's
own parser, which rejects them as an `unknown_flag`. Keep them in the workspace
slot.

Both failures share the diagnostic code `missing_workspace_command`: it fires
when a command flag precedes the delegated command, and when no delegated
command follows the workspace flags at all ("workspace requires a delegated
command").

## Execution model

Each member is run as an ordinary **per-recipe invocation** of the delegated
command — exactly as if you had `cd`'d into that member and run
`pekit <command> ...` there, with the delegated flags and any positional
selectors carried through unchanged.

### Concurrency: `--jobs`

`--jobs N` sets how many members run concurrently. `N` must be a **positive
integer**; a missing value is `missing_flag_value` and a non-positive or
non-numeric value is `invalid_flag_value` ("--jobs requires a positive
integer"). The **default is 1**, i.e. members run one at a time in id order.

### Fail-fast: `--fail-fast`

By default the workspace attempts **every** member even if some fail: it runs
them all and then fails the overall command at the end if any member failed
(`workspace_failed`, "N workspace member(s) failed").

With `--fail-fast`, the first member failure sets a stop flag so that **no
further members are started**. Members that had not yet begun are recorded as
**skipped**.

> [!NOTE]
> `--fail-fast` stops *starting* new members; it does **not** cancel members
> that are already running. In-flight members run to completion. There is no
> mid-member cancellation.

### No inter-member dependencies

Members are independent. Pekit does **not** compute a dependency graph between
members, does not topologically order them, and does not make one member's build
visible to another as an input. The only ordering is the lexicographic id order
used for dispatch. If a member needs another member's output, that relationship
must be expressed through the normal recipe machinery (sources, package
dependencies), not through workspace membership.

### Summary

At the end of a run pekit emits a `workspace_summary` event tallying members as
**succeeded**, **failed**, or **skipped**:

```text
12 succeeded, 1 failed, 3 skipped
```

*Succeeded* and *failed* count members that actually ran; *skipped* covers
members not started because of `--fail-fast`, and members skipped by selector
planning (below). The overall command exits non-zero if any member failed.

## Per-member strictness

The delegated command and its flags are validated once, when the tail is parsed.
Beyond that, strictness is evaluated **per member**, because members differ: one
recipe may define a `build` target or a package selector that another does not.

- Each member runs the delegated command against its own recipe. If a flag or
  selector cannot be honoured by that member's recipe, **that member errors** —
  its failure is reported (`member_failed`) and counted, and the other members
  are unaffected (unless `--fail-fast` is set).
- **`--allow-unused`** relaxes this per member. When a run carries positional
  selectors (target names, or `def:variant` package selectors) together with
  `--allow-unused`, pekit plans which selectors apply to which member:
  - selectors that a member cannot satisfy are **suppressed** for that member
    (reported via `workspace_selector_suppressed`);
  - a member to which **none** of the requested selectors apply is **skipped**
    ("no requested selectors apply to member");
  - if a requested selector applies to **no** member at all, the whole run
    fails with `missing_selector` ("workspace selector … matches no selected
    member").

  `--allow-unused` also suppresses per-member *unsupported version selection*
  rather than failing that member.

Without `--allow-unused`, selectors are passed to every member verbatim and a
member that does not recognise one fails in the usual way.

## Cross-member publish collision detection

When the delegated command is `publish`, two members could be configured to
publish an artifact to the **same destination** — a mistake that would have them
overwrite each other. Pekit catches this **before** publishing anything.

A **preflight** pass runs each member's publish planning in dry-run mode and
reserves every resolved destination path in a shared registry. Destinations
are keyed by path, each with an **owner** of the form `<member-id>:<instance>`. Reserving a path that a *different* owner already holds
fails the entire run with `publish_collision` ("publish destination collision
between … and …"). Reserving the same path for the same owner is idempotent, so a
member does not collide with itself.

Because this is a preflight, a destination clash is reported up front, before any
member has written to a publish target.

## Shared configuration

### `[env]` and `[wrap]`

`[env]` and `[wrap]` declared at the workspace level are shared defaults
contributed to **every** member, layered together with each member recipe's own
`[env]` and `[wrap]`. For how these compose with recipe- and environment-file
values — and the precedence between them — see
[Environments and keyrings](/pekit/recipes/environments-and-keyrings.md). The
`[wrap]` command follows the same `{{command}}` placeholder rules as a recipe
wrap. Keyrings behave similarly: a named `--keyring` on a workspace run
resolves against the workspace root once, and the values are shared with
every member.

### Policy: symbol-version floors

The `[policy]` table carries distro-wide derivation policy. Its one current
sub-table is `[policy.symbol_versions]`, a map from a shared-library **soname**
to the **symbol-version token prefix** whose tokens are commensurable with the
providing package's version:

```toml
[policy.symbol_versions]
"libc.so.6" = "GLIBC_"
"libstdc++.so.6" = "GLIBCXX_"
```

This governs which sonames receive a symbol-version floor when pekit derives a
package's dependencies across the workspace — so that, for example, a binary
using a `GLIBC_2.34` symbol gains an appropriate lower bound on its libc
dependency. An unknown sub-table under `[policy]` is rejected
(`unknown_key`, "policy.… is not a known policy table"). For how derived
dependencies and claims work, see
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md).

## Where to go next

For the recipes a workspace's members are, read [Anatomy of a recipe](/pekit/recipes/anatomy.md).

For how workspace `[env]` and `[wrap]` compose with a recipe's own, read [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md).

For the full flag surface and remote locators, read [Invocation and flags](/pekit/running/invocation.md).

---

# Signing and provenance

_pekit / Running pekit_

> How pekit signs .peipkg artifacts and PIP-signs binaries, the provenance every manifest records, corresponding-source packages, and the gates publish enforces.

A published package is a claim: *these bytes were built from these inputs, by
this recipe, with this tool*. Pekit backs that claim end to end. On the input
side, fetched sources are pinned trust-on-first-use in
[`pekit.lock`](/pekit/recipes/sources.md#the-lockfile) and upstream release
signatures can be verified against committed keys — both covered on the
[Sources](/pekit/recipes/sources.md) page. This page covers the output side:
signing the artifacts pekit writes, the provenance recorded in every
manifest, the corresponding-source package that keeps a build's exact inputs
publishable, and the checks `publish` runs before shipping anything.

## Signing artifacts

Package signing is configured through one well-known keyring entry,
`signing.package_key`, whose value is the path to an **Ed25519 private
key** — either the raw 32-byte seed or a PKCS#8 PEM as produced by
`openssl genpkey -algorithm ed25519`. A relative path resolves against the
invocation's working directory. Because it rides the
[keyring mechanism](/pekit/recipes/environments-and-keyrings.md), the key path
lives in a per-developer, gitignored file — or arrives inline as
`--keyring.signing.package_key=<path>` — and private key material never
enters a recipe.

```console
$ pekit publish --keyring=dev
```

With a key configured, **every peipkg-format artifact the run produces** —
package members and the corresponding-source package alike — is packed with
an embedded signature naming the key's fingerprint (the lowercase hex SHA-256
of the raw 32-byte public key), and each emits a `sign` event. Sign events
stay visible under `--quiet`, so a quiet run still shows what was signed with
which key.

Two failure behaviours are deliberate:

- A configured key that cannot be loaded is a hard `signing_key` error. Pekit
  never falls back to silently writing unsigned output — a signing setup that
  degrades quietly would defeat the point.
- Without a key, `package` writes unsigned artifacts (fine for local
  development), but `publish` refuses peipkg-format packages with
  `unsigned_publish` unless you pass `--allow-unsigned`.

## Signing binaries for PIP

Package signing protects the artifact in transit; it says nothing to the
kernel about the programs inside. The Peios kernel derives a process's
[Process Integrity Protection](/peios/advanced-peios/pspk/binary-signing-and-pip/process-integrity-protection.md)
tier from a second, separate signature carried by the binary itself, and
pekit produces that signature too — as a step of the **build target**, not of
packaging.

A build target lists the files to sign in a `sign` table, keyed by signature
kind. The `pip` kind maps paths or globs relative to the target's `$PEKIT_OUT`
to the keyring entry that holds the signing key:

```toml
[build.main]
command = "make && make install DESTDIR=$PEKIT_OUT"

[build.main.sign.pip]
"usr/bin/peinit"  = "tcb.priv"
"usr/lib/*.so.*"  = "tcb.priv"
```

```toml
# dev.keyring.pekit.toml — per-developer, gitignored
[tcb]
priv = "kacs-tcb-dev.key"     # openssl genpkey -algorithm ML-DSA-65
```

The value names any keyring leaf; there is no fixed entry name. It holds the
path to an ML-DSA-65 private key — PKCS#8 PEM, or a raw 32-byte seed —
resolved against the invocation's working directory when relative. The kind
decides only the format and the storage location: for `pip` that is the
[fixed 3310-byte blob](/peios/advanced-peios/pspk/binary-signing-and-pip/signature-format.md) in an
ELF section named `.peios.sig`.

Signing runs after the target's command exits 0 and before any dependent
target or package sees the output, so it is the last thing to touch the
binary. That ordering is why this is a build step: the signature covers the
whole file, so every strip, debug split or `patchelf` a recipe performs has to
come first, and those all live in build commands. It also means `pekit test`
exercises the signed binary rather than an unsigned stand-in.

For each matched file, pekit:

1. Adds a zero-filled `.peios.sig` section, or reuses one the build already
   reserved (it must be `SHT_PROGBITS` and exactly 3310 bytes). Adding a
   section appends to the file and never moves existing bytes, so program
   headers and loadable segments are unaffected.
2. Hashes the file with the section contents zeroed, signs the hash with pure
   ML-DSA-65 (deterministic, empty context) and writes the blob into the
   reserved bytes.
3. Verifies the finished file the way the kernel will before returning, and
   emits a `sign` event naming the keyring entry and the key's fingerprint —
   the SHA-256 of the raw public key, the same bytes a kernel key table
   carries.

Two properties are deliberate:

- **Every failure is fatal.** A keyring entry that is missing or does not
  load, a pattern that matches nothing, a file that is not a 64-bit
  little-endian ELF, a reserved section of the wrong size, and two patterns
  naming different keys for one file all abort the target. There is no
  `--allow-unsigned` equivalent, because an unsigned binary is not a weaker
  binary: it runs with no tier and no diagnostic, and build time is the only
  place the mistake is visible.
- **Only the ELF section form is produced.** The specification also allows
  the signature in an extended attribute, but package payloads
  [carry no extended attributes](/peios/advanced-peios/pspu/package-format-and-repository-protocol/determinism.md),
  so nothing pekit ships could use it. Scripts and other non-ELF files take
  their interpreter's tier and cannot be usefully signed.

Which tier a signature confers is a property of the key, not of anything in
the recipe: the kernel looks the verifying key up in its compiled-in table.
Getting a key into that table is a kernel-build question — see the `tcb.pub`
entry the pkm recipe consumes — and a binary signed with a key the kernel does
not carry is simply unsigned there.

## What the manifest records

Every peipkg manifest pekit writes carries a `build` block stating where the
artifact came from:

| Field | Value |
|---|---|
| `timestamp` | The run's start time, UTC RFC 3339. |
| `farm_id` | `local` — pekit builds are local builds. |
| `source_ref` | The source [provenance ref](/pekit/recipes/sources.md#source-roots-and-provenance): `git:<url>@<commit>`, `url:<url>#sha256:<hash>`, and so on. |
| `recipe_ref` | The recipe tree's own git commit, `git:<commit>`. Suffixed `+dirty` when the work tree has uncommitted changes anywhere — including a freshly written, not-yet-committed `pekit.lock`. Omitted when the recipe is not inside a git work tree. |
| `builder` | The producing pekit's own revision: `pekit/<12-hex-commit>`, `+dirty` when built from a modified tree, falling back to the module version when the binary carries no VCS stamp. |
| `source_package` | The name of the corresponding-source package emitted from this recipe (below); empty when none is. |

Together these state the full provenance chain — the exact upstream inputs,
the exact recipe that drove the build, and the exact tool that performed
it. `recipe_ref` deliberately resolves the **enclosing** repository: a
workspace member's identity is the workspace repository's commit, not some
per-recipe notion. A dirty tree is over-marked rather than under-marked,
because a commit id alone does not describe a build whose tree had local
changes.

## Corresponding-source packages

A recipe with a reproducible source automatically emits a
**corresponding-source package** whenever it packages `peipkg`-format
members. This is how the distribution meets copyleft source-availability
obligations: publishing a binary and its source package through the same
channel keeps the exact inputs of every published build available for as long
as the binary is.

One source package covers every peipkg member the recipe produces. It is
emitted when all of these hold:

- `source_package.enabled` is not set to `false` (emission is the default);
- the source actually materialised as **git with a resolved commit** or
  **url** — a local override never qualifies, and neither does a git source
  built from a bare branch ref with no selected version (a deliberately
  moving target has no stable corresponding source);
- at least one emitted member has `format = "peipkg"`.

The package is named `<recipe-dir>-source` by default (rename with
`source_package.name`), is `noarch`, and is versioned identically to the
members — members that disagree on version are a
`source_package_version_conflict` error. Its license is the conjunction of
the members' distinct licenses, joined with ` AND `; its homepage is kept
only when every member agrees on one; and it publishes to the first member's
[`[publish]`](/pekit/reference/supporting-files.md#publish-targets) destinations.

Everything installs under `/usr/src/dist/<name>-<version>/` (with any
`-source` suffix stripped from `<name>`):

- `upstream/` — the pristine source input. For a url source, the downloaded
  artifact byte-for-byte, so its hash matches the committed `pekit.lock`; for
  a git source, a `git archive` export of the locked commit — deterministic
  for a commit and independent of the mutable checkout.
- `patches/` — the recipe's [patch series](/pekit/recipes/sources.md#patches),
  when one is declared. The applied series is the shipped series by
  construction, and it ships under the fixed `patches/` name even when
  `[source].patches` picks a different directory.
- `recipe/` — the build-controlling files from the recipe directory:
  `pekit.toml`, `pekit.lock`, package definitions (including
  `packages.pekit/`), env files, and `keys/`. `*.keyring.pekit.toml` files
  are **never** included — developer key material stays out.

Each binary member's manifest names the source package in
`build.source_package`, linking every binary to its corresponding source. The
recipe-side schema is in the
[recipe format reference](/pekit/reference/recipe-format.md#source_package).

## What publish checks before shipping

`publish` is packaging plus shipping, and the shipping half is gated:

- **Unsigned peipkg packages are refused** (`unsigned_publish`) unless
  `--allow-unsigned` is passed, as above.
- **Unanchored provenance is refused** (`unanchored_provenance`) unless
  `--allow-unanchored` is passed. Unanchored means a url source with neither
  a `checksum` nor a lock entry — a state a real resolve immediately locks
  away, so in practice this gates dry-run publish plans for never-fetched
  sources. A plain `package` from an unanchored source warns instead.
- **An existing destination file is refused** (`publish_exists`) when the
  target's `overwrite = false`.
- **Two artifacts resolving to the same destination** in one run is a
  `publish_collision`; across workspace members, the
  [publish preflight](/pekit/running/workspaces.md#cross-member-publish-collision-detection)
  catches the same collision before any member ships anything.

## Where to go next

For the input side of the chain — the lockfile, `--repin`, and upstream
signature pinning — read [Sources](/pekit/recipes/sources.md).

For the keyring files that carry the signing key, read
[Environments and keyrings](/pekit/recipes/environments-and-keyrings.md).

For every flag mentioned here, read the
[command-line reference](/pekit/reference/cli.md).

---

# Recipe format reference

_pekit / Reference_

> The complete schema for pekit's pekit.toml and package.pekit.toml: every section, key, type, default, and the exact strictness rules the loader enforces.

This is the normative schema for the two TOML files at the heart of a pekit
recipe: `pekit.toml` (the recipe proper) and `package.pekit.toml` (what to
package). It enumerates every section, every key, its type, whether it is
required, and its meaning. Behaviour that a key merely *triggers* is
cross-linked to the concept pages; this page is the field-by-field contract.

Two strictness rules hold throughout and are called out per-section below:

- **Unknown keys are hard errors.** Every table the loader owns has a fixed
  set of accepted keys. Any other key — including a camelCase spelling of a
  known snake_case key, such as `outDir` — is rejected with an
  `unknown_key` diagnostic. There is no lenient/forward-compatible mode.
- **Types are checked on decode.** A key declared here as a string must be a
  string, a table must be a table, and so on; a mismatch is an `invalid_type`
  error.

## `pekit.toml`

### Worked example

```toml
out_dir = "dist"

[env]
CFLAGS = "-O2"
PREFIX = "/usr"

[wrap]
command = ["pesb", "run", "--", "{{command}}"]

[source.git]
url = "https://github.com/example/tool.git"
ref = "v{{version}}"
versions = ">=1.0"
tag_regex = "^v(?P<version>[0-9.]+)$"

[build]
command = "make PREFIX=$PREFIX"

[build.docs]
command = ["make", "docs"]
needs = ["main"]

[build.linked]
command = "make link"
dependencies.pesb-dev.libc = ">=2.38"

[test]
command = "make check"

[install]
command = "make install DESTDIR=$PEKIT_OUT"

[clean]
command = "make clean"
```

### Top-level keys

The recipe file accepts exactly these top-level keys. Anything else is an
`unknown recipe key` error.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `out_dir` | string | no | Output directory name, relative to the recipe root. Default `"out"`. |
| `env` | table | no | Environment variables exported to every target command. See [`[env]`](#env). |
| `wrap` | table | no | Command wrapper applied to every target. See [`[wrap]`](#wrap). |
| `source` | table | no | Where the recipe's source tree comes from. See [`[source]`](#source). |
| `delegate` | bool or table | no | Borrow build/env/wrap/package definitions from the source tree. See [`[delegate]`](#delegate). |
| `source_package` | table | no | Corresponding-source package emission control. See [`[source_package]`](#source_package). |
| `build` | table | no | Build target(s). See [`[build]` / `[test]` / `[install]` / `[clean]`](#targets). |
| `test` | table | no | Test target(s). |
| `install` | table | no | Install target(s). |
| `clean` | table | no | Clean target(s). |
| `gen` | table | no | Source-generating target(s). See [`[gen]`](#gen). |

### `[env]`

A flat table of environment-variable name → value. Every entry is exported
into each target command's environment (layered on top of the `PEKIT_*`
contract; see [Anatomy of a recipe](/pekit/recipes/anatomy.md)).

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| *&lt;NAME&gt;* | string | — | Value for variable *NAME*. |

Each *NAME* must match `^[A-Za-z_][A-Za-z0-9_]*$`; anything else is an
`invalid_env` error. Values must be strings. Declaration order in the file is
preserved, so a later entry may reference an earlier one via shell expansion
in the target command.

### `[wrap]`

Wraps every target command in another command (for example, running it inside
a sandbox launcher).

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `command` | string or array of strings | no | The wrapper. Must contain the placeholder `{{command}}` exactly once. |

No other key is permitted under `[wrap]`. The `{{command}}` rules mirror a
target `command`:

- **String form:** the whole string is run through a shell; it must contain
  the substring `{{command}}` exactly once.
- **Array form:** `{{command}}` must appear as a *complete* argument (not a
  substring of a larger argument) exactly once, and never as the first element
  (element `0` is the wrapper program itself).

An omitted `command` yields an empty wrap (no wrapping).

### `[source]`

Selects and configures the source tree. It has three sub-tables. At most one
**reproducible** source (`git` or `url`) may be present — declaring both is a
`mixed_source` error. A `local` source may be combined with a reproducible one
(the local path acts as an override). Unknown keys directly under `[source]`
are rejected.

| Sub-table | Meaning |
| --- | --- |
| `[source.git]` | Clone from a git repository. |
| `[source.url]` | Download from a URL (optionally an archive). |
| `[source.local]` | Use a directory on disk. |

One direct key is accepted alongside the sub-tables:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `patches` | string | no | Name of a recipe-root directory (a single path segment) whose `series` file pekit applies to the materialised source tree before any target runs. Requires a reproducible source (`patches_source` otherwise). See [Patches](/pekit/recipes/sources.md#patches). |

See [Sources](/pekit/recipes/sources.md) for materialisation, caching, and
provenance behaviour. Resolved git and url sources are pinned
trust-on-first-use in the recipe's machine-written
[`pekit.lock`](/pekit/reference/supporting-files.md#pekitlock).

#### `[source.git]`

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `url` | string | **yes** | Git remote URL to clone. |
| `ref` | string | no | Ref (tag/branch/commit) to check out. Templated. Default `"{{version}}"`. |
| `versions` | string | no | Version **cap**: a constraint string filtering enumerated or requested versions (see [Versions](/pekit/recipes/versions.md)). |
| `tag_regex` | string | no | Regex extracting version numbers from tag names during enumeration. |

#### `[source.url]`

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `url` | string | **yes** | URL to download. Templated with the version. |
| `extract` | bool | no | Treat the download as an archive and extract it. Default `false`. |
| `root` | string | no | Sub-directory within the extracted tree to use as the source root. Default `"."`. |
| `versions` | string | no | Version **cap**: a constraint string filtering enumerated or requested versions (see [Versions](/pekit/recipes/versions.md)). |
| `file_regex` | string | no | Regex extracting version numbers when enumerating from a listing. |
| `checksum` | string **or** table | no | Expected checksum. A bare string applies to all versions; a table maps version → checksum. |
| `signature` | table | no | Upstream signature verification — see `[source.url.signature]` below. |

#### `[source.url.signature]`

Optional sub-table of `[source.url]`. Its presence makes upstream signature
verification **required**: the detached signature is fetched and verified
against the pinned keys before a version is locked or built. A missing
signature is `signature_missing`; a failed verification is
`signature_invalid`; either aborts the run and writes no lock entry.
Verification is in-process OpenPGP — no host `gpg` is involved.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `key_files` | string array | **yes** | Committed public key files (armoured or binary OpenPGP), resolved relative to the recipe root. Must be non-empty. |
| `url` | string | no | Signature URL template. `{{source_url}}` expands to the rendered artifact URL; version variables are also available. Default `"{{source_url}}.sig"`. |
| `of` | string | no | What the signature covers: `"artifact"` (the published file, default) or `"decompressed"` (its decompressed content — kernel.org's `.tar.sign` signs the uncompressed tar). Any other value is `invalid_signature`. |
| `fingerprints` | string array | no | Allowlist of signer fingerprints (hex; spaces and `0x` ignored, case-insensitive). When set, a valid signature by any other pinned key is `signature_untrusted_key`. |

#### `[source.local]`

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | **yes** | Path to the source directory, resolved relative to the recipe root. |

### `[delegate]`

Lets a recipe borrow definitions that live *inside* the fetched source tree
rather than in the recipe directory. May be written as a bare boolean
(`delegate = true`, equivalent to `all = true`) or as a table.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `all` | bool | no | Delegate everything (build, env, wrap, packages). |
| `build` | bool | no | Delegate build targets. |
| `env` | bool | no | Delegate `[env]`. |
| `wrap` | bool | no | Delegate `[wrap]`. |
| `packages` | bool | no | Delegate package definitions. |

Any other key is an `unknown delegate key` error. A category is delegated when
either its own flag or `all` is set.

### `[source_package]`

Controls the **corresponding-source package** a recipe emits alongside its
binary packages. Emission is automatic — the table exists only to opt out or
rename. A recipe emits one when both of these hold: it has a reproducible
`[source]` (`git` or `url`; local overrides and bare branch refs never
qualify), and it produces at least one `peipkg`-format package.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | no | Package name. Default `<recipe-dir>-source`. |
| `enabled` | bool | no | Set `false` to emit no source package. Default `true`. |

Any other key is an `unknown source_package key` error.

The emitted package is `noarch`, versioned identically to the recipe's package
members (members that disagree on version are a
`source_package_version_conflict` error), licensed as the conjunction of the
members' licenses, and installs under `/usr/src/dist/<name>-<version>/`
(with any `-source` suffix stripped from `<name>`):

- `upstream/` — the pristine source input: a url source's downloaded artifact
  byte-for-byte, so its hash matches the committed `pekit.lock`, or a
  `git archive` export of the locked commit.
- `patches/` — the recipe's patch series, when a `patches/` directory exists.
- `recipe/` — the build-controlling files from the recipe directory:
  `pekit.toml`, package definitions (including `packages.pekit/`), env files,
  `pekit.lock`, and `keys/`. `*.keyring.pekit.toml` files are never included.

Every `peipkg`-format member the recipe emits carries the source package's
name in its manifest's `build.source_package` field, linking each binary to
its corresponding source.

### `[build]` / `[test]` / `[install]` / `[clean]` {#targets}

These four sections define **targets** — the commands pekit runs. Each section
takes one of two shapes, distinguished by whether a `command` key is present
directly in the section:

- **Bare target:** `command` (and the other target keys) sit directly under
  the section, e.g. `[build]` with `command = "make"`. This defines a single
  target named `main`.
- **Named targets:** the section contains sub-tables, e.g. `[build.docs]` and
  `[build.linked]`, each a target. Each name must be a valid selector.

Mixing the two — a bare `command` alongside a named sub-table in the same
section — is a `mixed_targets` error. (A sub-table whose key is itself a target
key, such as `[build.dependencies]`, is read as part of the bare target, not as
a named target.)

Fields of a single target:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `command` | string or array of strings | **yes** | The command. String form runs through a shell; array form is an argv executed directly. An empty array is rejected. |
| `needs` | array of strings | no | Names of other targets in the same section that must run first. |
| `clear_out` | bool | no | Wipe this target's output directory before running. Default `true`. |
| `dependencies` | table | no | **Build targets only.** Build-time dependencies the target needs provisioned. See below. |
| `sign` | table | no | **Build targets only.** Files in the target's output to sign after the command succeeds, by signature kind. See [`sign`](#target-sign) below. |

Any other key in a target table is an `unknown target key` error. `dependencies`
and `sign` are accepted only in `[build]` targets; using either in
`test`/`install`/`clean` is an unknown key.

The `dependencies` table is keyed by **provider** (a selector), each mapping
dependency names to version constraints:

```toml
[build.linked.dependencies]
pesb-dev.libc = ">=2.38"
pesb-dev."pkgconfig(zlib)" = "*"
```

| Level | Type | Meaning |
| --- | --- | --- |
| provider | table | A dependency-provider selector. |
| provider.*&lt;name&gt;* | string | Version constraint for the capability *name*. Must be non-empty; use `"*"` for any version. |

Dependency names may be real package names or virtual capabilities (sonames,
`pkgconfig(...)`, etc.) and are validated against peipkg's capability grammar.
See [Commands and targets](/pekit/running/commands-and-targets.md).

#### `[build.<name>.sign]` {#target-sign}

The `sign` table is keyed by **signature kind**. Each kind is a table mapping
a path or glob, relative to the target's `$PEKIT_OUT`, to the dotted path of
the keyring entry holding the signing key:

```toml
[build.main.sign.pip]
"bin/peinit"  = "tcb.priv"
"lib/*.so.*"  = "tcb.priv"
```

The kind decides the signature format and where it is stored; the keyring
entry is resolved through the ordinary [keyring mechanism](/pekit/recipes/environments-and-keyrings.md#keyrings)
when the target runs. Signing happens after the target's command exits 0 and
before any dependent target or package sees the output.

| Level | Type | Meaning |
| --- | --- | --- |
| kind | table | A signature kind. The only kind is `pip` — the Peios binary signature that confers a [Process Integrity Protection](/peios/advanced-peios/pspk/binary-signing-and-pip/process-integrity-protection.md) tier. Any other name is `unknown_key`. |
| kind.*&lt;pattern&gt;* | string | A relative path or glob (the same glob syntax as `[files]`). The value is a keyring leaf path such as `tcb.priv` whose value is the path to the private key. Must be non-empty. |

For `pip`, the key is an ML-DSA-65 private key — PKCS#8 PEM as produced by
`openssl genpkey -algorithm ML-DSA-65`, or a raw 32-byte seed — and every
matched file must be a 64-bit little-endian ELF. A pattern that matches
nothing (`sign_target_missing`), a keyring entry that is absent or does not
load (`signing_key`), a file that cannot carry the signature (`sign_failed`),
and two patterns naming different keys for one file (`sign_conflict`) are all
errors; pekit never leaves a listed file unsigned. The mechanism is described
in [Signing and provenance](/pekit/running/signing-and-provenance.md#signing-binaries-for-pip).

### `[gen]` {#gen}

A `[gen]` section defines **source-generating** targets: they run at the recipe
root and write generated source *into the tree* rather than producing an
`out_dir` artifact. Bare/named shapes work exactly as for the target sections
above (`[gen]` is `gen.main`; `[gen.<name>]` names a target). Gen targets have
their own fields — they take **no** `needs` or `clear_out`:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `command` | string or array of strings | **yes** | The generator. Runs at the recipe root; writes generated source in place. |
| `verify_command` | string or array of strings | no | The drift gate: exit 0 = in sync, non-zero = stale. Run by `pekit verify` and by the consuming-command pre-flight. |
| `verify_on_build` | array of strings | no | Build target names this gate applies to. **Absent** gates every build; **`[]`** gates none; a list gates only those. |
| `verify_on_test` | array of strings | no | As `verify_on_build`, for the `test` namespace. |
| `dependencies` | table | no | Build-time dependencies for `command` (and for `verify_command`, unless overridden). Same shape as a build target's `dependencies`. |
| `verify_dependencies` | table | no | When present, **fully replaces** `dependencies` for the `verify_command` run. |

Any other key is an `unknown target key` error. Setting `verify_on_build`,
`verify_on_test`, or `verify_dependencies` without a `verify_command` is an
`invalid_gen` error (there is nothing to gate).

A gen target's `$PEKIT_OUT` is a pekit-managed scratch directory
(`<out_dir>/.scratch/gen/<name>`) outside the artifact area, cleaned on success
and retained on failure — a convenient place for `verify_command` to regenerate
into for comparison. For the command surface, the pre-flight, and `--no-verify`,
see [Commands and targets](/pekit/running/commands-and-targets.md#generating-source-gen-and-verify).

## `package.pekit.toml`

The package file describes one or more package artifacts to emit from the built
source. The same schema applies to the base file (`package.pekit.toml`) and to
per-member member files (`<name>.package.pekit.toml`); see
[Multi-package recipes](/pekit/recipes/multi-package.md) and
[Supporting files](/pekit/reference/supporting-files.md) for how the files are
discovered and layered.

### Worked example

```toml
format = "peipkg"
builds = ["main"]

[package]
name = "tool"
version = "{{version}}"
architecture = "x86-64-v3"
description = "An example tool"
license = "MIT"
homepage = "https://example.com/tool"
default_root = "opt.tool"

[dependencies]
libc = ">=2.38"
zlib = { constraint = ">=1.3", root = "opt.tool" }

[optional_dependencies]
libcurl = ">=8.0"

[provides]
tool = "{{version}}"

[files]
"main:bin/tool" = "usr/bin/tool"
"@recipe:share/" = { path = "usr/share/tool/", override = true }

[symlinks]
"usr/bin/t" = "tool"

excludes = ["@recipe:share/*.tmp"]

[claims.provides.editor.default]
path = "usr/bin/editor"
target = "usr/bin/tool"

[[publish.localdir]]
path = "dist"
overwrite = false
```

### Top-level keys

The package file accepts exactly these top-level keys; anything else is an
`unknown package key` error.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `format` | string | no | Artifact format: `"tar"` or `"peipkg"`. Default `"tar"`. |
| `clear_out` | bool | no | Wipe the package stage directory before building. Default `true`. |
| `builds` | array of strings | no | Names of build targets this package requires before staging. |
| `package` | table | no | Package metadata. See [`[package]`](#package). |
| `files` | table | no | Payload file mappings. See [`[files]`](#files). |
| `symlinks` | table | no | Symlinks to embed in the payload. See [`[symlinks]`](#symlinks). |
| `excludes` | array of strings | no | Refs/globs to drop from directory payloads. See [`excludes`](#excludes). |
| `multipack` | table | no | Expand into a family of instances. See [`[multipack]`](#multipack). |
| `publish` | table | no | Where to copy finished artifacts. See [`[publish]`](#publish). |
| `dependencies` | table | no | Runtime dependencies. See [`[dependencies]`](#dependencies). |
| `optional_dependencies` | table | no | Optional runtime dependencies (same schema as `dependencies`). |
| `conflicts` | table | no | name → constraint of packages this one conflicts with. |
| `provides` | table | no | name → version of capabilities this package provides. |
| `replaces` | table | no | name → constraint of packages this one replaces. |
| `side_effects` | array of strings | no | Declared install-time side effects. |
| `sd_overrides` | table | no | path → SDDL security-descriptor overrides. |
| `claims` | table | no | Claim slots on provides/dependencies. See [`[claims]`](#claims). |

> [!IMPORTANT]
> The `tar` format carries payload only: it **cannot express manifest
> metadata**. If a `tar` package sets `version`, `architecture`,
> `description`, `license`, `homepage`, or any of `dependencies`,
> `optional_dependencies`, `conflicts`, `provides`, `replaces`,
> `side_effects`, or `sd_overrides`, the build fails with
> `unsupported_manifest_field`. The `peipkg` format requires
> `[package].version`, `[package].architecture`, and `[package].license` —
> the license is optional at the format level, but pekit requires it so every
> package states its redistribution terms and the corresponding-source
> package can derive its own.

Most string-valued fields (metadata, dependency names/constraints, file refs
and destinations, claim paths/targets) are run through the template engine,
where `{{version}}` and `{{multipack}}` are available. See
[Versions](/pekit/recipes/versions.md) and
[Multi-package recipes](/pekit/recipes/multi-package.md).

### `[package]`

Package metadata. Accepts exactly these keys; any other is an
`unknown package metadata key` error. All values are strings.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | no | Package name. Defaults to the package selector (suffixed with the multipack value for multipack instances) when omitted. |
| `version` | string | no | Package version. Required for `peipkg`. |
| `architecture` | string | no | Target architecture. Required for `peipkg`. |
| `description` | string | no | Human-readable description. |
| `license` | string | no | License identifier. Required for `peipkg`. |
| `homepage` | string | no | Project homepage URL. |
| `default_root` | string | no | Preferred [named root](/peios/using-peios/package-management/claims.md) for a top-level install. Must be a dotted named reference matching `[a-z0-9][a-z0-9_-]*` per segment, never a filesystem path (a `/` is rejected). |
| `special_system_package` | boolean | no | Declares the package exempt from the payload layout rules, for the rare package whose job is to lay down the structure those rules protect — the base-filesystem package that mints the mountpoint tree is the archetype. Setting it disables pekit's layout validation for this package. It grants nothing at install time: whoever installs or composes the result must *also* pass `--dangerously-bypass-path-restrictions`, so a recipe can propose its own exemption but never grant one. Defaults to `false`. |

Note that `dependencies`, `provides`, `conflicts`, and the rest are **top-level
tables**, not sub-keys of `[package]`.

### `[dependencies]`

Runtime dependency constraints. `[optional_dependencies]` has the identical
schema. Each entry is either a bare constraint string (the short form) or a
table that additionally places the dependency in a named root.

| Form | Type | Meaning |
| --- | --- | --- |
| *&lt;name&gt;* `= "constraint"` | string | Version constraint; the dependency lands in the depender's own root. |
| *&lt;name&gt;* `= { constraint, root }` | table | Table form (below). |

Table form keys — only these two are accepted (`unknown_key` otherwise):

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `constraint` | string | no | Version constraint. Omitting it matches any version. |
| `root` | string | no | Named root to place the dependency in. Same grammar as `default_root`. |

Dependency names may be real or virtual capabilities. See
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md).

### `[files]`

Maps a **source ref** (the key) to a destination path within the package
payload (the value). The value is either a string destination or a table.

| Value form | Meaning |
| --- | --- |
| `"ref" = "dest/path"` | Copy the ref to `dest/path`. |
| `"ref" = { path, override }` | Table form (below). |

Table form keys — only these are accepted:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | **yes** | Destination path in the payload. Must be non-empty. |
| `override` | bool | no | Mark the payload as an override entry (skips manifest payload validation for `peipkg`). Default `false`. |

The source ref selects where the file is read from. A bare path with no prefix
is resolved against the recipe root (or the layer's owner). Explicit forms:
`@recipe:<path>`, `@source:<path>`, `@workspace:<path>`, and
`<build-target>:<path>` (reading a build target's output; `:<path>` alone uses
the `main` build target). Destinations ending in `/` and glob patterns are
supported. See [Packages](/pekit/recipes/packages.md) for ref resolution and glob
semantics.

### `[symlinks]`

Maps a destination path within the payload (the key) to a symlink target (the
value).

| Value form | Meaning |
| --- | --- |
| `"usr/bin/x" = "target"` | Create a symlink at `usr/bin/x` pointing at `target`. |
| `"usr/bin/x" = { target, override }` | Table form (below). |

Table form keys — only these are accepted:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `target` | string | **yes** | Symlink target text. Must be non-empty. |
| `override` | bool | no | Mark as an override entry. Default `false`. |

### `excludes`

An array of refs/globs. During directory payload expansion, files matching an
exclude pattern (resolved against the same root as the file mapping being
expanded) are dropped from the payload. Patterns use the same ref forms as
`[files]` keys.

### `[multipack]`

Expands one definition into a family of package instances, one per enumerated
value. The only accepted key is `enum`, which takes one of two shapes:

**Explicit list:**

```toml
[multipack]
enum = ["gtk3", "gtk4"]
```

Each value must be a valid selector, non-empty, and unique.

**Enumerated from files:**

```toml
[multipack.enum.files]
path = "@source:themes/*/theme.ini"
regex = "^(?P<value>.+)$"
```

`[multipack.enum.files]` accepts exactly `path` and `regex`, both **required**:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | **yes** | A ref glob (templated) whose matches are enumerated. |
| `regex` | string | **yes** | Regex applied to each match's basename. Must have exactly one capture group, or a single named `value` group, which supplies the instance value. |

See [Multi-package recipes](/pekit/recipes/multi-package.md).

### `[publish]`

Where finished artifacts are copied. The only accepted target is `localdir`,
written as an **array of tables**:

```toml
[[publish.localdir]]
path = "dist"
overwrite = false
```

Each entry accepts only `path` and `overwrite`:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | **yes** | Destination directory, relative to the workspace root (or recipe root outside a workspace). Templated. The artifact keeps its own filename. |
| `overwrite` | bool | no | Allow replacing an existing file at the destination. Default `true`. |

### `[claims]`

Attaches **claim** slots to provides and dependency entries (a claim is a
symlink slot a provider fills or a consumer expects). The `[claims]` table has
two sides — `provides` and `dependencies` — each keyed by the provides or
dependency name, then by slot, then to a descriptor. Any side other than
`provides`/`dependencies` is an `unknown_key` error.

```toml
[claims.provides.<name>.<slot>]
path = "usr/bin/editor"
target = "usr/bin/tool"

[claims.dependencies.<name>.<slot>]
path = "usr/bin/editor"
```

A slot descriptor accepts exactly these two keys (anything else is rejected);
both are strings and both are templated:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | no | The symlink location (set on a consumer, or as a provider default). |
| `target` | string | no | The holder file within this package (set on a provider). A provider's target must point at a file the package ships. |

Provider-side slots (`claims.provides`) attach to matching `[provides]`
entries by name; consumer-side slots (`claims.dependencies`) attach to matching
`[dependencies]`/`[optional_dependencies]` entries. See
[Dependencies and claims](/pekit/recipes/dependencies-and-claims.md) and the Peios
[claims](/peios/using-peios/package-management/claims.md) model for the resolution behaviour.

## See also

- [Supporting files](/pekit/reference/supporting-files.md) — the workspace, env, keyring, lockfile, and patch-series schemas.
- [Anatomy of a recipe](/pekit/recipes/anatomy.md) — the narrative walk-through of these files.
- [Packaging files](/pekit/recipes/packages.md) — how `[files]` refs and destinations resolve.

---

# Supporting files and reference tables

_pekit / Reference_

> Exact schemas for pekit's non-recipe files (workspace, env, keyring, pekit.lock, patch series) plus template variables, source-ref roots, and publish targets.

This page documents the TOML files that sit *alongside* a recipe — the
workspace file, env files, and keyring files — and gives exhaustive reference
tables for the three substitution systems used throughout pekit: template
variables, source-ref roots, and publish targets.

All files are TOML. Every loader rejects unknown top-level keys with an
`unknown_key` diagnostic, so the key tables below are complete: any key not
listed is an error.

For the recipe file itself (`pekit.toml` / `*.pekit.toml`), see
[recipe-format](/pekit/reference/recipe-format.md).

---

## `workspace.pekit.toml`

A workspace file marks a directory as a pekit workspace and declares
distro-wide defaults.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `include` | array of strings | **yes** | Member patterns the workspace builds. Must be present and non-empty, or the load fails (`missing_key`). |
| `exclude` | array of strings | no | Patterns removed from the `include` set. |
| `[env]` | table | no | Workspace-level environment variables. See [env table](#env-table). |
| `[wrap]` | table | no | Workspace-level command wrapper. See [wrap table](#wrap-table). |
| `[policy]` | table | no | Distro-wide derivation policy. See below. |

### `[policy]`

The `[policy]` table currently carries exactly one sub-table. Any other key
under `[policy]` is rejected.

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `[policy.symbol_versions]` | table (string → string) | no | Maps a shared-library soname to the symbol-version **token prefix** whose tokens are commensurable with the providing package's version. Governs which sonames get a symbol-version floor during dependency derivation. |

Each entry in `[policy.symbol_versions]` is `soname = "PREFIX_"`:

```toml
[policy.symbol_versions]
"libc.so.6"     = "GLIBC_"
"libstdc++.so.6" = "GLIBCXX_"
```

---

## `env.pekit.toml`

An env file provides a *named, selectable* layer of environment variables,
command wrapper, and dependency-provider override.

An env file must declare **at least one** of `[env]`, `[wrap]`, or
`dependency_provider`; a file with none of them is rejected (`missing_key`).

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `[env]` | table | one of the three | Environment variables. See [env table](#env-table). |
| `[wrap]` | table | one of the three | Command wrapper. See [wrap table](#wrap-table). |
| `dependency_provider` | string | one of the three | Selector naming which of a build target's declared dependency-provider blocks (`[build.<target>.dependencies.<provider>]`) is exported as the `PEKIT_DEPENDENCIES*` variables. Validated as a [selector](#selectors). |

### `--env <name>` selection

The active env file is chosen from the `--env <name>` flag (default `main`),
resolved relative to the recipe root:

| `--env` value | File loaded | Must exist? |
| --- | --- | --- |
| *(unset)* or `main` | `env.pekit.toml` | no (silently absent) |
| `none` | *(no env file loaded)* | n/a |
| any other `<name>` | `<name>.env.pekit.toml` | **yes** — a missing file is an error |

So `--env prod` loads `prod.env.pekit.toml`, and it must exist. The default
`env.pekit.toml` is optional.

### `[env]` table

Shared by `workspace.pekit.toml`, `env.pekit.toml`, and the recipe's own
`[env]`. It is a flat table of `NAME = "value"` string pairs.

| Rule | Detail |
| --- | --- |
| Value type | Every value must be a string. |
| Name grammar | Names must match `^[A-Za-z_][A-Za-z0-9_]*$`. |
| Reserved prefix | User env may not set any `PEKIT_*` name — those are pekit-managed. A `PEKIT_`-prefixed name is rejected at build time (`reserved_env`). |
| Layering | Applied workspace → (delegated source) → recipe → env file, later layers overriding earlier ones. |
| Expansion | Values are shell expressions, not literals. `$NAME` expands when the target runs, so a value may reference a managed `PEKIT_*` variable or an env value declared before it. Values are word-split-safe: spaces are preserved, so a flag list needs no quoting of its own. |

```toml
[env]
CC = "clang"
CFLAGS = "-O2 -pipe"
CXXFLAGS = "$CFLAGS -Wp,-D_GLIBCXX_ASSERTIONS"
TOOL_PATH = "$PEKIT_ROOT/tools"
```

`$` always expands — a literal `$` is not expressible here, and `\$` does not
escape it. A value needing one (an `$ORIGIN` rpath, say) belongs in the target's
`command`, where the recipe has the full shell.

### `[wrap]` table

Shared by `workspace.pekit.toml`, `env.pekit.toml`, and the recipe. Wraps every
target command in another command (sandbox, `nice`, a toolchain shim, etc.).

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `command` | string or array of strings | no | The wrapper. Must contain exactly one `{{command}}` placeholder. |

`{{command}}` rules:

- **String form** — the string must contain `{{command}}` exactly once.
- **Array form** — exactly one element must be the literal `{{command}}` (as a
  *complete* argument, not a substring), and it may not be element `0` (the
  program). An element that merely contains `{{command}}` inside other text is
  rejected.

```toml
[wrap]
command = ["bwrap", "--ro-bind", "/usr", "/usr", "{{command}}"]
```

Note: `{{command}}` here is a wrap-only placeholder handled by the wrap
machinery. It is **not** one of the [template variables](#template-variables)
below and is not available in ordinary recipe strings.

---

## `*.keyring.pekit.toml`

A keyring file supplies secrets to the build environment as `PEKIT_KEYRING_*`
variables. Keyrings are opt-in per invocation (`--keyring`); a keyring named
`<name>` resolves to `<name>.keyring.pekit.toml`, searched in the workspace
root then the recipe root (a path-like value is used verbatim).

### Schema

The file is a tree of nested TOML tables whose **leaves are strings**. Each
leaf's dotted path is converted into an environment-variable name:

- Prefix `PEKIT_KEYRING_`, then the dotted path uppercased.
- Every run of non-alphanumeric characters (including the `.` separators)
  collapses to a single `_`.
- Trailing `_` is trimmed.

| Leaf path | Exported variable |
| --- | --- |
| `token` | `PEKIT_KEYRING_TOKEN` |
| `github.token` | `PEKIT_KEYRING_GITHUB_TOKEN` |
| `registry.api-key` | `PEKIT_KEYRING_REGISTRY_API_KEY` |

```toml
# placeholders only — never commit real secrets
token = "REPLACE_ME"

[github]
token = "REPLACE_ME"

[registry]
api-key = "REPLACE_ME"
```

### Restrictions

| Rule | Detail |
| --- | --- |
| Leaf type | Every leaf must be a string. A non-string leaf is an error. |
| Nesting | Sub-tables nest arbitrarily; each nesting level adds a segment to the exported name. |
| Typed entries | A table containing a `path` or `content` key is a *typed* entry and is **not supported yet** (`unsupported_keyring_entry`). |
| Collisions | A keyring export that collides with a normal or managed env variable is an error (`env_collision`). |

### Well-known entries

Most keyring values are opaque to pekit — they exist to be exported. The
entries below are additionally read by pekit itself:

| Leaf path | Meaning |
| --- | --- |
| `signing.package_key` | Path to an Ed25519 private key — a raw 32-byte seed, or PKCS#8 PEM as produced by `openssl genpkey -algorithm ed25519`. When present, every peipkg-format artifact the run produces — package members and `-source` packages alike — is packed with an embedded signature naming the key's fingerprint, and each emits a `sign` event. A relative path resolves against the invocation's working directory. A configured key that cannot be loaded is an error (`signing_key`); pekit never silently falls back to unsigned output. |

Without a configured signing key, `package` writes unsigned artifacts, and
`publish` refuses peipkg-format packages unless `--allow-unsigned` is passed
(`unsigned_publish`).

A build target's [`sign` table](/pekit/reference/recipe-format.md#target-sign)
reads further entries, but which ones is the recipe's choice: each `sign.pip`
value is the dotted path of a leaf holding the path to an ML-DSA-65 private
key. Those entries are still exported to the target like any other.

---

## `pekit.lock`

The machine-written source lockfile, created and updated by pekit in the recipe
directory beside `pekit.toml`. It pins fetched source inputs trust-on-first-use:
the first resolve of a version records what was fetched, and every later
resolve verifies against the record — see
[Sources](/pekit/recipes/sources.md#the-lockfile) for the behaviour. Commit it,
and do not edit it by hand: `pekit lock --repin --version <v>` is the one
supported way to change an existing entry. Loading is strict — an unknown key
is `unknown_key` and an unsupported `schema` is `lock_schema`.

### Schema

| Key | Type | Meaning |
| --- | --- | --- |
| `schema` | integer | Lockfile schema version. Currently `1`. |
| `[[source]]` | array of tables | One entry per locked version, kept sorted by version. |

Each `[[source]]` entry carries the keys for its source kind:

| Key | Set for | Meaning |
| --- | --- | --- |
| `version` | all | The locked version. Empty for a versionless url fetch. |
| `url` | url sources | The rendered URL fetched at lock time. Provenance only — the hash, not the address, is the assertion. |
| `sha256` | url sources | SHA-256 of the fetched artifact. |
| `ref` | git sources | The rendered ref the version resolved through. |
| `commit` | git sources | The commit the ref resolved to — the assertion. |
| `signature_key` | url sources with `[source.url.signature]` | Hex fingerprint of the pinned upstream key that verified the artifact at lock time. |
| `locked_at` | all | UTC timestamp of the pinning run (RFC 3339). |

### Worked example

```toml
schema = 1

[[source]]
  version = "0.5.12"
  url = "https://example.org/dash-0.5.12.tar.gz"
  sha256 = "6a474ac46e8b0b32916c4c60df694c82058d3297d8b385b74508030ca4a8f28a"
  signature_key = "9f0b7ddc1325a3e2c40e5f0a88b1c62f3d94ab07"
  locked_at = "2026-08-10T11:00:00Z"

[[source]]
  version = "1.2.0"
  ref = "v1.2.0"
  commit = "8f3a1bc99e2f6d41c07b21ac1886d63b7f2e5d10"
  locked_at = "2026-08-10T11:00:00Z"
```

---

## The patches directory

The directory named by `[source] patches = "<dir>"` in `pekit.toml`
(conventionally `patches`, and always a single path segment). pekit applies
its series to the materialised source tree before any target runs — see
[Sources](/pekit/recipes/sources.md#patches) for the behaviour — and the whole
directory ships in the recipe's source package under `patches/`.

### The series file

`<dir>/series` lists the patches in apply order: one slash-separated relative
path per line. `#` starts a comment, whole-line or trailing; blank lines are
ignored; entries must not contain whitespace. Subdirectories are allowed
(`misc/fix.patch`).

Loading is strict:

| Condition | Error |
| --- | --- |
| `series` missing or unreadable | `patch_series` |
| An entry that repeats, contains whitespace, or escapes the directory | `patch_series` |
| A series entry with no file behind it | `patch_missing` |
| A `*.patch` file in the directory the series does not list | `unused_patch` (permitted by `--allow-unused`) |

Application is `git apply` per entry, in series order, with no fuzz: a failed
hunk is `patch_apply` and a patch that matches nothing is `patch_skipped`.

### Worked example

```
patches/
  series
  misc/
    mke2fs-root-sd.patch
```

with a `series` of:

```
# Applied in dependency order:
misc/mke2fs-root-sd.patch   # stamp the root inode's SD at format time
```

Give each patch file a short header — what it changes and its upstream status
(submitted, backport, Peios-specific). The series ships with the
corresponding source, where that header is a third party's only context.

---

## Template variables

Recipe and package **value strings** (source refs, `ref`, package metadata,
file/symlink paths, excludes, claims, multipack `enum.files.path`, publish
`path`) are run through the template engine before use. The syntax is
`{{name}}`, where `name` matches `[A-Za-z0-9_]+`. An unknown name is an error.

| Variable | Source | Availability |
| --- | --- | --- |
| `{{version}}` | the selected version, verbatim (`Raw`) | Errors when no version is selected. |
| `{{major}}` | major component | Available once a version is selected. |
| `{{minor}}` | minor component | Errors if the selected version has no minor component. |
| `{{patch}}` | patch component | Errors if the selected version has no patch component. |
| `{{prerelease}}` | pre-release component (after `-`) | Empty string when absent. |
| `{{buildmeta}}` | build-metadata component (after `+`) | Empty string when absent. |
| `{{multipack}}` | the current multipack instance value | Only available while expanding a multipack package's per-instance values; errors in any non-multipack context. |

A version string parses as `major[.minor[.patch]][-prerelease][+buildmeta]`, so
`{{minor}}` / `{{patch}}` are only present when the version actually carries
them.

### Shell targets use `$PEKIT_*` instead

The `{{...}}` engine applies to recipe *configuration* strings, not to the shell
bodies of build/test/install/clean commands. Target commands instead read the
pekit-managed environment. The version equivalents are:

| Template variable | Environment variable |
| --- | --- |
| `{{version}}` | `$PEKIT_VERSION` |
| `{{major}}` | `$PEKIT_VERSION_MAJOR` |
| `{{minor}}` | `$PEKIT_VERSION_MINOR` |
| `{{patch}}` | `$PEKIT_VERSION_PATCH` |
| `{{prerelease}}` | `$PEKIT_VERSION_PRERELEASE` |
| `{{buildmeta}}` | `$PEKIT_VERSION_BUILDMETA` |

Alongside these, the managed environment also exports `PEKIT_RECIPE_ROOT`,
`PEKIT_SOURCE_ROOT`, `PEKIT_LITERAL_ROOT`, `PEKIT_ROOT`, `PEKIT_OUT_BASE`,
`PEKIT_OUT`, `PEKIT_COMMAND`, `PEKIT_TARGET`, `PEKIT_WORKSPACE_ROOT`,
`PEKIT_BUILD_TIMESTAMP`, `PEKIT_SOURCE_TIMESTAMP`, a `PEKIT_<DEP>_OUT` per
declared `needs` dependency, and any `PEKIT_KEYRING_*` from active keyrings. The
full environment contract is documented in the
[command-line reference](/pekit/reference/cli.md).

---

## Source-ref roots

Package `files` sources, `excludes`, and `multipack.enum.files.path` are
*references* whose leading token selects which root the path is resolved
against. A reference is `ROOT:path` (or a bare path, which normalises to the
owner's default root).

| Reference form | Root | Meaning |
| --- | --- | --- |
| `target:path` | the build target's output stage | Reads from the output of build target `target`. |
| `:path` | build target `main`'s output stage | Same as above with the target name defaulted to `main`. |
| `@source:path` | the materialised source tree (`LiteralRoot`) | Reads from the checked-out / extracted source. |
| `@recipe:path` | the recipe directory | Reads a file shipped next to the recipe. |
| `@workspace:path` | the workspace root | Reads a file from the workspace. Errors if used outside a workspace. |
| bare `path` | owner default | A path with no `:` normalises to the owning layer's root: `@recipe:` for recipe packages, `@source:` for source packages, `@workspace:` for workspace packages. |

Any reference containing a `:` that is not one of the `@`-prefixed roots is a
**build** reference; the token before the `:` is the target name. See
[packages](/pekit/recipes/packages.md) for how these are used in a package's
`[files]` table.

---

## Publish targets

The `publish` command copies a package's built artifact to declared destinations. There is
exactly one publish kind: `localdir`. Any other `[publish.*]` key is rejected
(`unknown_key`). A package selected for publishing that declares no target is an
error.

Targets are an array of tables:

```toml
[[publish.localdir]]
path = "dist/{{version}}"
overwrite = false
```

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `path` | string | **yes** | Destination **directory**, relative to the base root. Templated with `{{version}}` and `{{multipack}}`. The artifact keeps its own filename. |
| `overwrite` | bool | no (default `true`) | Whether an existing destination file may be replaced. When `false`, an existing destination is an error (`publish_exists`). |

Resolution details:

| Aspect | Behaviour |
| --- | --- |
| Base root | The workspace root when building in a workspace, otherwise the recipe root. `path` is joined onto it. |
| Final destination | `<base>/<path>/<artifact-filename>`. |
| Collisions | Two instances resolving to the same destination is an error (`publish_collision`) unless they are the same artifact. |

<a id="selectors"></a>

## Selectors

Several names above are validated as **selectors**: target names, package
selectors, multipack values, and `dependency_provider`. A selector must match
`^[A-Za-z0-9_.+-]+$`, must not be empty, must not start with `-`, and must not
contain `/` or `:`.

## See also

- [Recipe format reference](/pekit/reference/recipe-format.md) — the `pekit.toml` and package-file schemas.
- [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md) — how env and keyring files are selected and layered.
- [Signing and provenance](/pekit/running/signing-and-provenance.md) — `signing.package_key` and `pekit.lock` in the trust chain.

---

# Command-line reference

_pekit / Reference_

> Every pekit flag with its value form, the command-by-flag capability matrix, validation rules, exit codes, and the full PEKIT_* environment contract.

This is the exhaustive reference for pekit's command line and the environment it
exports to the commands it runs. Every flag listed here is a real flag accepted
by the parser; there are no others. For a narrative walk-through of the grammar
and recipe location, see [Invocation and flags](/pekit/running/invocation.md);
for what each command does, see
[Commands and targets](/pekit/running/commands-and-targets.md).

## Synopsis

```text
pekit [global flags] <command> [args and flags]
pekit [global flags] workspace [workspace flags] <command> [args and flags]
```

`<command>` is exactly one of `build`, `test`, `install`, `package`, `publish`,
`clean`, `gen`, `verify`, `lock`, or `workspace`. Flags may appear before or after the command word and
are parsed identically on both sides — the sole exception is the `workspace` tail
(see [Workspace flags](#workspace-flags)). A bare `--` ends flag parsing; every
following token is captured verbatim as a positional selector. Positional
selectors may not begin with `-`.

There is **no `--help` and no `help` command**, and no version banner: `-V` is an
alias for the `--version` *selector*, not a print-version flag.

## Flag value forms

The shape of a flag determines how its value may be written.

| Shape | How a value is written |
|---|---|
| No-value | Never takes a value. `--flag=x` errors with `unexpected_flag_value`. |
| Required-value | `--flag value` **or** `--flag=value`. A missing next token, a next token that is `--` or begins with `-`, or an empty `--flag=` all error with `missing_flag_value`. |
| Optional-value | Value **only** via `--flag=value`. The bare `--flag` is valid and means an empty value; it never consumes the following token. |

## Global flags

Accepted by every command, before or after the command word. These are never
subject to the capability matrix below.

| Flag | Value form | Semantics |
|---|---|---|
| `--recipe <path\|dir>` | Required-value | Use this recipe instead of searching upward from the cwd. A directory means `<dir>/pekit.toml`. |
| `--workspace <path\|dir>` | Required-value | Use this workspace file/root. Valid **only** with the `workspace` command. |
| `--allow-unused` | No-value | Downgrade "command does not support this recognised flag" from a fatal error to a suppressed notice. |
| `--dry-run` | No-value | Run the full planning pipeline but stop before any side effect; emit plan events instead. |
| `--quiet` | No-value | Human renderer, routine progress suppressed (only `warning`, `artifact`, `publish`, `workspace_summary`, `lock`, `sign` events). |
| `--verbose` | No-value | Human renderer plus extra staging/progress events. |
| `--json` | No-value | Emit newline-delimited JSON events on stdout instead of the human log. |

The renderer is chosen from these flags: `--json` selects the JSON renderer
(with `--dry-run` it buffers and prints a single `plan` object); otherwise the
human renderer runs, in quiet mode when `--quiet` is set.

## Version, source, and build flags

These are command-specific: each is only accepted by the commands marked in the
[capability matrix](#capability-matrix). Passing one to a command that does not
support it is `unsupported_flag` unless `--allow-unused` is set.

| Flag | Value form | Meaning |
|---|---|---|
| `--version <v>`, `-V <v>` | Required-value | Select a single recipe version. `-V` is an exact alias. |
| `--latest` | No-value | Select only the newest available version. |
| `--all-versions` | No-value | Act on every available version. |
| `--local[=<path>]` | Optional-value | Use a local source tree instead of resolving source; bare form uses the default, `=<path>` overrides it. |
| `--prefer-local[=<path>]` | Optional-value | Use the local source when present, otherwise resolve normally. |
| `--no-build[=<list>]` | Optional-value | Reuse already-staged build targets. Bare form reuses all; `=a,b,c` reuses only the named build targets. Only affects `build`-kind targets whose stage already exists. |
| `--no-verify[=<list>]` | Optional-value | Skip the [gen drift-check pre-flight](/pekit/running/commands-and-targets.md#generating-source-gen-and-verify). Bare form skips all; `=a,b,c` skips only the named gen targets (an unknown name errors). On `package`/`publish` it passes through to the builds they trigger. |
| `--env <name>` | Required-value | Select the environment file layer. `main` (default) loads `env.pekit.toml`; `<name>` loads `<name>.env.pekit.toml`; `none` loads no env file. |
| `--keyring <name\|path>` | Required-value | Load a keyring file. A bare name resolves to `<name>.keyring.pekit.toml` searched in the workspace root then recipe root; a path-like value is used directly. Repeatable. |
| `--keyring.<path>=<value>` | Dotted, `=` required | Set a single keyring value inline. Requires a non-empty dotted path and the `=`; otherwise `invalid_keyring_flag`. Repeatable. |
| `--refresh-source` | No-value | Force source re-materialisation, ignoring any source cache. The [lockfile](/pekit/reference/supporting-files.md#pekitlock) is still enforced against the fresh download. |
| `--allow-unanchored` | No-value | Permit `publish` from source with unanchored provenance — no checksum **and** no lock entry (otherwise `unanchored_provenance`). |
| `--allow-unsigned` | No-value | Permit `publish` of peipkg-format packages without a configured [signing key](/pekit/reference/supporting-files.md#well-known-entries) (otherwise `unsigned_publish`). |
| `--repin` | No-value | `lock` only: re-download the selected version and **replace** its lock entry — the explicit accept-changed-upstream-bytes ceremony. Requires an exact single `--version`. |
| `--all` | No-value | `package`/`publish`: act on every package definition instead of a selector. `gen`/`verify`: act on every gen target. |
| `--output-only` | No-value | `clean`: remove managed output only, without running a clean target. |
| `--target-only` | No-value | `clean`: run the clean target only, without removing managed output. |

`--version`/`-V`, `--latest`, and `--all-versions` form one mutually-exclusive
group ("version selection"); `--local` and `--prefer-local` form the
"local source flags" group.

## Workspace flags

`workspace` delegates to another command. Its tail is parsed specially: the two
workspace-only flags must appear **after** `workspace` and **before** the
delegated command word. Everything after the delegated command is parsed as an
ordinary invocation of that command.

| Flag | Value form | Meaning |
|---|---|---|
| `--jobs <N>`, `--jobs=<N>` | Required-value, positive integer | Maximum concurrent members. Default `1`. A non-integer or value `<= 0` errors with `invalid_flag_value`. |
| `--fail-fast` | No-value | Abort the whole workspace run on the first member failure. |

Global flags (`--dry-run`, `--json`, `--recipe`, …) may also appear in the tail
before the delegated command. A **version/source/build** flag placed there is an
error (`missing_workspace_command`, *"workspace command flag … must appear after
the delegated command"*): those belong after the delegated command. The same
code fires when no delegated command follows at all. See
[Workspaces](/pekit/running/workspaces.md).

## Capability matrix

Which command accepts which flag group. Global flags and `workspace` flags are
omitted (global flags are accepted everywhere). A blank cell means the flag group
is unsupported for that command.

| Command | version selection | local source | `--no-build` | `--no-verify` | `--env` | `--keyring` | `--refresh-source` | `--allow-unanchored` | `--allow-unsigned` | `--all` | clean mode | `--repin` |
|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| `build` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | | | | |
| `test` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | | | | |
| `install` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | | | | |
| `package` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | | Yes | | |
| `publish` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | |
| `clean` | | | | | Yes | Yes | | | | | Yes | |
| `gen` | | | | | Yes | Yes | | | | Yes | | |
| `verify` | | | | | Yes | Yes | | | | Yes | | |
| `lock` | Yes | | | | | | Yes | | | | | Yes |

"clean mode" is the `--output-only` / `--target-only` pair.

## Validation and mutual-exclusion rules

The parser rejects contradictory or malformed invocations before doing any work.

| Rejected condition | Diagnostic |
|---|---|
| `--quiet` with `--verbose` | `--quiet and --verbose cannot be used together` |
| `--quiet` with `--json` | `--quiet and --json cannot be used together` |
| `--recipe` with `--workspace` | `--recipe and --workspace cannot be used together` |
| `--workspace` without the `workspace` command | `--workspace can only be used with the workspace command` |
| No command word | `missing_command` |
| More than one of `--version` / `--latest` / `--all-versions` | mutually exclusive |
| `--local` with `--prefer-local` | mutually exclusive |
| `--output-only` with `--target-only` | mutually exclusive |
| A recognised flag the effective command does not support | `unsupported_flag` (suppressed under `--allow-unused`) |
| `clean --output-only` with `--env` or `--keyring` | *"clean --output-only does not run a target, so … is unused"* (suppressed under `--allow-unused`) |
| `clean` with more than one selector | `invalid_selector` (clean accepts at most one) |
| `package`/`publish` with `--all` **and** a selector | *"--all cannot be combined with package selectors"* |
| `gen`/`verify` with `--all` **and** a selector | *"--all cannot be combined with gen/verify target selectors"* |
| `lock` with any selector | `invalid_selector` (lock does not accept selectors) |
| `lock --repin` without an exact single `--version` | `invalid_flags` (`--repin requires exactly one exact --version`) |
| `--no-verify` naming an unknown gen target | `missing_target` |
| A positional selector beginning with `-` | `invalid_selector` (use `--` before selector-like values) |

`--allow-unused` relaxes **only** the "recognised-but-unused" and
"clean `--output-only` unused env/keyring" checks; it never excuses unknown
flags, malformed values, the mutual-exclusion rules, or bad selectors.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Any error — bad invocation, missing recipe, failed target, and so on. |

There are no other exit codes.

## Environment contract

Before running a target's command, pekit constructs its environment in layers:
managed `PEKIT_*` variables, then keyring exports, then user-declared env
(workspace → source → recipe → env-file). The process inherits the parent
environment with these overlaid. User env **may not** define any `PEKIT_*`
variable — doing so is `reserved_env`. The command runs with its working
directory set to the source root (or the recipe root for a `clean` target).

### Always set

Exported for every target pekit runs.

| Variable | Value |
|---|---|
| `PEKIT_RECIPE_ROOT` | Directory containing the resolved recipe (`pekit.toml`). |
| `PEKIT_SOURCE_ROOT` | Root of the materialised source tree. Equals the recipe root for a non-delegated recipe. |
| `PEKIT_LITERAL_ROOT` | Root used to resolve `@source:` payload references. |
| `PEKIT_ROOT` | Per-source pekit work base (staging lives beneath it). |
| `PEKIT_OUT_BASE` | Base directory for this source's managed output. |
| `PEKIT_OUT` | This target's own stage directory (where it should write). |
| `PEKIT_COMMAND` | The command kind running the target (`build`, `test`, `install`, `clean`, `gen`, `verify`). |
| `PEKIT_TARGET` | The target's name. |
| `PEKIT_BUILD_TIMESTAMP` | Run start time, Unix seconds. |
| `PEKIT_SOURCE_TIMESTAMP` | Source provenance time, Unix seconds. |
| `PEKIT_WORKSPACE_ROOT` | Workspace root, or an **empty string** when not in a workspace (always set). |
| `PEKIT_DEPENDENCIES_FILE` | Path to the JSON dependency payload pekit writes for this target. |
| `PEKIT_DEPENDENCY_PROVIDER` | Selected env dependency provider (empty when none). |
| `PEKIT_DEPENDENCIES` | Resolved dependencies as `name constraint` lines, one per line (empty when none). |

### Set only when a version is selected

Exported only when the run resolves a non-empty version.

| Variable | Value |
|---|---|
| `PEKIT_VERSION` | Full version string. |
| `PEKIT_VERSION_MAJOR` | Major component. |
| `PEKIT_VERSION_MINOR` | Minor component. |
| `PEKIT_VERSION_PATCH` | Patch component. |
| `PEKIT_VERSION_PRERELEASE` | Pre-release component (may be empty). |
| `PEKIT_VERSION_BUILDMETA` | Build-metadata component (may be empty). |

### Per-dependency and per-keyring variables

| Variable pattern | When set | Value |
|---|---|---|
| `PEKIT_<NEED>_OUT` | One per entry in the target's `needs` | Stage directory of that needed build target. `<NEED>` is the target name upper-cased with `-` and `.` replaced by `_`. Two needs mapping to the same name is `env_collision`. |
| `PEKIT_KEYRING_<PATH>` | One per keyring leaf loaded via `--keyring` / `--keyring.<path>=<value>` | The keyring value. `<PATH>` is the dotted keyring path upper-cased, with each run of non-alphanumeric characters collapsed to a single `_` and trailing `_` trimmed. For example `--keyring.token=…` exports `PEKIT_KEYRING_TOKEN`, and a nested `[registry] token = …` exports `PEKIT_KEYRING_REGISTRY_TOKEN`. |

A keyring export that collides with a managed `PEKIT_*` variable or with a normal
env variable is `env_collision`.

## See also

- [Invocation and flags](/pekit/running/invocation.md) — the narrative walk-through of the grammar.
- [Commands and targets](/pekit/running/commands-and-targets.md) — what each command selects and runs.
- [Environments and keyrings](/pekit/recipes/environments-and-keyrings.md) — the layers behind the environment contract.
