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 }

path is the package destination (required; an empty destination is an error), and override — default false — exempts the entry from the package format's layout policy (see override). The bare-string form "src" = "dest" is exactly "src" = { path = "dest" }. The field-by-field schema is in the recipe format reference.

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:

PrefixRoot
@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"

The reference table of source-ref roots gathers every ref form — build refs, the rooted prefixes, and bare paths — in one place.

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.

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 metacharacters — *, ?, 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 a glob metacharacter is the base; matched paths are re-rooted under the destination from there. So share/icons/*.pngusr/share/hello/icons/ places foo.png at usr/share/hello/icons/foo.png, and lib/**/*.so preserves the sub-directory structure below lib/ 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:

RefDestinationResult
":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 overwrite 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] 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]. A peipkg package must set [package].version, [package].architecture, and [package].license; a tar artifact carries payload only and is normalised for determinism (entry timestamps zeroed, ownership dropped).

The .peipkg on-disk format itself is documented in Package management.

Where to go next #

For the rest of the manifest — dependencies, provides, claims — read Dependencies and claims.

For splitting one recipe into several packages, read Multi-package recipes.

For the exhaustive key-by-key schema, read the recipe format reference.

Edit this page