Reference
Single-page view · as markdown
Recipe format reference
pekit / Reference
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 anunknown_keydiagnostic. 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_typeerror.
pekit.toml #
Worked example #
= "dist"
[]
= "-O2"
= "/usr"
[]
= ["pesb", "run", "--", "{{command}}"]
[]
= "https://github.com/example/tool.git"
= "v{{version}}"
= ">=1.0"
= "^v(?P<version>[0-9.]+)$"
[]
= "make PREFIX=$PREFIX"
[]
= ["make", "docs"]
= ["main"]
[]
= "make link"
= ">=2.38"
[]
= "make check"
[]
= "make install DESTDIR=$PEKIT_OUT"
[]
= "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 | Managed output directory strictly below the recipe root. Relative paths resolve from the recipe root; an absolute path is accepted only when it remains below that root. Default "out". |
env | table | no | Environment variables exported to every target command. See [env]. |
wrap | table | no | Command wrapper applied to every target. See [wrap]. |
source | table | no | Where the recipe's source tree comes from. See [source]. |
input | table | no | Additional authenticated upstream inputs, keyed by name. See [input.<name>]. |
delegate | bool or table | no | Borrow build/env/wrap/package definitions from the source tree. See [delegate]. |
source_package | table | no | Corresponding-source package emission control. See [source_package]. |
tags | array of strings | no | Labels for selecting this recipe as a workspace member with --tag and --exclude-tag. Each tag is a selector-style name; duplicates are rejected. Only the member recipe's own tags count: a delegated source's tags are ignored. See Workspaces. |
build | table | no | Build target(s). See [build] / [test] / [install] / [clean]. |
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]. |
[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).
| Key | Type | Required | Meaning |
|---|---|---|---|
| <NAME> | 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 (element0is the wrapper program itself).
An omitted command yields an empty wrap (no wrapping).
[source] #
Selects and configures the source tree. It has four sub-tables. At most one
reproducible source (git, url, or pypi) may be present — declaring
more than one 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.pypi] | Discover and download a standardized PyPI source distribution. |
[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. |
See Sources for materialisation, caching, and
provenance behaviour. Resolved git, url, and PyPI sources are pinned
trust-on-first-use in the recipe's machine-written
pekit.lock.
[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). |
tag_regex | string | no | Regex filtering tags during enumeration. A named version capture supplies the complete version; named major, minor, and patch captures compose a dotted version. Optional revision (fourth numeric component), suffix (unseparated), prerelease, and buildmeta captures may extend it. Unnamed captures have no extraction semantics. |
tracked_path | string | no | Track one repository-relative regular file on a fixed moving ref. Versions are synthesized from changed blob observations and matching lock history. Requires a non-templated ref; incompatible with tag_regex. See Tracking one file on a moving ref. |
signature | table | no | Upstream release-signature verification. See [source.git.signature] below. Incompatible with tracked_path. |
[source.git.signature] #
Optional sub-table of [source.git]. If present, every resolve requires a
valid OpenPGP signature on the release. The signature is checked after the
mirror fetch and before the lock is written or the tree is checked out. An
unsigned or lightweight tag, or an unsigned commit, is signature_missing. An
SSH or X.509 signature is signature_unsupported. A signature that fails, or a
tag object that names another tag or points somewhere other than the resolved
commit, is signature_invalid. Each of these aborts the run and writes no lock
entry. The verified signer's fingerprint is recorded as the lock entry's
signature_key.
| Key | Type | Required | Meaning |
|---|---|---|---|
object | string | no | What carries the signature: "tag" (the rendered ref is an annotated, signed tag naming the resolved commit; default) or "commit" (the resolved commit has a gpgsig header). Any other value is invalid_signature. |
key_files | string array | yes | Pinned public keys (armored or binary, one or more per file), relative to the recipe directory. An empty or absent list is missing_key. |
fingerprints | string array | no | Allowlist of signer primary-key fingerprints, as for [source.url.signature]. A valid signature by any other pinned key is signature_untrusted_key. |
ignore_expiry | bool | no | Permit a signature made after the pinned signing key expired, as for [source.url.signature]. Default false. |
[source.url] #
| Key | Type | Required | Meaning |
|---|---|---|---|
url | string | yes | URL to download. Templated with the version. |
listing_url | string | no | Explicit URL to fetch when enumerating versions. Use when releases are listed separately from their download paths; default is derived from url. |
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 ".". |
exclude | string array | no | Exact files or directory subtrees, relative to root, that extraction never creates. Canonical relative paths only: no .., trailing slash, backslash, glob or template syntax. A nonempty list requires extract = true. See Archive extraction. |
versions | string | no | Version cap: a constraint string filtering enumerated or requested versions (see Versions). |
file_regex | string | no | Regex extracting versions when enumerating from a listing. Named version, major, minor, patch, revision, suffix, prerelease, and buildmeta captures map filenames with the same semantics as Git tag_regex; unnamed captures only filter. |
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. |
patch_series | table | no | Incremental upstream patch series layered over each major.minor base archive — see [source.url.patch_series] below. |
When patch_series is present, the selected version must be stable
major.minor.patch. The primary url, root, checksum-table lookup, and
signature render against major.minor; the complete selected version remains
the package version.
[source.pypi] #
| Key | Type | Required | Meaning |
|---|---|---|---|
project | string | yes | PyPI project name. Must start and end with an ASCII letter or digit and otherwise contain only letters, digits, ., _, or -. |
artifact | string | yes | Distribution kind. Currently must be exactly "sdist". |
versions | string | no | Version cap: a constraint string filtering enumerated or requested versions (see Versions). |
Pekit requests https://pypi.org/simple/<normalized-project>/ as
application/vnd.pypi.simple.v1+json. It accepts one non-yanked, stable,
Pekit-compatible, PEP 625 .tar.gz sdist per version, requires that entry's
SHA-256, extracts its standardized root, and records the exact URL and digest
in pekit.lock. Wheels and legacy sdist formats are not candidates. There is
no configurable index URL in this source kind.
[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 expiry is evaluated at the signature's signed creation time, so a later key expiry does not invalidate an older release. Current key revocations and explicit signature expiry are still enforced, and a signature dated after key expiry or materially in the future is rejected by default.
| 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 (yes with of = "checksums") | Signature URL template — the checksum manifest's location with of = "checksums". {{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), "decompressed" (its decompressed content — kernel.org's .tar.sign signs the uncompressed tar), or "checksums" (a clear-signed checksum manifest at url that must list the artifact exactly once; see Signed checksum manifests). Any other value is invalid_signature. |
fingerprints | string array | no (yes with of = "checksums", full fingerprints only) | 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. |
ignore_expiry | bool | no | Permit a new signature made after the pinned signing key expired. Default false. This bypasses key expiry only; cryptographic verification, fingerprint allowlisting, present-time revocation, explicit signature expiry, and timestamp checks remain enforced. |
[source.url.patch_series] #
Optional sub-table of [source.url] for upstreams that publish a base archive
and numbered incremental patches. Version major.minor.N materialises the
base plus patches 1 through N. Enumeration always exposes patchlevel zero and
every contiguous published patchlevel; a gap is url_patch_gap.
| Key | Type | Required | Meaning |
|---|---|---|---|
url | string | yes | Patch artifact URL template. Must contain {{patch}}; {{major}}, {{minor}}, and other version variables are available. |
file_regex | string | no | Regex used against the patch-directory listing. Must contain a named patch capture. When omitted, Pekit derives the matcher from url. |
patch_width | integer | no | Zero-pad {{patch}} to this width. Default 0 (no padding). |
strip | integer | no | Non-negative path-component count passed to patch -p. Default 0. |
signature | table | no | Per-patch detached-signature verification. Uses the [source.url.signature] schema and defaults its URL to {{source_url}}.sig. |
Patches are applied in order using the host patch command with batch mode,
forward-only application, and fuzz disabled. All patch hashes and configured
signatures are verified before Pekit writes the version's lock entry.
[source.local] #
| Key | Type | Required | Meaning |
|---|---|---|---|
path | string | yes | Path to the source directory, resolved relative to the recipe root. |
[input.<name>] #
An additional authenticated upstream, for a build that genuinely consumes more
than one. A recipe may declare any number, each under its own name. Names are
lower-case alphanumeric words separated by hyphens (invalid_name otherwise),
and must stay distinct after upper-casing and mapping - to _, because that
is the $PEKIT_INPUT_<NAME> a target reads.
Accepts the same keys as [source.url], including a nested
signature table, with two differences:
| Difference | Meaning |
|---|---|
versions | Must pin exactly one version — "= 7.0.9" or a bare "7.0.9". A range is invalid_versions: inputs are never selected by --latest or --all-versions. |
patch_series | Not accepted (unknown_key). A patch series applies to the source tree. |
{{version}} inside an input block renders that input's own version, never the
recipe's. Each input is fetched, signature-checked and pinned in
pekit.lock as an [[input]]
entry, materialised at $PEKIT_INPUT_<NAME>, bind-mounted read-only under
isolation, and carried in the source package under upstream/<name>/.
When a recipe delegates build, the fetched source tree's own [input.<name>]
tables are part of the build too. The delegating recipe resolves and pins them
in its own pekit.lock, and their key_files resolve beside the source's
pekit.toml. A recipe input with the same name replaces the source's as a
whole. See Additional inputs.
[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, together with the [input.<name>] tables the source declares for them. |
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 controls naming, emission and
additional shared source inputs. A recipe emits one when both of these hold: it has a reproducible
[source] (git, url or PyPI; 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. |
workspace_inputs | array of relative paths | no | Additional shared workspace inputs for this recipe. Under workspace isolation they are copied into read-only worker mounts and included byte-for-byte in the source bundle. Missing, ignored or escaping inputs fail. |
license | string | no | SPDX expression covering material that only the source package carries, such as upstream test programs or build helpers that no member ships. It is added to the members' licenses, never substituted for them. An expression that isn't valid SPDX is invalid_value. |
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 license (see below; and classed as the most encumbered member's class —
proprietary over firmware over unknown over free, with an
undeclared member counting as unknown), and installs under /usr/src/dist/<name>-<version>/
(with any -source suffix stripped from <name>).
The conjunction is built term by term. Each expression is split into its
top-level AND terms. A term containing OR keeps its parentheses, so the
surrounding ANDs can't rebind it. Repeated terms appear once, and the terms
are sorted. Members licensed GPL-2.0-or-later OR LGPL-3.0-or-later,
GPL-3.0-or-later and GPL-3.0-or-later AND LGPL-2.1-or-later produce
(GPL-2.0-or-later OR LGPL-3.0-or-later) AND GPL-3.0-or-later AND LGPL-2.1-or-later.
The package's content:
upstream/— the pristine source input: a url source's downloaded artifact byte-for-byte, so its hash matches the committedpekit.lock, or agit archiveexport of the locked commit.source/— the prepared tree, with patches already applied. Large trees travel inprepared-source.tarinstead;rebuild.pyverifies and reconstructssource/before invoking Pekit.workspace/— the complete captured recipe, shared inputs, inherited policy and environment profiles, preserving their relative paths.recipe/andpatches/— compatibility views.acquisition/andbuild-environment/— captured vendoring outputs and dependency identities when provided by the isolated job.build-inputs.json,rebuild.py,REBUILD.md— schema-2 or schema-3 hashes, modes, provenance and the outside-checkout reconstruction command.
Output/cache state, ignored developer files, keyring/credential files and configured key paths are excluded. Missing or escaping source links fail closed. See source reconstruction for required dependency services, signing keys and the bounds of reconstruction.
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] #
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]withcommand = "make". This defines a single target namedmain. - 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. |
keyring_inputs | string array | no | Dotted keyring entries requested by this target. Default none. The operator keyring must grant public access, or acquisition for build.vendor. Signing entries cannot be exported. |
dependencies | table | no | Build and test targets. Dependencies the target needs provisioned in its root. Under a peipkg provider the composed root holds only what is declared — a test stage that shells out needs dash (pekit runs command through /usr/bin/sh) and whatever else its script calls. See below. |
gate | bool | no | Test targets only. When true, package and publish run this test after staging its needs and before writing any artifact. Default false; --no-gates explicitly bypasses gated tests and the lint gate. |
sign | table | no | Build targets only. Files in the target's output to sign after the command succeeds, by signature kind. See sign below. |
Any other key in a target table is an unknown target key error. sign is
accepted only in [build] targets, gate only in [test], and dependencies
in [build] and [test]; using them elsewhere is an unknown key.
The dependencies table is keyed by provider (a selector), each mapping
dependency names to version constraints:
[]
= ">=2.38"
= "*"
| Level | Type | Meaning |
|---|---|---|
| provider | table | A dependency-provider selector. |
| provider.<name> | 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.
[build.<name>.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:
[]
= "tcb.priv"
= "tcb.priv"
The kind decides the signature format and where it is stored; the keyring entry is resolved through the ordinary keyring mechanism 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. pip produces Peios binary/firmware signatures; module signs .ko and .ko.zst with a kernel PKCS#7 trailer. Other names are unknown_key. |
| kind.<pattern> | 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.
[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.
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 and
Supporting files for how the files are
discovered and layered.
Worked example #
= "peipkg"
= ["main"]
[]
= "tool"
= "{{version}}"
= "x86-64-v3"
= "An example tool"
= "MIT"
= "https://example.com/tool"
= "opt.tool"
[]
= ">=2.38"
= { = ">=1.3", = "opt.tool" }
[]
= ">=8.0"
[]
= "{{version}}"
[]
= "usr/bin/tool"
= { = "usr/share/tool/", = true }
[]
= "tool"
= ["@recipe:share/*.tmp"]
[]
= "usr/bin/editor"
= "usr/bin/tool"
[[]]
= "dist"
= 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". |
dependency_provider | string | no | The dependency namespace this package's runtime [dependencies] belong to, matching an env file's dependency_provider. Workspace ordering follows a package's runtime dependencies only when this equals the selected env's provider. Inherited through package layers like format, so a workspace sets it once in its shared package.pekit.toml. Undeclared means the runtime dependencies do not affect ordering. |
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]. |
files | table | no | Payload file mappings. See [files]. |
symlinks | table | no | Symlinks to embed in the payload. See [symlinks]. |
excludes | array of strings | no | Refs/globs to drop from directory payloads. See excludes. |
multipack | table | no | Expand into a family of instances. See [multipack]. |
publish | table | no | Where to copy finished artifacts. See [publish]. |
dependencies | table | no | Runtime dependencies. See [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. "*" means every version, as it does in dependencies; the manifest records such an entry with no constraint. |
side_effects | array of strings | no | Declared install-time side effects. |
sd_overrides | table | no | payload path → SDDL security descriptor. See [sd_overrides]. |
claims | table | no | Claim slots on provides/dependencies. See [claims]. |
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. After
[package].version is rendered, {{release}} is also available: the package's
own full version, including its revision (1.13.2-3 for
version = "{{version}}-3"). Pin packages built from the same source to each
other with "= {{release}}", so the pins follow the revision automatically; the
package.dependencies lint rule reports relations that spell out
"{{version}}-N" by hand. See
Versions and
Multi-package recipes.
[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, where the rendered value must be a peipkg version — [epoch:]upstream-revision (PSPU book 5 §5.5), so "{{version}}-1" and never a bare "{{version}}". A value that does not parse is an invalid_package_version error when the package is expanded, before anything is built. |
architecture | string | no | Target architecture. Required for peipkg. |
description | string | no | Human-readable description. |
license | string | no | License identifier. Required for peipkg. |
license_class | string | no | Licence class: unknown, free, firmware or proprietary (PSPU book 5 §18). Any other value is an invalid_license_class error. Absent means unknown; set it whenever license carries a LicenseRef- term, which is exactly when the expression alone cannot say. |
homepage | string | no | Project homepage URL. |
default_root | string | no | Preferred named root 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 |
|---|---|---|
<name> = "constraint" | string | Version constraint; the dependency lands in the depender's own root. |
<name> = { 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.
[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 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:
[]
= ["gtk3", "gtk4"]
Each value must be a valid selector, non-empty, and unique.
Enumerated from files:
[]
= "@source:themes/*/theme.ini"
= "^(?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. |
[publish] #
Where finished artifacts are published. localdir copies an artifact by
basename and is written as an array of tables:
[[]]
= "dist"
= 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. |
peipkg publishes peipkg-format artifacts into a complete signed repository
and is written as one table:
[]
= "dist/repository"
= "experimental"
= "keyring:signing.repository_key"
| Key | Type | Required | Meaning |
|---|---|---|---|
path | string | yes | Repository state directory, relative to the workspace root (or recipe root outside a workspace). |
name | string | no | Name written into a newly initialized repository's signed descriptor. Defaults to the final component of path; it does not alter an existing repository. |
signing_key | string | yes | Repository metadata signing key. A normal value is a key-file path; keyring:<dotted.entry> reads the path from that keyring leaf. |
If the directory named by path is absent or empty, it is initialized before
the first package batch is published. A non-empty directory that is not already
publisher state is refused rather than overwritten. Only peipkg-format
artifacts may use this target. See
Publish targets for key
resolution, batching, and repository semantics.
[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.
[<name>.<slot>]
= "usr/bin/editor"
= "usr/bin/tool"
[<name>.<slot>]
= "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 and the Peios
claims model for the resolution behaviour.
See also #
- Supporting files — the workspace, env, keyring, lockfile, and patch-series schemas.
- Anatomy of a recipe — the narrative walk-through of these files.
- Packaging files — how
[files]refs and destinations resolve.
[sd_overrides] #
Each key is a payload path — exactly as it appears in the package, with no leading slash — and each value is an SDDL string. pekit compiles it to a binary security descriptor at pack time and puts the result in the manifest.
[]
= "O:SYG:SYD:P(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;;FX;;;WD)"
An entry with no override gets its descriptor by inheritance from its parent directory when the consumer creates it, and that is what almost every entry should get. Declare one where a path needs to be more restrictive than its parent would make it, needs access for a principal the parent's inheritable ACEs do not name, or needs to start a new inheritance scope of its own.
Three constraints are worth knowing before writing one:
- The path must name a real payload entry, and it must be a regular file or a directory — never a symlink, which carries no descriptor of its own. A consumer rejects the whole package otherwise.
- Absolute SID aliases only. The compile happens on a build host
with no domain and no machine to resolve against, so
SY,BA,CO,WDand literal SIDs are fine while domain- and machine-relative aliases (DA,EA,LAand the rest) are an error. - A rights field may end in a hex literal.
FRFX,0x1200afandFRFX0x6all parse, the last of them as file read, file execute and the two bits after that. Use it for rights with no two-letter mnemonic —FILE_ADD_FILEandFILE_ADD_SUBDIRECTORY, in particular — rather than writing the whole mask as hex and losing what the mnemonics said. The literal consumes the rest of the field, so it can only come last.
Supporting files and reference tables
pekit / Reference
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.
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. |
[wrap] | table | no | Workspace-level command wrapper. See wrap table. |
[policy] | table | no | Distro-wide derivation policy. See below. |
[isolation] | table | no | enabled (bool, default false) requires workspace sandbox policy; inputs (array of relative paths) grants copied, read-only shared workspace inputs, also captured in source bundles. |
[source_package] | table | no | inputs (array of canonical relative paths) captures coordinator-side support source, such as root-preparation scripts, without mounting it into workers. Workspace configuration, package/lint policy and environment profiles are captured automatically. |
[policy] #
The [policy] table accepts package-version floors and exact ELF capabilities.
Unknown keys are rejected.
| Key | Type | Required | Meaning |
|---|---|---|---|
symbol_capabilities | array of strings | no | SONAMEs whose exact GNU ELF version nodes become elfver(soname:token) dependencies and provides, independently of package versions. |
[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_":
[]
= ["libstdc++.so.6"]
[]
= "GLIBC_"
= "GCC_"
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], [sandbox], or
dependency_provider; a file with none of them is rejected (missing_key).
| Key | Type | Required | Meaning |
|---|---|---|---|
[env] | table | one of the four | Environment variables. See env table. |
[wrap] | table | one of the four | Command wrapper. See wrap table. |
dependency_provider | string | one of the four | 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. |
[lint.allow] | table (rule → reason) | no | Whole lint rules exempted for artifacts built in this environment. See below. |
[sandbox] #
A workspace profile's command (string or argv array, required) prepares the
fresh PEKIT_SANDBOX_ROOT destination. No {{command}} placeholder is used.
Optional network_targets is an array containing only "build:vendor".
Optional entry is an argv array whose first element is a clean absolute path
inside the prepared root. Pekit runs it inside the sandbox ahead of every target
command of the profile, passing the target's shell invocation as its remaining
arguments; the entry must exec them. The preparer must supply the program as
an executable regular file, or the job fails with sandbox_entry before the
worker starts. Processes the entry leaves behind end with the job.
Pekit owns process isolation; the preparer only supplies dependencies. This table
requires [isolation] enabled = true in the workspace. Member/source profiles
cannot override it. See isolated production jobs.
If a prepared root has a missing or empty /etc/hosts, pekit supplies fixed
IPv4 and IPv6 localhost entries before starting the worker. A populated regular
hosts file is left intact; a symlink is replaced without following its target.
Offline loopback tests therefore work without importing the host's aliases or
granting network access.
[lint.allow] #
Some lint findings are a property of the build environment rather than of any recipe: a foreign toolchain whose startup objects carry no CET notes, say. An env file names those rules, each with its reason:
[]
= "Debian startup/runtime objects carry no CET markers; native builds enforce the rule"
The exemptions apply to the package lint gate
and to a versioned pekit lint run under that --env, and to nothing built in
another environment. Each key must be a known rule — not a parameter — and each
reason must be non-empty. Both the workspace's and the recipe's selected env
file contribute. An exemption a lint.pekit.toml already states keeps that
file's reason, and allowed findings are reported like any other exemption.
Use this only for an environment's own limitation; a package's defects belong
in its recipe's lint file, or fixed.
--env <name> selection #
The active env file is chosen from the --env <name> flag (default main),
resolved at the workspace root and then the recipe root:
--env value | File loaded at each root | Must exist? |
|---|---|---|
(unset) or main | env.pekit.toml | no (silently absent at either root) |
none | (no env file loaded) | n/a |
any other <name> | <name>.env.pekit.toml | yes — in at least one root |
When both files exist, the workspace file is the base and the recipe file
overlays it for [env], [wrap], and dependency_provider. Outside a
workspace only the recipe root is searched. The default env.pekit.toml is
optional everywhere. When both paths resolve to the same underlying file, it is
loaded only once.
[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 → workspace env file → (delegated source) → recipe → 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. |
[]
= "clang"
= "-O2 -pipe"
= "$CFLAGS -Wp,-D_GLIBCXX_ASSERTIONS"
= "$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 element0(the program). An element that merely contains{{command}}inside other text is rejected.
[]
= ["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
below and is not available in ordinary recipe strings.
*.keyring.pekit.toml #
A keyring file supplies coordinator values and explicitly granted worker inputs. 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 or { value = "...", access = "..." } entries.
Requested worker inputs use an environment-variable name derived from their path:
- 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 |
# placeholders only — never commit real secrets
= "REPLACE_ME"
[]
= "REPLACE_ME"
[]
= "REPLACE_ME"
Restrictions #
| Rule | Detail |
|---|---|
| Leaf type | A string (coordinator-only), or a typed table with string value and access. |
| Nesting | Sub-tables nest arbitrarily; each nesting level adds a segment to the exported name. |
| Typed access | public permits requesting targets; acquisition permits only build.vendor; signing is coordinator-only. path and content forms remain unsupported. |
| Collisions | A keyring export that collides with a normal or managed env variable is an error (env_collision). |
Well-known entries #
Targets receive only entries listed in keyring_inputs and permitted by their
access grant. Inline overrides are coordinator-only. The following entry is 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
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 private-key entries are never exported to workers.
lint.pekit.toml #
Enables and parameterises the rules pekit lint checks. pekit turns no rule
on by itself; a tree with no lint file has nothing to lint.
| Key | Type | Required | Meaning |
|---|---|---|---|
[package], [source], [build], [payload], [split], [elf] | tables | no | Rule keys and parameters, one table per area. A key's dotted path is the rule's ID. Every key must be a known rule or parameter (unknown_key) with a value of the shape it accepts (invalid_value). |
[allow] | table (rule ID → reason) | no | Exemptions. Each key names a rule; the value is a non-empty reason (missing_reason). A parameter key is not a rule and is rejected. |
Files are found from the recipe directory up to the workspace root, plus a
delegated recipe's source root, and merge outermost first with the nearest
file winning per key. A nearer file may not set a rule an outer file enabled
to false (lint_rule_disabled); it exempts it through [allow] instead.
Every rule, its accepted values and the merge order are on
Linting.
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 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 ordinary git or URL entry. Tracked-path
git history is append-only, so changed bytes get a new date version and
--repin is rejected. 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. |
[[input]] | array of tables | One entry per locked version of each [input.<name>], kept sorted by name and then 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 and PyPI sources | The rendered or selected URL fetched at lock time. Provenance only — the hash, not the address, is the assertion. |
sha256 | url and PyPI 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. |
repository | tracked-path git sources | Git repository URL bound by the snapshot. |
path | tracked-path git sources | Fixed repository-relative regular file selected by the recipe. |
blob | tracked-path git sources | Git blob object ID reached by commit:path. |
blob_sha256 | tracked-path git sources | SHA-256 of the exact blob bytes; the transport-independent content assertion. |
signature_key | url sources with [source.url.signature]; git sources with [source.git.signature] | Hex fingerprint of the pinned upstream key that verified the artifact or release at lock time. A git entry locked before its recipe gained the signature block records it on the next resolve. |
[[source.patch]] | url sources with [source.url.patch_series] | Ordered patch inputs applied for this version. Each carries url, sha256, and optional signature_key with the same meanings as the base artifact fields. |
locked_at | all | UTC timestamp of the pinning run (RFC 3339). |
Each [[input]] entry is addressed by name and version, and asserts what a url
[[source]] entry does. The entries include inputs declared by a delegated
source tree, which are pinned here, in the delegating recipe's lock:
| Key | Meaning |
|---|---|
name | The input's name, matching its [input.<name>] table. |
version | The version its versions constraint pins. Moving the constraint adds an entry the first time the new version resolves. Existing entries stay, so builds that still name an older version keep verifying against it. |
url | The rendered URL fetched at lock time. Provenance only. |
sha256 | SHA-256 of the fetched artifact — the assertion. |
signature_key | Hex fingerprint of the pinned upstream key that verified it, when a signature block is configured. |
locked_at | UTC timestamp of the pinning run (RFC 3339). |
Worked example #
= 1
[[]]
= "0.5.12"
= "https://example.org/dash-0.5.12.tar.gz"
= "6a474ac46e8b0b32916c4c60df694c82058d3297d8b385b74508030ca4a8f28a"
= "9f0b7ddc1325a3e2c40e5f0a88b1c62f3d94ab07"
= "2026-08-10T11:00:00Z"
[[]]
= "https://example.org/dash-0.5-patches/dash05-001"
= "9b44d9f7a36d1a20537817f92da72bf10eb81f66df860f23298b2d4e830a6d8d"
= "9f0b7ddc1325a3e2c40e5f0a88b1c62f3d94ab07"
[[]]
= "1.2.0"
= "v1.2.0"
= "8f3a1bc99e2f6d41c07b21ac1886d63b7f2e5d10"
= "2026-08-10T11:00:00Z"
[[]]
= "2026.09.06.2"
= "https://github.com/example/upstream.git"
= "refs/heads/release"
= "security/trust/certdata.txt"
= "6b839fbb82801a7a5f5e37df0864c149fddda2b7"
= "65fddc0b380650ee16aa47e4e28e3a5e2c95f201"
= "fa5092f93c2d5352ebf6c9f4f26f4232a2e2bc0fd8a8950756c1984984b490e0"
= "2026-09-06T12: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 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. |
{{suffix}} | unseparated upstream suffix | Empty string when absent. |
{{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. |
{{release}} | the package's own rendered version, including its revision (1.13.2-3) | Only available in a package definition's fields after [package].version has been rendered, such as dependencies, provides and conflicts. Errors anywhere else, including in version itself. |
A version string has one or more dot-separated numeric components, an optional
unseparated alphanumeric suffix beginning with a letter (for example 2026c),
and optional -prerelease and +buildmeta tails. {{version}} preserves the
full string. {{major}}, {{minor}}, and {{patch}} expose the first three
numeric components for compatibility, so {{minor}} / {{patch}} are only
present when the version actually carries them; fourth and later components
are available through {{version}} only.
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 |
{{suffix}} | $PEKIT_VERSION_SUFFIX |
{{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.
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 for how these are used in a package's
[files] table.
Publish targets #
The publish command sends a package's freshly built artifact to its declared
destinations. There are two publish kinds: localdir and peipkg. Any other
[publish.*] key is rejected (unknown_key). A package selected for publishing
that declares no target is an error.
localdir #
Targets are an array of tables:
[[]]
= "dist/{{version}}"
= false
| Key | Type | Required | Meaning |
|---|---|---|---|
path | string | yes | Destination directory, relative to the base root. Templated with {{version}} and {{multipack}}, where {{version}} is the rendered package version (1.26.2-3), not the upstream one. The upstream-version tokens ({{major}}, {{prerelease}} and the rest) do not describe a package version and are refused with a diagnostic. 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. |
peipkg #
The singular [publish.peipkg] target publishes all peipkg artifacts selected
for one package/version plan that name the same directory as one batch through
the Peipkg repository publisher:
[]
= "dist/repository"
= "experimental"
= "keyring:signing.repository_key"
| Key | Type | Required | Meaning |
|---|---|---|---|
path | string | yes | Repository state directory, relative to the base root. It is not package-instance templated. |
name | string | no | Stable repository name used only when initializing path. Defaults to the final path component. An existing repository's signed descriptor is authoritative. |
signing_key | string | yes | Ed25519 private key used to sign the repository indexes. A bare value is a key-file path. keyring:<dotted.entry> reads a key-file path from that keyring leaf. |
A direct relative signing_key path resolves from the base root. A path read
from a keyring resolves from the invocation's working directory, like
signing.package_key. Both raw 32-byte Ed25519 seeds and PKCS#8 PEM files are
accepted.
If the directory named by path is absent or empty, Pekit initializes a
complete repository there: signed descriptor, public keys, and empty active and
archive indexes. The repository metadata key becomes a trust key. When the
freshly built packages use a different signing.package_key, its public half
is also recorded as a trusted key, so consumers can verify those packages. An
existing repository is never reinitialized or given new trust implicitly; its
descriptor must already trust both the metadata-signing key and the
package-signing key.
Pekit then publishes that batch into the repository's canonical
p/<name>/<version>/ tree and regenerates both signed indexes once for the
batch. The batch is published only after every selected package of the
recipe has been written and has passed the lint gate: if any of them fails,
none is published, so a partly failed publish --all cannot leave a family at
mixed revisions. Separate selected versions and workspace members are
serialized but remain separate repository publications, so a workspace run
that stops at a failing member keeps the members already published before it. The target rejects non-peipkg
artifacts and already-published name/version/architecture identities. Workspace
members sharing one repository are serialized so their whole-index updates
cannot overwrite each other.
The directory is the repository and can be served or copied verbatim. This target does not upload it to remote storage; combine deployment with a separate step when the serving tree is not locally mounted.
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 — the
pekit.tomland package-file schemas. - Environments and keyrings — how env and keyring files are selected and layered.
- Signing and provenance —
signing.package_keyandpekit.lockin the trust chain.
Command-line reference
pekit / Reference
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; for what each command does, see Commands and targets.
Synopsis #
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, lint, workspace, help, or version. 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). A bare -- ends flag parsing; every
following token is captured verbatim as a positional selector. Positional
selectors may not begin with -.
Help and version #
| Invocation | Output |
|---|---|
pekit help, pekit --help, pekit -h | The overview: every command with a line each, the global flags, and a pointer to this page. Exit 0. |
pekit help <command>, pekit <command> --help | That command's usage line, what its selectors mean, and its flags. Generated from the same tables the parser uses, so it cannot list a flag the command rejects. Exit 0. |
pekit (no arguments) | The overview on stderr, exit 1 (missing_command): nothing ran. |
pekit version | pekit <module version> (<commit>[, modified]), from the binary's build info. --version and -V are the version selector, so the print form is a command. |
--help wins over validation: pekit clean --version 1 --help prints help
rather than unsupported_flag. help and version accept the global flags
and no others; workspace cannot delegate to either.
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. An existing local path takes precedence over remote-locator spelling such as a .git suffix. |
--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. 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. |
--locked | No-value | Make --latest, --all-versions or a version constraint select from the versions already in the recipe's pekit.lock instead of discovering them upstream. Opt-in; see Selecting from the lock. |
--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, and a stage left behind by a target that did not finish is refused with stage_incomplete rather than reused. |
--no-verify[=<list>] | Optional-value | Skip the gen drift-check pre-flight. 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. |
--no-gates | No-value | package/publish only: skip every gate = true test in the recipe and the lint gate, for rapid iteration. Pekit warns with the skipped gate names. |
--strict | No-value | build/test/package/publish only: require a committed catalogue (other than pekit.lock), a source pinned in pekit.lock, and no bypass flag (strict_bypass, strict_dirty, strict_provenance). On publish, also check every install and upgrade closure of the resulting repository. See Strict mode. |
--env <name> | Required-value | Select environment-file layers. main (default) loads optional env.pekit.toml; <name> loads <name>.env.pekit.toml, which must exist at the workspace or recipe root; a recipe file overlays the workspace file. none loads no env files. |
--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 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 (otherwise unsigned_publish). |
--replace | No-value | publish only: let a peipkg package overwrite an already-published package with the same name, version and architecture, instead of refusing it. Each overwritten package is reported with its old digest. This breaks the repository's promise that a published version never changes, so use it only on a repository nobody consumes yet, such as one being bootstrapped. --strict refuses it. |
--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. |
--journal <file>, --journal=<file> | Required-value, path | Record each member that succeeds; repeating the same run skips them. Creating <file>.stop stops the run between members (workspace_stopped). A journal from a different run is journal_mismatch. See Resumable runs. |
Two member-selection flags follow the delegated command, with its own flags.
Outside workspace they are an error (invalid_flags).
| Flag | Value form | Meaning |
|---|---|---|
--tag <tag> | Required-value, repeatable | Run only members whose recipe tags include any given tag. |
--exclude-tag <tag> | Required-value, repeatable | Skip members whose recipe tags include any given tag. Applied after --tag. |
A selection that matches no member fails with empty_workspace.
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.
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 | --no-gates | --env | --keyring | --refresh-source | --allow-unanchored | --allow-unsigned | --replace | --all | clean mode | --repin | --strict |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
build | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | |||||||
test | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | |||||||
install | Yes | Yes | Yes | Yes | Yes | Yes | Yes | ||||||||
package | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | |||||
publish | Yes | Yes | Yes | 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 | ||||||||||||
lint | Yes | Yes | Yes | Yes | |||||||||||
help | |||||||||||||||
version |
"clean mode" is the --output-only / --target-only pair. Plain lint
acquires delegated recipe files automatically. Its version-selection and
local-source flags make that source choice explicit and additionally select an
existing build stage for payload rules; it never
builds.
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 |
--locked without --latest, --all-versions or a version constraint, or on lock | invalid_flags |
--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) |
lint with any selector | invalid_selector (lint checks every package) |
lint --all | unsupported_flag |
help <name> where <name> is not a command | unknown_command |
version with any selector | invalid_selector |
workspace help or workspace version | missing_workspace_command |
--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 | The source's own date, Unix seconds: a Git source's commit time; a URL or PyPI source's newest archive member (fixed by the locked bytes), or the recipe's last commit when the download is not a tar archive; a local tree's, or a sourceless recipe's, last commit touching that directory. 0 when none is available (a local tree outside Git takes the run start instead). |
SOURCE_DATE_EPOCH | The same value, under the reproducible-builds name that compilers, documentation generators and archivers read, so they stamp the release rather than the build. Not set when PEKIT_SOURCE_TIMESTAMP is 0. A recipe or workspace [env] may override it. |
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 in an isolated workspace #
When the workspace enables isolation, the profile's root preparer and the sandboxed target see a few more variables.
| Variable | Seen by | Value |
|---|---|---|
PEKIT_SANDBOX_ROOT | Root preparer | The fresh directory the preparer populates as the target's root. |
PEKIT_JOB_STATE | Root preparer | Coordinator-only storage for the job, never mounted into the sandbox. |
PEKIT_NATIVE_ROOT | Target | 1 when the profile's dependency provider is peipkg and the target has no network access; unset otherwise. A recipe built by more than one provider can use it to tell the composed native root apart. |
Set only when a version is selected #
Exported only when the run resolves a non-empty version.
| Variable | Value |
|---|---|
PEKIT_VERSION | Full version string, including fourth and later numeric components. |
PEKIT_VERSION_MAJOR | First numeric component. |
PEKIT_VERSION_MINOR | Second numeric component. |
PEKIT_VERSION_PATCH | Third numeric component. |
PEKIT_VERSION_SUFFIX | Unseparated upstream suffix (may be empty). |
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 — the narrative walk-through of the grammar.
- Commands and targets — what each command selects and runs.
- Environments and keyrings — the layers behind the environment contract.