Anatomy of a recipe

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

The recipe directory #

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

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

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

FilePurposePage
pekit.tomlThe recipe: where source comes from, how to build/test/install/clean it, shared env.this page
package.pekit.tomlThe base package: how staged output maps into a .peipkg, plus dependencies and metadata.Package files
*.package.pekit.toml, packages.pekit/Additional package members for a recipe that produces more than one package.Multi-package recipes
env.pekit.toml, *.env.pekit.tomlReusable environment variables and a shell wrapper to run commands under.Environments and keyrings
*.keyring.pekit.tomlNamed collections of secrets exported into the build as PEKIT_KEYRING_*.Environments and keyrings
workspace.pekit.tomlSits one level up and groups a directory of recipes into a workspace pekit can drive as a unit.Workspaces

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

pekit.toml #

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

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

The one pekit itself ships with is deliberately minimal:

out_dir = "out"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[clean]
command = "cargo clean"

The top-level keys break down as follows:

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

Targets are shell commands #

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

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

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

[build]
command = "make"

…or in named form, one subtable per target:

[build.main]
command = "make"

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

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

The PEKIT_* environment contract #

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

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

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

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

The full contract — every managed variable, its exact value, and when it is set — is the environment contract in the command-line reference. It also covers the timestamp variables, the other root and output paths, and the PEKIT_DEPENDENCIES* variables that carry build dependencies into the command.

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

Shell variables, not templates #

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

How pekit finds the recipe #

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

You can override this:

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

The full flag and locator surface is on the invocation page.

Where to go next #

Edit this page