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
+105
View File
@@ -227,6 +227,20 @@ pub async fn build(
.to_str()
.ok_or("Invalid package directory path")?;
// Reproducibility: export SOURCE_DATE_EPOCH from the changelog entry,
// like dpkg-buildpackage does.
match ctx.read_file(&package_dir.join("debian/changelog")) {
Ok(content) => {
if let Ok(entry) = crate::debian::parse_changelog_entry_from_str(&content) {
env.insert(
"SOURCE_DATE_EPOCH".to_string(),
entry.timestamp.to_string(),
);
}
}
Err(e) => log::debug!("cannot read changelog for SOURCE_DATE_EPOCH: {}", e),
}
// Apply quilt patches if the package provides a patch series
apply_quilt_patches(package_dir_str, &env, ctx.clone(), &ui, &sink)?;
@@ -318,6 +332,97 @@ pub async fn build(
);
}
// Generate the upload metadata (.buildinfo + .changes) natively, the
// equivalent of dpkg-genbuildinfo -b + dpkg-genchanges -b, consuming
// debian/files produced by the build. Failures are logged but do not
// discard the produced binaries.
if let Err(e) =
generate_upload_metadata(package_dir_str, build_root, arch, cross, &env, &ctx)
{
warn!("failed to generate .buildinfo/.changes: {}", e);
}
Ok(())
}
/// Generate `.buildinfo` and `.changes` for the finished binary build,
/// inside the build context.
fn generate_upload_metadata(
package_dir: &str,
build_root: &str,
arch: &str,
cross: bool,
env: &HashMap<String, String>,
ctx: &Arc<Context>,
) -> Result<(), Box<dyn Error>> {
use std::path::Path;
let changelog_path = Path::new(package_dir).join("debian/changelog");
let changelog_content = ctx.read_file(&changelog_path)?;
let entry =
crate::debian::parse_changelog_entry_from_str(&changelog_content)?;
// Build architecture: the machine inside the build context.
let build_arch = ctx
.command("dpkg")
.arg("--print-architecture")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(crate::get_current_arch);
let host_arch = if cross {
arch.to_string()
} else {
build_arch.clone()
};
// Vendor resolution inside the context (falls back to the host view).
let vendor = ctx
.read_file(Path::new("/etc/dpkg/origins/default"))
.ok()
.and_then(|content| {
for line in content.lines() {
if let Some(v) = line.strip_prefix("Vendor:") {
let v = v.trim();
if !v.is_empty() {
return Some(v.to_string());
}
}
}
None
})
.unwrap_or_else(crate::build::env::current_vendor);
let profiles = crate::build::env::resolve_build_profiles(
&[],
&vendor,
);
let source_date_epoch = env
.get("SOURCE_DATE_EPOCH")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(entry.timestamp);
let opts = crate::build::binary::BinaryMetadataOptions {
profiles,
vendor,
parallel: crate::build::env::num_parallel(),
source_date_epoch,
build_arch,
host_arch,
};
let (buildinfo, changes) = crate::build::binary::generate_binary_metadata(
ctx,
Path::new(package_dir),
Path::new(build_root),
&opts,
)?;
log::info!(
"generated upload metadata: {} and {}",
buildinfo.display(),
changes.display()
);
Ok(())
}
+6 -2
View File
@@ -165,14 +165,18 @@ async fn build_binary_package_impl(
}
}
// Retrieve produced .deb files
// Retrieve produced artifacts (.deb files plus the upload metadata
// (.buildinfo/.changes) generated natively after the build)
if let Some(u) = ui {
u.phase(Phase::RetrievingArtifacts);
}
let remote_files = build_ctx.list_files(Path::new(&build_root))?;
let deb_files: Vec<PathBuf> = remote_files
.into_iter()
.filter(|f| f.extension().is_some_and(|ext| ext == "deb"))
.filter(|f| {
f.extension()
.is_some_and(|ext| matches!(ext.to_str(), Some("deb") | Some("buildinfo") | Some("changes")))
})
.collect();
let total_debs = deb_files.len();