Replace dpkg-checkbuilddeps with a native implementation: - full dependency grammar: comma clauses, | alternatives, << <= = >= >> relations, :arch qualifiers (any/native/specific), [arch lists] and <profile restriction> formulas per alternative; - restriction reduction against active build profiles and the host arch at parse time (reduce_restrictions semantics); - evaluation against a parsed dpkg status database with Multi-Arch semantics (foreign/allowed) and versioned Provides rules (unversioned provides never satisfy versioned deps; versioned ones must satisfy the relation); - clause simplification with implication-based deduplication, rendering dpkg-compatible 'unmet build dependencies/conflicts' diagnostics. check_build_depends() consumes debian/control + CheckOpts (-A/-B/-I equivalents). run_source_build performs the check when forced (-D parity); source-only builds skip it entirely like dpkg-buildpackage, and unsatisfied deps propagate as UnmetBuildDependencies -> exit 3. Unit tests port the Dpkg_Deps.t reduction matrices; differential gate runs 24 scenarios (alternatives, versions, arch/profile restrictions, Multi-Arch, Provides, conflicts, -A/-B flags) against real dpkg-checkbuilddeps comparing exit status and diagnostics.
23 KiB
Native build pipeline — replacing the dpkg-buildpackage shell-out
Status: Phase 0 + Phase 1 (source builds) implemented — see §11
Reference codebase: https://salsa.debian.org/dpkg-team/dpkg (main branch, analyzed 2026-08)
0. TL;DR — honest take
| Scope | Verdict | Effort | Risk |
|---|---|---|---|
A. Re-implement the orchestrator (dpkg-buildpackage.pl, 1209 lines of Perl) natively in Rust, still invoking dpkg-source, debian/rules, etc. as subprocesses |
Doable, worth it | ~2–4 weeks | Low–medium |
B. Additionally replace the cheap satellite tools natively (dpkg-parsechangelog, dpkg-checkbuilddeps, dpkg-architecture, dpkg-genchanges, dpkg-genbuildinfo) |
Doable incrementally | +1–2 weeks each | Medium |
C. Replace dpkg-source (tarball/diff generation, quilt integration, .dsc assembly) |
Not recommended | 2–3+ months, then endless edge cases | High |
The key insight: dpkg-buildpackage itself is a thin (~1200-line) Perl sequencer. The real complexity lives in dpkg-source (~4000 lines of Perl across Dpkg/Source/*). A source build fundamentally requires dpkg-source-class functionality (orig tarballs, debian diffs, .dsc assembly), so "no dpkg tools at all" is not a realistic goal — but "no dpkg-buildpackage, full control of the pipeline" absolutely is, and it unlocks things pkh currently cannot do (live UI on source builds, .changes/.buildinfo generation on binary builds, unified error classification).
1. Where pkh stands today
Two divergent build paths:
Source builds — build_source_package()
Shelled out on the host:
dpkg-buildpackage -S -I -i -nc -d [--sign-keyid=<id> | --no-sign]
Problems:
- Requires
dpkg-devon the host. - Output bypasses the live UI entirely (
DebUi/LineSink) — no phase tracking, no log classification, unlike binary builds. - No control over failure semantics beyond exit status.
Binary builds — deb::local::build()
Already does not use dpkg-buildpackage. Hand-rolled sequence: ephemeral unshare chroot → apt-get update → essentials → manual quilt push -a → apt-get build-dep → debian/rules build → fakeroot debian/rules binary → retrieve .debs.
What it silently skips compared to a real build:
debian/rules cleanbefore building,dpkg-source --before-build/--after-buildlifecycle (it re-implements patch application crudely),.buildinfoand.changesgeneration,- build-conflicts checking,
Rules-Requires-Rootsemantics, SOURCE_DATE_EPOCHexport (reproducibility).
So pkh already pays part of the "native orchestrator" cost without getting its benefits. A native implementation would unify both paths.
Existing native assets to reuse
- Changelog header/footer parsing —
changelog.rs(needs extension to full metadata: timestamp, urgency, trailer key/values). - GPG key discovery —
utils/gpg.rsviagpgme. - Hashing crates already present:
sha2,md-5(need to addsha1); compression:flate2,xz2; archives:tar. - Command execution through remote-capable contexts —
ContextCommand(local/ssh/schroot/unshare/capture). - Live UI phases —
ui/deb.rs.
2. What dpkg-buildpackage actually is (measured)
It is a Perl script (use v5.36). Measured sizes on main:
| Component | Lines | Role |
|---|---|---|
scripts/dpkg-buildpackage.pl |
1209 | Orchestrator |
scripts/dpkg-source.pl |
806 | Driver; work in Dpkg/Source/* |
scripts/dpkg-genbuildinfo.pl |
637 | .buildinfo generation |
scripts/dpkg-genchanges.pl |
626 | .changes generation |
scripts/dpkg-architecture.pl |
498 | Arch name/triplet tables |
scripts/dpkg-gensymbols.pl |
398 | (called by rules, not by us) |
scripts/dpkg-checkbuilddeps.pl |
270 | Dep checking vs status file |
scripts/dpkg-parsechangelog.pl |
195 | Changelog CLI wrapper |
scripts/dpkg-distaddfile.pl |
99 | Registers files in debian/files |
Dpkg/*.pm + Dpkg/Source/*.pm (subset examined) |
~11,600 | Shared library code |
Total relevant Perl surface ≈ 16k LOC, of which the orchestrator is only ~8%.
3. The exact pipeline (verified against source)
What dpkg-buildpackage does, in order:
- Config: load
buildpackage.conf(Dpkg::Conf), inject as leading argv. - Option parsing (~200 lines): build types
-F/-g/-G/-b/-B/-A/-S(+--build=full,source,binary,any,all), signing (-us/-uc/-ui/-k/-p/--no-sign/--force-sign),-j/-J/--jobs-force,-r<root-cmd>,-R<rules>,-T<targets>,-a/-t/--target-arch,-P<profiles>,-d/-D,-nc/-tc, hooks--hook-<name>=<cmd>, passthrough buckets fordpkg-source/dpkg-genchanges/dpkg-genbuildinfo. - Build type → rules targets:
binary→build+binary; arch-dep only→build-arch+binary-arch; indep only→build-indep+binary-indep. - Implied flags:
-ncalone implies-b;-nc -Sdisables build-dep checks. - Environment prep:
parallel=autodefault →DEB_BUILD_OPTIONS=parallel=Nexported;- forced jobs additionally appended to
MAKEFLAGS; DEB_BUILD_PROFILESexported if-P;- optional
.dscinput →dpkg-source --extractfirst; SOURCE_DATE_EPOCH ||= changelog timestamp || time()(reproducible-builds.org spec);- full env dump of
dpkg-architecture -f [-a…][-t…]imported into the environment (allDEB_BUILD_*,DEB_HOST_*,DEB_TARGET_*incl.*_OS/CPU/MULTIARCH/GNU_TYPE/ARCH_BITS/ENDIAN); - OpenPGP key resolution:
--sign-keyfile>--sign-keyid> maintainer userid; secrets probed up-front; UNRELEASED distribution ⇒ signing disabled unless--force-sign.
- Pre-flight:
Dpkg::BuildDriver->pre_check()(rules file exists/executable);dpkg-source --before-build .(applies patches for quilt formats);dpkg-checkbuilddeps [-A|-B|-I]unless-d(exit 3 on unmet). - Hooks at 12 points:
preinit init preclean source build binary buildinfo changes postclean check sign done, with%p/%v/%s/%u/%asubstitution. - Preclean:
debian/rules cleanvia BuildDriver (gain-root perRules-Requires-Root). - Source build (if any SOURCE component):
dpkg-source -b .→.dsc+ tarballs in... - Binary build (if any BINARY component):
run_build_task(build-target)thenbinary-targetthrough BuildDriver (RRR-aware root command; skips separate non-rootbuildpass when running rootless). - Metadata generation:
dpkg-genbuildinfo→../<pkg>_<ver_noepoch>_<arch>.buildinfo(Format 1.0; recordsInstalled-Build-Dependssnapshot from the dpkg status DB, build environment, checksums);dpkg-genchanges→../<pkg>_<ver_noepoch>_<arch>.changes(Format 1.8; aggregatesdebian/files+ changelog + control).
- Post: optional
-tcclean;dpkg-source --after-build .(unapplies patches it applied); human summary ("full upload (original source is included)" etc. derived from theFilesfield); optional lintian-style check command. - Signing cascade (inline/clearsig via OpenPGP backend gpg|sequoia|sop):
- sign
<pkg>_<ver>.dsc→ recompute its checksums inside.buildinfo; - sign
.buildinfo→ recompute dsc+buildinfo checksums inside.changes(rewriting bothChecksums-*and legacyFilesentries); - sign
.changes.
- sign
Exit codes matter: e.g. unsatisfied build-deps ⇒ exit 3.
4. Sub-tool inventory and replacement strategy
| Tool | Used for | Complexity to replace natively | Strategy |
|---|---|---|---|
dpkg-parsechangelog |
source/version/maintainer/distribution/timestamp | Low — documented format; crates exist (debian-changelog, deb822-parser ecosystem) |
Replaced (see §11, metadata.rs) |
dpkg-version compare |
epoch/upstream/revision ordering | Low — small well-specified algorithm; crate debversion |
Replaced (§11, debian/version.rs: Ord/compare/later_than) |
dpkg-architecture |
arch ↔ triplet tables, multiarch tuple, env dump | Low-medium — embed cputable/ostable/tupletable/abitable data (stable for years) | Replaced (§11, debian/arch.rs) |
dpkg-checkbuilddeps |
deps vs installed status | Medium — Dpkg::Deps grammar (alternatives, arch qualifiers, <profiles> restrictions, versioned Provides subtleties, Multi-Arch facts) + status-file scan |
Replaced (§11, debian/deps.rs; wired into the pipeline behind -D, source-only builds skip it like dpkg-buildpackage) |
dpkg-genbuildinfo |
.buildinfo |
Medium — deb822 emit + status snapshot + checksums | Native (see §11, buildinfo.rs) |
dpkg-genchanges |
.changes |
Medium — deb822 emit + debian/files consumption + .deb control extraction (ar+tar, trivial with crates) |
Native for source uploads (§11, changes.rs); binary aggregation next |
dpkg-distaddfile/debian/files protocol |
build outputs registry | Trivial — one append-only line format | Native (files.rs) |
| OpenPGP signing | inline clearsign of dsc/buildinfo/changes | Low — gpgme (already a dependency) supports clearsigning |
Native (sign.rs) |
dpkg-source |
orig tarball, debian diff, patches, .dsc |
Very high — V1/V2/quilt/native formats, byte-exact tar normalization, quilt bookkeeping, --include-binaries, hundreds of validation warnings |
Keep as subprocess (see §6) |
debian/rules execution |
the actual build | N/A (foreign code) | Keep, via ContextCommand |
5. Feasibility detail per scope
Scope A — native orchestrator (recommended)
The 1209-line script decomposes into clean Rust pieces:
src/build/
├── mod.rs // pipeline driver (the equivalent of main())
├── buildtype.rs // BUILD_SOURCE|ARCH_DEP|ARCH_INDEP bitflags + target mapping
├── metadata.rs // changelog + control resolution, version splitting
├── env.rs // SOURCE_DATE_EPOCH, DEB_BUILD_OPTIONS, arch env dump
├── buildinfo.rs // native .buildinfo writer
├── changes.rs // native .changes writer
├── files.rs // debian/files registry emulation
├── checksums.rs // md5/sha1/sha256 registry
├── control.rs // deb822 parser/writer
└── sign.rs // gpgme clearsign + post-sign checksum cascade
Estimated ~1.5–3 kLOC. Everything is deterministic file munging + subprocess sequencing — no daemons, no parsing of arbitrary upstream code (that's dpkg-source's job, which we keep).
pkh-specific simplifications (legitimate because pkh controls the environment):
- Builds run as real root inside ephemeral unshare chroots ⇒ the entire gain-root/fakeroot matrix collapses: run
debian/rulesdirectly when the context is root (keepfakerootfallback for host-side source builds). - pkh's option surface is a fraction of dpkg's:
-S/-b/-B/-A/-g/-G,-us/-uc,-k,-j,-a,-P,-d/-D,-nc/-tc,-R,-Tcover everything pkh passes today plus obvious headroom. Hooks and--hook-*can be dropped initially. - Vendor hooks (
run_vendor_hook) are rarely used in the build path; Ubuntu/Debian differences pkh cares about are already handled viadistro_info.yml.
What this buys pkh concretely:
- Live UI + log classification for source builds (today's
-Spath is a black box). .changes/.buildinfofor binary builds — currently missing entirely; needed for uploads/PPA submissions and lintian checks.- Correct
dpkg-source --before-build/--after-buildlifecycle replacing the manualquilt push -ahack inlocal.rs(handles3.0 (quilt)properly, incl. unapply-on-exit and format detection instead of sniffingdebian/patches/series). SOURCE_DATE_EPOCHreproducibility for free.- No host
dpkg-devrequirement for orchestration decisions;dpkg-sourcestill needed inside build environments where pkh already installs packages anyway.
Scope B — satellite tools
Incremental, each independently testable against the real tool (differential testing). Crates from the rust-debian-* ecosystem (maintained by Jelmer Vernooij) cover most parsing: deb822-parser, debian-control, debian-changelog, debversion. Priority order if pursued: version compare → changelog → architecture tables → checkbuilddeps.
Scope C — dpkg-source
The honest numbers: Dpkg/Source/Package/V2.pm alone is 847 lines, V1 is 599, plus Archive/Quilt/Functions/BinaryFiles modules, GNU diff generation, and — critically — byte-level tar normalization (mtime/uid/gid/mode canonicalization, pax header handling) that reproducible builds depend on. Parity means being bug-compatible with dpkg against ~40k archive source packages. This is a multi-month project with a long tail, and it buys pkh almost nothing since dpkg-source is guaranteed present inside the very chroots pkh creates. Do not do this.
6. Proposed phasing
Phase 0 — quick win (days)
Route the existing dpkg-buildpackage -S call through the UI capture machinery (like cap() does for binary builds): phase display + LineSink classification. No behavior change otherwise.
Phase 1 — native orchestrator (2–4 weeks)
Implement §5 scope A. Both entry points converge:
pkh build -S→ native pipeline,dpkg-source -bsubprocess, native signing.pkh build(binary) → same pipeline skeleton inside the ephemeral context: preclean → before-build → dep check →rules build→rules binary[-arch|-indep]→ native buildinfo/changes → after-build. Deletes the manual quilt logic.
Deliverables:
src/build/*module tree (§5).- Native
.buildinfo(Format 1.0) and.changes(Format 1.8) writers emitting exactly the field sets observed indpkg-genbuildinfo.pl/dpkg-genchanges.pl, in dpkg's canonical field order:- buildinfo:
Format, Source, Binary, Architecture, Version, Binary-Only-Changes, Checksums-Md5/Shа1/Sha256, Build-Origin, Build-Architecture, Build-Kernel-Version(opt), Build-Date, Build-Path(opt), Build-Tainted-By(opt), Installed-Build-Depends, Environment - changes:
Format, Date, Source, Binary, Built-For-Profiles, Architecture, Version, Distribution, Urgency, Maintainer, Changed-By, Description, Changes, Checksums-Sha1/Sha256, Files
- buildinfo:
- Post-signature checksum cascade implemented exactly as dpkg does (sign dsc → patch buildinfo checksums → sign buildinfo → patch changes
Files+Checksums-*→ sign changes). - Differential test harness (§8).
Phase 2 — satellite replacement (optional, incremental)
Swap subprocesses for native implementations, one tool at a time, gated by differential tests. Start with version-compare + changelog (already half-present in pkh).
Explicitly out of scope
dpkg-source internals, dpkg-deb packing, apt resolution (pkh correctly delegates to apt-get build-dep + dose3 for explanations already).
7. Tricky details to get right (gotchas list)
- Version splitting:
<epoch:>upstream<-revision>;.dsc/tarball names use upstream portion without epoch;.changesname uses version without epoch but with revision ($sversionin the script). -ncimplication chain:-nc⇒ binary build implied;-nc -S⇒ no dep check.- UNRELEASED ⇒ auto-disable all signing (warn),
--force-signoverrides. - Signing invalidates checksums transitively (dsc → buildinfo → changes); get the cascade order right or archive tools reject the upload.
debian/filesis the contract betweendebian/rules(viadh_builddeb/dpkg-gencontrol/dpkg-distaddfile) and the changes generator: lines offilename section priority [key=value...].- Arch selection for names:
archsuffix in artifact filenames ishost-archfor arch-dep builds,allfor indep-only,sourcefor source-only. dpkg-source --before-buildmust run even for binary-only builds (patch application), and--after-buildat the end — this replaces pkh's current manual quilt step and fixes3.0 (quilt)correctness.- Environment parity:
dpkg-architecture -fdump must be imported wholesale (not cherry-picked) — packages testDEB_HOST_GNU_TYPE,DEB_BUILD_MULTIARCH,DEB_BUILD_ARCH_ENDIAN, etc. If embedding tables later, mirror the full variable set. - Exit codes: preserve dpkg conventions (3 = unmet build-deps) so wrappers/scripts behave identically.
- Locale: dpkg sets
LANG=C-ish determinism for subprocesses — pkh already does this inlocal.rs; keep for all pipeline steps. - Multiline field rendering: values starting with
\nrender asField:+ indented continuation lines (no inline first line, no trailing space) — this is how dpkg emitsChanges,Files,Installed-Build-Depends,Environment. - Artifact ordering in
Checksums-*/Filesis insertion order (dsc → tarballs in dsc-field order → debs → buildinfo), not alphabetical.
8. Testing strategy
- Differential harness: run real
dpkg-buildpackageand the native pipeline over a corpus (packages covering:1.0non-native,3.0 (quilt)with/without patches, native, binaries-only, indep-only, cross, RRR variants, UNRELEASED) and diff artifacts modulo timestamps/signatures. - Port unit cases from dpkg's own
t/tests for version compare, deps parsing, changelog parsing. - Golden-file tests for
.changes/.buildinfowriters. - Pin the reference dpkg version in CI commentary (behavior drift across dpkg releases is the main maintenance cost).
9. Risks
| Risk | Mitigation |
|---|---|
| Behavior drift vs future dpkg releases (new fields, format bumps) | Differential tests pinned to a reference version; changes/buildinfo formats are extremely stable (1.8 / 1.0 for years) |
Archive/upload tooling rejects our .changes/.buildinfo |
Validate with lintian + a real PPA upload early in Phase 1 |
| License contamination | dpkg is GPL-2+. Write from documented behavior/format specs and observation, do not transliterate Perl; alternatively accept GPL for pkh (currently no license field in Cargo.toml — decision needed) |
| Scope creep toward Scope C | Hard rule: dpkg-source stays a subprocess |
| Remote-context divergence | All tree-touching steps go through ContextCommand; metadata steps operate on locally-synced copies like changelog.rs already does |
10. Effort summary
| Item | Estimate |
|---|---|
Phase 0 (UI capture for -S) |
1–2 days |
| Phase 1 (native orchestrator + buildinfo/changes/signing) | 2–4 weeks |
| Phase 2 per satellite tool | 3 days – 2 weeks each |
Scope C (dpkg-source) |
2–3+ months — rejected |
Bottom line: yes, it's doable — for the orchestrator. Treat dpkg-source as a permanent subprocess dependency, and the task becomes a well-bounded, high-value refactor that also fixes real gaps in pkh's binary path.
11. Implementation status (Phase 1 — source builds)
Landed in src/build/ (~2.8 kLOC incl. tests), wired behind the historical
entry point build_source_package() so pkh build
now runs the native pipeline:
| Module | Role |
|---|---|
control.rs |
deb822 paragraph parser/writer (dpkg-compatible multiline rendering) |
checksums.rs |
md5/sha1/sha256 registry, insertion-ordered like dpkg's artifact accumulation |
metadata.rs |
version splitting/validation, full changelog entry parse (incl. binNMU binary-only), control info |
buildtype.rs |
build-type bitflags + rules-target/artifact-suffix mapping |
env.rs |
SOURCE_DATE_EPOCH, DEB_BUILD_OPTIONS, dpkg-architecture env dump, vendor/profiles, sanitized Environment field |
files.rs |
debian/files registry (parse/atomic save) |
buildinfo.rs |
.buildinfo writer + Installed-Build-Depends closure over the dpkg status DB |
changes.rs |
.changes writer (canonical field order, legacy Files + Checksums-Sha1/256) |
sign.rs |
gpgme clearsigning + key-id validation |
Still delegated to subprocesses: dpkg-source -b/--before-build/--after-build
(by design, see §5 Scope C).
Differential validation — automated in
build/mod.rs (mod differential_tests, runs by
default with cargo test --lib):
- a corpus of 12 synthetic fixtures covering: native/quilt/1.0 formats,
epochs,
~pre-releases, Ubuntu/Debian series (focal, noble, jammy, trixie, unstable), high urgency, multiple binary stanzas, one/two quilt patches, binNMU (binary-only=yes+ previous-entry metadata),(Closes: #…)extraction, UNRELEASED no-sign, extra source-stanza fields; - each case builds the tree twice (
cp -acopies) — once with realdpkg-buildpackage -S -I -i -nc -d --no-sign, once with the native pipeline — then compares:.dscpayload byte-for-byte,.changesfield-by-field (checksum lines of the.buildinfoitself excluded),.buildinfostructure (machine-dependent fields excluded);
- real archive packages are pulled with pkh's own
pull(archive download mode) and compared the same way, viadifferential_real_archive_package()which takes(package, dist, series):- always-on CI tests:
hello@ ubuntu/noble,dosfstools@ debian/trixie,sl@ ubuntu/focal; - an additional
#[ignore]-gated test for ad-hoc broad runs:PKH_DIFF_PACKAGES="bash coreutils curl" PKH_DIFF_DIST=ubuntu \ PKH_DIFF_SERIES=noble cargo test --lib \ differential_real_archive_packages -- --ignored
- always-on CI tests:
Manual validation additionally confirmed: signed builds verify with
gpg --verify on all three artifacts; the patch apply/unapply lifecycle is
correct for quilt formats.
Known divergences (documented, all informational fields): no
Build-Tainted-By (vendor hook), no dpkg-buildflags origin tracking in the
Environment field, vendor default profiles approximated
(derivative.ubuntu noudeb for Ubuntu).
Next steps: binary-build adoption of the same pipeline inside ephemeral
contexts (.changes/.buildinfo generation for pkh deb, replacing the
manual quilt step), then Phase 2 satellite replacement.