Commit Graph
346 Commits
Author SHA1 Message Date
vhaudiquet 69bb29a291 pull: ignore the pax global header when detecting a tarball wrapper
Git-archive tarballs open with a pax global header record
('pax_global_header'): GNU tar consumes it and never lists or
extracts it, but the tar crate yields it as an entry. Counting it
for the wrapper detection made every git-archive orig look
multi-rooted - linux-riscv's orig stayed nested under 'linux-6.8/'
and its .diff.gz failed to apply with 'can't find file to patch'.

Skip the record outright: unpack would no-op on it anyway, so this
only keeps it out of the wrapper detection (and the tree).
2026-10-02 01:18:48 +02:00
vhaudiquet 3227c0abb9 pull: unwrap multi-orig components into their addon directory
dpkg-source assembles a multi-orig source by unpacking the main orig
first and the extra components over it, each component's content at
'<root>/<addon>' ('pigeonhole' for 'pkg_1.0.orig-pigeonhole.tar.gz'),
so component files win where they overlap the main tree: process the
archives in that order, and unwrap each archive's wrapper into where
dpkg-source expects it - the root for the main orig, '<root>/<addon>'
for a component, replacing any pre-existing addon directory.

Recognizing the wrapper per archive (the one first component every
entry of THIS archive shares) rather than from the shape of the tree
root keeps every component of a multi-orig package unwrapping after
the first one filled the root: dovecot's pigeonhole sources used to
be left nested one level too deep, and dpkg-source -b then refused
the tree over unexpected upstream changes.
2026-10-02 01:18:22 +02:00
vhaudiquet b143fa661d docs: document the e2e package harness 2026-10-01 22:24:08 +02:00
vhaudiquet 87f5b31602 test: add the e2e package harness
Exercises pkh the way it is used in anger: 63 real source packages
(from hello and micro to dracut, grub2 and the kernel) run through
pull -> chlog --rebuild -> build -> deb, each stage timed, logged and
checked for its artifacts, with the catalog alternating between debian
and ubuntu within every category.

run.py keeps runs self-contained (per-package logs, progressive
results.json, a markdown summary), pulls in parallel while builds run
sequentially to keep pkh's shared chroot/apt/ccache caches warm,
resumes interrupted runs reusing the stages that already succeeded,
and marks known-broken entries as xfail with a reason.

The first catalog run caught four pkh defects within an hour - the
unstable/sid suite alias, the bz2 origs, the hard link extraction and
the wrapper unwrapping - which is exactly what it is for.
2026-10-01 22:24:07 +02:00
vhaudiquet a4be046aaa deb: export the dpkg architecture environment for the rules
The rules run outside dpkg-buildpackage, which exports none of the
dpkg architecture variables anyway - only sbuild does. Packages using
$(DEB_HOST_MULTIARCH) or $(DEB_BUILD_GNU_TYPE) without including
/usr/share/dpkg/architecture.mk expanded them empty: micro's rules
cd'd into 'obj-' and file configured --libdir=/usr/lib, putting its
libraries where dh_install's multiarch glob could not find them.

Run 'dpkg-architecture -s' in the build context and export the whole
DEB_* set, sbuild-style, for native builds; cross builds already carry
the target's set from cross::setup_environment, whose output parser
this reuses.
2026-10-01 22:23:55 +02:00
vhaudiquet ca0782488f deb: resolve suite aliases in the target series
The changelog (and -s) may carry a suite name instead of the series
codename: Debian conventionally writes 'unstable' where the series
data carries 'sid'. The name was used verbatim downstream, so every
NCR rebuild of a Debian package keyed its chroot tarball cache on
'unstable-buildd' (missing the cached sid tarball), tried to bootstrap
a tarball nobody waits for, and finally died on 'Unknown series:
unstable' in the distro-info lookup - with each queued build waiting
out the 300s stale-lock grace on the way.

Resolve the alias right where the series is picked, so the codename
identifies the target everywhere downstream; the codename is also what
the caches and distro-info data key on.
2026-10-01 22:23:55 +02:00
vhaudiquet 913ef2027a pull: fix archive extraction and tree assembly
Three archive-side defects broke pulled trees, each surfacing as a
later build failure rather than at pull time:

- '.tar.bz2'/'.tbz2' origs (busybox) were downloaded but never
  extracted: the dispatch only knew gz and xz. dpkg-source -b then
  packed the missing tree as a mass of ignored deletions and the
  chroot's quilt run failed on every patch. Extract bz2 through
  bzip2-rs, and match every '.tar.*' of the source package so an
  unsupported compression fails loudly instead of silently skipping
  the extraction.
- Hard link entries (jq) aborted the whole extraction: resolve their
  targets against the archive root, where tar guarantees the shared
  entry was already extracted, with the same confinement checks as
  regular entries.
- The 'package-version/' wrapper was detected from explicit tar
  directory entries (tree has none, so its sources stayed nested one
  level too deep) and relocated through a copy that flattened
  top-level symlinks (htop's 'README -> README.md' became a plain
  file, which dpkg-source refuses to represent). Recognize the
  wrapper by the shape of the extraction result - exactly one real
  directory - and unwrap it by rename, preserving links and
  timestamps.
2026-10-01 22:23:43 +02:00
vhaudiquet 2d57b1f0bc deps: add bzip2-rs for bz2 orig tarballs
Decode-only pure-Rust bzip2, matching flate2's pure-Rust default: no
new system packages for building pkh, and the archive-side origin of
the need is the busybox class of '.tar.bz2' upstream tarballs.
2026-10-01 22:23:34 +02:00
vhaudiquet 8ab69991ce fmt: apply rustfmt
CI / build (push) Successful in 6m25s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 12m9s
2026-10-01 16:41:28 +02:00
vhaudiquet ed76b3ef7f deb,build: export ddebs from binary builds
CI / build (push) Failing after 54s
CI / test (push) Skipped
CI / snap (push) Skipped
CI / publish (push) Skipped
dh_strip registers debug-symbol packages in debian/files under their
renamed .ddeb name; they are deliverables of the build just like the
regular .debs. Both artifact-type filters, the one collecting the
build's artifacts and the one feeding the .buildinfo/.changes
checksum sections, accepted only deb and udeb, so every ddeb a build
produced was silently discarded: nothing exported next to the .debs,
nothing in the upload metadata.

The filter is now a shared FilesEntry::is_binary_artifact() predicate
(plus unit tests) used by both call sites, so the two cannot drift
apart again.

One dpkg distinction has to be copied rather than shared:
dpkg-genchanges lists only deb/udeb packages in the changes' Binary:
and Description (a ddeb travels in Files and the checksum sections
but is not a listed binary of the upload), while dpkg-genbuildinfo
lists every binary artifact's package, ddebs included. The package
list is therefore accumulated twice, once per consumer.

The binary-metadata differential fixture gains a Package-Type: ddeb
package, so the native .changes/.buildinfo generation is compared
against real dpkg with a ddeb present. Without the fix the comparison
fails on the checksum sections (golden distributes the ddeb, ours
dropped it); with it every field matches.
2026-09-30 14:18:45 +02:00
vhaudiquet 72992f6ec8 deb: create the chroot's device nodes as char devices
The buildd tarballs carry their /dev entries as char-device members,
but the tar crate cannot create special files: unrecognized entry
types are written out as regular ones, so every device entry in the
chroot extracts to an empty regular file. The old readiness guard
only checked that /dev/null existed, which a regular file satisfies,
so the rm + mknod repair never ran.

Redirects into a regular /dev/null still succeed, so the build
proceeds until something reads the node back. The first reader in
practice is dh_strip's dbgsym path, where dpkg-gencontrol gets
-T/dev/null (dbgsym packages have no substvars file) and aborts on
"bad line in substvars file /dev/null at line 1". Packages without
debug symbols never reach that call, which is why earlier builds
looked fine.

The guard now requires every node to be a char device, and the node
set covers all entries the tarballs ship (console, full, null, ptmx,
random, tty, urandom, zero), not just null and zero: a regular-file
/dev/urandom or /dev/console is a silent hazard for anything reading
it later in the build.

Regression tests pin both shapes: the regular files extraction
leaves behind must not count as ready, and real char devices (via
symlinks to the host's nodes, so the test runs unprivileged) must.
2026-09-30 13:47:39 +02:00
vhaudiquet d946af8bb6 deb,cli,test,docs: record and show dep-install and build durations
Every binary build measures its dependency installation (apt-get
update, the essential and injected packages, each build-dependency
pass) and its package build (debian/rules build plus binary) into a
timings accumulator; the record manifest stores both as an optional
`timings` field when the run ends, whatever the outcome — a failed
build keeps its partial durations, and a run that died before any
timed command records nothing.

`pkh deb list` shows the two durations as DEPS and BUILD columns,
formatted like the age ("45s", "2m41s", "1h02m"); records predating
the field show dashes. The field is serde-optional, so the manifest
schema stays at 1 and existing records remain discoverable and
forkable.
2026-09-30 12:45:13 +02:00
vhaudiquet 6385cf6dc1 deb,cli,test: add --mirror to build and resolve from a pinned mirror
Pinned runners (a caching proxy, a chosen mirror) must resolve every
package from one base URL. 'pkh deb --mirror <url>' passes the mirror
to mmdebstrap as its mirror positional, so the chroot tarball's apt
sources carry it from the bootstrap on and every apt inside the build
follows - no per-build source rewriting. The mirror must serve every
suite and architecture the build resolves (the security pocket and,
for cross builds, the target archive included); that contract is
documented, not enforced.

The tarball cache is keyed per mirror (a short sha prefix before the
'-buildd.tar.xz' suffix prune matches): a --mirror run over a stale
distro-default tarball would silently resolve from the distro, and
'--incremental' records must never fork across a mirror change.

The runtime source passes recognize the pinned URL as an archive
source (it carries no official host for the host-based checks): the
pocket pass and the universe pass treat it as the archive, and the
cross setup gives it every component and suite, no arch scoping (the
mirror serves every architecture by contract) and no ports entry.
'pkh test' passes no mirror: its tarballs stay distro-default.
2026-09-30 00:04:50 +02:00
vhaudiquet 8cf9489072 logfmt: teach the make classifier kbuild status tags and warnings
Kernel builds abbreviate every invocation to an uppercase status tag
(CC, LD, HOSTCC, MODPOST, ...) instead of printing the full command
line, so the substring rules that catch classic make output never
fired and the rolling pane went static for the whole kbuild stream.
Recognize the common tags after leading whitespace and show those
lines.

Also surface warnings instead of hiding them: gcc's lowercase
'file.c:12:3: warning:' diagnostics and kbuild's line-leading
'WARNING:' banners (modpost, etc.) now map to Action::Warning.
2026-09-29 16:35:30 +02:00
vhaudiquet 2de191cf6d deb: print raw build output again with --verbose
The record-log wrapper provides a sink for every command, which removed
the runner's subprocess-inherit fallback -- and with it the raw output
of --verbose runs (and of piped stdout, where the live view disables
itself): Quiet's None sink got wrapped inside, every line went to
build.log, and the terminal showed nothing but pkh's own logs, breaking
what --help still promises. When the caller's view has no sink of its
own, a raw terminal sink (stdout/stderr writers, one per stream) now
takes its place inside the record-log wrapper: the terminal shows the
raw lines again while build.log keeps the full tee. A view that
consumes lines itself (the live view on a tty) is wrapped exactly as
before.
2026-09-29 15:19:17 +02:00
vhaudiquet 562626e878 deb: report a kept record honestly when no staged tree exists
Every --incremental run claimed "kept its staged tree" and set
kept_upper=true at teardown — even when the run died before staging
created tree-upper, leaving a manifest that lied about the record being
a fork candidate and a message contradicting the disk. The retention
now follows the disk through upper_retention(requested, has_upper()):
a keep with a staged tree keeps it as before, a plain run drops it, and
a keep with nothing staged keeps the record as metadata + log with
kept_upper=false and says so ("no staged tree was created — the run
ended before staging"). The interrupt path records the same honest
state — failed outcome, kept_upper only if a tree exists — and the
SIGINT hook's message no longer claims a tree that is not there.
2026-09-29 15:05:08 +02:00
vhaudiquet ab0c65dc37 deb: skip unreadable paths while snapshotting the staged parent
The snapshot walk runs over the whole staged PARENT, which contains
trees unrelated to the build; one unreadable path anywhere under it —
a root-owned sysroot left there by the user's own setup script — made
fs::read_dir or symlink_metadata fail and aborted the whole build
seven seconds in, before any staging could happen. The walk is now
best-effort below its root: unreadable directories and entries lost to
a mid-walk race are warned about and skipped, and only the root itself
being unreadable propagates (the package tree being invisible is a
real error). Absent entries are already the handled case for the
whiteout sync: they are simply untracked.
2026-09-29 14:57:39 +02:00
vhaudiquet 6a8c301705 deb: integrate ccache for content-keyed compile reuse
Fresh chroots also mean fresh compiles: nothing survived to make the
recompile-heavy middle of a run cheap. Every deb build now wires a
shared ccache — /var/tmp/pkh/ccache/<slug>/, bind-mounted at the
chroot's /var/cache/pkh-ccache — through a PATH shim rather than CC
environment variables: gcc/g++/cc/c++ symlinks in /usr/lib/pkh-ccache-
shim resolve to the ccache the essentials install, so rules that call
the plain compiler names are cached while explicit exotic compilers
stay untouched. CCACHE_COMPILERCHECK=content is critical — the default
is mtime-based and would never hit across fresh chroots, where every
toolchain's mtime is the extraction's — with CCACHE_BASEDIR=/tmp/pkh-
build and CCACHE_NOHASHDIR=true keying the staged tree's relative
paths, and CCACHE_MAXSIZE=10G (PKH_CCACHE_MAXSIZE overrides).

No mtime clamping anywhere: the tarball extraction preserves mtimes,
so the toolchain and libc headers stay older than carried objects and
make keeps them, while build-dependency reinstalls are newer and make
rebuilds their dependents — ccache turns those into hits, and a real
archive library change is a miss that rebuilds them, exactly the
signal clamping would silence. `pkh deb --no-ccache` skips the shim,
the env and the mount entirely: an escape hatch for rules that
mishandle compiler wrappers (kbuild-style flavour configs).
2026-09-29 14:39:54 +02:00
vhaudiquet 0e1e37b2e6 deb: share the apt download cache across builds
With the chroot now per-run scratch, every build re-downloaded the
packages its dependency install needed — the very cost the chroot reuse
used to hide. Every chroot's /var/cache/apt/archives is now a bind
mount over a shared cache at /var/tmp/pkh/apt-cache/<series>-<arch>/
(the chroot's architecture, like the tarball cache), which one run's
installs fill and the next runs reuse: apt keeps downloaded packages
through a dropped conf (APT::Keep-Downloaded-Packages), and an
apt-get autoclean at the end of each run drops the debs the archive no
longer serves. pkh prune removes caches untouched for a week.

The run holds a host-side flock on the cache for its whole lifetime:
apt's own archives lock is acquired inside the chroot's pid namespace,
where a concurrent build's apt shows the holder as "process 0" — an
invisible process it refuses to wait for — so pkh serializes the
traffic itself; concurrent builds of the same target wait for each
other. Any bind-mount failure falls back to the chroot's own cache
with a warning (correct, just slower), and the temp-chroot teardown now
unmounts everything under the chroot before rm -rf, so the removal can
never reach through the cache bind mount and delete the shared cache
itself.
2026-09-29 14:22:49 +02:00
vhaudiquet dd2634abe3 deb: shrink records to upper, journal and log; keep metadata for every build
Records carried a full chroot each, and space turned out to be the hard
constraint: five records per target multiplied by a multi-GB chroot does
not fit. The chroot is now per-run scratch — every build extracts a
fresh one from the sha-verified mmdebstrap tarball cache into its
record's chroot/ dir and removes it at the end (success, failure and
interrupt included); resume_chroot, the .pkh-ready probe and
chroot_is_usable are gone, and crashed-run leftovers are swept when the
next run of that identity starts.

What a record keeps instead: manifest.json (renamed, now with the
tarball sha it used, a kept_upper flag and a tree-only journal — the
environment phases rerun in every fresh chroot), a log/build.log full
tee of the build output (kept for EVERY build; plain runs become
metadata-only records, so keep-iff-incremental now applies to the
staged tree alone), and — for --incremental runs — tree-upper/ with its
host-files.list. The fork gate carries a base's staged tree only while
its recorded tarball sha still matches the cache and the changelog
version is unchanged; a fork candidate is any record with a staged tree.

The keep-last-5 cap now applies to staged trees: a sixth retention
degrades the oldest record to metadata-only instead of deleting it.
New `pkh deb log [-f] [<id>]` prints (or follows) a record's build log,
and `pkh deb list` shows the staged-tree state and plain-run records
too. README rewritten around the shrunk model.
2026-09-29 14:00:34 +02:00
vhaudiquet 81f0d52f85 deb: rework build sessions into immutable per-build records
The single mutable session per identity loses the last-good state: a
failed run after a good one adopts the session in place and leaves only
the failed state behind, so "continue from the last success" was
impossible. Each build now records an immutable record under
/var/tmp/pkh/sessions/<slug>/<id>/ (chroot, tree-upper, journal,
manifest), and --incremental forks a NEW record — a full copy of the
base's chroot and, at the same changelog version, its staged artifacts
— under the base's flock, never mutating the base itself.

Selection picks the newest successful record of the identity among the
last five usable, falling back to the newest when none succeeded, and
--incremental <id> pins an exact record (failed ones allowed: their
environment may be fine). The lifecycle is strict keep-iff-incremental:
plain runs build in a throwaway record discarded on success, failure
and interrupt alike (the SIGINT cleanup hook follows the same policy),
while --incremental records are always kept. Each identity keeps its
newest five records; pkh prune's 7-day sweep and orphaned-lock cleanup
follow the nested layout.

--resume and --keep are removed (no aliases): their adoption-in-place
and keep-on-success semantics are exactly what the record model
replaces. pkh deb list shows id, outcome, tree version, fork lineage
and age. README rewritten around the record workflow.
2026-09-29 12:56:29 +02:00
vhaudiquet d65d3264f4 deb: add opt-in --incremental flag for single-flag iterate loops
Iterating on a package currently needs two flags (--resume --keep):
without --keep a successful build consumes the session, and without
--resume nothing is reused, so incremental iteration never "just
works" opt-in.

--incremental is the single-flag form: with no --resume it adopts the
newest usable session of this tree (like `pkh deb --resume`), and it
always keeps the session on success, so the loop (edit, `pkh deb
--incremental`, edit, ...) reuses the chroot, build deps and objects
and only recompiles what changed. With an explicit `--resume <id>` it
only adds keep.

A plain `pkh deb` is unchanged: fresh session, consumed on success.
2026-09-29 12:17:05 +02:00
vhaudiquet a3f6b97335 clippy: elide the needless lifetimes of series_patches_to_apply
CI lints with -Dwarnings; the explicit 'a lifetime on
series_patches_to_apply triggers needless_lifetimes and would fail
the gate.
2026-09-29 12:12:38 +02:00
vhaudiquet 6bb31ea533 chlog: add -f/--file to target a specific changelog
The changelog path was hardcoded to debian/changelog in the series
detection, the entry generation and the editor invocation. Packages
keeping the changelog elsewhere (e.g. a staging copy) could not use
chlog on it.

Add -f/--file defaulting to debian/changelog, threaded through all
three uses. The library side was already parameterized on the file,
so no changelog.rs behavior changes beyond a test pinning the
non-default path.
2026-09-29 12:03:49 +02:00
vhaudiquet babddad505 chlog: cap the no-tag history search with --limit
Without a version tag, the boundary of a new entry was found by walking
the whole history and diffing each commit's tree against the changelog
path. On large repositories that per-commit tree diff takes 10+ minutes.

Add --limit (default 500, 0 = unlimited) capping that walk: when no
changelog commit is found within the last N commits, the oldest visited
commit becomes the boundary — the version tag probe is skipped there,
since it could jump the boundary arbitrarily far back and defeat the
cap — and a warning explains the fallback. A found version tag still
takes precedence and is unaffected by the limit.
2026-09-29 12:02:43 +02:00
vhaudiquet e7cad0b1ca deb: apply real patches in single-debian-patch trees
The f055b70 skip assumed a single-debian-patch tree carries all patch
content ambient in the working tree. A tree that declares
single-debian-patch while listing real quilt patches in
debian/patches/series (e.g. a pkh-scaffolded package that grew patches,
like lite-xl) was built from unpatched sources: the binary silently
lost the patch content, while plain dpkg-buildpackage - and Launchpad,
which never sees debian/source/local-options - applies the series.

Mirror dpkg-source --before-build semantics: apply the series minus the
ambient 'debian-changes[-<version>]' entries, dropping those entries
from the staged series so 'quilt push -a' tolerates the ambient content.
A series holding only the ambient entry keeps skipping, as before
(covered by apply_phase_skips_single_debian_patch_trees); the
classification core is unit-tested in
series_patches_to_apply_classification.
2026-09-28 14:04:22 +02:00
vhaudiquet d927dc93ab deb: survive crashed sessions and config-blind resumes
CI / build (push) Successful in 3m9s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m46s
Two failures from resumed kernel sessions:

A resumed build re-runs debian/rules build, but the kernels keep
their wrapper-step stamps in debian/stamps and the flavour config
rule there exports .config from the annotations with no config
prerequisite: the resumed build silently kept the previous
attempt's configuration and a config edit never reached the .deb.
Dropping the stamp cache unconditionally would impose the kernel
packaging's shape on every package, so it is data-driven instead:
packages declare the caches under a resume_clear quirk, and
resumed builds remove those tree-relative paths before the build.
The inner kbuild keeps its own incremental state, so only the
cheap wrapper passes re-run and config-affected objects recompile.

A SIGKILLed build (OOM) leaves its overlay mounts and /proc bind
mount behind, and overlayfs creates root-owned work state inside
the workdir: the next plain build (recording is always on, it
replaces the session of its identity) failed to clear the
leftovers with a permission error. Session clearing now unmounts
everything under each entry at depth (re-reading /proc/mounts,
tolerating mount stacks from consecutive crashes) and escalates
through sudo; when the tree still cannot be cleared, the build
reports it and continues without a session instead of layering
over a half-cleared one.
2026-09-28 00:05:14 +02:00
vhaudiquet 6bb8ab5016 deb,prune: clean up orphaned session lock files
Session locks live next to the session root, and failed or
interrupted runs left them behind after the root was gone (seven
stray locks on a used machine). Session removal now unlinks its own
lock file, and prune collects the orphans: by default only the ones
no live build holds (probed with a non-blocking flock), with --all
every one of them. Locks guarding an existing session root are
never touched.
2026-09-27 23:32:21 +02:00
vhaudiquet 961f6bc234 deb: wipe the staged tree when overlay staging falls back to a copy
On a resumed session whose overlay mount fails (no overlayfs, mount
error, non-overlay driver), the fallback used to copy the host tree
into the staged tree of the previous attempt: a merge that keeps the
old build stamps and resurrects files deleted on the host — a
silently half-stale build, exactly what resume must never produce.
The driver now reports the failure instead of copying, and the
caller wipes the staged build root and re-stages a fresh copy
(environment resumable, build artifacts discarded).
2026-09-27 23:32:16 +02:00
vhaudiquet 318bcea341 docs: document pkh test
CI / build (push) Successful in 3m6s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m22s
Add the DEP-8 runner to the command list and a usage section: default
chroot testbed sharing the deb tarball cache, binary reuse from pkh deb
with in-process rebuilds, the local and vm modes, and the 0/1/2 exit
contract. autopkgtest joins the shell-out list (vm mode) and the
roadmap bullet comes off — the feature exists.
2026-09-27 21:33:11 +02:00
vhaudiquet 8513805f98 test: point at root in the local-mode apt failure
A local run fails apt-get update exactly like a local build does when
the user is not root; say so in the error instead of leaving a bare
failure (the chroot modes bootstrap their own root environment).
2026-09-27 21:33:11 +02:00
vhaudiquet d35ffefacb test: add the differential harness against autopkgtest
The native runner's trust anchor: on a tree of the caller's choosing
(PKH_TEST_DIFF_TREE, --ignored, deliberate: both runs land on the host
testbed), the harness runs pkh's local mode and the installed
autopkgtest with its null runner over the same tree and binaries, then
fails on any per-test verdict disagreement — pinning the semantics the
DEP-8 documents under-specify (verdict classes, argid names, edge-case
expansions). The comparison itself is pure and always tested.
2026-09-27 21:33:11 +02:00
vhaudiquet 7790b9452d test: add --mode vm, the autopkgtest escape hatch
VM virtualization is the one backend pkh does not reimplement: the qemu
runner carries the serial-console protocol, image overlay management
and reboot support, and it is the only one satisfying
isolation-machine/needs-reboot tests. pkh execs the installed
autopkgtest with its qemu runner (--image required, --cpus/--ram-size
passthrough) in its own process group so Ctrl+C reaches the testbed
cleanup, parses the --summary file back into the pkh report model and
maps autopkgtest's exit classes onto the 0/1/2 contract (12/14/16/20
are runtime errors; the raw code rides the JSON report). Pockets, PPAs
and injection are refused in vm mode for now: they would need the
keyring setup inside the image.
2026-09-27 21:33:11 +02:00
vhaudiquet 15cc19007f prune: garbage-collect pkh test run directories
Test artifacts land under /var/tmp/pkh/tests (<source>-<series>-<arch>-
<timestamp>/), one directory per run: they are disposable results, not
state worth keeping. Prune removes them on the same 7-day retention as
build sessions, and with --all; the root is overridable through
PKH_TESTS_DIR like the sessions root.
2026-09-27 21:08:22 +02:00
vhaudiquet 2a9287d942 test,cli: wire pkh test into the CLI
Argument wiring only, per the report ports: the live view carries
phases and the outcome, the per-test report renders after the run
(text or --json) and decides the exit code; runtime errors exit 2.
--list-tests prints the parsed debian/tests/control and exits without
booting a testbed.
2026-09-27 21:04:03 +02:00
vhaudiquet 299b5c28c3 test: add pkh test, the native DEP-8 runner for local and chroot
The DEP-8 run-time contract is one page, so pkh implements it natively
instead of wrapping autopkgtest (see plans/pkh-test.md): the testbed is
a pkh context — chroot mode reuses the deb ephemeral chroots and their
cached mmdebstrap tarballs, local mode runs on the current context —
and the binaries under test are the freshness-guarded pkh deb output
next to the tree, rebuilt through the deb flow when stale or missing.

Per test: restrictions gate on the testbed (isolation-machine,
needs-reboot and container isolation skip with a pointer at --mode vm),
the stanza Depends are expanded (@, @builddeps@, @recommends@) and
installed, then the script or Test-Command runs in the staged tree with
the AUTOPKGTEST_* (and legacy ADT_*) environment, a per-test timeout
and the stderr/exit-77 verdict rules; flaky tests retry once. stdout,
stderr and $AUTOPKGTEST_ARTIFACTS are collected per test under
/var/tmp/pkh/tests/ (pruned separately). --shell/--shell-fail drop
into the testbed for debugging.

pin_pocket and install_injected_packages become crate-internal helpers
shared with the deb flow, which they mirror.
2026-09-27 21:00:34 +02:00
vhaudiquet d30b179430 report,ui: name the tee log by flow kind
BuildTarget::source_only carried two states for what is a three-way
distinction now that pkh test exists: replace it with a TargetKind
(Source/Binary/Test) and derive the tee log prefix (build-/deb-/test-)
from it, so test runs get their own log family instead of borrowing
the deb one.
2026-09-27 20:41:40 +02:00
vhaudiquet 067e0614e6 debian: add the DEP-8 tests-control parser
Parse debian/tests/control into runnable test descriptions: one test
per Tests: script and per Test-Command value, carrying the shared
Depends/Restrictions/Tests-Directory stanza fields and the autopkgtest
command1 enumeration (test-name overrides advance the counter).
Depends substitution is deliberately left raw: expanding @ and friends
needs the built binary list, so it belongs to the runner.

First piece of pkh test (see plans/pkh-test.md); the runner expands
these stanzas against the deb822 primitives.
2026-09-27 20:35:01 +02:00
vhaudiquet 1c8f4cf35b docs: document incremental builds and the session design
CI / build (push) Successful in 3m6s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m25s
README: the build-session workflow (auto-recording, --resume,
'pkh deb list', --keep, prune retention) under a new section.
plans/: the design spec with the decisions taken along the way
(opt-in resume, --keep for the iteration loop, /var/tmp/pkh/
sessions, Ctrl+C keeping the session) and the implementation notes
recording the as-built deviations.
2026-09-26 11:52:07 +02:00
vhaudiquet 1b71a6814c cli: add deb --resume/--keep and the deb list subcommand
Wiring for the session feature: --resume optionally takes a session
id (bare --resume adopts the newest session of the tree), --keep
keeps the session after a successful build, and 'pkh deb list'
renders the read-only session table for the current tree. Listing
is a subcommand, not a mode-switching flag: pkh deb with flags
always starts a build.
2026-09-26 11:51:55 +02:00
vhaudiquet aaa6723d2f prune: garbage-collect resumable build sessions
Sessions accumulate under /var/tmp/pkh/sessions for every failed,
interrupted or --keep build and can reach tens of gigabytes for
kernel-sized chroots: prune is their primary GC. By default remove
the sessions untouched for longer than the retention window (7
days) plus the corrupt leftovers; --all removes everything. Session
roots hold the same mounts as residual chroots (/proc bind mount,
overlays), so they are unmounted before removal.

prune_in() (the testable core) deliberately stays session-free:
the test suite runs prune tests and e2e builds concurrently, and
scanning the real sessions root from the tests deleted live
sessions mid-build. Session pruning is an explicit opt-in of
prune_in_roots(), used by the production prune().
2026-09-26 11:51:46 +02:00
vhaudiquet 8ff423ffb8 deb: add resumable build sessions
Every local build records a session under /var/tmp/pkh/sessions:
the bootstrapped chroot, the installed build dependencies, the
phase journal and the staged tree with a persistent overlay
upperdir. Rebuilding after a failure currently redoes the tarball
extraction, apt update, build-dep resolution and the whole compile;
a kernel-sized package loses half an hour per iteration.

Recording is always on, reuse is opt-in (--resume [<id>]): the
newest session of the tree, or the one matching an id as shown by
'pkh deb list' (one session per series/arch/cross identity, ids
are build-start timestamps). Resume skips the chroot bootstrap
(integrity marker + tarball check), re-mounts the previous
upperdir so make recompiles only what changed, pops the quilt
series first when debian/patches changed, and clears debian/files
before re-packaging so artifact collection stays exact. A version
bump keeps the environment and discards the build artifacts.

Explicit selectors disagreeing with the adopted session are an
error, never a silent environment switch. Failures and Ctrl+C keep
the session (the interrupt hook unmounts but preserves the tree); a
success consumes it unless --keep. Concurrent same-identity builds
serialize on a lock file kept OUTSIDE the session root: teardown
removes the root while holding the lock, and a lock inside it would
be deleted under the holder, letting the next opener lock a fresh
inode. The apt phases rerun on resume (idempotent, seconds-cheap)
rather than being stamp-gated.
2026-09-26 11:51:06 +02:00
vhaudiquet 0f1446ad4d context: stage trees over a caller-supplied overlay upperdir
Resumable build sessions need to re-mount the staged tree over the
same overlay upperdir on every attempt: the build artifacts written
inside the chroot (object files) live there, while the host tree
stays visible as the live lowerdir. Add an optional
ensure_available_with_overlay staging path (OverlayStaging reports
whether the persistent upperdir was actually mounted or a fresh copy
was staged instead) with a default implementation rejecting the
request, so drivers without overlay support degrade cleanly.

The overlay mount itself is factored out of try_overlay_mount into
mount_overlay, taking optional pinned upper/work directories; a
failed mount only cleans up the directories it created, never the
caller-pinned ones.
2026-09-26 11:50:39 +02:00
vhaudiquet 02415fa450 pkh: move version to 0.2.0 2026-09-26 11:49:08 +02:00
vhaudiquet 46407375d0 deb/cross: stop exporting PKG_CONFIG_LIBDIR for cross builds
CI / build (push) Successful in 3m13s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m15s
dpkg-buildpackage and sbuild export no pkg-config redirection: the
environment is the package's to set.  Pointing the whole build at the
target multiarch pkgconfig dirs makes every pkg-config-consuming tool
resolve against the target libraries, the host-side tools of the same
build included: the host linker is then handed target-arch -L paths
(the kernel's resolve_btfids logging 'skipping incompatible
/usr/lib/aarch64-linux-gnu/libelf.so') and only survives through
fallbacks when the build-architecture libraries happen to be installed
too.

Packages whose cross builds need target pkg-config data must arrange
for it themselves, the way the kernel packaging builds rtla statically
against no target system libraries.
2026-09-25 20:54:06 +02:00
vhaudiquet ce19c5648b quirks: inject the kernel's host-tool libraries as :native for cross
The resolute and stonking kernels declare libelf-dev, libdw-dev and
libssl-dev unqualified, unlike the Debian control which carries the
same names qualified :native for the host-side tools (resolve_btfids,
gendwarfksyms, sign-file).  The dpkg cross rules resolve an unqualified
Multi-Arch: same name against the host architecture only, so a cross
build installs no build-architecture variants and the kernel's
host-side tools cannot link (-ldw, -lelf, -lssl all fail).

Inject the :native variants for the affected series — the declared
unqualified dependencies stay, so the target-side tools keep their
host-architecture libraries.  Series-scoped so the entry can be
dropped once the control is fixed upstream.
2026-09-25 20:53:10 +02:00
vhaudiquet 99c6f63cd4 deb: pin the cross variant choice of M-A:same build-deps in tests
An unqualified Multi-Arch: same build-dependency resolves to the
host-architecture variant only, even when a build-architecture
candidate exists: that is what dpkg's checker accepts (KnownFacts
accepts foreign, host and all instances) and what sbuild installs,
whose dummy package carries the parsed Build-Depends unqualified and
whose sbuild-cross-resolver only filters foreign M-A:foreign and
Essential packages out of the apt universe — verified by resolving a
sbuild-style dummy:arm64 against the resolute/stonking indexes, which
yields libelf-dev:arm64 & co and never the amd64 variants.

The build-architecture side of such a library is only ever installed
through an explicit :native declaration.  Pin both rules so a resolver
regression towards dual-installing unqualified dependencies cannot
reappear unnoticed.
2026-09-25 20:53:02 +02:00
vhaudiquet 4b52f8e9e9 docs: add crates.io installation instructions
CI / build (push) Successful in 3m1s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m26s
pkh v0.1.0 is published to crates.io, so cargo install pkh is now the
primary installation path; building from source stays as the
alternative. Drop the 'no distribution channel' notice.
2026-09-24 22:19:24 +02:00
vhaudiquet 6df490cf9a ci: install build-essential in the publish job
CI / build (push) Successful in 3m3s
CI / test (push) Skipped
CI / snap (push) Successful in 6m9s
CI / publish (push) Successful in 2m11s
cargo publish verifies the packaged tarball with a full build before
uploading, and that build needs a C linker for build-script crates
(proc-macro2 was the first to fail). The job's container setup step
omitted build-essential, unlike the build and test jobs, so the
verification failed with 'linker cc not found' on the v0.1.0 tag.
v0.1.0
2026-09-24 21:41:15 +02:00
vhaudiquet f5b7704647 data: add an agent SKILL.md teaching pkh usage
CI / build (push) Successful in 3m1s
CI / test (push) Skipped
CI / snap (push) Successful in 6m1s
CI / publish (push) Failing after 53s
Add data/skill/SKILL.md in the agent-skills open format: a skill
directory that agents (Claude Code, Codex, OpenCode, ...) discover and
load on demand. It documents the shared option surface, the pull,
chlog, build, deb, lint, put workflow and the flags that keep runs
non-interactive; the command reference was generated from the live
--help output of every subcommand.

It lives under data/ so a later module can embed it and ship it with
the binary, for example a 'pkh skill' installer writing it into the
agent skill directories.
2026-09-22 23:55:56 +02:00