vhaudiquet ed76b3ef7f
CI / build (push) Failing after 54s
CI / test (push) Skipped
CI / snap (push) Skipped
CI / publish (push) Skipped
deb,build: export ddebs from binary builds
dh_strip registers debug-symbol packages in debian/files under their
renamed .ddeb name; they are deliverables of the build just like the
regular .debs. Both artifact-type filters, the one collecting the
build's artifacts and the one feeding the .buildinfo/.changes
checksum sections, accepted only deb and udeb, so every ddeb a build
produced was silently discarded: nothing exported next to the .debs,
nothing in the upload metadata.

The filter is now a shared FilesEntry::is_binary_artifact() predicate
(plus unit tests) used by both call sites, so the two cannot drift
apart again.

One dpkg distinction has to be copied rather than shared:
dpkg-genchanges lists only deb/udeb packages in the changes' Binary:
and Description (a ddeb travels in Files and the checksum sections
but is not a listed binary of the upload), while dpkg-genbuildinfo
lists every binary artifact's package, ddebs included. The package
list is therefore accumulated twice, once per consumer.

The binary-metadata differential fixture gains a Package-Type: ddeb
package, so the native .changes/.buildinfo generation is compared
against real dpkg with a ddeb present. Without the fix the comparison
fails on the checksum sections (golden distributes the ddeb, ours
dropped it); with it every field matches.
2026-09-30 14:18:45 +02:00
2026-09-21 01:36:40 +02:00
2026-09-26 11:49:08 +02:00
2026-09-26 11:49:08 +02:00

pkh

pkh is a packaging helper for Debian/Ubuntu packages.

Installation

From crates.io:

cargo install pkh

Or build from source (the same system packages are needed either way):

sudo apt install pkg-config libssl-dev libgpg-error-dev libgpgme-dev
git clone https://git.vhaudiquet.fr/vhaudiquet/pkh.git
cd pkh
cargo install --path .

At runtime pkh shells out to the Debian packaging toolchain (git, dpkg-dev, quilt, mmdebstrap, lintian, pristine-tar, autopkgtest, ...): install the ones your workflows use, or build the classic snap from snap/snapcraft.yaml (snapcraft pack), which carries them.

Usage and features

Basic concepts

pkh aims at wrapping the different debian tools and workflows (git, git-ubuntu, pull-debian-sources, pull-lp-sources, pull-ppa-sources, dch, dpkg-buildpackage, sbuild, dpkg-source, quilt, ...) into one tool, that would have the same interface for everything, while being smarter at integrating all workflows.

Thus, pkh uses similar options for all subcommands (with very few command-specific options):

Options:
  -s, --series <series>    Target package distribution series
  -d, --dist <dist>        Target package distribution (debian, ubuntu)
  -v, --version <version>  Target package version
  -a, --arch <arch>        Target architecture (amd64, arm64, riscv64, ...)
  -p, --pocket <pocket>    Target distribution pocket (updates, security, proposed, ...)
      --ppa <ppa>          Do the action in/for a specific PPA

Commands and workflows include:

Commands:
  new    Scaffold a new Debian source package (buildable right away)
  pull   Pull a source package from the archive or git
  chlog  Auto-generate changelog entry, editing it, committing it afterwards
  build  Build the source package (into a .dsc)
  put    Upload the built source package to a PPA
  deb    Build the source package into binary package (.deb)
  test   Run the package's DEP-8 as-installed tests (autopkgtests)
  lint   Lint the package (lintian wrapper + pkh-native checks)
  prune  Prune residual pkh build artifacts and caches
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Examples

A typical workflow to patch an Ubuntu package hello on the development release could be:

# Obtain the package source
git ubuntu clone hello
cd hello
# Apply the patch to the package
...
dpkg-source --commit
# Increment version number
dch
# Test that the patch builds
git ubuntu export-orig
dpkg-buildpackage -S -I -i -nc -d
sbuild --dist resolute --arch amd64 ../hello_xxx.dsc
# Upload the package to a ppa
dput ppa:user/hello_xxx ../hello_xxx_source.changes
# Commit the changes to git
git add debian/patches/xxx.patch
git commit -m "Applied patch xxx"
git add debian/changelog
git commit -m "changelog"
git checkout -b xxx
git push xxx user-fork

That is a lot of different tools and operations. With pkh, the same workflow:

# Obtain the package source (and orig tarball)
pkh pull hello # needs -d ubuntu if you are not running Ubuntu
# Apply the patch to the package
...
git add debian/patches/xxx.patch
git commit -m "Applied patch xxx"
pkh chlog
git add debian/changelog
git commit -m "d/changelog"
# Test that the package builds
pkh build
pkh deb
# Upload the package to a ppa
pkh put --ppa user/hello_xxx
# Push the commits to your fork
git push xxx user-fork

pkh chlog edits debian/changelog by default; point it at another file with --file <path> (-f).

pkh chlog walks the commit history back to the previous changelog entry. Without a version tag, that search is capped at the last 500 commits (--limit <n>, 0 = unlimited): beyond the cap, the entry simply starts at the oldest commit searched, which keeps the run fast on large histories.

Incremental builds (build records)

Every pkh deb build records its metadata under /var/tmp/pkh/sessions/<target>/<id>/: the manifest (identity, fork lineage, outcome, the sha of the chroot tarball it used) and a full log/build.log tee of the build output. The chroot itself is per-run scratch — extracted fresh from the sha-verified mmdebstrap tarball cache on every run and removed at the end, plain and --incremental alike — so records stay small. What --incremental buys is the record's staged tree (tree-upper/): the overlay upperdir where the build's writes land, holding the compiled objects.

Fresh chroots do not mean fresh downloads: every chroot's /var/cache/apt/archives is a bind mount over a shared apt download cache at /var/tmp/pkh/apt-cache/<series>-<arch>/, which the installs of one run fill and the next runs reuse (apt is configured to keep the downloaded packages, and an apt-get autoclean at the end of each run drops the debs the archive no longer serves). pkh prune removes the caches untouched for a week.

Fresh chroots do not mean fresh compiles either: every run's chroot gets a shared ccache (at /var/tmp/pkh/ccache/<target>/, mounted at /var/cache/pkh-ccache) wired in through a compiler PATH shim — gcc, g++, cc and c++ resolve to ccache — with content-keyed cache checks (CCACHE_COMPILERCHECK=content), so cache hits survive the fresh toolchain of a new chroot. The cache is capped at 10G per target (override with PKH_CCACHE_MAXSIZE); pkh deb --no-ccache bypasses the shim and cache entirely, an escape hatch for rules that mishandle compiler wrappers (kbuild-style flavour configs).

--incremental forks a previous record — a copy, the base is never modified — so a failed run can never destroy the last-good state it was built from. The staged tree is carried only when the base's recorded tarball sha still matches the cache (the toolchain did not move) and the changelog version is unchanged; otherwise the fork builds from a fresh tree:

pkh deb                 # plain run: fresh chroot, fresh tree; the
                        # record keeps only metadata and the log
                        # (nothing is kept even on failure or
                        # interrupt)
pkh deb --incremental   # fork the newest successful record of the
                        # target (among the last five carrying a staged
                        # tree) and keep this run's staged tree, for
                        # the next fork to carry
pkh deb --incremental <id>
                        # fork that exact record (see `pkh deb list`);
                        # a failed record is fine if its staged tree is
pkh deb list            # the records of this tree: id, outcome,
                        # version, fork lineage, staged-tree state,
                        # dep-install and build times, age
pkh deb log [-f] [<id>] # print a record's build log; -f follows a
                        # live build
pkh deb --no-ccache     # build without the shared compile cache

Each target keeps at most five staged trees: when a sixth is retained, the oldest degrades to a metadata-only record (manifest and log stay), and pkh prune removes records untouched for a week. pkh deb --incremental never forks a record built for a different series/architecture.

Packages whose debian/rules keeps an input-untracked stamp cache (the kernels' debian/stamps) declare it in the quirks data (resume_clear): forked builds drop the cache so the wrapper steps — including the kernel's flavour config export — re-run over the incremental inner builds, and a config edit is never silently ignored. A build killed hard (OOM) can leave its chroot behind; the next run of that target sweeps it (unmounting and escalating through sudo when needed), and pkh prune collects the rest.

On a pinned runner — a machine that must resolve every package from one mirror, typically a caching proxy — pkh deb --mirror <url> builds the chroot and resolves every package from that base URL: the mirror is passed to mmdebstrap at bootstrap, so the chroot tarball's apt sources carry it and every apt inside the build follows. The mirror must serve every suite and architecture the build resolves (the security pocket and, for --cross builds, the target architecture's archive included); PPAs and other third-party sources are left alone. The tarball cache is keyed per mirror, so pinned and unpinned builds keep separate tarballs (and --incremental records never fork across a mirror change).

Running the DEP-8 tests

pkh test runs the package's as-installed tests (debian/tests/control, declared by Testsuite: autopkgtest in debian/control) through a native runner on a pkh testbed, and reports one verdict per test:

pkh test                 # test this tree: builds it first if needed
pkh test --mode local    # on the current context, no isolation
pkh test --mode vm --image img.qcow2   # in a VM, via autopkgtest
pkh test --json --fail-on skip         # CI-shaped
pkh test --list-tests    # what does this package test?

By default the tests run in an ephemeral unshare chroot bootstrapped from the same cached tarballs as pkh deb, against the binaries pkh deb just built next to the tree (stale or missing output is rebuilt first; --no-build refuses, --debs overrides). Pockets and PPAs resolve test dependencies like in pkh deb (-p, --ppa), and --test-name, --skip-test, --shell-fail and --setup-commands cover the day-to-day debugging loop. Tests needing a real machine (isolation-machine, needs-reboot) run only in --mode vm, which execs the installed autopkgtest with its qemu runner.

Exit codes: 0 all tests passed (skips and flaky allowed), 1 at least one test failed (or a --fail-on trigger), 2 runtime errors (no declared tests, no usable testbed, ...).

Future improvement ideas

  • pull: try to fetch the correct git branch for series on Debian
  • deb: asynchronous build, detachable and monitorable
  • put: allow uploads to Debian or Ubuntu archives
  • pull: cache Sources.gz files to improve speed
  • pull: 'pkh pull' in a package tree should git pull and re-fetch orig tgz
S
Description
pkh is a packaging helper for Debian/Ubuntu packages
Readme
3.7 MiB
Languages
Rust 98.2%
Python 1.7%