build: add differential tests against real dpkg-buildpackage

Add an automated differential test harness in build/mod.rs that builds
the same source tree twice - once with real 'dpkg-buildpackage -S
-I -i -nc -d --no-sign', once with the native pipeline - and compares
all produced artifacts:

- .dsc payload byte-for-byte,
- .changes field-by-field (checksum lines of the .buildinfo itself
  excluded, as its content legitimately differs on machine-dependent
  fields),
- .buildinfo structure (Installed-Build-Depends, Environment,
  Build-Date and Build-Tainted-By excluded).
This commit is contained in:
2026-08-23 11:05:36 +02:00
parent 4a8ff9ac0d
commit 0ad020ae19
3 changed files with 735 additions and 13 deletions
+64
View File
@@ -3,6 +3,7 @@
use std::path::Path;
use chrono::DateTime;
use regex::Regex;
use super::version::DebianVersion;
@@ -30,6 +31,9 @@ pub struct ChangelogEntry {
/// Value for the `.changes` `Changes` field: header line, blank lines
/// converted to `.`, body lines verbatim; without the trailer line.
pub changes_field: String,
/// Bug numbers collected from `(Closes: #NNN)` mentions in the body,
/// sorted numerically and de-duplicated (like dpkg's `find_closes`).
pub closes: Option<String>,
}
/// Parse the most recent entry of a Debian changelog file.
@@ -163,6 +167,8 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
changes_field.push_str(&body_lines.join("\n"));
}
let closes = find_closes(&body_lines);
Ok(ChangelogEntry {
source,
version,
@@ -174,9 +180,67 @@ pub fn parse_changelog_entry(path: &Path) -> Result<ChangelogEntry, Box<dyn std:
date_raw,
timestamp,
changes_field,
closes,
})
}
/// Extract bug numbers from `(Closes: #NNN)` mentions in changelog body
/// lines, sorted numerically and de-duplicated (a lenient port of dpkg's
/// `find_closes`).
fn find_closes(body_lines: &[String]) -> Option<String> {
let re = Regex::new(r"(?i)\(closes:\s*([^)]*)\)").ok()?;
let mut numbers: Vec<u64> = Vec::new();
for line in body_lines {
for capture in re.captures_iter(line) {
if let Some(inner) = capture.get(1) {
for token in inner.as_str().split(|c: char| !c.is_ascii_digit()) {
if let Ok(n) = token.parse::<u64>() {
numbers.push(n);
}
}
}
}
}
if numbers.is_empty() {
return None;
}
numbers.sort_unstable();
numbers.dedup();
Some(
numbers
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(" "),
)
}
/// Return the version of the *previous* changelog entry (the second header
/// in the file), or `None` when only one entry exists.
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))?;
let mut seen_first = false;
for line in content.lines() {
let line = line.trim_end();
if looks_like_header(line) {
if !seen_first {
seen_first = true;
continue;
}
let open = line.find('(').ok_or_else(|| {
format!("invalid changelog header in '{}': {}", path.display(), line)
})?;
let close = line[open..]
.find(')')
.ok_or_else(|| format!("unbalanced parenthesis in changelog header '{}'", line))?;
return Ok(Some(line[open + 1..open + close].to_string()));
}
}
Ok(None)
}
/// Heuristic check for a changelog entry header line
/// (`name (version) dist; urgency=...`).
fn looks_like_header(line: &str) -> bool {