deb: build the rules as an unprivileged user in pkh chroots, the same way Launchpad's buildds and sbuild do - setpriv drops to an on-demand pkh-build user owning the staged tree, the scratch HOME and the ccache; dependency installation and quilt stay root. Validated by the e2e canary: coreutils (which refuses a root configure) now passes. * rootless-builds: test: drop the coreutils xfail, rootless builds clear it deb: run the user probes through sh with stderr dropped deb: build the rules as an unprivileged user in pkh chroots
pkh end-to-end package harness
Exercises pkh the way it is used in anger: real packages, the whole
workflow. For every package in catalog.yml it runs
pkh pull <name> --archive # sources from the Debian/Ubuntu archive
pkh chlog --rebuild # no-change rebuild entry (1.0-1 -> 1.0-1build1)
pkh build # .dsc + _source.changes next to the tree
pkh deb # binary packages in an ephemeral chroot
The catalog spans ~60 source packages: smoke-test smalls (hello,
micro, jq...), core utilities, server software, databases, desktop
stacks, toolchains and the weird corners — dracut, opensbi, grub2,
a cross u-boot, the kbuild-based kbuild, and (opt-in, hours each)
gcc-14, glibc, rustc, libreoffice, firefox, the Ubuntu linux
kernel...
Prerequisites
- A pkh binary: the harness prefers the repo build
(
target/release/pkh, thentarget/debug/pkh), falls back topkhon PATH; override with--pkh PATHor build first with--build-pkh. - The pkh runtime toolchain:
mmdebstrap,sudo(passwordless, for the unshare chroot builds),dpkg-dev,distro-infodata, and network access to the Debian and Ubuntu archives. - Python 3 only (stdlib; PyYAML is used when installed, with a built-in fallback parser for the catalog's YAML subset).
- Disk: tens of GB under
runs/for trees and artifacts, plus pkh's own caches (/var/tmp/pkh); expect a full default-tier run to take many hours — the tiers exist so you never have to.
Usage
./run.py --list # what would run, with tiers and notes
./run.py --dry-run # the exact commands, nothing executed
./run.py # smoke + standard tiers
./run.py --tier long # the multi-hour builds
./run.py --tier all # everything
./run.py --only hello,dracut # exact names
./run.py --category database # or server, desktop, boot-firmware...
./run.py --tag cross --arch arm64 # tag/arch sweeps
./run.py --stages pull # catalog sanity: can we pull everything?
./run.py --resume runs/<id>/results.json # continue an interrupted run
Useful global pass-throughs: --dist, --series, --arch, --cross,
--pocket, --ppa user/ppa (deb dependencies), --jobs N,
--no-ccache, --git (pull through the VCS instead of the archive).
Exit codes: 0 nothing unexpected failed, 1 at least one package
failed (or xpassed under --strict), 2 usage/catalog error, 130
interrupted.
Selection: tiers, categories, tags
Entries carry a tier: smoke (minutes), standard (the default,
up to ~1-2h each), long (hours: compilers, browsers, the kernel —
only run when asked). Default selection is smoke + standard. On top of
that, filters compose: --category, --tag, --only, --skip.
An entry can be marked xfail: <reason> when it is known-broken; it
then reports xfail on failure and xpass on unexpected success
(xpass fails the run only under --strict).
What a run produces
runs/<id>/
logs/<pkg>.log # full output of every stage of that package
<pkg>/<pkg>/ # the pulled source tree
<pkg>/*.dsc ... # build artifacts, next to the tree as pkh puts them
results.json # machine-readable, written progressively
results.md # the summary table
The run directory is per invocation; --resume <id>/results.json
reuses it, keeps the recorded outcomes and only reruns packages that
did not finish. Already-pulled trees are kept (pkh pull refuses
existing directories anyway — delete runs/<id>/<pkg>/ to force a
re-pull), and stages that already succeeded are reused rather than
re-run, so a resumed chlog does not bump the version twice; change
--dist/--series/--arch/--cross and everything re-runs.
Pulls run a few at a time (--pull-jobs), the build pipeline is
sequential by design: one heavy build at a time keeps pkh's shared
chroot tarball, apt and ccache caches warm, so repeat runs get
dramatically faster.
Each stage gets the package's timeout budget (tier default, overridable
per entry with timeout:); on timeout the whole build process group is
killed and the package is marked timeout.
Adding packages
Edit catalog.yml. Fields are documented at the top of the file;
keep source names as they exist in the archive — not binary names
(make-dfsg and rust-ripgrep, not make and ripgrep;
xz-utils, not xz; postgresql-17, not postgresql). The file
stays within a small YAML subset so the stdlib fallback parser can read
it: flat mappings, inline lists, no colons in values.