Commit Graph
247 Commits
Author SHA1 Message Date
vhaudiquet 27ab4cb9ad put: fix ssh_config negation semantics and file precedence
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).
2026-09-17 17:52:17 +02:00
vhaudiquet 3501096107 new: keep flat-tarball entries when repacking the orig tarball
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.
2026-09-17 17:45:25 +02:00
vhaudiquet 174a13df39 new: re-probe wizard defaults when packaging a different directory
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.
2026-09-17 17:14:25 +02:00
vhaudiquet dd006f7b80 new: validate the command name before generating files
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,
+ - . _).
2026-09-17 17:03:43 +02:00
vhaudiquet d5b76ec8d8 new: validate wizard defaults like typed answers
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.
2026-09-17 17:00:22 +02:00
vhaudiquet 70e375a34d new: let an explicit --lang win over build-system detection
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.
2026-09-17 16:44:35 +02:00
vhaudiquet 8ad50aaf83 put: check the SFTP close status after uploads
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.
2026-09-17 16:36:36 +02:00
vhaudiquet efb18bfa37 new: fix dead patterns in the generated debian/.gitignore
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.
2026-09-17 16:33:02 +02:00
vhaudiquet 57db98d776 put: expand ~ in ssh_config IdentityFile paths
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.
2026-09-17 16:31:04 +02:00
vhaudiquet 775e3d3b8a fmt 2026-09-17 16:27:34 +02:00
vhaudiquet a7cd4244b2 test: hide progress spinners in test runs
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).
2026-09-17 15:32:55 +02:00
vhaudiquet d2bb311f74 net: retry empty index bodies and report by-hash failures in the error
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.
2026-09-17 15:32:55 +02:00
vhaudiquet 4c26122357 test: keep cargo test output quiet with per-test logs and a failure matrix
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
2026-09-17 15:18:23 +02:00
vhaudiquet 3ed95725e4 net: retry flaky archive fetches and pin index downloads via by-hash
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
2026-09-17 15:18:14 +02:00
vhaudiquet afedde1f2b new: keep generated builds away from local build outputs and vendored autotools files
CI / build (push) Failing after 2m55s
CI / test (push) Skipped
CI / snap (push) Skipped
2026-09-17 11:33:33 +02:00
vhaudiquet bc3d07abaa new: ignore vendored rust artifacts in the generated gitignore 2026-09-17 10:37:58 +02:00
vhaudiquet bac82f0afe new: skip the git-init question inside existing repositories 2026-09-17 10:37:54 +02:00
vhaudiquet 05e7c55d32 new: detect downloaded tarballs by magic bytes, not extension 2026-09-17 02:15:23 +02:00
vhaudiquet f055b70281 deb: skip quilt patch application for single-debian-patch trees 2026-09-17 02:15:15 +02:00
vhaudiquet 77420e723a new: add upstream-aware orig tarball origins and the orig-vendor component 2026-09-17 01:31:35 +02:00
vhaudiquet 8e06b2074d new: flag rust-toolchain.toml pins in pkh new 2026-09-16 23:59:16 +02:00
vhaudiquet 84824f61c6 new: surface cargo vendor failures and pin the vendoring toolchain 2026-09-16 23:24:43 +02:00
vhaudiquet 9cb3e29a3e new: add Standards-Version to generated control 2026-09-16 22:52:21 +02:00
vhaudiquet c0b7e341fc ui: cleaner transient and completion output for pkh put
CI / build (push) Failing after 2m51s
CI / test (push) Skipped
CI / snap (push) Skipped
2026-09-16 22:43:09 +02:00
vhaudiquet e8d4b98f52 put: verify the changes signature locally with gpgme 2026-09-16 22:24:53 +02:00
vhaudiquet 0c2cf0ac5e put: refuse uploads superseded by published PPA versions 2026-09-16 21:51:33 +02:00
vhaudiquet f27d27ea99 new: add pkh put, a native dput replacement for PPA uploads
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.
2026-09-16 21:38:53 +02:00
vhaudiquet 9228ff448b new: give wizard select labels their own separator
CI / build (push) Failing after 2m47s
CI / test (push) Skipped
CI / snap (push) Skipped
2026-09-16 14:01:23 +02:00
vhaudiquet 9b98f5c7c3 new: add interactive wizard and remaining ecosystem templates 2026-09-16 13:49:29 +02:00
vhaudiquet d044f757e9 new: scaffold new Debian source packages (non-interactive core) 2026-09-16 12:14:09 +02:00
vhaudiquet 9c3394750d distro_info: accept case-insensitive dist in effective_series, fix UNRELEASED fallbacks 2026-09-16 12:12:15 +02:00
vhaudiquet 60976d3feb ui: generalize interactive prompts into ui::prompt 2026-09-16 11:15:31 +02:00
vhaudiquet ae420989f9 distro_info: add UNRELEASED series handling, use it in deb and chlog 2026-09-16 11:15:09 +02:00
vhaudiquet 4af8dbddb0 fmt: fix clippy warnings on all targets
CI / build (push) Successful in 2m48s
CI / test (push) Skipped
CI / snap (push) Failing after 11s
Drop an unused test fixture constant, move download_trust_ppa_key above
the test module that precedes it, and simplify two test borrows.
2026-09-16 09:15:52 +02:00
vhaudiquet 3a454b0811 build: record the actual build environment in .buildinfo
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).
2026-09-16 04:22:43 +02:00
vhaudiquet 50ae12cafe prune: match real log names, order retention by time, spare fresh locks
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.
2026-09-16 04:04:50 +02:00
vhaudiquet 38562abe2c debian: fix deb822 writer/parser asymmetries
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.
2026-09-16 03:42:30 +02:00
vhaudiquet dc6a019a13 build: parse .dsc checksum fields through one shared, validating parser
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.
2026-09-16 03:20:01 +02:00
vhaudiquet f72b35acfa Handle malformed remote and edge-case data instead of panicking
- 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)
2026-09-16 02:44:45 +02:00
vhaudiquet 6a5c5a7106 context: fix deadlock and panics when resolving parented contexts
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.
2026-09-16 02:27:11 +02:00
vhaudiquet 592e98c1e9 deb: pass the build context explicitly instead of swapping the global
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.
2026-09-16 02:05:40 +02:00
vhaudiquet 512a1cb778 deb: unmount and remove the ephemeral chroot on Ctrl-C
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.
2026-09-16 01:55:35 +02:00
vhaudiquet 93176aa479 deps: give legacy < and > relations their documented dpkg semantics
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.
2026-09-16 01:30:25 +02:00
vhaudiquet 60ee99adc8 pull: search pockets in release order by default
Without an explicit --pocket, find_package stops at the first pocket
containing the package, but the search order listed '-proposed' first
and never included '-security': unreleased proposed packages won by
default and security-only updates were unreachable. Search the main
archive first, then updates, security, and proposed last.
2026-09-16 01:27:25 +02:00
vhaudiquet f6fed7328b pull: reject malformed --ppa values instead of silently using the archive
A --ppa value that was not exactly 'user/name' (full URL, extra
segment, empty halves) made base_url None and pulled the package from
the main archive without any warning. Error out naming the expected
format instead, and document the format in --help.
2026-09-16 01:24:02 +02:00
vhaudiquet 213668fa82 pull: authenticate archive indexes against signed Release files
The Sources index was downloaded with no authentication: per-artifact
checksums were verified, but against hashes taken from an index a MITM
could substitute along with the artifacts. Fetch each suite's InRelease
(or Release + Release.gpg), verify the signature with gpgv against the
archive keyring (or the PPA signing key) the same way apt does, and
checksum-check every Sources index against it before parsing.

Distro archives and PPAs verify strictly: an invalid or unverifiable
signature, or a missing gpgv binary, is a hard error. Flat repositories
keep working without a Release file or without a verifiable one (warned
as unauthenticated), but tampering evidence is a hard error there too.

Also switches all archive, PPA and keyring base URLs to https, and
reads suite components from the verified Release instead of fetching
them separately over an unauthenticated channel.
2026-09-16 01:22:22 +02:00
vhaudiquet f508f20846 context: quote program, args, cwd and env in ssh, schroot and unshare drivers
All three non-local drivers assembled remote/chroot command strings by
raw concatenation: ssh pushed args verbatim (TODO: escape), schroot
interpolated env values raw so DEB_BUILD_OPTIONS='parallel=4 nocheck'
made sh treat 'nocheck' as the command, and unshare wrapped args in
unescaped double quotes letting quotes break out and $/backticks
expand. Add a shared POSIX shell_quote helper and use it for every
component interpolated into a shell string, including ssh copy_path
(which used Rust's {:?}, not shell quoting) and schroot write_file
(which also switches echo -ne to printf %s so backslash sequences in
content are no longer interpreted).
2026-09-16 00:36:02 +02:00
vhaudiquet 685538e637 apt: round-trip sources in place instead of consolidating them into sources.list
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.
2026-09-16 00:10:45 +02:00
vhaudiquet ea70ddc10d apt: keep the keyring cache private to the invoking user
The shared world-writable /tmp/pkh-keyrings directory, combined with the
skip-if-exists logic, let any local user pre-plant keyrings that pkh
then trusts into the chroot's trusted.gpg.d. Use a per-uid 0700
directory instead, refuse to reuse a pre-existing directory that is not
owned by the current user or is group/other-writable, and drop the now
unnecessary world-accessibility chmods (mmdebstrap in unshare mode runs
with the same real uid).
2026-09-15 23:47:21 +02:00
vhaudiquet 48f6e6ce4e pull: confine tar extraction to the destination directory
Entry::unpack performs no path sanitization, so a malicious or malformed
tarball (PPA, flat repository) could write files outside the package
directory via '..' components or absolute entry paths. Refuse such
entries with an error naming the offending path.
2026-09-15 23:30:28 +02:00