Workspaces
A workspace lets you drive a whole collection of recipes as one unit. Instead
of running pekit build in each recipe directory by hand, you declare which
recipes belong together in a workspace.pekit.toml file and run a single
pekit workspace build, which delegates that command to every member. This is
how a distro-scale tree of packages — dozens or hundreds of recipes — is built,
tested, packaged, and published in one go.
A workspace does three things:
- Discovers members from
include/excludeglobs (each member is a directory containing apekit.toml). - Shares configuration —
[env],[wrap], and a distro-wide[policy]— down to every member. - Schedules and delegates a single command across all members, with optional concurrency, fail-fast, and cross-member safety checks.
The workspace.pekit.toml file #
The workspace file is TOML and lives at the root of the tree it governs. Its
schema is small — exactly five top-level keys are accepted, and anything else
is rejected with unknown_key: include and exclude
(globs selecting and removing member directories, relative to the workspace
root), [env] and [wrap] (shared configuration contributed to every member),
and [policy] (distro-wide derivation policy — see
Policy). The key-by-key schema is in
Supporting files.
include is mandatory: an omitted or empty include fails with missing_key
("workspace include is required"). Everything else is optional.
# workspace.pekit.toml at the distro root
= ["core/*", "extra/**"]
= ["extra/experimental/*"]
[]
= "1700000000"
[]
= ["nice", "-n", "10", "{{command}}"]
[]
= "GLIBC_"
Member discovery #
include and exclude are glob patterns matched against the directory tree
under the workspace root. Matching uses doublestar globbing, so ordinary
wildcards (*, ?, character classes) work and ** matches across directory
boundaries. Patterns are always workspace-root-relative.
Discovery proceeds as:
- Every
excludepattern is expanded and its matches recorded. - Every
includepattern is expanded. A match is kept only if it was not excluded and the matched directory contains apekit.toml.
A matched directory with no pekit.toml is silently ignored — it is simply not a
recipe, so it is not a member. This means you can point include at a broad glob
(extra/**) and only the directories that actually hold recipes become members.
If discovery yields no members at all, the run fails with empty_workspace
("workspace has no members").
Locating the workspace #
When you run pekit workspace, pekit finds the workspace file by walking upward
from the current directory looking for workspace.pekit.toml (the same
upward search recipes use). You can override this with --workspace <path>,
which accepts a path to a workspace.pekit.toml file, a directory containing
one, or a remote locator (for example a github.com/... reference).
Member identity #
Each member has a stable id: its workspace-relative directory path, written
with forward slashes (for example core/zlib or extra/tools/ripgrep). This id
is the member's name everywhere it appears:
- in diagnostics and error messages (
member_failed,member_skipped); - in structured JSON events and in output-line prefixes;
- in the publish-collision owner labels (see below);
- in the end-of-run summary.
Members are processed in a deterministic order: ids are sorted lexicographically. With the default single-worker execution this is the exact order members run in; with concurrency it is the order in which work is dispatched.
Invocation #
pekit [global flags] workspace [workspace flags] <command> [args and flags]
workspace is itself a command, but it does no building of its own — it
delegates another command — any of build, test, install, package,
publish, clean, gen, verify, or lock — to every member. That delegated command and its arguments
come after the workspace flags.
Strict flag placement #
The tail after workspace is parsed specially, and the
placement rules are strict:
- Workspace flags —
--jobs Nand--fail-fast— must appear afterworkspaceand before the delegated command. They are recognised only in this slot. - Command-level flags (version selection,
--local,--env,--keyring,--refresh-source,--all, …) belong to the delegated command and must come after it. Placing one before the delegated command is an error with a hint: "workspace command flag<name>must appear after the delegated command." - Global flags (
--dry-run,--quiet,--verbose,--json,--allow-unused,--workspace) may appear either beforeworkspaceor in the tail before the delegated command.
# correct: workspace flags before the command, command flags after
$ pekit workspace --jobs 4 --fail-fast build --version 1.2.3
# also correct: a global flag may precede workspace
$ pekit --dry-run workspace build
# error: --version is a command flag placed before the delegated command
$ pekit workspace --version 1.2.3 build
# workspace command flag version selection must appear after the delegated command
Because --jobs/--fail-fast are recognised only in the workspace slot,
writing them after the delegated command hands them to the delegated command's
own parser, which rejects them as an unknown_flag. Keep them in the workspace
slot.
Both failures share the diagnostic code missing_workspace_command: it fires
when a command flag precedes the delegated command, and when no delegated
command follows the workspace flags at all ("workspace requires a delegated
command").
Execution model #
Each member is run as an ordinary per-recipe invocation of the delegated
command — exactly as if you had cd'd into that member and run
pekit <command> ... there, with the delegated flags and any positional
selectors carried through unchanged.
Concurrency: --jobs #
--jobs N sets how many members run concurrently. N must be a positive
integer; a missing value is missing_flag_value and a non-positive or
non-numeric value is invalid_flag_value ("--jobs requires a positive
integer"). The default is 1, i.e. members run one at a time in id order.
Fail-fast: --fail-fast #
By default the workspace attempts every member even if some fail: it runs
them all and then fails the overall command at the end if any member failed
(workspace_failed, "N workspace member(s) failed").
With --fail-fast, the first member failure sets a stop flag so that no
further members are started. Members that had not yet begun are recorded as
skipped.
No inter-member dependencies #
Members are independent. Pekit does not compute a dependency graph between members, does not topologically order them, and does not make one member's build visible to another as an input. The only ordering is the lexicographic id order used for dispatch. If a member needs another member's output, that relationship must be expressed through the normal recipe machinery (sources, package dependencies), not through workspace membership.
Summary #
At the end of a run pekit emits a workspace_summary event tallying members as
succeeded, failed, or skipped:
12 succeeded, 1 failed, 3 skipped
Succeeded and failed count members that actually ran; skipped covers
members not started because of --fail-fast, and members skipped by selector
planning (below). The overall command exits non-zero if any member failed.
Per-member strictness #
The delegated command and its flags are validated once, when the tail is parsed.
Beyond that, strictness is evaluated per member, because members differ: one
recipe may define a build target or a package selector that another does not.
-
Each member runs the delegated command against its own recipe. If a flag or selector cannot be honoured by that member's recipe, that member errors — its failure is reported (
member_failed) and counted, and the other members are unaffected (unless--fail-fastis set). -
--allow-unusedrelaxes this per member. When a run carries positional selectors (target names, ordef:variantpackage selectors) together with--allow-unused, pekit plans which selectors apply to which member:- selectors that a member cannot satisfy are suppressed for that member
(reported via
workspace_selector_suppressed); - a member to which none of the requested selectors apply is skipped ("no requested selectors apply to member");
- if a requested selector applies to no member at all, the whole run
fails with
missing_selector("workspace selector … matches no selected member").
--allow-unusedalso suppresses per-member unsupported version selection rather than failing that member. - selectors that a member cannot satisfy are suppressed for that member
(reported via
Without --allow-unused, selectors are passed to every member verbatim and a
member that does not recognise one fails in the usual way.
Cross-member publish collision detection #
When the delegated command is publish, two members could be configured to
publish an artifact to the same destination — a mistake that would have them
overwrite each other. Pekit catches this before publishing anything.
A preflight pass runs each member's publish planning in dry-run mode and
reserves every resolved destination path in a shared registry. Destinations
are keyed by path, each with an owner of the form <member-id>:<instance>. Reserving a path that a different owner already holds
fails the entire run with publish_collision ("publish destination collision
between … and …"). Reserving the same path for the same owner is idempotent, so a
member does not collide with itself.
Because this is a preflight, a destination clash is reported up front, before any member has written to a publish target.
Shared configuration #
[env] and [wrap] #
[env] and [wrap] declared at the workspace level are shared defaults
contributed to every member, layered together with each member recipe's own
[env] and [wrap]. For how these compose with recipe- and environment-file
values — and the precedence between them — see
Environments and keyrings. The
[wrap] command follows the same {{command}} placeholder rules as a recipe
wrap. Keyrings behave similarly: a named --keyring on a workspace run
resolves against the workspace root once, and the values are shared with
every member.
Policy: symbol-version floors #
The [policy] table carries distro-wide derivation policy. Its one current
sub-table is [policy.symbol_versions], a map from a shared-library soname
to the symbol-version token prefix whose tokens are commensurable with the
providing package's version:
[]
= "GLIBC_"
= "GLIBCXX_"
This governs which sonames receive a symbol-version floor when pekit derives a
package's dependencies across the workspace — so that, for example, a binary
using a GLIBC_2.34 symbol gains an appropriate lower bound on its libc
dependency. An unknown sub-table under [policy] is rejected
(unknown_key, "policy.… is not a known policy table"). For how derived
dependencies and claims work, see
Dependencies and claims.
Where to go next #
For the recipes a workspace's members are, read Anatomy of a recipe.
For how workspace [env] and [wrap] compose with a recipe's own, read Environments and keyrings.
For the full flag surface and remote locators, read Invocation and flags.