The Ur Project

Documenting the craft of stacking things so they remain stacked. For a very long time.

pkg-build(1)

NAME

pkg-build - build a recipe directory into a signed local repository

SYNOPSIS

pkg build recipe-dir --out DIR [--with SPEC]... [--trust-key HEX]... [--work DIR] [--keep] [--replace]

DESCRIPTION

You've got a recipe. Maybe you wrote it, maybe you changed one line in an existing one. Now you want to see it build, install the result, and maybe hand it to somebody. You shouldn't need a build farm for that, and you don't.

pkg build builds one recipe on this machine, with the same build code the build farm runs, and publishes the result into a local repository you own. From there you can install it, compose with it, or roll it into a .pkgtar bundle for somebody else.

recipe-dir is a recipe directory the way the registry keeps it: recipe.toml, plus any patches/ and overrides.lua. Its [recipe] block names the package, version and release that come out.

What happens

  1. Load the build key. Without one, pkg build stops and points you at pkg-keygen(1).
  2. Fetch the upstream sources the recipe names, check their SHA-256, and detect which build-system probe applies: autotools, cmake, meson, cargo, and so on.
  3. Resolve a buildroot from the repository chain; see THE BUILDROOT.
  4. Compose that buildroot into a scratch sysroot, verifying every manifest's signature. When the sysroot ships a sysusers.d, generate its /etc/passwd and /etc/group from it with the host's systemd-sysusers.
  5. Run the recipe's configure, build and install phases once, sandboxed against that sysroot, and split the result into subpackages.
  6. Publish every subpackage into --out, signed with the build key.

Is it the same package the distribution ships?

Usually, no. And it doesn't pretend to be.

pkg build applies the build policy to whatever the chain offers today and produces a new artifact. It makes no claim that the bytes match the published package of the same version. Usually they won't, because the buildroot is different. If what you want is to reproduce a published build from its pinned inputs, that's pkg provenance rebuild; see pkg-provenance(1).

THE BUILD KEY

Every build is signed with one key per user, $XDG_CONFIG_HOME/pkg/build.key, which pkg-keygen(1) creates. pkg build never makes one up on its own:

error: build: no build key at /home/u/.config/pkg/build.key; create one with pkg keygen

The key signs each manifest and the output repository's TUF metadata. That includes the self-signed metadata/1.root.json the first build writes. That file is the repository's trust anchor, and whoever consumes the repository pins it once. One key per contributor, rather than one per build, is what makes that pin last.

THE BUILDROOT

The sysroot a recipe builds in comes from the same repository chain pkg-install(1) uses: the drop-ins in /etc/pkg/repos.d, or --repo-url and --tuf-root when you give them. Never from the host's /usr. What goes into it:

+build

The build floor: the toolchain, and the build systems every recipe compiles with. Some repository in the chain has to publish this set:

error: build: no repo in the chain publishes +build, the build floor every recipe is staged onto; configure one that does

+build-probe
The extra tools the winning probe needs on top of the floor, when the chain publishes such a set (+build-rustcrate, +build-pep517). A probe with no set of its own needs nothing past the floor.
[build_deps]
The recipe's needed and host_only entries.
--with
Extra packages, for this run only.
elfutils
Always, for eu-strip.

That whole set is closed over runtime requirements, so a tool's own libraries and interpreters come along. Everything the chain can't supply is reported in one go, and the build doesn't start:

error: build: the chain cannot supply the buildroot: zstd/staticdev: zstd has no subpackage staticdev ghost: no repo in the chain pins ghost

The buildroot also has to contain a /bin/sh, since every build phase starts with one. A floor missing the filesystem skeleton or a shell is refused by name, instead of failing later with a bare ENOENT.

pkgconfig: build dependencies (the -sys crate case) aren't supported yet, and are refused by name.

Who signed the buildroot?

Every buildroot manifest has to be signed by a key you trust. That's any key that signed a package already installed on this machine, your own build key, and any you give with --trust-key. So a package an earlier pkg build published into a local repository can be a dependency of the next build without being installed first. Any of these keys is trusted for any package name: a local build is meant to chain local builds, bundles and path repositories, so pkg build does not scope a key to the names it signed the way pkg offline upgrade does. On a machine with nothing installed from the chain you build against, pass its key with --trust-key.

Which subpackages does a name bring in?

Not the same ones pkg-install(1) would. A bare name here doesn't follow the runtime content rule of pkg-subpkgs(7). It follows the rules the build farm stages its buildroots by, so a recipe builds the same way in both places:

+build and +build-<probe> members
Host content: bin, data, lib and main. Never dev. The floor and the probe tools get run, not compiled against.
[build_deps] needed, host_only and --with
Everything that matters to a build. dev and staticdev are in. Only debug, debugsrc, source, mod, man, info, doc and lang are left out.
name/subpkg
Exactly that subpackage, anywhere in the list above. That's how the floor asks for glibc/dev, while the rest of glibc stays at host content.

--with only takes those two forms. +set and name=version aren't accepted there.

THE OUTPUT REPOSITORY

--out is an ordinary signed repository; see pkg-repository-layout(7). The first build creates it, and every build after that grows it. Each build moves the repository's pointer for its package to the version it just built. So the repository's current set is the latest build of everything ever built into it. Older builds stay on disk as history, and nothing resolves to them.

Building the same N-V-R twice

A second build of an N-V-R the repository already holds is accepted when it reproduces the same bytes. When it doesn't, it's refused, just like a published repository would refuse it:

error: build: ...; bump [recipe].release, or pass --replace to retract the build already in /home/u/work/out

While you're iterating on a recipe, --replace retracts the old build and publishes the new one in its place. Bump release once the result goes to anybody else.

USING THE OUTPUT

Bundling the output repository takes one command; see Rolling a .pkgtar, below. Installing or composing from it means putting it in the chain, above the repositories it was built against. A drop-in in /etc/pkg/repos.d does that (see pkg-repos.d(5)):

name = "local" path = "/home/u/work/out" priority = 30 enabled = true consumes = ["Sandcastle-Common"]

A local path repository takes no trust anchor. From then on pkg-install(1), pkg-compose(1) and pkg-bundle(1) resolve through it like any other repository. Its own packages are read from the directory, and their dependencies are fetched from the rest of the chain.

Rolling a .pkgtar

pkg bundle create --from-repo-dir /home/u/work/out \ --output mywork.pkgtar

--from-repo-dir puts the output repository on top of the configured chain for that one command, and reads its metadata/ for the roots. So bundling needs no drop-in. The bundle's own TUF metadata is signed with the build key and lists everything the bundle carries. That's what lets the dependencies travel with it, visibly.

With no --closure, the bundle holds everything the output repository currently ships, every package built into it and every subpackage, and nothing else. The repository directory is the unit. If you want something left out of a bundle, build it into a different --out. Pass --full-closure to also carry what those packages need at runtime, for a machine that has none of the distribution to resolve against; see pkg-bundle(1).

To bundle only part of it, name the roots with --closure, in the spec grammar of pkg(1):

name
The package's runtime content: every subpackage except debug, debugsrc, source, man, doc, info, lang, dev, staticdev and server. --closure which bundles which/bin. This is the install rule, not the buildroot rule above.
name/subpkg
Exactly that subpackage, as in --closure which/man. The name is split at the first /, so coreutils/lang/af is coreutils' subpackage lang/af.
+set
Every member of a set published in the chain.

A name is resolved by chain position: the first repository in the chain that carries it supplies it. That's why the output repository has to sit above the repositories it was built against, and why --from-repo-dir puts it on top. name=version is refused, because a bundle is built from the chain's current set. No spec takes an @repo suffix either.

Without --closure, every package the output repository lists has to resolve through the chain to exactly the build it lists. The same bytes, not just the same version. If the repository isn't in the chain, or a higher-priority repository carries the same name, the bundle is refused and the packages are named. Silently carrying some other build would be worse.

OPTIONS

recipe-dir
Required. The recipe directory to build.
--out DIR
Required. The local repository to publish into. Created on first use.
--with SPEC
Can be repeated. An extra package for the buildroot, name or name/subpkg, staged the way a [build_deps] entry would be. It's for trying out a build dependency before you declare it. A recipe that needs it should say so.
--trust-key HEX
Can be repeated. An Ed25519 public key trusted to sign buildroot manifests, on top of the keys that signed this machine's installed packages and your own build key. You need it on a machine that has nothing installed from the chain it builds against.
--work DIR

Scratch directory for the sysroot, the source cache and the build tree. Defaults to $XDG_CACHE_HOME/pkg/build/<name>-<version>-<release>, or ~/.cache/pkg/build/... without it. As root, it's /var/cache/pkg/build/..., whatever the environment says. A sysroot runs to hundreds of megabytes, which is why the default isn't /tmp.

The tree is removed after a successful build unless you pass --keep. So a directory you name here has to be new, empty, or one pkg made itself (it carries a .pkg-work marker). Anything else is refused before it's touched.

--keep
Keep the scratch directory after a successful build. A failed build always keeps it, and tells you where.
--replace
Retract a build of the same N-V-R from --out before publishing, instead of refusing the new one.

All global options apply; see pkg(1). -v reports each phase with its counts. The build log itself always goes to stderr.

OUTPUT

published 5 subpackage(s) into /home/u/work/out which/bin 2.25-9.x86_64 which/debug 2.25-9.x86_64 which/debugsrc 2.25-9.noarch which/info 2.25-9.noarch which/man 2.25-9.noarch trust anchor /home/u/work/out/metadata/1.root.json (key 63d3...cdff)

JSON

{ "ok": true, "out": "/home/u/work/out", "root_json": "/home/u/work/out/metadata/1.root.json", "public_key": "63d3...cdff", "packages": [ { "name": "which", "subpkg": "bin", "version": "2.25", "release": 9, "arch": "x86_64", "payload_sha256": "d830...d6c5" } ] }

EXAMPLES

A first build

pkg keygen pkg build ./which --out ~/work/out

Iterating on a recipe

$EDITOR ./which/recipe.toml pkg build ./which --out ~/work/out --replace

Trying a build dependency before declaring it

pkg build ./foo --out ~/work/out --with zlib --with xz/dev

Looking inside a build

pkg -v build ./which --out ~/work/out --keep ls ~/.cache/pkg/build/which-2.25-9/sysroot

Installing what you built

With the output repository in the chain (see USING THE OUTPUT):

pkg install which pkg query which

Handing it to someone

pkg bundle create --from-repo-dir ~/work/out --output which.pkgtar

Just the binary and what it needs:

pkg bundle create --from-repo-dir ~/work/out \ --output which-bin.pkgtar --closure which

EXIT STATUS

0 when every subpackage was published. 1 on any failure: no key, a recipe that doesn't load, no probe that applies, a buildroot the chain can't supply, a failed build phase, or a refused publish.

ENVIRONMENT

XDG_CONFIG_HOME
Where the build key lives. Falls back to ~/.config.
XDG_CACHE_HOME
Base for the scratch directory. Falls back to ~/.cache. Ignored when running as root.
UR_SYSUSERS_BIN
The systemd-sysusers binary to run against the sysroot. The build host needs it, and unshare(1) from util-linux as well.

FILES

$XDG_CONFIG_HOME/pkg/build.key
The build key. See pkg-keygen(1).
$XDG_CACHE_HOME/pkg/build/
Default scratch space, one directory per N-V-R. /var/cache/pkg/build/ for root.
<out>/metadata/1.root.json
The output repository's trust anchor.

SEE ALSO

pkg(1), pkg-keygen(1), pkg-bundle(1), pkg-install(1), pkg-provenance(1), pkg-repo(1), pkg-repos.d(5), pkg-chain(7), pkg-sets(7), pkg-subpkgs(7), pkg-repository-layout(7)

docs/components/pkg-cli.md, docs/components/pkg-build.md, docs/design/buildroot.md, docs/design/recipes.md