On this page
Packaging files
A package definition answers one question: which files, from where, go where in
the package? Everything else — metadata, dependencies, claims, multi-package
fan-out — hangs off that payload. This page is about the payload itself: the
[files] and [symlinks] tables, how source references resolve, and the rules
that decide the final layout of the .peipkg.
Package definitions live in their own files, not in the recipe:
package.pekit.toml (the base definition) and <name>.package.pekit.toml
(named members). How those layer together, and how [multipack] expands one
definition into many packages, is covered in
Multi-package recipes. Manifest metadata,
dependencies, and claims are covered in
Dependencies and claims. This page
assumes a single definition and concentrates on the file mapping.
The [files] table
[files] maps sources to package destinations. The mapping direction is
source on the left (the key), package destination on the right (the value):
[files]
":bin/hello" = "usr/bin/hello"
"@recipe:default.conf" = "etc/hello/hello.conf"
The left side is a source ref naming what to copy; the right side is the path the file takes inside the package. Destinations are always relative (see Destination rules below).
Each value may also be a table, which adds an override flag:
[files]
"@recipe:layout.json" = { path = "usr/share/hello/layout.json", override = true }
| Field | Meaning |
|---|---|
path |
The package destination (required; an empty destination is an error). |
override |
Exempt this entry from the package format's layout policy. Defaults to false. See override. |
The bare-string form "src" = "dest" is exactly "src" = { path = "dest" }.
Source refs
A source ref names where a file comes from. There are three families.
Build-output refs — target:path
A ref containing a colon is a build ref. The part before the colon is a build target name; the part after is a path inside that target's staged output:
[files]
"tools:bin/hc" = "usr/bin/hc" # from build target "tools"
":bin/hello" = "usr/bin/hello" # empty target = "main"
:path is shorthand for main:path — an empty target name resolves to the
target main. Referencing a build target in [files] automatically pulls that
build into the package's build plan, so you do not have to list it separately.
Rooted refs — @source:, @recipe:, @workspace:
A ref beginning with @<root>: is anchored to a named root rather than a build
output:
| Prefix | Root |
|---|---|
@source: |
The materialised source tree (the checked-out / extracted source). |
@recipe: |
The directory containing the recipe (and its package files). |
@workspace: |
The workspace root. Used outside a workspace, this is an error (missing_workspace). |
[files]
"@source:LICENSE" = "usr/share/licenses/hello/LICENSE"
"@recipe:hello.service" = "usr/lib/systemd/system/hello.service"
Plain paths are owner-relative
A ref with no prefix and no colon is a plain path — and its meaning is set by the file that declared it, not by any global default. At decode time, pekit rewrites each plain path to a rooted ref based on the file's owner:
| The ref was declared in… | A plain path becomes… |
|---|---|
| a recipe-side package file | @recipe: |
| a delegated source-side package file | @source: |
| a workspace-side package file | @workspace: |
So in a recipe's own package.pekit.toml:
[files]
"README.md" = "usr/share/doc/hello/README.md"
"README.md" is normalised to "@recipe:README.md" — it resolves against the
recipe directory. The identical line in a source-delegated package file would
normalise to "@source:README.md" and resolve against the source tree instead.
This normalisation is what lets the same package file mean the right thing
whether it ships beside the recipe or is discovered inside a delegated source.
Refs that are already rooted (@…:) or already build refs (contain a :) are
left untouched by this rewrite.
The owner-relative rule also applies to the source path of
[multipack]file enumeration and to every entry inexcludes— both are normalised the same way.
Path cleaning
The path portion of every rooted ref is cleaned: . and duplicated slashes are
collapsed, and a path that escapes its root with .. (or is absolute) is
rejected:
invalid_ref: "@recipe:../secrets" escapes its root
Globs
A source ref's path may contain glob magic — *, ?, or a [...] character
class — and ** for recursive matching:
[files]
"@source:share/icons/*.png" = "usr/share/hello/icons/"
"tools:lib/**/*.so" = "usr/lib/"
Rules:
-
Matching happens only within the ref's source root. A glob never reaches outside the root it is anchored to.
-
Matches are sorted, and a match already covered by a matched parent directory is dropped (the directory carries its contents).
-
By default a glob that matches nothing is an error:
missing_payload: file source /…/share/icons has no matches -
Placement is relative to the non-glob prefix. Everything up to the first segment containing glob magic is the base; matched paths are re-rooted under the destination from there. So
share/icons/*.png→usr/share/hello/icons/placesfoo.pngatusr/share/hello/icons/foo.png, andlib/**/*.sopreserves the sub-directory structure belowlib/under the destination.
A plain (non-glob) ref that names a directory copies the whole tree; empty directories are emitted as explicit entries (so a pure directory skeleton is not lost).
Excludes
excludes prunes files after expansion. Each entry uses the same source-ref
syntax as [files] (and is owner-normalised the same way), so an exclude is
scoped to a root:
[files]
"@source:share/**" = "usr/share/hello/"
excludes = [
"@source:share/**/*.tmp",
"@source:share/internal/**",
]
An exclude is applied only to expansions that share its root; it is matched
against each candidate with the same glob engine. Unlike [files], an exclude
that matches nothing is allowed — it is a filter, not a required input.
Destination rules
The right-hand side of a [files] entry (and every symlink destination) is a
path inside the package. It must be relative and stay in-bounds:
- No leading
/and no..— an absolute or escaping destination is rejected. - No NUL bytes; an empty destination is an error.
.segments and duplicate slashes are normalised away.
Directory vs single-file destinations
For a single, non-glob match, the trailing slash on the destination decides the shape:
| Ref | Destination | Result |
|---|---|---|
":bin/hello" |
"usr/bin/hello" |
Renamed to usr/bin/hello. |
":bin/hello" |
"usr/bin/" |
Placed as usr/bin/hello (basename kept). |
For a glob or a multi-match source, the destination is always treated as a directory prefix and matched paths are re-rooted beneath it.
Collisions are errors
Two payload inputs that resolve to the same package path is a hard error — pekit will not silently let one input clobber another:
payload_collision: multiple package inputs map to usr/bin/hello: …
(Sibling checks catch two package instances writing the same artifact file, or emitting the same package name.)
override
override = true exempts an entry from the package format's layout policy
only. The peipkg format validates that non-override payload paths obey its
layout rules; an override entry skips that validation, letting you place a file
somewhere the policy would otherwise reject.
Override does not relax the safety rules above: destinations are still
cleaned, still may not escape the package with .., and collisions are still
errors. It only opts out of format-level layout policy.
[symlinks]
[symlinks] adds symbolic links to the payload. Here the key is the package
destination (where the link lives) and the value is the link's target text:
[symlinks]
"usr/bin/hello-latest" = "hello"
"usr/lib/libhello.so" = { target = "libhello.so.1", override = true }
The target text is stored literally — it is the exact string the symlink
points at and is not resolved through any source root. It may be relative or
absolute as a link target; pekit only checks that it is non-empty and contains
no NUL. The bare-string form is "dest" = "target"; the table form adds
override, which behaves as for files.
The destination key follows the same destination rules as
[files].
The result
Packaging stages the resolved payload and writes one package artifact per
package instance under the recipe's output directory (out_dir). The format is
chosen by the package's format (defaulting to tar); a peipkg package emits
a file named <name>_<version>_<architecture>.peipkg, with the name,
architecture, and other manifest fields coming from [package].
The rest of the manifest — version, architecture, dependencies, provides,
claims, and so on — is documented in
Dependencies and claims, and the
.peipkg on-disk format itself in
Package management. Full key-by-key syntax
lives in the recipe format reference.