One deb block per package cannot express two series needing different
rules. Make pull and deb lists of entries, each with its own series
scope: every matching entry applies, in file order, so a stonking
entry can carry its own dependency rules next to the resolute one.
Thread the series through find_package_directory for the
package_directory lookup, which keeps its deb-then-pull fallback.
The resolute linux and linux-riscv controls declare llvm-21-dev
unqualified while their other llvm pieces are :native, so the dpkg
cross rules resolve it against the host architecture — whose
dependency closure needs python3:riscv64, conflicting with the
python3 the control itself declares :native. No resolver can install
that set; sbuild fails on it identically.
Give the deb quirks a dependencies rule set rather than a one-off
native-qualification knob: replace rewrites a declared dependency by
name with a full dependency string (qualifier, version and
restrictions included), inject adds dependencies resolved as if
declared, drop ignores declared ones. Entries are scoped by series so
they can be dropped when the upstream packaging catches up, and rule
names that match nothing are warned about so stale quirks surface.
Ship the llvm-21-dev entry for the resolute kernels: resolve it as
llvm-21-dev:native, keeping the declared <!stage1> restriction.
The e2e pulled linux-riscv from questing, which went EOL in July
2026 and has since been removed from the mirrors entirely — not even
old-releases carries a Release file for it, so the chroot bootstrap
cannot resolve the series and the e2e can no longer run.
Point it at resolute (26.04), the current LTS: linux-riscv is
packaged there like in every supported series, and an LTS stays
pullable for years where an interim series turns the test stale
within months.
Keep a noble fixture next to it, also not run by default: noble-era
controls declare their build tools unqualified (the :native idiom
landed in later series), so exact dpkg semantics demand
host-architecture instances of them (python3:riscv64, gcc-13:riscv64,
...), and the two-architecture install set is unsolvable — t64
libraries conflict with their own foreign-arch variant, and the
riscv64 toolchain instances drag depends chains that do not resolve
from the chroot sources.
Replace the apt-get build-dep passes with a native resolver: the
Build-* clauses are evaluated against the context's package state by
crate::debian::deps (the dpkg-checkbuilddeps equivalent), and the
unsatisfied ones install through explicitly architecture-qualified
names.
apt's --host-architecture build-dep resolution is coarser than the
checker it feeds: unqualified Multi-Arch: same libraries only ever
get the host-architecture variant, and there is no way to also
satisfy the build-architecture needs of a cross build without a
native re-resolution pass — which installs co-install partners that
break unpacking for -dev packages whose variants conflict on
arch-differing files (curl-config; the libcurl4-gnutls-dev
regression). The resolver now applies the same dpkg rules as the
checker, preferring the runnable build-architecture variant of
Multi-Arch: foreign tools, and passes virtual names through to apt.
Build-Conflicts are now checked before anything installs, which the
build-dep passes never did, and a failed dose3 diagnosis no longer
masks the resolver error (a latent flaw of the passes: binary-only
builds have no .dsc for dose-builddebcheck to read).
An end-to-end test pins the expected failure: a cross build whose
build-dependencies cannot satisfy dpkg's cross semantics aborts,
naming the dependency.
In-tree tools locate their libraries with the *host* pkg-config during
cross builds (the kernel's tools/build feature checks derive their
cflags/ldflags from 'pkg-config --cflags/--libs'), whose search path
only covers the build architecture's pkgconfig dirs. Cross builds of
linux-riscv died in rtla's Makefile.config: libtraceevent/libtracefs
were reported missing although the riscv64 -dev packages were
installed, because pkg-config never saw their .pc files.
The host-arch -dev packages that used to make those checks pass came
from the unscoped arch-indep build-dep pass, whose native
re-resolution b34e86d correctly scoped to the host arch — removing the
accidental co-install along with the bug it papered over. Export
PKG_CONFIG_LIBDIR for the target multiarch instead, so the checks
resolve target-arch libraries directly: no native build-dep bloat and
no wrong-arch linking.
Instead of carrying raw UI in core, the ports now represent everything
the CLI used to do inline:
- Prompter::present shows context outside of a question (the wizard
summary screen, the vendoring notice spacing); TerminalPrompter
prints it on stdout exactly like the println!s it replaces, server
embeds forward it as a display event.
- generate_entry returns the generated entry (package, versions,
series, path) instead of printing; the CLI renders the same lines.
- BuildTarget carries a flow-composed display line and a tee_log flag:
the terminal adapter renders it verbatim ("Building source package
...", "Building ... for series/arch", "Uploading ... to ...") and
uploads open no build log.
- The unmet build-dependency diagnostics are rendered by the CLI from
the typed error, in the original order (details, then summary).
- --verbose constructs no live view at all (an idle widget used to
linger), and the re-vendor offer only logs when it is actually
asked, so headless runs print the error exactly once.
The chlog target-series resolution becomes changelog::series_candidates
(UNRELEASED pinning, development-series default and fallbacks modeled
by SeriesCandidates), PPA references get package_info::split_ppa
(shared by pull and deb, now also rejecting empty parts), and SSH
endpoints get context::ContextConfig::from_endpoint — so a library
consumer can resolve series, validate PPAs and build context
configurations without reimplementing the CLI's rules. All three carry
unit tests; main.rs keeps only parsing of flags and error handling.
The binary build joins the source build on the reporting ports:
build_binary_package takes a DebBuildOptions struct (replacing eleven
positional arguments), reports target, phases, progress and the
outcome through the environment-agnostic BuildView, and the Phase enum
with its default classifiers moves from the terminal widget into the
deb module (announced through the enter_phase helper). DebUi loses its
inherent event methods and only implements the port; tee logging and
the SIGINT behavior are unchanged.
No behavior change for the CLI; headless consumers pass report::Quiet.
Core flows no longer reach into the terminal UI: build_source_package
takes a BuildSourceOptions struct (source tree, domain options, view,
prompter) and reports phases, messages and outcomes through the
environment-agnostic ports in the new report module. The classifiers
move from ui/logfmt to the core logfmt module, DebUi becomes a
BuildView adapter, the re-vendor retry asks the prompter instead of
checking for a TTY, and artifact/success printing moves to the CLI.
Headless consumers pass report::Quiet; an embedding (e.g. a builder
server forwarding events to a web frontend) implements BuildView and
maps the plain-data events onto its own wire format.
cargo test used to be unreadable: subprocesses inherited the terminal, so
dpkg-buildpackage, apt and configure output interleaved with the harness
summary, and env_logger lines from parallel tests crossed each other.
New test_support module, compiled into test binaries only (inert stubs
otherwise) and initialized before main via .init_array:
- all log output goes to target/pkh-test-logs/<test>.log, one file per
test thread, so concurrent tests never interleave
- context-launched commands are captured line by line into the same file
(driver-level wrapper); test-code spawns use run_logged()
- a panic hook records failures and an atexit callback prints a matrix
(test name, panic location, message, log path) after the libtest
summary; tests panicking on purpose can opt out with a guard
Also fixes two test bugs found on the way:
- diff_checkbuilddeps_matrix compared dpkg-checkbuilddeps diagnostics
against English messages without pinning the locale
- run_source_build in differential tests now captures output like the
live-UI path does
The binary build exported DEB_BUILD_OPTIONS='parallel=<context nproc>
nocheck' (or the -j override) but the generated .buildinfo recomputed
the environment from host state: host core count, no nocheck, and
vendor profiles that ignored DEB_BUILD_PROFILES (a cross build recorded
no 'cross' profile). generate_binary_metadata now records the exact env
map that was exported to the build steps, and the recorded profiles
come from the exported DEB_BUILD_PROFILES when set.
Also unifies vendor parsing on one helper (the context-side copy lacked
the Origin: fallback of the source-build path).
- distro_info: malformed CSV rows are skipped with a warning, dates
that fail to parse become None, and all plain HTTP requests go
through a shared reqwest client with connect/total timeouts
- package_info: the Sources stanza iterator is iterative (a crafted
index with many blank stanzas overflowed the stack), stanzas missing
a Version are skipped, and failed series/pocket probes are summarized
in the final 'not found' error instead of being silently dropped
- pull: no double unwrap on the remote-derived artifact filename, an
empty series list is an error, and streaming downloads get a
per-request timeout
- deb/cross: dpkg-architecture output parsing skips unexpected lines
and its exit status is checked, as is dpkg --add-architecture
- changelog: version increments parse as u64 with checked arithmetic
(1.0-20250123123456 used to panic on the u32 parse)
set_current held the config RwLock for writing across make_context,
which for a context with a parent re-entered the same lock through
Context::new's global-manager lookup, deadlocking 'pkh context use'.
Context building is now lock-free by construction: make_context
resolves parent chains against a snapshot map (also rejecting parent
cycles), and neither set_current nor remove_context holds a guard
while building a Context.
A corrupt contexts.json no longer aborts every command at manager
init: load falls back to the default local-only config, backs the
corrupt file up to contexts.json.bak so a later save cannot silently
destroy it, and a dangling current/parent context falls back to local
with an error log instead of panicking.
build_binary_package installed its ephemeral chroot context into the
process-global manager and read it back with context::current(),
ignoring its ctx parameter: two concurrent builds would re-point each
other's global and each drop would clean up whichever chroot was
current at the time. The guard now keeps the Arc of the context it
created (parented directly on the base context, not on a config-name
lookup), exposes it via context(), and Drop cleans up exactly that
context and restores the exact handle that was current at creation,
so overlapping builds no longer cross-destroy each other.
The SIGINT handler libc::_exit(130)s, skipping EphemeralContextGuard's
drop and leaking the freshly bootstrapped chroot with its bind-mounted
/proc and overlay mounts. Resources now register a self-contained
cleanup hook in a process-global registry that the handler drains right
before exiting: the hook unmounts everything under the chroot path
(children first, lazy fallback) and removes the tree, using only stored
paths and direct umount/rm subprocesses so it cannot deadlock on a lock
the interrupted thread may hold; sudo -n keeps it from ever hanging on
a password prompt. Drop deregisters the hook first, so the normal
cleanup path is unchanged.
Also fixes the hex grouping of the CRC-24 polynomial in apt::release.
Saving the modified source entries with save_legacy rewrote every entry
into /etc/apt/sources.list in legacy format, destroying deb-src entries
and signed-by/trusted options, duplicating every distro entry that came
from a deb822 file (which stayed in place), and hardcoding the Ubuntu
keyring on cross builds. Entries now remember the file and format they
were loaded from and are written back there; new entries (PPAs, ports)
go to a pkh-owned /etc/apt/sources.list.d/pkh-added.list, and a one-time
<path>.pkh-backup copy is made before overwriting an existing file.
Also fixes: 'Types: deb deb-src' stanzas are split instead of being
treated as binary-only, commented-out legacy entries are kept disabled
instead of deleted, debian.sources is actually read on Debian (the old
else-if never fired), and the double blank lines save_legacy emitted.
Without --host-architecture, the second build-dep pass re-resolves the
whole Build-Depends field for the native architecture: apt swaps
host-arch -dev packages for native ones (e.g. libcurl4-gnutls-dev,
whose arch-differing curl-config makes dpkg refuse the co-install) and
breaks the cross build environment.
Per dpkg-checkbuilddeps, both Build-Depends and Build-Depends-Indep
resolve for the host architecture in cross mode, so pass
--host-architecture to the second pass as well. Skip the pass entirely
when the source declares no Build-Depends-Indep.
Add an end-to-end regression test building a package that declares
libdb-dev in both fields and links a host-arch binary against it: the
test only passes if the arch-indep pass did not swap the arm64 -dev
packages for native ones.
Add a --repository flag taking the full suite URL of an external flat
repository (e.g. https://pkg.noctalia.dev/deb/resolute/), i.e. one with
no dists/ hierarchy, like apt's exact-path suites ('Suites: resolute/').
The suite name is read from the root Release file (Codename/Suite), the
sources index is fetched from the repository root as Sources.xz/gz/plain,
and package files are resolved against the URL root, ignoring the stanza
Directory field like apt does. As with PPAs, the stanza Vcs-Git is never
used for external repositories, so the source always comes from the
repository itself.
Also make the sources index parser detect compression by magic bytes
(gz/xz/plain) instead of assuming gzip, and fix extraction of archives
with './'-prefixed entries, which previously aborted and are now
extracted in place instead of being relocated.
build_binary_package_impl copied the whole parent directory into the
build root (ensure_available) and then retrieved every *.deb / *.changes
/ *.buildinfo it found there. That surfaced stale files already sitting
next to the package tree in the "Built in Ns:" summary.
Now local::build returns the exact set of artifacts produced by this
build — the binary packages registered in debian/files plus the
generated .buildinfo/.changes from generate_binary_metadata — and
build_binary_package_impl retrieves that list instead of globbing the
build root. Only files genuinely produced by the current build are
printed.
By default the number of parallel jobs is detected with nproc inside
the build context. Add a -j/--jobs option so an explicit count can be
honored instead, threading it through build_binary_package and
local::build into DEB_BUILD_OPTIONS=parallel=N.
Extend the metadata writers to binary-only uploads and wire them into
the 'pkh deb' flow:
- build/binary.rs generates <pkg>_<ver>_<arch>.buildinfo/.changes
through any Context: debian/files consumption, encounter-order
Architecture accumulation (sorted in .buildinfo like dpkg-genbuildinfo),
sorted Binary lists, dpkg-formatted Description lines with udeb
suffixes, Installed-Build-Depends closure over the context status DB,
and binNMU handling (Source: pkg (prev), Binary-Only-Changes, previous
.dsc redistribution);
- artifact digests are computed inside the context via coreutils
(md5sum/sha1sum/sha256sum/stat) so chrooted/remote trees work;
- deb/local.rs runs the generation after 'rules binary', exports
SOURCE_DATE_EPOCH from the changelog (reproducibility), and resolves
vendor/profiles inside the context; deb/mod.rs retrieves the new
artifacts alongside the debs;
- reusable helpers added: FilesList::parse/render,
parse_changelog_entry_from_str, parse_previous_version_from_str,
installed_build_depends_from_content.
Differential gate: same tree built with real 'dpkg-buildpackage -b' and
with the pkh flow; .changes/.buildinfo compared field-by-field modulo
machine-dependent fields, artifact checksums included.
'proposed' pockets are marked 'NotAutomatic' in their Release file,
which gives them an apt priority of 1: apt would ignore them during
build-dependency resolution even when the suite is enabled.
Write an apt preferences entry pinning '{series}-proposed' at
priority 600 so build-dependencies are actually resolved from the
requested pocket.
Apt preferences are global: a single 'release' pin matches the pinned
suite on every repository carrying it (archive, security and ports),
for all architectures, so this also covers cross-builds pulling
dependencies from 'ports.ubuntu.com'.
- pull: select the pocket to download the source package from
- deb: build against dependencies from a specific pocket:
- local mode: enable '{series}-{pocket}' suite on archive sources
- cross mode: include the pocket suite in required repositories