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.

KeyTypeRequiredMeaning
includearray of stringsyesMember patterns the workspace builds. Must be present and non-empty, or the load fails (missing_key).
excludearray of stringsnoPatterns removed from the include set.
[env]tablenoWorkspace-level environment variables. See env table.
[wrap]tablenoWorkspace-level command wrapper. See wrap table.
[policy]tablenoDistro-wide derivation policy. See below.
[isolation]tablenoenabled (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]tablenoinputs (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.

KeyTypeRequiredMeaning
symbol_capabilitiesarray of stringsnoSONAMEs whose exact GNU ELF version nodes become elfver(soname:token) dependencies and provides, independently of package versions.
[policy.symbol_versions]table (string → string)noMaps 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_":

[policy]
symbol_capabilities = ["libstdc++.so.6"]

[policy.symbol_versions]
"libc.so.6"     = "GLIBC_"
"libgcc_s.so.1" = "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).

KeyTypeRequiredMeaning
[env]tableone of the fourEnvironment variables. See env table.
[wrap]tableone of the fourCommand wrapper. See wrap table.
dependency_providerstringone of the fourSelector 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)noWhole 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:

[lint.allow]
"elf.cet" = "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 valueFile loaded at each rootMust exist?
(unset) or mainenv.pekit.tomlno (silently absent at either root)
none(no env file loaded)n/a
any other <name><name>.env.pekit.tomlyes — 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.

RuleDetail
Value typeEvery value must be a string.
Name grammarNames must match ^[A-Za-z_][A-Za-z0-9_]*$.
Reserved prefixUser env may not set any PEKIT_* name — those are pekit-managed. A PEKIT_-prefixed name is rejected at build time (reserved_env).
LayeringApplied workspace → workspace env file → (delegated source) → recipe → recipe env file, later layers overriding earlier ones.
ExpansionValues 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.
[env]
CC = "clang"
CFLAGS = "-O2 -pipe"
CXXFLAGS = "$CFLAGS -Wp,-D_GLIBCXX_ASSERTIONS"
TOOL_PATH = "$PEKIT_ROOT/tools"

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

[wrap] table #

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

KeyTypeRequiredMeaning
commandstring or array of stringsnoThe wrapper. Must contain exactly one {{command}} placeholder.

{{command}} rules:

  • String form — the string must contain {{command}} exactly once.
  • Array form — exactly one element must be the literal {{command}} (as a complete argument, not a substring), and it may not be element 0 (the program). An element that merely contains {{command}} inside other text is rejected.
[wrap]
command = ["bwrap", "--ro-bind", "/usr", "/usr", "{{command}}"]

Note: {{command}} here is a wrap-only placeholder handled by the wrap machinery. It is not one of the template variables 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 pathExported variable
tokenPEKIT_KEYRING_TOKEN
github.tokenPEKIT_KEYRING_GITHUB_TOKEN
registry.api-keyPEKIT_KEYRING_REGISTRY_API_KEY
# placeholders only — never commit real secrets
token = "REPLACE_ME"

[github]
token = "REPLACE_ME"

[registry]
api-key = "REPLACE_ME"

Restrictions #

RuleDetail
Leaf typeA string (coordinator-only), or a typed table with string value and access.
NestingSub-tables nest arbitrarily; each nesting level adds a segment to the exported name.
Typed accesspublic permits requesting targets; acquisition permits only build.vendor; signing is coordinator-only. path and content forms remain unsupported.
CollisionsA 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 pathMeaning
signing.package_keyPath 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.

KeyTypeRequiredMeaning
[package], [source], [build], [payload], [split], [elf]tablesnoRule 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)noExemptions. 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 #

KeyTypeMeaning
schemaintegerLockfile schema version. Currently 1.
[[source]]array of tablesOne entry per locked version, kept sorted by version.
[[input]]array of tablesOne 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:

KeySet forMeaning
versionallThe locked version. Empty for a versionless url fetch.
urlurl and PyPI sourcesThe rendered or selected URL fetched at lock time. Provenance only — the hash, not the address, is the assertion.
sha256url and PyPI sourcesSHA-256 of the fetched artifact.
refgit sourcesThe rendered ref the version resolved through.
commitgit sourcesThe commit the ref resolved to — the assertion.
repositorytracked-path git sourcesGit repository URL bound by the snapshot.
pathtracked-path git sourcesFixed repository-relative regular file selected by the recipe.
blobtracked-path git sourcesGit blob object ID reached by commit:path.
blob_sha256tracked-path git sourcesSHA-256 of the exact blob bytes; the transport-independent content assertion.
signature_keyurl 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_atallUTC 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:

KeyMeaning
nameThe input's name, matching its [input.<name>] table.
versionThe 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.
urlThe rendered URL fetched at lock time. Provenance only.
sha256SHA-256 of the fetched artifact — the assertion.
signature_keyHex fingerprint of the pinned upstream key that verified it, when a signature block is configured.
locked_atUTC timestamp of the pinning run (RFC 3339).

Worked example #

schema = 1

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

  [[source.patch]]
    url = "https://example.org/dash-0.5-patches/dash05-001"
    sha256 = "9b44d9f7a36d1a20537817f92da72bf10eb81f66df860f23298b2d4e830a6d8d"
    signature_key = "9f0b7ddc1325a3e2c40e5f0a88b1c62f3d94ab07"

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

[[source]]
  version = "2026.09.06.2"
  repository = "https://github.com/example/upstream.git"
  ref = "refs/heads/release"
  path = "security/trust/certdata.txt"
  commit = "6b839fbb82801a7a5f5e37df0864c149fddda2b7"
  blob = "65fddc0b380650ee16aa47e4e28e3a5e2c95f201"
  blob_sha256 = "fa5092f93c2d5352ebf6c9f4f26f4232a2e2bc0fd8a8950756c1984984b490e0"
  locked_at = "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:

ConditionError
series missing or unreadablepatch_series
An entry that repeats, contains whitespace, or escapes the directorypatch_series
A series entry with no file behind itpatch_missing
A *.patch file in the directory the series does not listunused_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.

VariableSourceAvailability
{{version}}the selected version, verbatim (Raw)Errors when no version is selected.
{{major}}major componentAvailable once a version is selected.
{{minor}}minor componentErrors if the selected version has no minor component.
{{patch}}patch componentErrors if the selected version has no patch component.
{{suffix}}unseparated upstream suffixEmpty 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 valueOnly 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 variableEnvironment 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 formRootMeaning
target:paththe build target's output stageReads from the output of build target target.
:pathbuild target main's output stageSame as above with the target name defaulted to main.
@source:paththe materialised source tree (LiteralRoot)Reads from the checked-out / extracted source.
@recipe:paththe recipe directoryReads a file shipped next to the recipe.
@workspace:paththe workspace rootReads a file from the workspace. Errors if used outside a workspace.
bare pathowner defaultA 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:

[[publish.localdir]]
path = "dist/{{version}}"
overwrite = false
KeyTypeRequiredMeaning
pathstringyesDestination 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.
overwriteboolno (default true)Whether an existing destination file may be replaced. When false, an existing destination is an error (publish_exists).

Resolution details:

AspectBehaviour
Base rootThe workspace root when building in a workspace, otherwise the recipe root. path is joined onto it.
Final destination<base>/<path>/<artifact-filename>.
CollisionsTwo 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:

[publish.peipkg]
path = "dist/repository"
name = "experimental"
signing_key = "keyring:signing.repository_key"
KeyTypeRequiredMeaning
pathstringyesRepository state directory, relative to the base root. It is not package-instance templated.
namestringnoStable repository name used only when initializing path. Defaults to the final path component. An existing repository's signed descriptor is authoritative.
signing_keystringyesEd25519 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 #

Edit this page