Commit Graph
395 Commits
Author SHA1 Message Date
vhaudiquet 946cd8e642 ci: skip crates.io publishing for prerelease tags 2026-10-04 13:00:21 +02:00
vhaudiquet 9acf650fc9 deb,config: the staged-tree budget replaces the size cap
The size cap was the wrong shape: an --incremental user asked for a
carried tree, and the invariant that matters is which trees survive —
the newest successful one (the next run's automatic fork base) and the
newest one (what a failed run is re-forked from explicitly). The cap
is gone, with the tree measurement and manifest field it needed; the
retention planner now works on the records' outcomes alone and fills a
budget: staged_trees staged trees per target, the newer ones first,
the pair always among them. When the budget cannot hold the pair (one
staged tree configured, the newest build failed and an older
successful tree present) only one of the two is successful and that
one is kept; with no successful tree at all, the most recent one is.

The budget is a setting of the nested config file:

    deb:
      incremental:
        staged_trees: 2

(default 2; 0 clamps to 1 with a warning — the newest successful tree
always survives). The fork-base selection no longer truncates the
candidate list to the budget before ranking it: with a budget of one
and a failed newest tree, that truncation hid the successful record
and forked the failed one instead.
2026-10-03 23:43:10 +02:00
vhaudiquet a9a6d2001c deb,config: keep the newest and the newest successful staged trees
The retention kept the newest five staged trees per target, so the
staged trees of a kernel-scale target alone could hold three-digit
GiB. The policy is now the pair that earns its disk: the newest
staged tree (what a failed run is re-forked from explicitly) and the
newest successful one (what the next --incremental run forks
automatically) — one record when they coincide; every other staged
tree degrades to metadata-only at the next build's retention. The
retention decision moved into a pure planner over (success, size) so
the policy is table-testable.

The size cap no longer drops a tree outright: an --incremental run's
own tree is always kept by its own run — the user asked for a carried
tree, and at least one always exists — and the cap only thins the kept
pair while another member covers the resume, the newest successful
tree being the last one standing. Oversized trees left by older pkh
versions or interrupted builds come down at the next build's
retention, once their lock is free.

The cap moves to the first user settings file, config.yml in
~/.config/pkh (YAML, like the data files; missing file or keys mean
defaults, a corrupt file is backed up, PKH_STAGED_MAXSIZE overrides),
replacing the JSON sketch from the previous commit. Default 20G, 0
disables the size rule. Every retained tree's size is measured once at
retention and recorded in the manifest, so sweeps never re-walk the
trees.
2026-10-03 21:29:07 +02:00
vhaudiquet 93943dcce0 deb: do not retain staged trees over the retention size cap
The per-target retention kept the newest five staged trees however
large they are: five carried kernel trees are three-digit GiB, most of
it compiled objects whose resume value does not scale with their size.

A staged tree larger than the retention cap (a user setting, see
below) is now dropped at the end of its own build — the record
degrades to metadata-only and the next build re-stages from scratch —
and the retention sweep also degrades older oversized trees, covering
records kept by older pkh versions and interrupted builds. The cap of
0 disables the size rule; the count cap is unchanged.

The cap is the first setting of a user configuration file,
~/.config/pkh/config.json (the directory already holds contexts.json):
missing file and missing keys mean defaults, a corrupt file is backed
up and the defaults apply, and PKH_STAGED_MAXSIZE overrides the file.
The default is 20G, the scale where a carried kernel tree stops
earning its disk.

Every retained tree's size is measured once at retention (the same
concurrent, mount-crossing-safe walker the prune report uses) and
stored in the record manifest, so sweeps reuse it instead of re-walking
the trees on every build.
2026-10-03 18:28:12 +02:00
vhaudiquet ed6d88008d test: rebuild a stale repo pkh binary before the e2e run
resolve_pkh preferred target/release/pkh however old it was: after a
commit without a release rebuild, the harness measured the previous
code — the 54-package run recorded before this change exercised the
pre-nested-namespace builds and recorded no phase profiles, the
profile feature itself being newer than that binary. A repo binary is
now rebuilt when it is older than HEAD or than the newest source edit;
an explicit --pkh still pins whatever binary it names.
2026-10-03 13:55:13 +02:00
vhaudiquet 2729f9d9e7 context,deb: run the rules as a nested-namespace build identity
Rootless builds handed the staged tree to the build user with a
recursive chown; through the overlay staging every lower-layer file
the chown touched copy-ups — the full data movement of the tree, 19%
of a kernel build's wall clock.

The rules steps are now flagged and the unshare driver enters the
chroot by pivot_root instead of chroot(2), then creates a nested user
namespace mapping the context's root onto a fixed build id: the
chroot's files are the invoking user's host-side and the context maps
that user to uid 0, so the staged tree is owned by the build identity
by construction — no chown, no data movement. The kernel refuses user
namespace creation to chrooted processes, hence the pivot, and the
bind is recursive and taken inside the namespace: a plain bind of the
chroot's host-made submounts fails EINVAL on locked children, while
the rbind carries them across the pivot, and a fresh /proc is mounted
before the old root is shed (umount resolves through /proc/mounts).

fakeroot's chown wrappers attempt the real chown and swallow only
EPERM; as the mapped identity, root targets are unmapped and fail
EINVAL, so mapped builds export FAKEROOTDONTTRYCHOWN — fakeroot's own
knob — and the fake ownership is still recorded. Files the build
writes come out as the invoking user host-side, which also fixes the
silently missing .buildinfo/.changes of rootless builds: the host-side
rewrite of debian/files hit a subuid-owned file with EACCES.

The wrapper is probed per build; when nested namespaces are
unavailable (old util-linux, userns disabled), the previous setpriv
plus selective parallel chown handover runs unchanged. A setpriv-era
staged tree is handed to the invoking user once, host-side and
metadata-only, before the overlay mounts it — afterwards overlayfs
serves stale owners from its attribute cache.
2026-10-03 13:00:26 +02:00
vhaudiquet 5544873107 deb: hand the staged tree to the build user selectively and in parallel
The handover chowned the whole build root with a serial 'chown -R'.
Through the overlay staging every lower-layer file it touched is
copy-up'd — the full data movement of the staged tree — which the
profile of a kernel build showed as 15m53s in 'Preparing the build
user', 19% of the whole run.

The handover is now a 'find ! -uid | xargs -P chown' pipeline: only
entries the build user does not own yet are chowned (staging and quilt
re-application flip entries back to root; a resumed tree is already
owned and the phase collapses to a metadata-only walk), and the
copy-ups of independent files parallelize across the chroot's cores.
On a 200k-file tree the handover drops from 9.9s to 0.9s.

The ccache handover ran only when the build user was fresh, which is
every build in practice (the chroot is per-run scratch); it now rides
the same selective pipeline on every run, still best-effort — a failed
handover costs cache hits, never the build.
2026-10-03 04:34:20 +02:00
vhaudiquet ae679f29d9 pull,ui,report: render the pull downloads as byte progress
CI / snap (push) Successful in 11m51s
CI / build (push) Successful in 6m18s
CI / test (push) Skipped
CI / publish (push) Skipped
The pull downloads reported through the plain count port only because
byte_progress landed on main after the branch was cut. PullView now
implements it with a byte-counter template in the transfer-style
family (same shape, {bytes}/{total_bytes} counters, human-formatted by
indicatif), downloads emit through it, and the trait doc lists the
event among the ones the pull flow emits — un-duplicating the merged
'message, progress and progress' sentence.
2026-10-03 02:28:22 +02:00
vhaudiquet a9f535bce5 Merge branch 'cleanup/errors-deps'
* cleanup/errors-deps:
  apt/release: type the file-verification and armor errors
  new: type the scaffold validator errors
  debian: type the errors of the format primitives
  deps: add thiserror
  pull,package_info,ui,cli: report the pull flow via BuildView
  deps: trim the tokio feature set to the APIs in use
  deps: drop the lazy_static dependency
  data,apt/release,distro_info,new: replace lazy_static with LazyLock
  deps: drop the unused cmd_lib dependency

# Conflicts:
#	src/ui.rs
2026-10-03 02:23:11 +02:00
vhaudiquet 82600b1d22 apt/release: type the file-verification and armor errors
verify_file returns FileVerifyError (no Release file, unlisted path,
checksum mismatch, size mismatch — the structured pieces carried, so
the by-hash retry in package_info can match on the failure instead of
parsing text) and the in-process armor/base64 decoders return
ArmorError; the gpgv staging and keyring-download paths keep their
diagnostic strings, rendered at the boundary. The existing
VerifyError(String) keeps its name and downcast role for suite
signature verification. Display texts are byte-identical to the old
format! strings.
2026-10-03 02:17:22 +02:00
vhaudiquet a59e99b107 new: type the scaffold validator errors
The leaf validators of the wizard (source name, command, upstream
version, maintainer, homepage, Depends, orig path, template id, file
collisions) now return a shared options::ValidationError carrying the
structured pieces (the rejected string, the wrapped DebianVersion or
Deps parse failure) instead of pre-formatted Strings; the wizard
resolve flow keeps its aggregate String errors and renders at the
boundary, and the Prompter validator closures adapt with to_string()
since the port's Validator deliberately exchanges re-ask explanations
as plain strings. Display texts are unchanged, as are the tests that
assert on them.
2026-10-03 02:17:22 +02:00
vhaudiquet 82a15c7426 debian: type the errors of the format primitives
Give every parser of the shared format layer a proper error enum
(thiserror): VersionParseError, ArchError, deps::ParseError (wrapping
the version and arch failures as #[source] so the context chain
survives), ControlParseError, ChecksumParseError, FilesParseError and
TestsControlParseError, each carrying its structured pieces (the
offending string, the stanza number, the field name) instead of a
pre-formatted String.

Display texts are byte-identical to the old format! strings, so
user-facing output and the message-asserting tests are unchanged; the
few tests asserting on error text now render the error first. Flow
boundaries that still take String (build::env, quirks, test::expand)
render with to_string() at the edge — typing those flows is a later
step. ControlInfo's FromStr impl follows parse_content's error type.
2026-10-03 02:17:22 +02:00
vhaudiquet 5c7627925f deps: add thiserror 2026-10-03 02:17:22 +02:00
vhaudiquet 8c8ee5beee pull,package_info,ui,cli: report the pull flow via BuildView
Retire the ProgressCallback option-of-closure, the one pre-port
reporting idiom left in the tree: pull and package_info::lookup now
take &dyn BuildView like every other core flow, and the announcements
map onto message (unsized work), progress (item counts: series probes,
git objects, extraction) and progress again for byte downloads, which
the old callback carried in the same 4-argument shape.

The terminal side swaps the (ProgressBar, closure) pair for PullView, a
BuildView implementation owning one bar of the shared MultiProgress
with the same spinner/transfer style switching; it releases the line on
drop, which the CLI triggers explicitly on both outcomes since
exit(1) skips destructors. Headless callers (tests, internal lookups)
pass &report::Quiet. git clone sideband parsing is skipped entirely
when the view is not enabled, matching the old None-callback shortcut.
2026-10-03 02:17:22 +02:00
vhaudiquet 9004a33bae deps: trim the tokio feature set to the APIs in use
The runtime (multi-thread, for Runtime::new + block_on and task
spawning), time (retries, timeouts), sync (channels) and macros
(#[tokio::test]) cover every tokio:: path in the tree; 'full' also
pulled fs, io, net and process support nothing uses. Cargo.lock loses
the corresponding mio/signal deps if nothing else needs them.
2026-10-03 02:17:22 +02:00
vhaudiquet 4d59890d5a deps: drop the lazy_static dependency 2026-10-03 02:17:22 +02:00
vhaudiquet 239bcb28df data,apt/release,distro_info,new: replace lazy_static with LazyLock
Unify the lazily-initialized statics on std::sync::LazyLock, which the
newer code already uses: the embed_data! macro expands to a plain
static now, which also removes the visibility-forwarding workaround the
lazy_static! matcher forced on it (the @expand arm wrapping vis in
parentheses), and the 'static ref' spelling is kept at the call sites.
distro_info's HTTP client, the apt keyring cache and the new-template
registry follow the same pattern.
2026-10-03 02:17:22 +02:00
vhaudiquet 486856da60 prune,ui: show the removal pass, prefix the sizing spinner
CI / build (push) Successful in 6m23s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 11m45s
Confirming a 500 GiB prune went silent under "Removing:" for minutes:
drive an indicatif bar from the plan's sizes through a new
apply_with_progress observer, in the deb live view's layout — the
running count behind the spinner on the first line, the byte bar with
its counters on the second. The removed path is not rendered: it
fights the bar on a narrow terminal, and the failed-removal errors
name the paths that matter. --force plans no sizes and gets the count
line only.

The dry-run size spinner lacked the "> " prompt prefix the other
one-line waits carry; wait_spinner renders it too now.
2026-10-03 01:34:47 +02:00
vhaudiquet f6f2b882a8 deb,report,ui: report the staged-tree copy while a record forks
Forking an incremental base copies its whole staged tree — bounded,
and without reflinks a matter of minutes — but the copy emitted no
build events: the live view showed the target line with nothing below
it, and an interrupt there printed raw cp errors with no hint of what
was running (the fork happens before the first bootstrap phase owns
the view).

Announce the copy through a new BuildView::byte_progress port. The
fork sizes the source up front (the copy's total), re-measures the
growing destination from a scoped thread while cp runs (cp has no
progress output to tap), and reports the completed copy when it
returns; unmeasurable or empty sources copy unreported. The terminal
view renders it like the other determinate bars, with byte-unit
counters.
2026-10-03 01:28:30 +02:00
vhaudiquet 83a853d259 utils/size,prune: share the concurrent tree walker and byte formatting
The incremental fork's staged-tree copy wants to report byte progress
against the same measurements the prune report is built from: move the
concurrent size walk and the human-readable byte formatting out of
prune into utils::size and rewire prune to them. Behavior is unchanged:
same-device walks, unreadable entries count as zero.
2026-10-03 01:26:45 +02:00
vhaudiquet dc01812847 deps: drop the unused cmd_lib dependency
No cmd_lib!/run_cmd!/run_fun! invocation remains anywhere in the tree;
the crate shells out through std::process::Command exclusively.
2026-10-03 01:24:54 +02:00
vhaudiquet 1cc64a37da deb: make apt wait for the shared archives lock
The shared apt download cache is bind-mounted from outside the
chroot, so when an apt of ANOTHER build holds the archives lock from
its own pid namespace, a concurrent build's apt sees the holder as
"process 0", cannot wait for it, and fails the build immediately
(observed as an e2e samba essentials install racing a kernel build's
build-deps transaction on the stonking-amd64 cache).

Set DPkg::Lock::Timeout to ten minutes in the shared-cache apt conf:
apt then waits for the holder instead of erroring. pkh itself
already serializes builds per cache with a host-side flock; the
timeout covers the window where a killed run's orphaned apt is still
winding down.
2026-10-02 23:52:56 +02:00
vhaudiquet b85a377b24 prune,ui,docs: color the digest, add -f/--force and a sizing spinner
- -f/--force skips the size pass and the confirmation prompt: remove
  immediately, reporting counts but no sizes. The fastest cleanup
  path; --dry-run still previews.
- The default path shows the grouped digest and asks before removing
  (the raw-mode yes/no prompt; non-interactive sessions keep their
  headless path, so scripts reach for --force).
- Sizes are computed under a spinner ("Computing sizes of
  directories to prune..."), and the digest colors the counts (bold)
  and the sizes (cyan) on terminals only.
2026-10-02 23:43:04 +02:00
vhaudiquet 74c74f0a61 prune: size the artifacts concurrently, drop the per-path info lines
The dry run still spammed one info line per artifact before the
grouped digest, and the size walk was serial: tens of seconds of pure
readdir/stat over the records' hundreds of thousands of files.

The per-path lines drop to debug (the grouped report is the
user-facing output), and the size pass now fans out over a shared
work queue with scoped threads — directory-level parallelism, same
du -x mount-crossing rule. 16s to 6.4s wall on a 440 GiB sessions
tree, totals unchanged.
2026-10-02 23:29:21 +02:00
vhaudiquet 2991237c91 prune,docs: group the prune report by kind with sizes
The prune output spammed one line per path. The report now groups
entries by artifact kind with their total size, and the size is
measured before removal whatever the mode, so --dry-run prints a
digest ("24 residual build directories for 1.2 GiB") and both modes
end with the freed total. The size walk never crosses a mount point:
residual chroots hold bind-mounted /proc trees whose virtual files
(/proc/kcore alone reports 128 TiB) would poison the total.

--all is also honest now: it removes recent build records and staged
trees (that is where the space is), and the flag description says so.
One behavior fix rides along: a record or lock a live build holds was
previously prunable with --all, which would wreck a running build;
held locks are now out of bounds whatever the policy.
2026-10-02 23:22:14 +02:00
vhaudiquet 8485e7272b deb: sync upper entries through the privileged removal path
A rootless build's upperdir entries are copy-up'd by whoever wrote
them during the build: the unprivileged build user, which host-side
is a subuid of the invoking user. The host-side sync of a forked
record removes the stale upper copy of every host-modified file, and
the plain fs removals failed with EPERM on those build-user-owned
entries: the kernel incremental run died with a bare
'Permission denied (os error 13)' and no context, ten minutes into
the run.

Route the sync removals through the privileged path (the same sudo
fallback the record teardowns use) and wrap the sync error at the
call site, so a failure names the operation instead of surfacing a
bare errno string.
2026-10-02 23:03:15 +02:00
vhaudiquet f1e8244e73 docs: drop the plans directory
The specs describe shipped features — incremental builds, the live
view, overlayfs staging, pkh new, pkh test, pkh lint phase 1 — and
the satellite-tools plan is implemented in full (no shell-outs to
dpkg-architecture, dpkg-checkbuilddeps or dpkg-parsechangelog
remain). The two unshipped proposals (the lint porting track and the
data/configuration architecture) stay in whatever notes the project
keeps outside the tree; the README remains the user-facing
documentation.
2026-10-02 21:31:48 +02:00
vhaudiquet b246b30eac docs: tighten the README prose
Plain-speech pass: drop the em-dash asides and mid-sentence colons,
reword the boldface digest constructions ("What --incremental buys
is ..."), and replace the fluffy intro ("aims at wrapping ... while
being smarter at integrating all workflows") with what pkh actually
does. No content changes.
2026-10-02 21:28:21 +02:00
vhaudiquet e4612ca128 deb: give the build-user setup and resume cleanup their own phases
The profile's build-dependency phase spanned everything between the
apt passes and the build step, and for a kernel incremental build
that hid ~13 minutes of housekeeping: the recursive chown handing
the staged tree to the build user, and the resume_clear quirk
dropping the previous attempt's stamp caches. Both now enter the
profiler (ClearingResumeCaches only when the quirk has paths and a
tree is carried, PreparingBuildUser only for rootless builds), so
the status bar and pkh deb profile show where the time actually
goes — on a large carried tree the chown dominates, which is the
honest answer to a 6m30s DEPS column next to a 19m19s phase.
2026-10-02 20:39:24 +02:00
vhaudiquet 4535255844 logfmt: collapse the rootless chown EPERM flood into one digest line
A rules chown failing for the unprivileged build user is expected
and harmless (see the revert of the build-step fakeroot wrap), but a
sysroot tree can print thousands of 'chown: changing ownership of
...: Operation not permitted' lines. MakeClassifier now shows one
warning digest for the flood and hides the rest; every other line,
make errors included, keeps its classification. The full tee in the
record's build.log is untouched.
2026-10-02 18:42:36 +02:00
vhaudiquet 6050da7dbb Revert "deb: run the rootless build step under fakeroot, sharing the state"
This reverts commit 5f36a54db42e0e5b8f5243dded89f68cd93fa689.

Fakeroot's IPC degrades under kernel-scale parallel builds: a
linux-riscv build with make -j plus LTO floods the faked daemon and
libfakeroot prints 'internal error: payload not recognized' per
mangled message, with the ownership database losing entries. The
benefit the wrap bought is vacuous anyway: the .deb is assembled in
the binary step's own fakeroot session, which records files unknown
to its database as root-owned — verified with a sysroot package
whose build chown fails: the produced .deb still carries root/root.

A rules chown failing for the unprivileged build user stays expected
and harmless; the visible noise is collapsed at the classifier level
instead (next commit). The bare build step also matches modern dpkg
semantics: with Rules-Requires-Root: binary-targets (pkh's default
export), dpkg-buildpackage itself runs the build targets unprivileged
and fakes root only for the binary targets.
2026-10-02 18:42:36 +02:00
vhaudiquet b8adb318bd deb: stage the pointed-at tree, not its parent
The staged tree was the parent of the tree the caller pointed at,
so building a tree sitting in a busy directory dragged every
sibling into the chroot: a /tmp checkout copied the whole host /tmp
(snap sockets, other jobs' scratch), and the build user's chown -R
then failed per foreign-owned file, or aborted the build outright.

Stage the tree itself: its content is the merge point of the build
root, the overlay lower is the tree and the record's upper stacks
on top. The package resolution treats the staged root as the
package when it carries debian/, keeping the pattern search for
nested layouts. The .debs, the upload metadata and the dose3
scratch follow dh_builddeb's .. (the package directory's parent)
instead of assuming the build root, and the host-tree snapshot
covers the tree only. Records staged the old way self-heal: their
snapshots carry parent-relative paths, so the next incremental
run's sync wipes the stale upper entries and rebuilds once.

Siblings are no longer visible to debian/rules — dpkg packaging
never crosses that boundary, and pkh test already staged the tree
alone.
2026-10-02 17:55:22 +02:00
vhaudiquet 5f36a54e7c deb: run the rootless build step under fakeroot, sharing the state
The rules run as the unprivileged build user, so an ownership change
they make during the build (a sysroot chown) failed per file with
EPERM, spamming the build view and log and silently not happening.

Wrap the build step in fakeroot like the binary step already is, and
share one session state between the two — the way dpkg-buildpackage
-rfakeroot wraps every rules target: the chown succeeds faked and
carries into the .deb assembly. With the context's own user the
build step stays bare. The state file lives in the staged build
root, outside the package tree, and is removed before each build so
an interrupted previous attempt cannot seed the session.
2026-10-02 17:36:05 +02:00
vhaudiquet 6967813fa0 cli: drop pkh deb --mode and tighten the deb flag help texts
Only one build mode exists and the flag only ever accepted its
default 'local'. The --mirror help keeps what a user acts on. The
URL feeds mmdebstrap so every apt resolves from it, and the mirror
must serve every suite and architecture the build resolves.
--incremental says the same things without the asides, and
--no-ccache is one line.
2026-10-02 15:45:49 +02:00
vhaudiquet 2ab2ff91e9 deb: record per-phase build timings, shown by pkh deb profile
Time every Phase the build flow announces through enter_phase — the
chroot bootstrap (tarball download, extraction, finalization), the
apt phases, patch application, the build and binary steps and the
artifact retrieval — and land the ordered profile in the record's
manifest of every run, failed builds included: their profile ends
at the phase the error propagated out of.

Timing rides the enter_phase choke point instead of new measurement
sites, so a phase added later profiles itself. The manifest field
is additive (serde default, skipped when absent), so older manifests
keep loading without a schema bump. The coarse timings stay the
source of the deb list columns: they measure the apt transactions
themselves, where the profile measures whole phases.

pkh deb profile renders a record's profile through the same
read-only record discovery as pkh deb log — the newest record of
the tree, or an id prefix as shown by pkh deb list — so
record_for_log becomes the neutral find_record. Phase keys are
storage names, distinct from the display labels; Phase::from_key
turns them back and a test keeps both in sync.
2026-10-02 14:17:15 +02:00
vhaudiquet 7055d69152 Merge branch 'rootless-builds'
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
2026-10-02 12:58:44 +02:00
vhaudiquet abc89c4544 test: drop the coreutils xfail, rootless builds clear it
The e2e canary against the rootless build records coreutils as
xpass: with the rules running as the build user, its configure no
longer refuses the build.
2026-10-02 12:43:32 +02:00
vhaudiquet 672d17136b deb: run the user probes through sh with stderr dropped
A missing build user is the expected state in every fresh ephemeral
chroot: without the redirect the id failures leak into the build log
as errors on every run.
2026-10-02 12:26:46 +02:00
vhaudiquet 7b47a3b32c deb: build the rules as an unprivileged user in pkh chroots
Launchpad's buildds and sbuild compile as a non-root user, and
packages rely on it: coreutils' configure refuses to run as root
(the one remaining standard-tier e2e failure). Run the rules - and
the fakeroot-wrapped binary step - as an on-demand 'pkh-build' user
whenever the build runs in a context pkh controls (the ephemeral
chroot); ssh and schroot contexts keep their own user.

setpriv drops to the user without touching the environment, so the
dpkg architecture variables and DEB_BUILD_OPTIONS survive the
transition. The staged tree, the scratch HOME and the shared ccache
directory are handed to the user on every build; everything before
the rules (dependency installation, quilt) legitimately stays root.

Minimal mmdebstrap chroots carry no passwd package, so the user is
appended to /etc/passwd and /etc/group directly when useradd is
missing, as sbuild's build-user setup does.
2026-10-02 12:23:02 +02:00
vhaudiquet decd092871 test: xfail gdb, grub2 and u-boot in the e2e catalog
All three fail on the current archive, none by pkh's doing: gdb's
PDF dies in texi2dvi against texinfo 7.3; sid's grub2 is the 2.06-2
branch whose control pins gcc-10, which sid no longer ships; and
cross u-boot compiles its kbuild host tools with the arm64 flags
dpkg-buildflags serves in cross mode (-mbranch-protection is amd64
rejected), the same under sbuild cross.
2026-10-02 12:14:55 +02:00
vhaudiquet d9c11cf0e7 test: xfail binutils in the e2e catalog
glibc 2.42 turned strstr into a macro with C2x redirect annotations
in string.h, and gprofng's libcollector does not survive it:
iolib.c:898 fails with 'expected identifier before _Generic'. Fixed
upstream after the 2.45 snapshot this rebuild uses - a distro
toolchain change hitting every rebuild, not pkh.
2026-10-02 11:54:53 +02:00
vhaudiquet 88fcafc274 deb: point the build HOME at a chroot-internal scratch directory
The build context inherits pkh's environment, so the rules ran with
HOME set to the invoking user's host path - which does not exist
inside the chroot. Tools keying user config on HOME then fail
mid-build: gdb's doc build dies in texi2dvi because the TeX live
setup cannot use the nonexistent home.

Point HOME at /tmp/pkh-home, created through the build context (all
drivers: local, unshare chroot, ssh, schroot). sbuild uses a
non-writable path, Launchpad's buildd user a real one; a writable
scratch directory matches the latter.
2026-10-02 11:54:53 +02:00
vhaudiquet 0ef0505829 pull: keep self-prefixed multi-orig components in place
A component tarball whose entries already carry the addon prefix
('www/...' in sqlite3's 'sqlite3_3.53.4.orig-www.tar.xz') extracts
to its end state, but the wrapper detection named it a wrapper and
unwrap_into then cleared the addon target - destroying the component
just extracted into it - and unwrapped the emptied directory into
itself: no 'www' at all, and the deb failed at dh_installdocs over
the missing 'www/*' docs.

dpkg-source unpacks a component into '<root>/<addon>': when the
tarball's own layout already is that, the extraction is the end
state; leave it alone.
2026-10-02 02:23:22 +02:00
vhaudiquet 14704db777 pull: create the addon directory when unwrapping a multi-orig component
unwrap_into pointed the component's unwrap at '<root>/<addon>' but
never created that directory: unwrap_wrapper moves the wrapper's
entries into it by rename, and a rename whose target parent is
missing fails with ENOENT - every entry, aborting the whole pull.
The multi-orig unit test exercised unwrap_wrapper directly and could
not see it (dovecot).

Also exercise the component through unwrap_into in the test, and
assert the wrapper's own layout does not leak to the root - the
shape dpkg-source -b rejects as unexpected upstream changes.
2026-10-02 01:43:54 +02:00
vhaudiquet 5e4b9cb231 fmt: apply rustfmt 2026-10-02 01:28:08 +02:00
vhaudiquet 61b95c0ac1 test: xfail systemd in the e2e catalog
python3-lxml 6.1 no longer consults custom resolvers for parameter
entities, and systemd's man build leans on exactly that (the
custom-entities.ent resolver in tools/xml_helper.py):
make-directive-index fails with 'Entity entities not defined'. A
distro toolchain change hitting every rebuild, not pkh - the same
breakage forced a backport in Yocto.
2026-10-02 01:19:45 +02:00
vhaudiquet 3038cdf5e7 deb: export DEB_RULES_REQUIRES_ROOT for the rules
dpkg-buildpackage normally exports the rules' root requirement from
debian/control; running the rules directly does not, and tools key
their assembly strategy on it - without it debputy refuses to
assemble packages shipping root-owned paths at all (apache2). Policy
defaults to 'binary-targets' when control does not declare it.
2026-10-02 01:19:33 +02:00
vhaudiquet bb90842ac9 deb: resolve pocket suites and serve Debian security updates
A changelog may target a pocket suite ('trixie-security') rather than
the bare series: split the pocket off so the build resolves against
the base series with that pocket enabled - the same target an
explicit '-s trixie -p security' names. The pocket must be one of
the base series' distro-info pockets, which also guards against
series names merely containing a dash; an explicit --pocket wins.

Debian serves security updates from a dedicated archive: appending
the security suite to the main mirror would 404 on 'apt-get update'
(ubuntu serves every pocket from the same archive, where appending
works) - clone the pinned source against deb.debian.org/debian-security
instead, as mmdebstrap does.
2026-10-02 01:19:24 +02:00
vhaudiquet 3c70ccce19 pull: include the patch output when a .diff.gz fails to apply
patch reports rejected hunks on stdout, not stderr: the error used
to carry only the diff path and an empty stderr, naming neither the
failing file nor the reason.
2026-10-02 01:18:58 +02:00
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