Supporting files and reference tables
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.