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.