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
+24 -19
View File
@@ -46,7 +46,15 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
e
)
})?;
parse_changelog_entry_from_str(&content)
}
/// Parse the most recent changelog entry from its textual content. `origin`
/// is used in error messages only.
pub fn parse_changelog_entry_from_str(
content: &str,
) -> Result<ChangelogEntry, Box<dyn std::error::Error>> {
let origin = "changelog";
let mut lines = content.lines().peekable();
// --- Header line: `package (version) distributions; urgency=medium[, key=value]`
@@ -55,17 +63,13 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
Some(l) if l.trim().is_empty() => continue,
Some(l) => break l.trim_end(),
None => {
return Err(format!("changelog '{}' is empty", path.display()).into());
return Err(format!("changelog '{origin}' is empty").into());
}
}
};
let open = header.find('(').ok_or_else(|| {
format!(
"invalid changelog header in '{}': {}",
path.display(),
header
)
format!("invalid changelog header in '{origin}': {header}")
})?;
let close = header[open..]
.find(')')
@@ -123,9 +127,8 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
let trailer_line = trailer.ok_or_else(|| {
format!(
"no maintainer trailer found in '{}': expected a line of the form \
' -- Name <email> Date'",
path.display()
"no maintainer trailer found in '{origin}': expected a line of the form \
' -- Name <email> Date'"
)
})?;
@@ -148,12 +151,7 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
let timestamp = DateTime::parse_from_rfc2822(&date_raw)
.map_err(|e| {
format!(
"cannot parse changelog date '{}' in '{}': {}",
date_raw,
path.display(),
e
)
format!("cannot parse changelog date '{date_raw}' in '{origin}': {e}")
})?
.timestamp();
@@ -220,7 +218,14 @@ fn find_closes(body_lines: &[String]) -> Option<String> {
pub fn parse_previous_version(path: &Path) -> Result<Option<String>, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read changelog '{}': {}", path.display(), e))?;
parse_previous_version_from_str(&content)
}
/// Return the version of the *previous* changelog entry from the textual
/// content of a changelog file.
pub fn parse_previous_version_from_str(
content: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let mut seen_first = false;
for line in content.lines() {
let line = line.trim_end();
@@ -229,12 +234,12 @@ pub fn parse_previous_version(path: &Path) -> Result<Option<String>, Box<dyn std
seen_first = true;
continue;
}
let open = line.find('(').ok_or_else(|| {
format!("invalid changelog header in '{}': {}", path.display(), line)
})?;
let open = line
.find('(')
.ok_or_else(|| format!("invalid changelog header: {line}"))?;
let close = line[open..]
.find(')')
.ok_or_else(|| format!("unbalanced parenthesis in changelog header '{}'", line))?;
.ok_or_else(|| format!("unbalanced parenthesis in changelog header '{line}'"))?;
return Ok(Some(line[open + 1..open + close].to_string()));
}
}