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.
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.
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.
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.
* 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- -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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.