# Writing recipes

---

# 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).
