Dependencies and claims
A finished .peipkg carries a manifest that says what the package needs (its
dependencies), what shared names it fills, and how it plugs into names other
packages share. Pekit builds that manifest from three sources, in order of
increasing automation:
- Declared package dependencies —
[dependencies]and[optional_dependencies]you write in a package file. - Derived dependencies — sonames and pkgconfig capabilities pekit reads out of the staged binaries at pack time, added on top of what you declared.
- Claims — the
[claims]table, wiring a package's provides and dependencies into the peipkg "exactly one holder of a shared name" mechanism.
A separate, unrelated mechanism — build dependencies exported to the build command — is also covered here because it shares the word "dependency". It never touches the package manifest.
Declared package dependencies #
Package dependencies live in a package file (package.pekit.toml or a
<name>.package.pekit.toml member), not in the recipe. Two tables produce them:
[dependencies] for hard requirements and [optional_dependencies] for soft
ones. Both parse identically.
[]
= ">= 3.0"
= "*"
= ">= 1.2.11"
[]
= "*"
Each entry maps a name to a version constraint string. A concrete
package is named by its canonical reverse-DNS identity; unqualified package
aliases are not a supported compatibility layer. A virtual capability instead
names an interchangeable interface, such as a soname (libc.so.6), a
pkgconfig(...) token, or an explicitly documented role such as sh. The constraint is any
peipkg constraint expression (">= 1.2", "< 2", and so on). Both "*" and the
long form below with the constraint omitted mean any version.
The table form: placing a dependency in a named root #
An entry may instead be a table, which is the only way to attach a placement root — the named root the dependency should be installed into, when it differs from the depender's own root:
[]
# short form: constraint only, depender's own root
= ">= 1.2"
# table form: constraint + explicit root
[]
= ">= 3"
= "system.lib"
A dependency table accepts exactly two keys, constraint and root; any other
key is an error. Omitting constraint matches any version. The root is a
named root reference (dotted lowercase segments, e.g. system.lib), never a
filesystem path — a / in it is rejected at build time. Roots are not carried
for conflicts, which stay root-local. There is no in-string IN root sugar;
the table is the only spelling.
These entries become the manifest's Dependencies and OptionalDependencies.
How they are matched to concrete packages at install time — capability lookup,
version satisfaction, root placement — is
dependency resolution in
peipkg; pekit only records the requirement.
Derived dependencies: ELF and pkgconfig scanning #
At pack time, after staging the payload and before writing the archive, pekit scans the files it is about to pack and adds derived provides and dependencies on top of whatever the package declared by hand. Two derivations run, both operating purely over the staged files:
- ELF derivation — reads the built binaries and shared objects.
- pkgconfig derivation — emits
pkgconfig(<name>)capabilities from any.pcfiles in the payload.
The results are merged additively: a derived provide or dependency whose name
already appears in the hand-declared set is skipped, so declaring something by
hand always wins. Non-fatal problems (see below) are printed to stderr as
warning: lines; they do not stop the build.
What ELF derivation detects #
For every ELF object in the payload, pekit reads two dynamic-section fields:
| ELF field | Becomes | Meaning |
|---|---|---|
DT_SONAME | a provides entry | the object's own ABI name, e.g. libfoo.so.3 |
DT_NEEDED | a dependency entry | each shared library the object loads |
The whole soname is the capability name. ABI-version identity lives in the
string and is matched by equality, so a dependency on libfoo.so.3 is never
satisfied by libfoo.so.4 — a soname bump is a different capability.
Two refinements keep the result accurate:
- Self-provided sonames are subtracted from the needs. A package that ships both a library and something that links against it does not end up depending on itself.
- Symlinks are skipped. A
-develpackage'slibfoo.so -> libfoo.so.3development symlink is not opened; following it would read the target'sDT_SONAMEand mis-attribute the provide to the package shipping the alias. The real object is derived where it actually lives.
If a file whose name looks like a shared library (lib*.so, lib*.so.N…)
carries no DT_SONAME, pekit warns that "nothing can depend on it" — such a
library is unusable as a dependency target.
Derivation is entirely automatic and takes no configuration beyond the
symbol-version policy below. In particular it has nothing to do with the
dependency_provider / PEKIT_DEPENDENCIES* mechanism described later — that is
a different feature that shares a word.
Retaining an older library ABI #
A runtime compatibility package contains the actual maintained older libraries, with their original SONAMEs. It does not alias a new ABI to an old filename or claim to replace a development interface. The resolver can install both versions when the required library filenames differ.
For OpenSSL, org.openssl.openssl tracks current upstream and supplies the
command and development interface through its normal split packages.
org.openssl.openssl3 follows authenticated 3.5 LTS patch releases and supplies
libcrypto.so.3, libssl.so.3, the OpenSSL 3 engines, and its legacy provider.
It has no command, headers, static libraries, or unversioned linker symlinks.
The current OpenSSL 4 command uses openssl rehash; upstream removed the
separate c_rehash command. This runtime package does not restore that command.
ELF dependencies on the .so.3 names select it automatically. A package that
explicitly pins an obsolete concrete OpenSSL package revision still needs its
metadata updated; ABI compatibility does not override version constraints.
OpenSSL 3 reads /etc/ssl/openssl3.cnf and
/etc/ssl/ct_log_list-openssl3.cnf; package defaults are installed under
/usr/etc/ssl. Its provider directory is
/usr/lib/x86_64-linux-peios/ossl-modules-3. OpenSSL 4 retains the canonical
configuration and provider paths. Both use /etc/ssl/cert.pem and
/etc/ssl/certs for the system trust store. Administrators with custom OpenSSL
configuration should apply the settings required by older consumers to the
OpenSSL 3 configuration too; explicit OPENSSL_CONF and OPENSSL_MODULES
overrides remain application-specific and must match the selected ABI.
The compatibility recipe retains the 3.5 LTS maintenance line, supported upstream through April 2030 (upstream support dates). Its discovery bound does not freeze patch releases; the shorter-lived 3.6 line is deliberately excluded. Remove the compatibility package only when supported consumers no longer require that ABI.
Foreign static Linux targets #
org.libc.musl-sysroot supplies a private static sysroot for Rust's
x86_64-unknown-linux-musl cross target. Its headers, static archives and startup
objects live under /usr/lib/x86_64-linux-peios/musl. It installs no dynamic
loader, shared process libc or command, and provides no generic libc capability.
Peios applications continue to use the native glibc toolchain.
This target follows upstream musl's Linux ABI. It does not promise Peios-native CET shadow-stack support: upstream assembly, including nonlocal jumps, does not implement that contract. Do not force ELF property notes to advertise it. The native compiler and native application packages retain their normal CET gates.
The mandatory sysroot gate compiles and runs a self-contained static PIE against
the staged headers and archives. It exercises thread creation/join, setjmp /
longjmp, signal-mask restoration with sigsetjmp / siglongjmp, and the patched
GB18030 decoder. It also checks non-executable stack, RELRO, eager relocation,
compressed relative relocations and absence of a dynamic loader or shared-library
dependencies. These are cross-target functional and layout checks, not a claim
that foreign binaries satisfy every Peios-native execution policy.
Symbol version policy #
By default a soname dependency is derived at soname granularity only: the
name must match exactly, with no version constraint. That alone cannot establish symbol-version compatibility: it cannot express "needs at least the glibc that introduced
GLIBC_2.34".
A workspace may opt specific sonames into finer, version-aware derivation
through the [policy.symbol_versions] table in the workspace file. It maps a
soname to the symbol-version token prefix whose tokens are numbered on the
same scale as the providing package's version:
# in the workspace file
[]
= "GLIBC_"
= "FOO_"
For a soname listed here, ELF derivation is refined on both sides:
- Consumer side — pekit reads the object's versioned symbol needs, takes the
highest token carrying the prefix (e.g.
GLIBC_2.34), and turns it into a>= 2.34constraint on that dependency. Reserved, non-numeric tokens such asGLIBC_PRIVATEare ignored. - Provider side — the soname's own provides entry is stamped with the building package's own version, so consumers have something to match against.
A soname absent from the policy is derived at soname granularity only, as
above. The policy is a distro-wide assertion and lives only in the workspace
file. It accepts symbol_versions and symbol_capabilities; unknown keys are errors. Recipes and package files cannot set it. When no workspace
policy is configured, every soname is derived at name granularity.
Exact ELF version capabilities #
Libraries such as libstdc++ number their ABI versions independently of their package releases. Select these SONAMEs explicitly:
[]
= ["libstdc++.so.6"]
Pekit derives elfver(libstdc++.so.6:GLIBCXX_3.4.30) and corresponding CXXABI
capabilities from the actual GNU ELF version tables. Providers advertise each
non-base version definition; consumers require each strong version need in
addition to the SONAME. These names carry no package-version constraint. The
library name scopes the token, so a definition from another library cannot
satisfy it. Weak version needs remain optional. Exact definitions are compared,
not an assumed numeric maximum or a hand-maintained GCC release mapping.
Symlinks and source/debug-source fixtures are excluded. A package's own library satisfies only the exact capabilities it defines. An incompatible bundled library, multiple objects supplying the selected SONAME, malformed version tables or unrepresentable names fail package creation. Such errors must not silently fall back to SONAME-only requirements.
For rollout, publish updated runtime providers before publishing consumers with these requirements. Existing binaries and manifests are not retroactively qualified. Consumers originally linked against an unversioned runtime need relinking against a versioned runtime; packaging cannot reconstruct symbol versions absent from the binary. An old provider with only a SONAME capability cannot satisfy a new versioned requirement. Normal resolver checks also reject runtime upgrades that would leave installed consumers' requirements unsatisfied. This does not pin an exact GCC release or guarantee compatibility across arbitrary C++ ABI-changing compiler flags; normal upstream ABI gates still apply.
Build dependencies exported to the build command #
Distinct from everything above, a build target in a recipe may declare
[build.<target>.dependencies.<provider>] — a set of dependencies grouped under
one or more provider names. These never enter the package manifest. Instead,
pekit renders them and hands them to the build command through the environment,
so a build script can fetch or pin its build-time inputs.
[]
= "make"
[]
= ">= 13"
= "*" # "*" means any version; a build dep constraint may not be empty
[]
= "*"
dependencies is only valid on build targets. Each provider block maps a
capability name to a non-empty constraint (use "*" for any). Names and
constraints are templated, so {{version}} and the other version variables are
available.
Which provider is active is chosen by the dependency_provider key in the
selected environment file:
# in env.pekit.toml (or a named <env>.env.pekit.toml)
= "peipkg"
For every target run, pekit writes a JSON payload to
$PEKIT_ROOT/.pekit/dependencies/<command>.<target>.json and exports three
variables into the build environment (with no provider selected,
PEKIT_DEPENDENCY_PROVIDER and PEKIT_DEPENDENCIES are empty strings and the
payload still carries all_providers):
| Variable | Contents |
|---|---|
PEKIT_DEPENDENCY_PROVIDER | the selected provider name |
PEKIT_DEPENDENCIES_FILE | absolute path to the JSON payload |
PEKIT_DEPENDENCIES | the selected provider's deps, one name constraint per line |
These three are part of pekit's managed environment; the full environment contract lists every exported variable.
The JSON payload carries the command, target, provider, the resolved
version, the selected provider's dependencies, and all_providers (every
declared provider block, so a script can inspect alternatives). If
dependency_provider names a provider that a target with dependency blocks
does not define, that target's run fails (missing_dependency_provider); a
target with no dependency blocks at all ignores the selection. If no provider
is selected, the exported dependency list is empty but the payload still
lists all_providers.
Pekit does not resolve or install these — it only renders the declared list and
passes it through. Consuming it is the build script's job. dependency_provider
is a name selector, not a command or hook.
Debian reference dependencies without a Debian package #
The e2fsprogs security-descriptor extensions depend on libpeios, which Debian
cannot install from APT. Its recipe therefore declares a private reference SDK
prerequisite. The input is the unchanged signed dev.peios.libpeios-source
0.5.0-1 publication, with its repository key, MIT licence, source commit and
verified archive hashes recorded beside the recipe. Acquisition refuses changed
bytes before extracting the source.
That older source publication did not contain a vendored dependency closure. The acquisition target runs its original lockfile-bound Cargo vendor command and retains the PKM UAPI from the same fixed Git revision. These generated inputs are captured in the e2fsprogs source bundle. The offline SDK target builds from them and runs the frozen source's tests, ABI/export checks and dynamic/static consumer probes. The main reference build receives the SDK as an explicit prerequisite.
The snapshot establishes the reference ABI0 dependency; it needs review when required APIs change or prerequisite security maintenance requires a refresh. It does not pin native or runtime libpeios packages and does not modify or republish the active first-party source tree.
Qualification evidence (2026-09-16): both providers passed the required build, test and archive lint gates. Each ran the upstream 19-test internal suite and 390-test filesystem suite with zero failures; 17 upstream skips remain (slow, privileged, unavailable optional features and platform exclusions). The Debian prerequisite ran 256 Rust tests and the frozen ABI consumer/export checks. Exact cbindgen 0.29.2 was unavailable, so the frozen prerequisite policy verified the committed ABI snapshot instead of regenerating it; it checked standalone C/C++ headers and 207 DSO exports.
All 22 resulting archives passed independent trusted-key signature and payload
hash verification. Both source bundles reconstructed 3,656 authenticated input
identities and the expected build dispatch. This checks source reconstruction,
not an additional rebuild. Candidate installation is still blocked by the
existing repository closure's missing libblkid.so.1; recipe qualification is
not a claim of image or public-release acceptance.
Claims #
A claim is how a package plugs into a shared filesystem name that at most one
installed package may hold at a time — the peipkg mechanism where several
packages can supply a name like /usr/bin/cc, but exactly one holder owns
the real file and the rest defer. See
claims for what this means at install time.
A package declares claims in the top-level [claims] table of a package file.
It has two sides:
[claims.provides]— the provider side: slots a[provides]entry fills.[claims.dependencies]— the consumer side: slots a dependency expects.
Under each side, the next key is the name of the provides or dependency entry the claim attaches to, and under that is one or more freely-named slots. Each slot is a table with just two possible fields:
| Field | Set by | Meaning |
|---|---|---|
target | provider | the holder file this package ships that the shared name resolves to |
path | consumer (or a provider default) | the shared filesystem location — where the claim symlink is materialised |
A worked example — a compiler package that offers to hold /usr/bin/cc, and a
consumer that expects cc to exist:
# provider: gcc.package.pekit.toml
[]
= "1"
[]
= "/usr/bin/gcc" # a file this package ships
= "/usr/bin/cc" # provider default for the shared name
# consumer: some-toolchain.package.pekit.toml
[]
= "*"
[]
= "/usr/bin/cc" # the shared name this package needs present
Claim path and target values are run through the template engine, so a
recipe may parameterise them (an arch triplet in a library path, for instance)
like any other manifest value.
At pack time pekit validates the provider side: every provider slot's target
must name a file the package itself ships. A target pointing outside the
package's own payload is a packaging error — the materialised symlink would
dangle or point at another package's file — and all offenders are reported
together. Consumer-side slots and provider slots that set only a path own no
target and are exempt from this check.
Where to go next #
For the package files these tables live in, read Packaging files.
For the environment file that selects a build dependency provider, read Environments and keyrings.
For the workspace [policy] table that refines soname derivation, read Workspaces.