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()));
}
}
+29 -10
View File
@@ -114,29 +114,31 @@ impl FilesList {
/// Load `debian/files`. A missing file yields an empty registry.
pub fn load(path: &Path) -> Result<FilesList, Box<dyn std::error::Error>> {
let mut list = FilesList::new();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(list),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(FilesList::new()),
Err(e) => {
return Err(format!("cannot read '{}': {}", path.display(), e).into());
}
};
FilesList::parse(&content)
.map_err(|e| format!("in '{}': {}", path.display(), e).into())
}
/// Parse a `debian/files` registry from its textual content
/// (`filename section priority [key=value...]` lines).
pub fn parse(content: &str) -> Result<FilesList, String> {
let mut list = FilesList::new();
for line in content.lines() {
if line.trim().is_empty() {
continue;
}
let tokens: Vec<&str> = line.split_whitespace().collect();
if tokens.len() < 3 {
return Err(format!("badly formed line in '{}': {}", path.display(), line).into());
return Err(format!("badly formed line: {line}"));
}
let mut entry = parse_filename(tokens[0]).ok_or_else(|| {
format!(
"badly formed file name in '{}': {}",
path.display(),
tokens[0]
)
})?;
let mut entry = parse_filename(tokens[0])
.ok_or_else(|| format!("badly formed file name: {}", tokens[0]))?;
entry.section = tokens[1].to_string();
entry.priority = tokens[2].to_string();
for attr in &tokens[3..] {
@@ -181,6 +183,23 @@ impl FilesList {
self.files.is_empty()
}
/// Render the registry to its textual `debian/files` representation.
pub fn render(&self) -> String {
let mut out = String::new();
for entry in self.iter() {
out.push_str(&entry.filename);
out.push(' ');
out.push_str(&entry.section);
out.push(' ');
out.push_str(&entry.priority);
for (k, v) in &entry.attrs {
out.push_str(&format!(" {k}={v}"));
}
out.push('\n');
}
out
}
/// Save atomically: write `<path>.new` then rename over `path`, like
/// dpkg does.
pub fn save_atomic(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
+4 -1
View File
@@ -19,7 +19,10 @@ pub mod deps;
pub mod files;
pub mod version;
pub use changelog::{ChangelogEntry, parse_changelog_entry};
pub use changelog::{
ChangelogEntry, parse_changelog_entry, parse_changelog_entry_from_str,
parse_previous_version_from_str,
};
pub use checksums::{Entry as ChecksumEntry, FileChecksums};
pub use control::{ControlInfo, Paragraph, parse_paragraphs, write_paragraph};
pub use files::{FilesEntry, FilesList};