build/binary: native .buildinfo/.changes for binary builds (pkh deb)

Extend the metadata writers to binary-only uploads and wire them into
the 'pkh deb' flow:

- build/binary.rs generates <pkg>_<ver>_<arch>.buildinfo/.changes
  through any Context: debian/files consumption, encounter-order
  Architecture accumulation (sorted in .buildinfo like dpkg-genbuildinfo),
  sorted Binary lists, dpkg-formatted Description lines with udeb
  suffixes, Installed-Build-Depends closure over the context status DB,
  and binNMU handling (Source: pkg (prev), Binary-Only-Changes, previous
  .dsc redistribution);
- artifact digests are computed inside the context via coreutils
  (md5sum/sha1sum/sha256sum/stat) so chrooted/remote trees work;
- deb/local.rs runs the generation after 'rules binary', exports
  SOURCE_DATE_EPOCH from the changelog (reproducibility), and resolves
  vendor/profiles inside the context; deb/mod.rs retrieves the new
  artifacts alongside the debs;
- reusable helpers added: FilesList::parse/render,
  parse_changelog_entry_from_str, parse_previous_version_from_str,
  installed_build_depends_from_content.

Differential gate: same tree built with real 'dpkg-buildpackage -b' and
with the pkh flow; .changes/.buildinfo compared field-by-field modulo
machine-dependent fields, artifact checksums included.
This commit is contained in:
2026-08-24 11:58:19 +02:00
parent dfaab0606a
commit 42fcfc2dfa
9 changed files with 808 additions and 45 deletions
+110 -1
View File
@@ -6,6 +6,7 @@
//! source-tree work (tarballs, diffs, patches) to `dpkg-source` as a
//! subprocess.
pub mod binary;
pub mod buildinfo;
pub mod buildtype;
pub mod changes;
@@ -771,7 +772,7 @@ mod differential_tests {
if matches!(key, "Checksums-Sha1" | "Checksums-Sha256" | "Files") {
let without_buildinfo = |v: &str| -> String {
v.lines()
.filter(|l| !l.trim_end().ends_with("_source.buildinfo"))
.filter(|l| !l.trim_end().ends_with(".buildinfo"))
.collect::<Vec<_>>()
.join("\n")
};
@@ -1125,6 +1126,114 @@ Provides: virtual-thing (= 2.0), plain-virtual
}
}
/// Differential check of the binary-build metadata generation against
/// real `dpkg-buildpackage -b`: both sides build the same tree (rules
/// driving dpkg-gencontrol/dpkg-deb directly, no debhelper needed),
/// then the produced `.changes`/`.buildinfo` are compared field by
/// field modulo machine-dependent values.
#[test]
fn diff_binary_build_metadata() {
const NAME: &str = "pkh-diff-m";
let control = format!(
"Source: {NAME}\nSection: utils\nPriority: optional\nMaintainer: {MAINTAINER}\nBuild-Depends: libc6\n\n\
Package: {NAME}\nArchitecture: any\nDescription: test package main\n long description\n\n\
Package: {NAME}-u\nPackage-Type: udeb\nArchitecture: all\nDescription: test udeb\n short\n"
);
let changelog = format!(
"{NAME} (1.0-1) unstable; urgency=medium\n\n * Binary build test.\n\n -- {MAINTAINER} {DATE}\n"
);
let rules = format!(
"#!/usr/bin/make -f\nV = $(shell dpkg-parsechangelog -S Version)\nA = $(shell dpkg-architecture -qDEB_HOST_ARCH)\n\nbuild:\n\tmkdir -p debian/tmp/usr/bin\n\tprintf '#!/bin/sh\\necho hi\\n' > debian/tmp/usr/bin/hello\n\tchmod 755 debian/tmp/usr/bin/hello\n\ttouch $@\n\nbinary: build\n\trm -rf debian/{NAME} debian/{NAME}-u\n\tmkdir -p debian/{NAME}/usr/bin debian/{NAME}/DEBIAN\n\tcp -r debian/tmp/. debian/{NAME}/\n\tdpkg-gencontrol -p{NAME} -Pdebian/{NAME}\n\tdpkg-deb --build debian/{NAME} ..\n\tmkdir -p debian/{NAME}-u/usr/share debian/{NAME}-u/DEBIAN\n\techo data > debian/{NAME}-u/usr/share/data.txt\n\tdpkg-gencontrol -p{NAME}-u -Pdebian/{NAME}-u\n\tdpkg-deb --build debian/{NAME}-u ..\n\tmv ../{NAME}-u_$(V)_all.deb ../{NAME}-u_$(V)_all.udeb\n\nclean:\n\trm -rf debian/tmp debian/{NAME} debian/{NAME}-u build-stamp debian/files debian/*.substvars\n\n.PHONY: build binary clean\n"
);
let write_tree = |root: &Path| {
fs::create_dir_all(root.join(format!("{NAME}/debian/source"))).expect("mkdir tree");
let tree = root.join(NAME);
fs::write(tree.join("debian/control"), &control).expect("write control");
fs::write(tree.join("debian/changelog"), &changelog).expect("write changelog");
fs::write(tree.join("debian/source/format"), "3.0 (native)\n")
.expect("write format");
fs::write(tree.join("debian/rules"), &rules).expect("write rules");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(tree.join("debian/rules"), fs::Permissions::from_mode(0o755))
.expect("chmod rules");
}
tree
};
let base = tempfile::tempdir().expect("tempdir");
let golden_root = base.path().join("golden");
let ours_root = base.path().join("ours");
fs::create_dir_all(&golden_root).expect("mkdir golden");
fs::create_dir_all(&ours_root).expect("mkdir ours");
let golden_tree = write_tree(&golden_root);
let ours_tree = write_tree(&ours_root);
// Golden side: real dpkg-buildpackage binary build.
let status = Command::new("dpkg-buildpackage")
.current_dir(&golden_tree)
.args(["-b", "-d", "--no-sign"])
.status()
.expect("run dpkg-buildpackage (is dpkg-dev installed?)");
assert!(status.success(), "golden dpkg-buildpackage -b failed");
// Ours: emulate the pkh deb flow (rules build + rules binary with a
// dpkg-buildpackage-like environment), then run the native metadata
// generation through a local context. dpkg-buildpackage runs the
// rules targets directly by default (missing Rules-Requires-Root is
// treated as 'no'), so no fakeroot wrapper here either.
let entry =
crate::debian::parse_changelog_entry_from_str(&changelog).expect("parse changelog");
let vendor = env::current_vendor();
let profiles = env::resolve_build_profiles(&[], &vendor);
let parallel = env::num_parallel();
let build_env_vars: Vec<(String, String)> = [
("LANG".to_string(), "C".to_string()),
("DEB_BUILD_OPTIONS".to_string(), format!("parallel={parallel}")),
("SOURCE_DATE_EPOCH".to_string(), entry.timestamp.to_string()),
]
.into_iter()
.collect();
for target in ["build", "binary"] {
let status = Command::new("debian/rules")
.current_dir(&ours_tree)
.envs(build_env_vars.clone())
.arg(target)
.status()
.expect("run rules target");
assert!(status.success(), "debian/rules {target} failed");
}
let ctx = std::sync::Arc::new(crate::context::Context::new(
crate::context::ContextConfig::Local,
));
let native_arch = crate::debian::arch::native().unwrap_or_else(|_| "amd64".into());
let opts = crate::build::binary::BinaryMetadataOptions {
profiles,
vendor,
parallel,
source_date_epoch: entry.timestamp,
build_arch: native_arch.clone(),
host_arch: native_arch,
};
crate::build::binary::generate_binary_metadata(&ctx, &ours_tree, &ours_root, &opts)
.expect("native binary metadata generation failed");
// Compare artifacts.
assert_changes_equivalent(
&golden_root.join(format!("{NAME}_1.0-1_amd64.changes")),
&ours_root.join(format!("{NAME}_1.0-1_amd64.changes")),
);
assert_buildinfo_equivalent(
&golden_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
&ours_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
);
}
#[test]
fn diff_native_minimal() {
differential_case(&FixtureSpec::new("pkh-diff-a", "1.0-1", "unstable"));