Sources
A source tells pekit where a recipe's code comes from. Before any build
target runs, pekit resolves the source into a concrete source tree on
disk — the directory that targets execute in and that @source: package
references read from. This page covers the source
kinds, how each is materialised, and how a recipe can borrow behaviour from its
source tree.
Sources are declared under [source.*] in the recipe. A recipe may have:
- no external source (sourceless) — the recipe directory itself is the tree;
- one reproducible source — one of
[source.git],[source.url], or[source.pypi], never more than one; - optionally a local override —
[source.local], on its own or alongside a reproducible source.
A source is reproducible when it is git, url, or PyPI — those pin to exact
bytes. Declaring multiple reproducible tables is an error (mixed_source:
"exactly one reproducible source table is allowed").
When writing URL listing filters, remember that file_regex searches the
downloaded listing text. Anchoring it with ^ and $ to a bare filename will
not match that filename inside an HTML page. Use a named version capture
when the package name itself contains digits, for example
m4-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\.tar\.xz, so those digits cannot
be mistaken for the release version.
The four source kinds #
[source.git] #
Clones a git repository and checks out a ref. It takes five fields —
a required url (passed to git clone --mirror), a ref to check out
(templated with the selected version; defaults to
{{version}} and must render non-empty), a versions cap that filters
enumerated or requested versions, and a tag_regex used when enumerating tags
(see Enumeration). The fifth, tracked_path, selects the
moving-ref snapshot mode described below. An optional [source.git.signature]
table verifies the upstream's release signatures; see
Signed Git releases. The field-by-field schema is in the
recipe format reference.
[]
= "https://github.com/example/widget.git"
= "v{{version}}"
= ">=2.0.0"
= "^v(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)$"
If ref renders empty (for example, a bare {{version}} with no version
selected) pekit fails with missing_version — pass --version or set a
non-templated ref.
Signed Git releases #
Many upstreams sign their release tags. A [source.git.signature] table pins
the signing maintainer's OpenPGP key, and pekit then requires a valid
signature for every version it resolves:
[]
= "https://git.kernel.org/pub/scm/libs/libtrace/libtraceevent.git"
= "libtraceevent-{{version}}"
= '^libtraceevent-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)$'
[]
= ["keys/steven-rostedt.asc"]
= ["5ED9A48FC54C0A22D1D0804CEBC26CDB5A56DE73"]
By default the rendered ref must be an annotated tag with an OpenPGP
signature. The tag object must name that same tag and point straight at the
commit the ref resolved to, so a signed tag from another release can't vouch
for this one. For an upstream that signs commits rather than tags, set
object = "commit": the resolved commit must then carry the signature.
Verification is in-process, like URL signatures: no host gpg, no keyring, no
Git signing configuration. It runs after the mirror fetch, which only stores
objects, and before the lock is written or the tree is checked out. A
lightweight tag, an unsigned tag or commit, or a signature pekit cannot read is
signature_missing (or signature_unsupported for SSH and X.509 signatures).
A bad signature is signature_invalid, and a valid signature from a key outside
fingerprints is signature_untrusted_key. Any of these stops the run with
nothing locked. Key expiry works the same way as for
URL signatures, including ignore_expiry.
Unlike a URL signature, a Git signature is checked on every resolve, not
only on the first. The signer's fingerprint goes into the version's lock entry,
and a later resolve that finds the release signed by a different pinned key
stops with lock_mismatch.
[source.git.signature] cannot be combined with tracked_path: a tracked
snapshot follows a branch, not a signed release.
An upstream that signs nothing — lightweight tags, no signed commits — cannot
use this table. The source.signature.required lint
rule reports it, and the recipe records why the upstream can't be verified as a
reasoned allowance in lint.pekit.toml. The exception is then a deliberate
declaration, not a silent default.
Tracking one file on a moving ref #
Some upstream data has no release tags of its own: one file on a stable branch
is the published stream. Set tracked_path to follow that file without
turning every unrelated branch commit into a package release:
[]
= "https://github.com/example/upstream.git"
= "refs/heads/release"
= "security/trust/certdata.txt"
= ">= 2026.01.01"
This is deliberately a bounded mode:
refmust be fixed and non-templated;tag_regexcannot be combined withtracked_path;tracked_pathis one clean repository-relative regular file, not a directory, symlink, or submodule;- discovery observes the fixed ref and compares the selected blob's SHA-256 with the newest matching lock entry. An unrelated commit, or any observation with the same bytes as that newest lock, creates no version;
- a changed blob receives the UTC discovery date
YYYY.MM.DD. Further changed blobs discovered and locked that day receive.2,.3, and so on. The suffix follows committed lock history, and a backwards wall clock clamps to the newest locked date, so the sequence remains monotonic; - the lock binds repository URL, fixed ref, path, immutable commit, Git blob
object ID, and SHA-256 of the blob bytes. It is append-only for this mode:
discover a change with
--latest;--repinis rejected.
--latest, constraints, and --all-versions check the moving ref. The
enumerated set is all matching historical lock entries plus the current
changed blob, if there is one; --all-versions therefore retains history
rather than replacing it with the tip. An exact locked version does not
resolve or fetch the moving ref. It uses the pinned commit and path, works
offline when those objects are cached, and may fetch only the pinned commit on
a cold cache. An exact version not yet locked is accepted only when it is the
date version Pekit computes for the currently observed changed blob.
The source root preserves the tracked relative path and contains no other
upstream file. Targets therefore read the example above at
$PEKIT_SOURCE_ROOT/security/trust/certdata.txt. This mode is suited to a
single independently versioned data input; use ordinary git mode when the
build needs a repository tree.
[source.url] #
Downloads a file over HTTP(S), optionally extracting it as an archive. The
required url is templated with the selected version; extract (default
false) treats the download as an archive, with root naming the subdirectory
inside the archive to promote as the source tree; versions is a version
cap (as for git); listing_url can name a release page when the download
template does not live in a browsable directory; file_regex is used when
enumerating that listing. Like Git tag_regex, named version or component
captures can map upstream filenames into the catalogue's version form; the URL
and archive root then render from that mapped version. checksum pins the download to an expected
digest — a single string or a per-version table (see below). The full schema is in the
recipe format reference.
[]
= "https://ftp.example.org/widget/widget-{{version}}.tar.gz"
= true
= "widget-{{version}}"
= "sha256:0f1e2d..."
checksum is a sha256:<hex> string, and it is the only algorithm the
verifier accepts — any other prefix fails with "unsupported checksum spec". It
may instead be a table keyed by version:
[]
= "sha256:aaaa..."
= "sha256:bbbb..."
When the table form is used and the selected version has no entry, pekit fails
with missing_checksum. You rarely need checksum, though: every fetched
version is pinned automatically in the recipe's
lockfile, and a URL source is only materialised
unanchored — not pinned to a digest — when it has neither a checksum nor a
lock entry, which matters for
reproducible packaging.
A URL source can also carry a [source.url.signature] table pinning the
upstream maintainer's release-signing key: pekit fetches the detached
signature beside the artifact and verifies it before anything is locked or
built, closing the trust-on-first-use gap for brand-new versions. The full
schema is in the recipe format reference.
A signing key that expires later does not invalidate an immutable historical
release: Pekit accepts the signature only when its signed creation time falls
within the key's validity period. A present-time key revocation, an explicitly
expired signature, a signature created after key expiry, or a materially
future-dated signature remains a hard failure by default. For an upstream that
knowingly continues to sign new releases after its key expires, a recipe may
set ignore_expiry = true in the signature table. This is an explicit
per-source exception to key expiry only: signature validity, the pinned
fingerprint, revocation, explicit signature expiry, and timestamp checks remain
mandatory.
Signed checksum manifests #
Some upstreams sign a checksum file rather than the artifact: a clear-signed
text file listing each release file's digest. of = "checksums" verifies such a
manifest instead of a detached signature. Dash is the case in the catalogue:
[]
= "checksums"
= "{{source_url}}.sha512sum"
= ["keys/herbert-xu.asc"]
= ["9F9D45FE50AE361530983792C7271D0A49B18BA7"]
Pekit fetches the manifest from url, verifies its clear signature against the
pinned keys, and then requires exactly one entry whose name is the artifact
URL's file name, character for character (./dash-0.5.13.5.tar.gz or a
directory-qualified name does not match). The artifact's digest must equal that
entry's, and its size too when the entry states one. The signer is recorded as
signature_key in the lock, as for a detached signature, beside the artifact's
sha256.
Because one manifest signature vouches for every file the manifest lists, this mode is stricter than a detached signature:
urlis required, since checksum manifests have no conventional name, andfingerprintsis required, each a full key fingerprint.- The manifest must be a clear-signed message and nothing else; text outside the signed block is refused.
- Three line forms are read:
<digest> <name>(coreutils, with*<name>for binary mode),<digest> <size> <name>(Dash's), andSHA512 (<name>) = <digest>(BSD). Digests must be SHA-256 or SHA-512. Any line pekit cannot read, including an MD5 or SHA-1 digest, makes the whole manifest unusable rather than being skipped.
A manifest that fails any of these checks is checksum_manifest_invalid; an
artifact that does not match its entry is checksum_mismatch. As with every
signature failure, nothing is locked or built.
Some upstreams publish one major.minor archive and then maintain it as an
incremental numbered patch series. [source.url.patch_series] models that
scheme without fetching anything from a build script:
[]
= "https://ftp.example.org/widget/widget-{{version}}.tar.gz"
= true
= "widget-{{version}}"
= ">= 5.3.0"
= 'widget-[0-9]+\.[0-9]+\.tar\.gz'
[]
= "https://ftp.example.org/widget/widget-{{major}}.{{minor}}-patches/widget{{major}}{{minor}}-{{patch}}"
= 3
= 0
For a selected 5.3.15, the primary url, root, checksum lookup, and base
signature render with base version 5.3; patch URLs render from 001 through
015. The resulting version is the base archive plus that ordered prefix.
Enumeration exposes 5.3.0, 5.3.1, …, 5.3.15, so ordinary runs select the
latest while --all-versions can reproduce the complete patchlevel history.
The patch listing must be contiguous from 1; a missing number is
url_patch_gap. A missing patch directory represents a new base release at
patchlevel zero.
[source.url.patch_series.signature] accepts the same fields and has the same
required-verification semantics as [source.url.signature]. Each patch is
downloaded, verified, and recorded in the selected version's lock entry before
that entry is written, so failure anywhere in the chain leaves no partial
lock. file_regex is optional; when present it must contain a named patch
capture. patch_width controls zero-padding and strip is the non-negative
path-component count passed to patch -p (both default to zero).
[source.pypi] #
Selects a project source distribution from PyPI's standardized JSON Simple API. This is the low-maintenance choice for recipes that should automatically discover new PyPI releases:
[]
= "asciidoc"
= "sdist"
= ">= 10.2.1"
All three fields are declarative rather than URL templates. project is the
PyPI project name; artifact is required and currently must be exactly
"sdist"; and versions is the usual optional source version cap. Pekit
normalizes the project name for the /simple/<project>/ request, asks for the
versioned JSON representation, and considers only
standardized .tar.gz sdists.
Automatic discovery deliberately excludes yanked files, prereleases, wheels,
legacy non-standard sdist filenames, and versions outside Pekit's version
grammar. A release must expose exactly one eligible sdist with a valid SHA-256:
none is pypi_sdist_missing when requested exactly, while multiple candidates
are pypi_sdist_ambiguous. Pekit never guesses between files.
The selected file's index-advertised SHA-256 is verified on download, and its
exact file URL and digest are written through the ordinary
pekit.lock mechanism. Once locked, an exact-version rebuild
does not need the live project index: it replays the pinned URL and hash from
the lock (using the artifact cache when present). --latest, constraints, and
--all-versions still consult the index so they can discover newly published
versions. A release that is yanked after it was locked therefore remains
rebuildable by exact version but is not selected during a fresh automatic
discovery.
[source.local] #
Points at a directory already on disk. Its single field is a required path —
the directory to use as the source tree, resolved relative to the recipe
directory.
[]
= "../widget-checkout"
[source.local] is an override, not a reproducible source: a recipe
with only [source.local] cannot be pinned or enumerated. The local tree is
only used when a local flag selects it — see below.
Additional inputs #
A recipe has one source. When a build genuinely consumes a second upstream,
declare it as an input: [input.<name>]. The Peios kernel is the case this
exists for — it is built from the pkm tree and the pristine Linux tarball, and
neither is the other's patch series.
An input is a [source.url] addressed by a name. It takes the same fields,
including a signature block, and pekit puts it through the same download,
the same signature verification and the same lockfile pinning.
[]
= "https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{{version}}.tar.xz"
= "https://cdn.kernel.org/pub/linux/kernel/v7.x/"
= 'linux-[0-9]+\.[0-9]+(?:\.[0-9]+)?\.tar\.xz'
= "= 7.0.9"
= true
= "linux-{{version}}"
[]
= "https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{{version}}.tar.sign"
= "decompressed"
= ["keys/greg-kroah-hartman.asc"]
= ["647F28654894E3BD457199BE38DBBDC86092693E"]
Three things differ from a source:
- An input carries its own version.
{{version}}inside an input block is that input's version, never the recipe's. The two move independently. - An input is pinned, not discovered. Its
versionsmust name exactly one version —"= 7.0.9", or a bare"7.0.9". A range is rejected (invalid_versions), because an input is never swept by--latestor--all-versions. Moving one is a deliberate edit. - An input takes no patch series. Patches apply to the source tree.
Targets read the materialised input at $PEKIT_INPUT_<NAME>, upper-cased
with hyphens becoming underscores, so [input.linux] is $PEKIT_INPUT_LINUX.
With extract = true it is the extracted tree, with root naming the directory
to promote; otherwise it is a directory holding the downloaded file. Under
isolation each input is bind-mounted read-only, so a target reads exactly the
verified bytes and cannot alter them.
[]
= 'cp -a "$PEKIT_INPUT_LINUX/." "$PEKIT_OUT/"'
Because pekit fetches and verifies inputs itself, before any build root exists, a build needs no network access and no PGP implementation of its own — which is what lets the same recipe work identically on a native rung and on a Debian reference rung, where only Debian packages are installed.
Each input's pristine archive is carried in the recipe's
source package under upstream/<name>/, and its hash is in
the lockfile, so a recipient can verify those bytes independently.
Inputs in a delegated source #
A source tree that carries its own pekit.toml can declare inputs there, next
to the targets that read them. When a recipe delegates its build to that tree
(delegate = true, or build = true in a [delegate] table), the source's
inputs come with the borrowed targets. The Peios kernel works this way: pkm
declares [input.linux] with kernel.org's signing key committed beside it, so
the Linux release it is derived from moves in the same commit as its patch
series, and the catalogue recipe that builds it stays a pointer.
- The delegating recipe does the fetching and pinning. Pins go in its own
pekit.lock, the one record of everything the build consumed. The source tree is never written to. - Keys resolve where they were declared. A relative
key_filespath names a file beside the source'spekit.toml, and lint reports the source's inputs against that file. - A recipe input overrides by name. If the delegating recipe declares an input with the same name, it replaces the source's input as a whole, the same rule that governs delegated targets.
Because each release of the source can name its own input version, the lock keys input pins by name and version. Building an older release verifies against the pin that release recorded.
Selecting the local override #
Two invocation flags choose the local override; they resolve differently and are mutually exclusive (passing both is an error).
--local(strict): require a local source. If neither[source.local]nor--local=<path>supplies a directory, pekit fails withmissing_local_source. Use this to force building from a working checkout.--prefer-local: use the local source if it is usable, otherwise fall back to the reproducible source. If the local tree is missing and the recipe has no reproducible source, the fallback fails.
Both accept an inline path:
--local/--prefer-localwith no=<path>uses the directory from[source.local](resolved relative to the recipe directory).--local=<path>/--prefer-local=<path>uses<path>resolved relative to the invocation's working directory (cwd), ignoring[source.local].
The chosen directory must exist and be a readable directory, or pekit errors.
Local sources require an exact --version — --latest, --all-versions,
and constraint selectors are rejected (unsupported_version_mode); with no
version, a local build uses the placeholder 0.0.0-localdev.
For a sourceless recipe, passing a local flag is an unsupported_flag
error — unless --allow-unused is set, in which case the flag is ignored with a
warning and the recipe stays sourceless.
Materialisation #
Resolving a source produces a source state: its kind, the source root (where targets run), a provenance ref, and a timestamp. What pekit does to produce the tree depends on the kind.
Sourceless (recipe). No work: the source root is the recipe
directory. Provenance is recipe:<recipe-root>.
Local (local). No copy or fetch: the source root is the local
directory itself; targets run in place. Provenance is local:<path>. Because
nothing is fetched, local sources are not cached and --refresh-source has no
effect on them.
Git (git). pekit keeps a bare mirror clone under
<out_dir>/_source_cache/git/<hash>/repo.git:
- If the mirror does not exist,
git clone --mirror <url>; otherwisegit fetch --prune --tagsto update it. A failed refresh is fatal only when nothing pins what the ref means: if the selected version is locked, the entry matches the rendered ref, and the pinned commit is already in the mirror, pekit warns and builds from the locked commit instead — so a rate-limited or unreachable upstream cannot block a locked build. Unlocked resolves still fail hard. - Resolve
refto an immutable commit (git rev-parse <ref>^{commit}). - When a version is selected, verify the resolved commit against the version's lockfile entry — or create the entry on first resolve.
- Clone the mirror into
<out_dir>/git-<hash>/source, thengit reset --hard <commit>andgit clean -fdxto pin the checkout. - Apply the recipe's patch series, when one is declared. Reset and clean restored the pristine tree, so the series re-applies on every resolve.
- Write a
source.pekit.jsonmanifest recording the URL, ref, resolved commit, and provenance.
Provenance is git:<url>@<commit> and the timestamp is the commit's committer
date, so a git build is anchored to a specific commit even when ref was a
branch or tag.
Tracked-path git (git). Pekit keeps a separate bare cache under
<out_dir>/_source_cache/git-tracked/<hash>/repo.git. Discovery shallow-fetches
the fixed ref with a blob:none partial-clone filter, resolves its commit, and
requests only tracked_path's blob. A server without partial-clone filtering
may transfer the other blobs in that shallow snapshot, but Pekit still
materialises and packages only the selected regular file. A locked resolve
uses the lock's commit directly and never consults the moving ref; a cold cache
therefore requires a server that permits fetching that pinned commit object.
The materialised tree is recreated from the verified blob on every resolve,
then any recipe patch series is applied. Provenance is
git:<url>@<commit>:<path>#sha256:<blob-sha256>.
URL (url). pekit caches the downloaded artifact under
<out_dir>/_source_cache/url/<hash>/:
- If the artifact is not already cached, download it (with a
pekit/2user-agent). - If a
checksumis set, verify it; on mismatch, re-download once and verify again before failing. - For a remote patch series, download patches 1 through the selected patchlevel. Verify the base and every patch against the version's lockfile entry, cache hits included — or atomically create the entry, verifying all configured upstream signatures first.
- Materialise the tree at
<out_dir>/url-<hash>/source: ifextractis set, extract the archive to a temporary directory, then promote therootsubdirectory (which must exist, elsemissing_source_root); otherwise copy the raw artifact into the source directory. Extraction preserves each regular file's archived mtime — release tarballs encode "generated outputs are newer than their inputs" in timestamps, and losing that fires autotools maintainer rebuild rules in environments without the maintainer tools. - Apply remote patches in numeric order with
patch, no fuzz, then apply the recipe's patch series, when one is declared. The series' content hash joins the materialisation scope, so an edited patch lands in a fresh extraction. - Write a
source.pekit.jsonmanifest.
The downloaded artifact is cached regardless. The materialised tree is reused, including for checksum-pinned sources, while its manifest's source identity matches: source kind, rendered URL, checksum, extraction settings, root, provenance and patches. Checksum and lock verification still run on every resolve. An identity change invalidates the materialised scope and its staged outputs.
The source timestamp is metadata, not part of that identity. Tar downloads
use their newest member's timestamp; anchored downloads without an archive
date fall back to the recipe directory's last commit time. A commit changing
that fallback date refreshes source.pekit.json without replacing or retiming
source files or discarding completed builds. Thus --no-build can reuse a
staged build across a notes or lockfile commit. New target executions receive
the current timestamp in their environment; reused build outputs are not
rebuilt just to change it. Package provenance checks still apply separately.
PyPI (pypi). Pekit first selects the one eligible sdist for the chosen
version as described above, then uses the URL materialisation path: the
advertised hash is verified, the pristine .tar.gz is cached, its standardized
top-level directory is extracted, patches are applied, and the exact file is
locked. Provenance is pypi:<normalized-project>@<version>#sha256:<hash>.
Caching and --refresh-source #
Pekit does not expose configurable cache policies. Caching is exactly the
behaviour above — a persistent raw cache (mirror repo / downloaded artifact)
plus a materialised tree — and the one knob is --refresh-source, which
discards the cache and rebuilds from scratch:
- git: removes the mirror repo and the checkout scope, forcing a fresh
clone --mirrorand checkout. Tracked-path git removes its sparse cache and refetches the selected immutable commit (for an exact lock) or moving ref (during discovery), without changing what the lock asserts. - url / PyPI: removes the cached artifact and the materialised scope, forcing a re-download and re-extract.
- local / sourceless: nothing to refresh.
A refresh does not relax the lock: the fresh download is still verified
against the lockfile, so --refresh-source is how you check upstream and
pekit lock --repin is how you accept a change.
The lockfile #
Fetched inputs are pinned trust-on-first-use in a machine-written
pekit.lock beside pekit.toml. The first time a source resolves for a
version, pekit records what it fetched — the artifact's SHA-256 for a url or
PyPI source, every ordered remote-patch URL and SHA-256 when present, or the
resolved commit for an ordinary git source, with the verified signer's
fingerprint when the recipe has a [source.git.signature]. A tracked-path git entry records
the repository, fixed ref, path, commit, blob object ID, and blob SHA-256. Every
later resolve verifies against
that entry instead, cache hits included. A mismatch is a hard
lock_mismatch stop: upstream's published bytes (or a tag) changed under a
version that was already pinned. Accepting such a change is an explicit
ceremony, never automatic for ordinary git and URL sources:
pekit lock --repin --version 1.2.0
Additional inputs are pinned in the same file, in their
own [[input]] entries keyed by name and version:
[[]]
= "linux"
= "7.0.9"
= "https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.0.9.tar.xz"
= "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4"
= "647f28654894e3bd457199be38dbbdc86092693e"
An input asserts the same thing a url source does — these bytes, verified by
this key — and stops the same way when they change. Moving an input to a new
version is an edit to its versions constraint. The new version is verified and
pinned the first time it resolves, like a new version of a source, and the old
pin stays.
Commit the lockfile with the recipe. Local sources, dry runs, and git sources
without a selected version (a bare branch ref is a deliberately moving target)
are never locked. The file's exact schema is in
Supporting files; the lock
command — including pre-locking versions without building — is in the
command-line reference.
Source packages #
A recipe with a reproducible source automatically emits a
corresponding-source package alongside its peipkg-format members — the
pristine upstream artifact, the patch series, and the recipe files, verifiable
against the committed pekit.lock — and every emitted manifest records the
provenance of its inputs, recipe, and builder. Both are covered in
Signing and provenance; opt out or
rename with the recipe's
[source_package] table.
Corresponding-source bundles preserve relative dangling symlinks used by upstream
test fixtures, provided every component remains inside the exported tree. Both
publication and rebuild.py reject absolute targets, escaping traversal, cycles
and non-directory parents. A missing link target is not a missing declared input:
all declared files and the original link text still undergo identity checks.
Compact prepared-source archives apply the same checks within source/.
Archive extraction #
For an extracted URL source, exclude optionally names files or directory
subtrees to omit, relative to the selected root:
[]
= "https://download.qemu.org/qemu-{{version}}.tar.xz"
= true
= "qemu-{{version}}"
= ["roms/edk2"]
Each entry is an exact, canonical relative path, without a trailing slash,
.. components, backslashes, glob patterns or templates. roms/edk2 omits
that entry and its descendants, but keeps roms/edk2-other. The root itself
cannot be excluded, and a nonempty list requires extract = true. A path
absent from the archive is harmless. The same option is available for
[input.<name>] archives.
Exclusions are applied before creating archive entries: an omitted subtree may contain an escaping symlink, but the symlink is never created. Every member name still undergoes the extraction-root containment check, including omitted members. All normal safety checks continue to apply to retained entries. Exclusion does not dereference links or permit otherwise unsafe retained entries.
The exclusion list participates in the extracted-source and input cache
identity; changing it cannot reuse a tree extracted with a different list.
Downloads, checksum/signature verification and lockfile hashes still cover
the complete upstream artifact. Corresponding-source packages retain that
pristine artifact, including excluded paths. Recipe patches apply to the
filtered source tree through the usual [source] patches mechanism.
Extraction is chosen by the artifact's filename suffix. Only these formats
are handled; anything else is an unsupported_archive error:
| Suffix | Handler |
|---|---|
.zip | native zip reader |
.tar | uncompressed tar |
.tar.gz, .tgz, .crate | gzip + tar (.crate is cargo's publish format — a plain gzipped tarball) |
.tar.bz2, .tbz2 | bzip2 + tar |
.tar.zst | zstd + tar |
.tar.xz, .txz | tar piped through the external xz -dc (requires xz on PATH) |
Extraction is hardened: entries that escape the extraction root (absolute
paths, .., or traversal through a symlinked parent), unsafe symlink targets,
NUL bytes, unsupported entry types, and colliding entries are all rejected as
unsafe_archive.
Patches #
A recipe can carry a patch series that pekit applies to the materialised
tree, declared as a directory name under [source]:
[]
= "patches"
The directory's series file lists patch files in apply order (one relative
path per line; # starts a comment). pekit applies them with git apply —
strictly, with no fuzz — immediately after the pristine tree is materialised
and before any target runs, so targets always see the patched tree and
@source: mappings package patched files. How that interacts with caching
follows the source kind:
- git sources re-apply the series on every resolve: the checkout is reset to the pinned commit and cleaned first, so an edited patch takes effect on the next run.
- url and PyPI sources materialise once per patch-set content: the series' hash joins the materialisation scope, so an edited patch extracts and patches a fresh tree.
- local sources are never patched — a local tree is your own working state, often with the series already applied or mid-rework. pekit emits a warning and continues.
A failed hunk is patch_apply; a patch that matches nothing is
patch_skipped; either aborts materialisation and leaves no half-patched
tree behind. The series grammar and its validation errors are in
Supporting files.
Patches are committed recipe content, so the lockfile is
uninvolved: it pins fetched bytes, and the patch series is already under
version control. The whole directory ships in the recipe's
source package under patches/ — the shipped series is
the applied series by construction.
This recipe-owned series is distinct from
[source.url.patch_series], whose patches are upstream inputs:
remote patches are signature-verified and locked, ship below
upstream/patches/, and are applied before any recipe-owned patches.
Rebasing on a version bump is deliberately manual: a stale patch fails
loudly, so materialise the new tree (pekit lock --version <v> is enough),
fix the patch, and run again.
Enumeration #
Reproducible sources can list the versions they offer upstream; this feeds
--latest, --all-versions, and constraint selectors (see
Versions).
- git:
git ls-remote --tags <url>, with tags optionally filtered bytag_regex. A namedversioncapture supplies the complete version. Namedmajor,minor, andpatchcaptures instead compose a dotted version. Optionalrevision,suffix,prerelease, andbuildmetacaptures are appended:revisionbecomes a fourth numeric component, whilesuffixis unseparated. For example,ref = "{{major}}{{minor}}{{patch}}"withtag_regex = '^(?P<major>\d{4})(?P<minor>\d{2})(?P<patch>\d{2})$'maps the tag20260810to version2026.08.10and renders it back to the same ref. A ref ending{{suffix}}{{revision_suffix}}can likewise round-trip eithermicrocode-20230516aas2023.05.16aormicrocode-20260210-rev1as2026.02.10.1. Without named version captures, pekit extracts the version from the ref template or an embeddedMAJOR.MINOR.PATCH; unnamed capture groups only filter tags. - tracked-path git: shallow-fetch the fixed ref and expose matching
versions already in
pekit.lock, plus one newly synthesized date version only when the current tracked blob differs from the newest lock. - url: fetch
listing_urlwhen set, otherwise derive a directory listing from theurltemplate (the part before the first{{…}}, up to the last/); filter entries byfile_regex, and extract versions from the matches. Named captures have the same mapping semantics as Gittag_regex: for example,(?P<major>\d{4})(?P<minor>\d{2})(?P<patch>\d{2})exposes20260810as2026.08.10, which a URL containing{{major}}{{minor}}{{patch}}renders back to the upstream filename. Without named version captures, Pekit extracts an embedded version as before. - PyPI: fetch the project's standardized JSON Simple API page once per
invocation and enumerate the eligible sdists described under
[source.pypi].
Local and sourceless recipes cannot enumerate
(version_enumeration_unavailable) — they require an exact version. The
versions field on a git, url, or PyPI source acts as a cap applied to the
enumerated (or requested) set; version selection itself is documented on the
Versions page.
Source roots and provenance #
The source root is the directory targets execute in and the base that
@source: file references resolve against. Every
resolution records a provenance ref identifying exactly what was
materialised:
| Kind | Provenance ref |
|---|---|
| sourceless | recipe:<recipe-root> |
| local | local:<path> |
| git | git:<url>@<commit> |
| tracked-path git | git:<url>@<commit>:<path>#sha256:<blob-sha256> |
| url (checksummed) | url:<url>#<checksum> |
| url (locked, no checksum) | url:<url>#sha256:<hash> |
| url with remote patches | the locked url ref above plus +patches:sha256:<series-hash> |
| url (no checksum, no lock entry) | url:<url> (marked unanchored — reachable only in a dry run, since a real resolve locks) |
| PyPI | pypi:<normalized-project>@<version>#sha256:<hash> |
For git, url, and PyPI sources the provenance is also written to a source.pekit.json
manifest beside the materialised tree, and pekit emits it as a source event
under --verbose.
Delegation #
A recipe can borrow behaviour from a pekit.toml that lives inside its
materialised source tree, using the [delegate] table (or the shorthand
delegate = true, which enables everything):
= true
# or, selectively:
[]
= true
= true
= true
= true
[delegate] has four surfaces plus all: build borrows the
build/test/install/clean targets from the source's recipe, env its
environment variables, wrap its command wrapper, and packages the package
definitions discovered in the source tree; all enables all four. (The key
table is in the recipe format reference.)
Delegation only happens when the source tree is a different directory from
the recipe (so a sourceless or in-place local recipe never delegates), and, for
build targets, only when a pekit.toml exists at the source root. The merge is additive with the base recipe winning: a
delegated target is adopted only if the base recipe does not already define a
target of the same kind and name, and adopted targets are tagged as owned by the
source. Env, wrap, and package delegation are likewise gated on their
corresponding [delegate] flags and only apply when the source tree differs
from the recipe root.
This lets a thin Peios recipe wrap upstream software that already ships its own
pekit recipe: point [source.git] at the upstream repo, set delegate = true,
and reuse its build and packaging while overriding only what you need.
Plain pekit lint acquires one delegated source snapshot and statically checks
these merged recipe and package definitions without building. An explicit
version or source selector additionally enables payload rules against that
selection's existing build stage.
Where to go next #
For how enumerated versions are selected and rendered, read Versions.
For turning the materialised tree into package payloads, read Packaging files.
For the exhaustive [source] schema, read Recipe format reference.