Compare commits

..
6 Commits
Author SHA1 Message Date
vhaudiquet 46407375d0 deb/cross: stop exporting PKG_CONFIG_LIBDIR for cross builds
CI / build (push) Successful in 3m13s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m15s
dpkg-buildpackage and sbuild export no pkg-config redirection: the
environment is the package's to set.  Pointing the whole build at the
target multiarch pkgconfig dirs makes every pkg-config-consuming tool
resolve against the target libraries, the host-side tools of the same
build included: the host linker is then handed target-arch -L paths
(the kernel's resolve_btfids logging 'skipping incompatible
/usr/lib/aarch64-linux-gnu/libelf.so') and only survives through
fallbacks when the build-architecture libraries happen to be installed
too.

Packages whose cross builds need target pkg-config data must arrange
for it themselves, the way the kernel packaging builds rtla statically
against no target system libraries.
2026-09-25 20:54:06 +02:00
vhaudiquet ce19c5648b quirks: inject the kernel's host-tool libraries as :native for cross
The resolute and stonking kernels declare libelf-dev, libdw-dev and
libssl-dev unqualified, unlike the Debian control which carries the
same names qualified :native for the host-side tools (resolve_btfids,
gendwarfksyms, sign-file).  The dpkg cross rules resolve an unqualified
Multi-Arch: same name against the host architecture only, so a cross
build installs no build-architecture variants and the kernel's
host-side tools cannot link (-ldw, -lelf, -lssl all fail).

Inject the :native variants for the affected series — the declared
unqualified dependencies stay, so the target-side tools keep their
host-architecture libraries.  Series-scoped so the entry can be
dropped once the control is fixed upstream.
2026-09-25 20:53:10 +02:00
vhaudiquet 99c6f63cd4 deb: pin the cross variant choice of M-A:same build-deps in tests
An unqualified Multi-Arch: same build-dependency resolves to the
host-architecture variant only, even when a build-architecture
candidate exists: that is what dpkg's checker accepts (KnownFacts
accepts foreign, host and all instances) and what sbuild installs,
whose dummy package carries the parsed Build-Depends unqualified and
whose sbuild-cross-resolver only filters foreign M-A:foreign and
Essential packages out of the apt universe — verified by resolving a
sbuild-style dummy:arm64 against the resolute/stonking indexes, which
yields libelf-dev:arm64 & co and never the amd64 variants.

The build-architecture side of such a library is only ever installed
through an explicit :native declaration.  Pin both rules so a resolver
regression towards dual-installing unqualified dependencies cannot
reappear unnoticed.
2026-09-25 20:53:02 +02:00
vhaudiquet 4b52f8e9e9 docs: add crates.io installation instructions
CI / build (push) Successful in 3m1s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m26s
pkh v0.1.0 is published to crates.io, so cargo install pkh is now the
primary installation path; building from source stays as the
alternative. Drop the 'no distribution channel' notice.
2026-09-24 22:19:24 +02:00
vhaudiquet 6df490cf9a ci: install build-essential in the publish job
CI / build (push) Successful in 3m3s
CI / test (push) Skipped
CI / snap (push) Successful in 6m9s
CI / publish (push) Successful in 2m11s
cargo publish verifies the packaged tarball with a full build before
uploading, and that build needs a C linker for build-script crates
(proc-macro2 was the first to fail). The job's container setup step
omitted build-essential, unlike the build and test jobs, so the
verification failed with 'linker cc not found' on the v0.1.0 tag.
2026-09-24 21:41:15 +02:00
vhaudiquet f5b7704647 data: add an agent SKILL.md teaching pkh usage
CI / build (push) Successful in 3m1s
CI / test (push) Skipped
CI / snap (push) Successful in 6m1s
CI / publish (push) Failing after 53s
Add data/skill/SKILL.md in the agent-skills open format: a skill
directory that agents (Claude Code, Codex, OpenCode, ...) discover and
load on demand. It documents the shared option surface, the pull,
chlog, build, deb, lint, put workflow and the flags that keep runs
non-interactive; the command reference was generated from the live
--help output of every subcommand.

It lives under data/ so a later module can embed it and ship it with
the binary, for example a 'pkh skill' installer writing it into the
agent skill directories.
2026-09-22 23:55:56 +02:00
7 changed files with 246 additions and 27 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ jobs:
- name: Set up container image - name: Set up container image
run: | run: |
apt-get update apt-get update
apt-get install -y nodejs sudo curl wget ca-certificates apt-get install -y nodejs sudo curl wget ca-certificates build-essential
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies - name: Install build dependencies
+7 -1
View File
@@ -6,7 +6,13 @@
## Installation ## Installation
No distribution channel is published yet; build from source: 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 sudo apt install pkg-config libssl-dev libgpg-error-dev libgpgme-dev
+20
View File
@@ -15,18 +15,38 @@ quirks:
# against the host architecture, whose dependency closure conflicts with # against the host architecture, whose dependency closure conflicts with
# the `:native` python3. Resolve it against the build architecture # the `:native` python3. Resolve it against the build architecture
# until the control is fixed upstream. # until the control is fixed upstream.
# The resolute and stonking kernels declare the host-tool libraries
# unqualified (libelf-dev for resolve_btfids, libdw-dev for
# gendwarfksyms, libssl-dev for sign-file), unlike the Debian control,
# which carries the same names qualified `:native`. The dpkg cross
# rules resolve an unqualified Multi-Arch: same name against the host
# architecture only, so a cross build installs no build-architecture
# variants and the kernel's host-side tools cannot link. Inject the
# build-architecture variants until the control is fixed upstream.
linux: linux:
deb: deb:
- series: [resolute] - series: [resolute]
dependencies: dependencies:
replace: replace:
llvm-21-dev: llvm-21-dev:native <!stage1> llvm-21-dev: llvm-21-dev:native <!stage1>
- series: [resolute, stonking]
dependencies:
inject:
- libelf-dev:native
- libdw-dev:native
- libssl-dev:native
linux-riscv: linux-riscv:
deb: deb:
- series: [resolute] - series: [resolute]
dependencies: dependencies:
replace: replace:
llvm-21-dev: llvm-21-dev:native <!stage1> llvm-21-dev: llvm-21-dev:native <!stage1>
- series: [resolute, stonking]
dependencies:
inject:
- libelf-dev:native
- libdw-dev:native
- libssl-dev:native
# Add more packages and their quirks as needed # Add more packages and their quirks as needed
# example-package: # example-package:
+130
View File
@@ -0,0 +1,130 @@
---
name: pkh
description: 'Drive pkh, a Debian/Ubuntu packaging helper: pull source packages, generate changelog entries, build .dsc/.deb, lint, and upload to a PPA. Use it whenever the task touches Debian or Ubuntu packaging: patching an existing package, preparing an SRU, backport or NMU, scaffolding a new .deb, rebuilding for a PPA, or uploading a source package. Trigger on "update the changelog", "package this", or a bare package name, even when the user never mentions Debian.'
---
# pkh
`pkh` wraps the Debian packaging toolchain (`dch`, `dpkg-buildpackage`,
`sbuild`, `dpkg-source`, `quilt`, `lintian`, PPA uploads) in one CLI.
The subcommands share one set of option names, so `-s` always targets
the series and `--ppa` always names the PPA. Each step also does more
than the raw tool it replaces: `pull` fetches the orig tarball with the
source, `chlog` commits the entry it writes, `deb` sets up a chroot and
installs the build dependencies.
Check the install with `pkh --version`. Each command lists its flags
with `pkh <command> --help`, so check there instead of guessing. pkh
shells out to host tools (git, dpkg-dev, quilt, mmdebstrap, lintian,
pristine-tar, schroot, ...). Install the ones your workflow uses, or
use the classic snap, which carries them.
## Shared options
| Option | Meaning |
|---|---|
| `-d, --dist <dist>` | Target distribution, `debian` or `ubuntu` |
| `-s, --series <series>` | Target series, for example `resolute` or `noble` |
| `-v, --version <version>` | Target package version |
| `-a, --arch <arch>` | Target architecture, for example `amd64` or `riscv64` |
| `-p, --pocket <pocket>` | Distribution pocket: `updates`, `security`, `proposed` |
| `--ppa <user/ppa>` | Act on the named PPA |
Defaults come from the host vendor, its development series, and its
architecture. When the target differs, pass the flags: packaging for
Ubuntu on a Debian host needs `-d ubuntu`, and a series or architecture
that differs from the host needs `-s` or `-a`.
## Patch an Ubuntu package
```
pkh pull hello # source and orig tarball; add -d ubuntu off an Ubuntu host
# edit the package, committing each patch to git
pkh chlog # generates the entry, opens it for editing, commits it
git add debian/changelog && git commit -m "d/changelog"
pkh build # source package, written next to the tree
pkh deb # binary build in a chroot with build deps installed
pkh lint # lintian plus pkh-native checks
pkh put --ppa user/hello_xxx # uploads the .changes file from the build
git push xxx user-fork # push the branch to your fork
```
Run `pkh chlog` and `pkh build` from the root of the source tree; they
act on the package in the current directory.
## Command reference
- `pkh new [name]` scaffolds a buildable source package. `--lang`
picks the build system (`rust`, `python`, `meson`, `cmake`,
`autotools`, `go`, `shell`, `makefile`); `--source <PATH>` packages
existing sources instead. `--upstream-version` and `--revision` set
the version. `--description`, `--homepage`, `--license <SPDX>`,
`--command`, `--maintainer "Name <email>"`, and `--depends` fill in
the package metadata, with the maintainer defaulting to
`DEBFULLNAME`/`DEBEMAIL` and then git config. `--quilt` and
`--native` choose the source format; `--orig-from
release|git|path|snapshot` and `--orig-path` control the orig
tarball. The changelog starts as `UNRELEASED`; `--release` targets
`--series` instead. `--defaults` answers every remaining question
with its default, which keeps the run non-interactive.
- `pkh pull <package>` fetches a source package from the archive or
git. `--archive` skips git. `--ppa user/ppa` and `--repository
<suite-url>` pull from a PPA or an external flat repository instead.
`-d`, `-s`, `-v`, and `-p` target an exact source.
- `pkh chlog` generates the changelog entry from the commits since the
last version tag, opens it for editing, and commits it. `--backport`,
`--nmu`, and `--rebuild` apply the matching numbering scheme
(`3.1-1ubuntu2~24.04.1`, `1.0-1.1`, `1.0-1build1`); `-v` sets an
explicit version instead.
- `pkh build` produces the .dsc. `--orig auto|always|never` controls
whether the upload includes the orig tarball; the default, `auto`,
includes it only when the upstream version changed.
- `pkh deb` builds the binary packages in an isolated context with the
build dependencies installed. `--ppa` (repeatable) adds dependency
sources, `--inject <package|.deb>` preinstalls a package, and `-j`
caps parallel jobs. `--cross` cross-compiles instead of using
qemu-binfmt, but most packages cannot cross-compile, so prefer qemu.
Leave `--mode` unset unless you need a specific build context.
- `pkh lint [path]` runs lintian plus the pkh-native checks. `--json`
emits a machine-readable report and `--list-tags` prints the native
tag catalog. `--fail-on` sets the severities that fail the run
(errors by default), `--suppress-tags` ignores tags, `--check` runs a
single native check, and `--info`, `--pedantic`, and
`--experimental` add detail. `--repack` packs the tree fresh instead
of reusing the existing build output.
- `pkh put [changes]` uploads a .changes file to `--ppa user/ppa`.
With no argument it uploads the .changes from this package's last
build, found next to the source tree. `--force` re-uploads a file
that was already uploaded.
- `pkh prune` removes build artifacts and caches. Run it with
`--dry-run` first to list them. `--all` also deletes the cached
chroot tarballs, which take long to download again, so use it when
you need the disk space.
## Notes for agent runs
- Pass `-d`, `-s`, and `-a` whenever the target differs from the host,
so runs are reproducible.
- Keep runs non-interactive. Pass explicit flags, use `pkh new
--defaults`, and set `EDITOR` before `pkh chlog` (`EDITOR=true` keeps
the generated text). Commands may ask short questions on the
terminal; flags avoid most prompts.
- Pass `RUST_LOG=debug` for pkh's own logs. `--verbose` on `pkh build`
and `pkh deb` prints raw tool output instead of the live view.
- pkh writes the build artifacts (.dsc, .changes, logs) next to the
source tree. `pkh put` finds them without arguments, and `pkh prune`
removes them again.
- Run `pkh lint` before `pkh put`. It exits nonzero when findings reach
the `--fail-on` level, which defaults to errors.
- pkh intercepts Ctrl+C, runs its cleanup hooks, and exits with status
130. `pkh prune` removes anything left over.
The upstream repository is https://git.vhaudiquet.fr/vhaudiquet/pkh.
Its README has longer workflow examples.
+7 -24
View File
@@ -62,20 +62,6 @@ pub fn setup_environment(
.map_err(|e| format!("Invalid UTF-8 in dpkg-architecture output: {e}"))?; .map_err(|e| format!("Invalid UTF-8 in dpkg-architecture output: {e}"))?;
parse_dpkg_architecture_output(&dpkg_architecture, env); parse_dpkg_architecture_output(&dpkg_architecture, env);
// In-tree tools locate their libraries with the *host* pkg-config during
// cross builds (the kernel's tools/build feature checks derive their
// cflags/ldflags from `pkg-config --cflags/--libs`), whose search path
// only covers the build architecture's pkgconfig dirs. Point it at the
// target's so `libtraceevent` & co resolve to target-arch libraries:
// linux-riscv cross builds die in rtla's Makefile.config otherwise, even
// with the target -dev packages installed.
if let Some(multiarch) = env.get("DEB_HOST_MULTIARCH").cloned() {
env.insert(
"PKG_CONFIG_LIBDIR".to_string(),
format!("/usr/lib/{multiarch}/pkgconfig:/usr/share/pkgconfig"),
);
}
env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string()); env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string());
Ok(()) Ok(())
@@ -304,21 +290,18 @@ mod tests {
assert!(cross_suites("noble", None, "not-a-distro").is_err()); assert!(cross_suites("noble", None, "not-a-distro").is_err());
} }
/// setup_environment exports the target multiarch pkg-config libdir: /// setup_environment exports the dpkg cross variables and the 'cross'
/// tools' feature checks run the *host* pkg-config, which must find the /// build profile, and nothing beyond what dpkg-buildpackage exports:
/// target's .pc files (rtla hard-errors on libtraceevent otherwise, /// no pkg-config redirection (the environment is the package's to
/// failing linux-riscv cross builds despite the target -dev packages /// set, and target-arch pkgconfig paths would also poison the
/// being installed). /// host-side tools of the same build).
#[test] #[test]
fn test_setup_environment_exports_cross_pkg_config_libdir() { fn test_setup_environment_exports_dpkg_cross_variables_only() {
let mut env = HashMap::new(); let mut env = HashMap::new();
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap()); let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap());
setup_environment(&mut env, "riscv64", ctx).unwrap(); setup_environment(&mut env, "riscv64", ctx).unwrap();
assert_eq!( assert!(!env.contains_key("PKG_CONFIG_LIBDIR"));
env.get("PKG_CONFIG_LIBDIR").map(String::as_str),
Some("/usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig")
);
assert_eq!( assert_eq!(
env.get("DEB_BUILD_PROFILES").map(String::as_str), env.get("DEB_BUILD_PROFILES").map(String::as_str),
Some("cross") Some("cross")
+33
View File
@@ -1154,6 +1154,39 @@ mod tests {
assert_eq!(spec, "t:amd64"); assert_eq!(spec, "t:amd64");
} }
/// Cross parity with dpkg's checker and sbuild's apt-resolved dummy
/// package: an unqualified Multi-Arch: same name resolves to the
/// host-architecture variant even when a build-architecture candidate
/// exists — the build-architecture side is only ever installed
/// through an explicit `:native` declaration (or a pkh quirk
/// injecting one), never alongside the host variant.
#[test]
fn install_spec_unqualified_same_package_never_dual_installs() {
let host = "arm64";
let build = "amd64";
let spec = install_spec_for(
&rel("t"),
&[cand("amd64", "same"), cand("arm64", "same")],
build,
host,
)
.unwrap();
assert_eq!(spec, "t:arm64");
// The build-architecture side comes from `:native` declarations:
// the co-installable build-arch variant of the same source
// package.
let spec = install_spec_for(
&rel("t:native"),
&[cand("amd64", "same"), cand("arm64", "same")],
build,
host,
)
.unwrap();
assert_eq!(spec, "t:amd64");
}
/// Qualified dependencies install exactly the variant their qualifier /// Qualified dependencies install exactly the variant their qualifier
/// selects per dpkg semantics; virtual names (no candidates) pass /// selects per dpkg semantics; virtual names (no candidates) pass
/// through unqualified for apt to resolve a provider. /// through unqualified for apt to resolve a provider.
+48 -1
View File
@@ -265,15 +265,62 @@ mod tests {
fn linux_dependency_quirks_are_series_scoped() { fn linux_dependency_quirks_are_series_scoped() {
for package in ["linux", "linux-riscv"] { for package in ["linux", "linux-riscv"] {
let rules = get_deb_dependency_quirks(package, "resolute"); let rules = get_deb_dependency_quirks(package, "resolute");
assert_eq!(rules.len(), 1, "the resolute entry applies"); assert_eq!(rules.len(), 2, "the resolute entries apply");
assert_eq!( assert_eq!(
rules[0].replace.get("llvm-21-dev").map(String::as_str), rules[0].replace.get("llvm-21-dev").map(String::as_str),
Some("llvm-21-dev:native <!stage1>") Some("llvm-21-dev:native <!stage1>")
); );
// Only the stonking entry exists there: the llvm-21-dev
// replacement above is a resolute-only packaging state.
let rules = get_deb_dependency_quirks(package, "stonking");
assert_eq!(rules.len(), 1);
assert!(rules[0].replace.is_empty());
assert!(get_deb_dependency_quirks(package, "noble").is_empty()); assert!(get_deb_dependency_quirks(package, "noble").is_empty());
} }
} }
/// The kernels of the series whose control dropped the Debian-style
/// `:native` qualifiers on the host-tool libraries inject the
/// build-architecture variants: the dpkg cross rules resolve the
/// unqualified Multi-Arch: same names against the host architecture
/// only, leaving nothing for the kernel's host-side tools to link.
#[test]
fn linux_injects_native_host_tool_libraries_for_cross() {
for series in ["resolute", "stonking"] {
let mut clauses = vec![vec![parse("libelf-dev <!stage1>")]];
crate::quirks::apply_dependency_quirks("linux", series, &mut clauses, &opts()).unwrap();
let injected: Vec<&PkgRelation> = clauses[1..]
.iter()
.flatten()
.filter(|rel| {
["libelf-dev", "libdw-dev", "libssl-dev"].contains(&rel.package.as_str())
})
.collect();
assert_eq!(
injected.len(),
3,
"one :native clause per host-tool library"
);
assert!(
injected
.iter()
.all(|rel| rel.arch_qualifier.as_deref() == Some("native"))
);
// The declared dependency itself is untouched: the host
// (target) variant still installs for the checker.
assert_eq!(clauses[0][0].arch_qualifier, None);
}
}
/// Other kernel series keep their declared dependencies untouched.
#[test]
fn noble_linux_dependencies_are_not_rewritten() {
let mut clauses = vec![vec![parse("libelf-dev")]];
crate::quirks::apply_dependency_quirks("linux", "noble", &mut clauses, &opts()).unwrap();
assert_eq!(clauses.len(), 1);
assert_eq!(clauses[0][0].arch_qualifier, None);
}
/// `replace` rewrites exactly the dependencies whose declared name /// `replace` rewrites exactly the dependencies whose declared name
/// matches, wholesale: the replacement carries its own qualifier and /// matches, wholesale: the replacement carries its own qualifier and
/// restrictions. /// restrictions.