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 location embedded in the log record was styled with crossterm: a
remote consumer of pkh's log records would receive ANSI codes inside
the message text. Plain text is the logger's business to style.
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.
generate_entry's status messages become log lines, and the unmet
build-dependency diagnostics travel inside the UnmetBuildDependencies
error (its Display carries the full report) instead of being printed
to stderr by the library: the caller renders both like any other
outcome.
put() loses its MultiProgress parameter: the summary, pre-flight and
connection spinners become view messages, the per-file SFTP transfer
reports determinate progress through view.progress (upload_file takes
a byte-count callback instead of an indicatif bar), and the display is
released through view.suspend on every exit path. The hardcoded
trust-on-first-use prompt in the SSH host-key verification becomes the
Prompter::accept_host_key port (fail-closed by default; the terminal
prompter prints the authenticity banner and confirms), so a remote
frontend can surface its own host-key dialog.
The interactive half of pkh new no longer touches the terminal prompt
module directly: run() takes a Prompter, picks the wizard or the plain
resolve path through interactive(), and every select/text/confirm
question (including the verification offers) goes through the port.
Cancellations propagate as Err, preserving Ctrl+C-aborts; the summary
and vendoring-notice prints become log lines. A builder-server embed
can now drive the whole scaffold wizard over its own wire format by
implementing Prompter.
Prompter gains interactive(), select() and text() (with the Validator
type), and confirm() now propagates cancellation as Err so flows abort
instead of silently taking a default when the user hits Ctrl+C. The
terminal prompter implements the full port; the port also re-exports
the path display helper, which is pure presentation formatting used by
events and messages rather than terminal code.
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.
v4's GHES check refuses to run on any non-github.com server, so the
snap artifact upload always failed on gitea; v3 uses the artifact API
gitea implements.
Move the four templates' static file bodies into .tpl files under
data/templates/<id>/, referenced by their manifests' files: lists —
the shell skeleton script (executable, {command}-named) with its
skeleton-only debian/install mapping, the empty template's stub README,
the makefile hello.c/Makefile skeleton with its skeleton-only install
mapping, and go's go.mod/main.go skeleton (the go directive of go.mod
stays a literal: nothing about it is answer-derived).
The empty template ends up hookless — zero Rust, its registry entry
points at no hooks — and src/new/templates/empty.rs is deleted. The
shell and go hooks shrink to their probes (plus go's {go_import_path}
context value); the makefile hooks keep only the existing-tree hint
probing the packaged Makefile for a phony install: target, since that
heuristic reads the tree and cannot be data.
Split the Template trait into a data half and a logic half. Every
template is now declared by a manifest under data/templates/<id>/
(CLI id, wizard label, detection markers, Build-Depends, architecture,
rules dh line, rules-extra body, control source fields, gitignore
entries and static file bodies with {placeholder} substitution),
embedded through the TEMPLATE_SOURCES index and parsed once into the
registry; the order of the index is the wizard menu order and the
detection priority at once. The logic half is the slim TemplateHooks
trait (probe, post_write, file-body overrides merged over the manifest
bodies by path shadowing, Build-Depends/architecture amendments and
extra context values), registered per template as a HOOKS static: a
template without hooks needs zero Rust.
- TemplateId becomes a Copy wrapper of the stable CLI string; the
enum, its all/as_str/display_name/from_label matches and the old
statics array collapse into the registry accessors.
- rust's rules overrides move to data/templates/rust/rules.extra.tpl
with {locked}/{artifact} hook context; python's backend table,
meson/cmake's pkg-config opt-in, autotools' gettext and python's
C-extension hints become hook amendments over the manifest baseline.
- detect.rs drops its hardcoded marker cascade: the manifests'
detect.files drive detection in registry order, with the shell
single-script heuristic and the never-detected empty template kept
as the code special cases they are. License sniffing is untouched.
- The template tests port to manifest validation: registry coverage
and stable order, placeholder presence in the rendering context,
rules composition, the Build-Depends/architecture/dh-line table now
asserted against the manifest data, and the hook shadowing merge.
The static skeleton bodies of the shell/empty/makefile/go templates
stay in their Rust hooks for now; the next commit moves them into
their manifests.
The Section check always read debian/control from the current working
directory, so 'pkh put --changes ../other/pkg_changes' validated the
wrong tree. With an explicit --changes the check now runs against that
file's own directory when it holds debian/control, and is skipped with
a warning otherwise; tree uploads are unchanged.
A failed or interrupted upload left the already-uploaded payloads — or
a truncated .changes — in the PPA's incoming area. On failure the
already-uploaded files are now removed best-effort in reverse upload
order with the failed file first, so a .changes never outlives the
payloads it references; the original upload error keeps precedence over
cleanup failures, and record-after-success semantics are unchanged (a
failed upload must not count as uploaded).
An unreadable or unparsable known_hosts file was swallowed with
'let _', silently downgrading to prompt-and-accept without telling the
user why their configuration was ignored: warn naming the file, then
continue. And when the pinned Launchpad fingerprint matches, a
DIFFERENT key recorded for that host in known_hosts was silently
bypassed: warn about the stale entry (diagnostic only — the published
fingerprint stays authoritative).
TcpStream::connect and the blocking libssh2 session had no timeouts: a
black-holed host hung pkh put forever, mid-resolution, mid-handshake or
mid-upload. Connect attempts now get a 15 s timeout per resolved
address, the session gets a 30 s API timeout for the handshake/auth
phase and a 300 s per-call timeout for SFTP operations (per low-level
libssh2 call, not per transfer — documented); failures name the
operation and host.
A corrupt upload log was silently treated as 'never uploaded',
disabling the duplicate-upload guard without a diagnostic, and
record_upload truncated the file in place — a crash mid-write produced
exactly that corrupt state. Parse failures now log an error, back the
file up to uploads.json.bak (so a later successful upload cannot
destroy the recoverable history) and continue with an empty log; the
log itself is written to a temp file and renamed into place.
The superseded check read only the first getPublishedSources page
(Launchpad defaults to 75 entries per page), so a source with a long
publication history could hide its true maximum published version and
let a superseded upload through, only to be rejected by the queue
hours later. Follow next_collection_link (ws.size=100, hard cap of 20
pages beyond which the check errors rather than risk a false 'not
superseded').
dpkg-source errors with 'building source for a binary-only release'
when asked to -b a tree whose newest changelog entry sets
binary-only=yes: the source publication is already in the archive and
is not being rebuilt. pkh instead built the fresh .dsc and then
produced binNMU-style metadata referencing the *previous* version's
.dsc and tarballs — behavior dpkg does not have at all.
Mirror dpkg: run_source_build now refuses binary-only entries outright,
which makes the previous-version references, the binNMU Source field
and the Binary-Only-Changes handling in the source pipeline dead code —
removed. Binary-only metadata stays in the binary pipeline, where it
matches dpkg-genchanges/genbuildinfo (diff_binmu_binary_metadata).
New tests: a unit test for the refusal, and a failure-parity
differential asserting both dpkg-buildpackage -S and the native
pipeline reject the same fixture.
The source pipeline used one checksum map for two documents with
different content: the .buildinfo (which, like dpkg-genbuildinfo, lists
only the referenced .dsc) and the .changes (which distributes the dsc,
the tarballs and the buildinfo itself). Because the tarballs and the
buildinfo were inserted into the shared map before the signing cascade
re-rendered the .buildinfo, every signed source build produced a
.buildinfo listing the tarballs — which dpkg-genbuildinfo never emits —
and itself, with the stale digest of its own pre-signature content.
Split the map: the .buildinfo renders from the referenced .dsc only
(refreshed after the .dsc is signed), the .changes keeps the full
distribution set with the signed buildinfo's fresh digests. Verified
with a throwaway GPG key: the signed .buildinfo lists exactly one entry,
the .dsc, matching the signed file.
dpkg-genchanges/genbuildinfo handle a binary-only upload by referencing
the previous source version textually (Source: pkg (prev),
Binary-Only: yes, Binary-Only-Changes) while distributing no source
files at all: pkh instead pulled the previous .dsc and its tarballs
into both documents whenever they sat next to the artifacts, re-uploading
the whole source on every binNMU.
Drop that redistribution (and include_dsc_artifacts with it), and emit
the missing Binary-Only: yes field, which the new differential test
against real dpkg-buildpackage -b caught. The binNMU case shares its
runner with the regular binary metadata differential; a unit test pins
the exclusion even with the previous artifacts present.
dpkg-genchanges includes the upstream tarballs in the .changes only when
the upload brings a new upstream: no previous changelog entry, a changed
upstream version or a renamed source. On a plain revision bump the
tarball already sits in the archive, and dpkg strips it (and its .asc)
from the distribution set.
pkh's native source pipeline listed every .dsc-referenced tarball
unconditionally, making every upload re-ship the orig. Implement the
dpkg source styles as --orig auto|always|never (auto being the -si
default; always/never are -sa/-sd), stripping the tarballs out of the
changes, buildinfo-free checksum set and artifact list like dpkg, with
the explicit 'never' ignored for native packages. Comparison uses the
epoch-less upstream version, exactly like dpkg's version().
Differential tests against real dpkg cover revision bumps, new upstream
versions and both forced styles.
Replace parse_previous_version/parse_previous_version_from_str with
parse_changelog_entries(path, limit: Option<usize>), parsing up to the
given number of entries (None: the whole file) newest-first through the
same strict entry parser instead of a header-only scan. The single-entry
helpers stay as thin wrappers, and callers needing the previous entry
now get its full source name and version, not just the raw string.
A versioned Provides whose version failed to parse was silently
skipped, so a corrupt dpkg status entry could yield a wrong 'unmet'
verdict where the truth is 'cannot decide': unparseable provided
versions now set lackinfos like unparseable installed versions do.
Provides alternatives with a non-= constraint are likewise rejected as
a whole field (dpkg rejects the entry), replacing the skip-per-
alternative behavior that contradicted the code's own comment.
DebianVersion::parse accepted '1.0-' (empty revision after rsplit on
the last hyphen), where dpkg rejects it with 'revision number is
empty'; downstream filename construction produced garbage like
'foo_1.0-.dsc'. Keep the start-digit warning-only semantics of dpkg
(no new check there) and the accepted '1.0--1' split.
The checksum model only carried md5/sha1/sha256 while deb-buildinfo(5)
defines Checksums-Sha512, and there was no way to parse a Checksums-*
field body back into entries. Add ChecksumKind::Sha512 (computed
alongside the others), a field parser validating the
'<hex> <size> <name>' grammar, and an only-if-populated
Checksums-Sha512 emission in .buildinfo — deliberately dormant in the
dpkg-parity flows, which never emit it, and .dsc/.changes untouched.
set only replaced the first match and appended otherwise, so a
paragraph holding both 'Depends:' and 'depends:' kept a stale second
value after an update, silently re-emitted on serialization. set now
updates the first match in place and removes any other case-insensitive
duplicate; the parser stays lenient and keeps duplicates reachable via
iter().
run_command_capturing discarded the pump threads' join results: a
reader that died mid-capture (UI sink or log writer failing) reported a
successful build with truncated captured logs. A reader panic now fails
the command; when the child itself failed first, its error keeps
precedence and the reader panic is logged so the truncated output is
not silently lost.
The source-build pipeline exported its computed DEB_BUILD_OPTIONS
verbatim, silently dropping options the user set in the environment
(e.g. terse) where dpkg-buildpackage prepends the inherited value.
Options are now merged inherited-first through a shared helper, with
whitespace normalized.
hashes_in_context never checked stat's exit status and parsed its size
with unwrap_or(0), silently recording zero-size artifacts in the
generated .changes/.buildinfo; stat failures and unparsable sizes are
now errors naming the file. current_vendor hardcoded
/etc/dpkg/origins/default while dpkg honors DPKG_ORIGINS_DIR (already
in the file's own ENV_ALLOWED list); the origins default is now
resolved against it with the usual fallback.
None mapped to Path::new("."), whose parent is the empty string:
the output-directory derivation then always failed with 'cannot
determine output directory', making the documented Option default a
guaranteed-failure trap. Resolve None to the process's absolute
current working directory instead.
Two read_file(...).unwrap_or_default() calls masqueraded IO errors as
empty data: an unreadable debian/files became 'binary build with no
binary artifacts found; cannot distribute', and an unreadable dpkg
status file produced an empty Installed-Build-Depends. Tolerate a
missing debian/files (first build in a fresh tree) but propagate real
read errors, and hard-error on an unreadable status file like the
source-build path does. installed_build_depends_from_content also
returned a bare newline for zero entries, defeating render_buildinfo's
empty-guard and emitting a malformed 'Installed-Build-Depends:' field
with a blank continuation; it now returns an empty string so the field
is omitted.
The binNMU path swallowed parse errors with a let-chain: a changelog
that could not yield the previous version silently produced a .changes
with plain 'Source: pkg', no Binary-Only-Changes and no redistributed
previous .dsc. Propagate the parse error like the source-build path
does (a single-entry changelog stays tolerated), and reuse the
changelog already read instead of reading the file a second time.
CheckOpts had no build-arch concept: the build-side facts and :native
qualifiers resolved against the host arch, so in a cross build
(-a armhf on amd64) 'Build-Depends: foo:native' looked for an armhf
package where dpkg-checkbuilddeps looks for an amd64 one. CheckOpts
gains build_arch (DEB_BUILD_ARCH), used for :native and the dpkg status
attribution; bracketed arch restrictions keep evaluating against the
host arch.
The release-tarball download capped the whole request at 30 s (large
tarballs on slow links always failed and fell through to worse origins)
and buffered the entire body in memory: keep a 10 s connect timeout
only and stream the body to the temp file. The bzip2 -dc child of a
failing repack was neither killed nor waited on (zombie + open pipe);
it is now reaped on both paths. git archive no longer pipes a stderr
nobody drains (a chatty git deadlocked the archive) and any failure
after the destination file was created removes the empty or partial
tarball.
Host pattern lists were evaluated per-pattern with 'any', so
'Host * !*.launchpad.net' matched ppa.launchpad.net via the wildcard;
a block now applies only if a positive pattern matches and no negated
one does (OpenSSH's rule). The system ssh_config was read first with
first-obtained-wins, inverting OpenSSH's user-over-system precedence;
the user file is read first now. A Match block also no longer leaks
the previous Host block's match state (its options are ignored until
the next Host).
The repack stripped the first path component of every entry, assuming a
single top-level directory: a flat archive ('tar czf up.tar.gz file1
file2') had all its entries dropped and wrote an accepted-but-empty
orig. The layout is now resolved from the leading entries (a lone
top-level directory is held back until the next entry confirms it as
the archive root or proves the archive flat) and flat entries keep
their whole path under the new top-level directory; classic archives
are repacked exactly as before.
The detection + probe pass ran against the cwd before the
source-location question, so answering 'another directory' still
offered the cwd's name, version, description, homepage and license
sniff as defaults (only the orig origin followed the chosen tree).
When the answer redirects the wizard to a different directory, the
detection + probe now run again there, feeding every subsequent
probe-derived default; the originally detected directory is not
re-scanned and explicit flags keep winning.
The command/binary name was accepted verbatim and interpolated into
debian/install, debian/rules, debian/tests/smoke, automake variables,
meson.build and [project.scripts]: a value with a space or quote broke
the install lines and shell snippets, 'my.tool' parsed as a nested TOML
table (silently dropping the console script) and produced non-canonical
automake variable names. Both --command and the wizard answer now go
through a shared validator (lowercase identifier: letters, digits,
+ - . _).
ask_text accepted its default on Enter without running the question's
validator, so a probed upstream version like 1.0-2 or v1.0 sailed
through the whole questionnaire and crashed resolve() at the end, and
an invalid git-derived maintainer default (e.g. 'Name <>') was accepted
verbatim. ask_text now takes the validator and applies it to both typed
answers and the offered default — a default that fails validation is
withheld and an invalid answer re-asks — and all question call sites
(incl. the maintainer loop) route through it.
The wizard overwrote cli.lang with the detected ecosystem even when the
user passed --lang, and re-asked the language question in the ambiguous
and skeleton cases despite the documented 'flag > detected > default'
merge order. The flag now short-circuits the language step entirely
(detection stays informational); behavior without the flag is
unchanged.
ssh2::File's Drop discards the close-handshake error ('too late to
recover'), so a quota or server-side abort surfacing in the final ACKs
was recorded as a successful upload of a truncated file. Close upload
handles explicitly and propagate the error; also applies to the ssh
context driver's write_file and upload_recursive, which had the same
silent-drop issue.
Patterns containing a slash are anchored relative to the directory
holding the .gitignore, so 'debian/files' inside debian/.gitignore
only ever matched debian/debian/files: every generated pattern was
dead and debhelper artifacts showed up as untracked. Write the
patterns relative to debian/ instead.
IdentityFile values were stored verbatim, so the near-universal
'IdentityFile ~/.ssh/key' spelling never matched an existing file and
the key was silently skipped during authentication. Expand a leading
~ (only that form; ~user and embedded tildes stay verbatim) against
the user's home directory when parsing.
Steady-tick spinner threads redraw straight to the real stderr, bypassing
both the harness capture and the per-test log files: 'Scaffolding' lines
from the pkh new tests kept leaking between test results. The scaffold
tests now pass a hidden draw target (the only MultiProgress not created
by the CLI).
CDNs occasionally answer 200 with a zero-byte body under load; the
checksum verification then reported the empty-string hash as a mismatch,
and the by-hash retry (subject to the same glitch) silently lost its own
failure reason. Treat empty bodies as transient in both fetch paths and
append the by-hash failure to the final VerifyError.
Includes a regression test serving an empty 200 followed by a valid body
on a local socket.
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
Busy mirrors and CDNs routinely break bulk fetches: pooled keep-alive
connections get closed remotely ('error sending request'), downloads are
cut short (surfacing as bogus checksum mismatches), and index generations
momentarily drift from the Release file fetched moments before.
- shared client: short idle-pool timeout and TCP keepalive, and a
bounded-retry GET helper now used for index, Release, keyring and
Launchpad fetches (previously reqwest::get, which has no timeouts)
- downloads: retry the whole download, and check the content length so
truncation is reported as such instead of a checksum mismatch
- sources index: on a checksum mismatch against the Release file, retry
pinned to the exact listed generation via Debian's by-hash mechanism;
body-read errors are retried and reported per component instead of
aborting the whole lookup
Upload built source packages over SFTP with host-key verification
(Launchpad fingerprints pinned in host_keys.yml, ask-to-accept
otherwise), Launchpad account discovery (git config lp.user), and
pre-flight checks the upload queue itself never does: changes file
discovery/validation, PPA existence via the Launchpad API, target
series validity, and debian/control Section validity (sections
bundled in distro_info.yml). Upload log prevents duplicate uploads
unless --force.
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).
Log retention only matched 'deb-*' logs, so source-build ('build-*')
and placeholder ('pkh-*') logs accumulated forever, and the 'keep the
newest' sort was lexicographic on names that sort by package/version
first, so arbitrary logs were kept. All three log shapes are matched
now and retention orders by the timestamp embedded in the name (mtime
fallback). Stale-lockfile pruning no longer deletes lockfiles younger
than 24h: a fresh <tarball>.lock is the mutual-exclusion signal of a
concurrent download and deleting it could corrupt the shared tarball
cache.
write_paragraph emitted a bare-space continuation line for empty lines
inside a value, which parse_paragraphs treated as a paragraph separator
and silently dropped the rest of the value; blank lines are now encoded
as ' .' like dpkg does and decoded back on read. Tab-indented
continuation lines now strip exactly one tab instead of keeping it.
Clearsigned .dsc content no longer leaks armor metadata into parsed
fields: the Hash:/Comment: header and the signature trailer are
stripped before parse_paragraphs at both .dsc parse sites.
The source-build pipeline and the binNMU metadata path had drifted into
two inline parsers with different acceptance rules: binary.rs filled
names from any line with a third column but partials only from
exactly-three-column lines, so a 4+ column Checksums line made
&partials[name] panic by map index. Both paths now share one parser
that accepts the modern 3-column and the legacy 5-column Files layout,
rejects anything else with an error naming the field and line, and all
remaining lookups go through .get() with a clear error instead of
indexing. Legacy 5-column Files md5s were previously attributed to the
section token instead of the file name.
- 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.
Debian Policy 7 defines the deprecated single-character spellings as
'earlier/later or equal' (i.e. <= and >=), and dpkg still accepts them
that way; the parser mapped them to the strict << and >> instead, so
'foo (< 1.0)' was wrongly reported unmet against installed 1.0.