build: stop redistributing the previous source on binNMU uploads
dpkg-genchanges/genbuildinfo handle a binary-only upload by referencing the previous source version textually (Source: pkg (prev), Binary-Only: yes, Binary-Only-Changes) while distributing no source files at all: pkh instead pulled the previous .dsc and its tarballs into both documents whenever they sat next to the artifacts, re-uploading the whole source on every binNMU. Drop that redistribution (and include_dsc_artifacts with it), and emit the missing Binary-Only: yes field, which the new differential test against real dpkg-buildpackage -b caught. The binNMU case shares its runner with the regular binary metadata differential; a unit test pins the exclusion even with the previous artifacts present.
This commit is contained in:
+109
-196
@@ -16,8 +16,6 @@ use std::sync::Arc;
|
|||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::debian::{ChecksumEntry, ControlInfo, FileChecksums, FilesList};
|
use crate::debian::{ChecksumEntry, ControlInfo, FileChecksums, FilesList};
|
||||||
|
|
||||||
use super::parse_checksum_field;
|
|
||||||
|
|
||||||
/// Digests of one artifact.
|
/// Digests of one artifact.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct ArtifactHashes {
|
struct ArtifactHashes {
|
||||||
@@ -57,7 +55,7 @@ pub struct BinaryMetadataOptions {
|
|||||||
/// `dpkg-genchanges -b`: sorted `Binary` list, encounter-order `Architecture`
|
/// `dpkg-genchanges -b`: sorted `Binary` list, encounter-order `Architecture`
|
||||||
/// accumulation, sorted `Description` lines formatted like dpkg, `.buildinfo`
|
/// accumulation, sorted `Description` lines formatted like dpkg, `.buildinfo`
|
||||||
/// registration in `debian/files`, and binary-NMU handling (`Source:
|
/// registration in `debian/files`, and binary-NMU handling (`Source:
|
||||||
/// pkg (prev)` + previous `.dsc` redistribution when present).
|
/// pkg (prev)` + `Binary-Only-Changes`, with no source files distributed).
|
||||||
pub fn generate_binary_metadata(
|
pub fn generate_binary_metadata(
|
||||||
ctx: &Arc<Context>,
|
ctx: &Arc<Context>,
|
||||||
package_dir: &Path,
|
package_dir: &Path,
|
||||||
@@ -137,28 +135,24 @@ pub fn generate_binary_metadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Binary-NMU: redistribute the previous source when present
|
// Binary-NMU: reference the previous source version, textually only
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
let sversion = entry.version.no_epoch();
|
let sversion = entry.version.no_epoch();
|
||||||
let mut source_display = entry.source.clone();
|
let mut source_display = entry.source.clone();
|
||||||
let mut binary_only_changes = None;
|
let mut binary_only_changes = None;
|
||||||
|
|
||||||
if entry.binary_only {
|
if entry.binary_only {
|
||||||
// A binary-only upload must reference the previous source version;
|
// Like dpkg-genchanges/genbuildinfo, a binary-only upload references
|
||||||
// a changelog that cannot yield it is a hard error, like in the
|
// the previous source version in the `Source` field and records the
|
||||||
// source-build path. Reuse the changelog read above instead of
|
// entry in `Binary-Only-Changes`, but distributes NO source files:
|
||||||
// reading the file a second time.
|
// the previous `.dsc` and its tarballs already sit in the archive,
|
||||||
|
// and are not re-uploaded even when present next to the tree.
|
||||||
if let Some(prev) = &previous_entry {
|
if let Some(prev) = &previous_entry {
|
||||||
source_display = format!("{} ({})", entry.source, prev.version.full());
|
source_display = format!("{} ({})", entry.source, prev.version.full());
|
||||||
binary_only_changes = Some(format!(
|
binary_only_changes = Some(format!(
|
||||||
"{}\n\n -- {} <{}> {}",
|
"{}\n\n -- {} <{}> {}",
|
||||||
entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw
|
entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw
|
||||||
));
|
));
|
||||||
let dsc_name = format!("{}_{}.dsc", entry.source, prev.version.no_epoch());
|
|
||||||
let dsc_path = upload_dir.join(&dsc_name);
|
|
||||||
if ctx.exists(&dsc_path)? {
|
|
||||||
include_dsc_artifacts(ctx, upload_dir, &dsc_name, &mut checksums)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +291,7 @@ pub fn generate_binary_metadata(
|
|||||||
date: entry.date_raw.clone(),
|
date: entry.date_raw.clone(),
|
||||||
source: source_display,
|
source: source_display,
|
||||||
binaries,
|
binaries,
|
||||||
|
binary_only: entry.binary_only,
|
||||||
built_for_profiles: opts.profiles.clone(),
|
built_for_profiles: opts.profiles.clone(),
|
||||||
architecture: arch_values.join(" "),
|
architecture: arch_values.join(" "),
|
||||||
version: entry.version.full(),
|
version: entry.version.full(),
|
||||||
@@ -400,90 +395,6 @@ fn hashes_in_context(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pull the `.dsc` checksums (and its referenced tarballs) into the
|
|
||||||
/// checksum registry, mirroring how binary-NMU uploads redistribute the
|
|
||||||
/// previous source.
|
|
||||||
fn include_dsc_artifacts(
|
|
||||||
ctx: &Arc<Context>,
|
|
||||||
upload_dir: &Path,
|
|
||||||
dsc_name: &str,
|
|
||||||
checksums: &mut FileChecksums,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
let dsc_content = ctx.read_file(&upload_dir.join(dsc_name))?;
|
|
||||||
let para = crate::debian::control::parse_paragraphs(
|
|
||||||
crate::debian::control::strip_clearsigned_armour(&dsc_content),
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| format!("'{dsc_name}' is empty"))?;
|
|
||||||
|
|
||||||
// Names and partial checksums are filled from the very same validated
|
|
||||||
// lines, so a listed name can never miss its checksum entry.
|
|
||||||
// Distribution order follows the Checksums fields (Checksums-Sha1 then
|
|
||||||
// Checksums-Sha256), like dpkg-genchanges; the `Files` field only
|
|
||||||
// supplements the md5 digests.
|
|
||||||
let mut names: Vec<String> = Vec::new();
|
|
||||||
let mut partials: BTreeMap<String, super::PartialChecksum> = BTreeMap::new();
|
|
||||||
for field in ["Checksums-Sha1", "Checksums-Sha256", "Files"] {
|
|
||||||
let Some(value) = para.get(field) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for cl in parse_checksum_field(field, value)
|
|
||||||
.map_err(|e| format!("cannot parse '{dsc_name}': {e}"))?
|
|
||||||
{
|
|
||||||
let slot = partials.entry(cl.name.clone()).or_default();
|
|
||||||
match field {
|
|
||||||
"Checksums-Sha1" => slot.sha1 = Some(cl.digest),
|
|
||||||
"Checksums-Sha256" => slot.sha256 = Some(cl.digest),
|
|
||||||
_ => slot.md5 = Some(cl.digest),
|
|
||||||
}
|
|
||||||
slot.size = Some(cl.size);
|
|
||||||
if field != "Files" && !names.contains(&cl.name) {
|
|
||||||
names.push(cl.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The .dsc itself is hashed fresh (it may be signed/rewritten); the
|
|
||||||
// tarballs reuse the .dsc-recorded digests, like dpkg-genchanges does.
|
|
||||||
let dsc_hashes =
|
|
||||||
hashes_in_context(ctx, upload_dir, std::slice::from_ref(&dsc_name.to_string()))?;
|
|
||||||
if let Some(h) = dsc_hashes.get(dsc_name) {
|
|
||||||
checksums.insert_entry(
|
|
||||||
dsc_name,
|
|
||||||
ChecksumEntry {
|
|
||||||
size: h.size,
|
|
||||||
md5: h.md5.clone(),
|
|
||||||
sha1: h.sha1.clone(),
|
|
||||||
sha256: h.sha256.clone(),
|
|
||||||
// No SHA-512 digest available (see above).
|
|
||||||
sha512: String::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for name in &names {
|
|
||||||
if name == dsc_name {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let p = partials
|
|
||||||
.get(name)
|
|
||||||
.ok_or_else(|| format!("file '{name}' listed in '{dsc_name}' has no checksum entry"))?;
|
|
||||||
checksums.insert_entry(
|
|
||||||
name,
|
|
||||||
ChecksumEntry {
|
|
||||||
size: p.size.unwrap_or(0),
|
|
||||||
md5: p.md5.clone().unwrap_or_default(),
|
|
||||||
sha1: p.sha1.clone().unwrap_or_default(),
|
|
||||||
sha256: p.sha256.clone().unwrap_or_default(),
|
|
||||||
// The `.dsc` records no SHA-512 (dpkg only writes
|
|
||||||
// sha1/sha256 there).
|
|
||||||
sha512: String::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -521,109 +432,11 @@ mod tests {
|
|||||||
assert!(!environment.contains("DEBIAN_FRONTEND"), "{environment}");
|
assert!(!environment.contains("DEBIAN_FRONTEND"), "{environment}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal previous-version `.dsc` with a 3-column Checksums-Sha1
|
|
||||||
/// field, a 4-column Checksums-Sha256 line and a 3-column `Files`.
|
|
||||||
/// Regression: the old code filled `names` from any line with a third
|
|
||||||
/// column but `partials` only from exactly-3-column lines, so the
|
|
||||||
/// "bogus" name landed in `names` alone and `&partials["bogus"]`
|
|
||||||
/// panicked. It must produce a build error instead.
|
|
||||||
#[test]
|
|
||||||
fn dsc_four_column_checksum_line_errors_instead_of_panicking() {
|
|
||||||
let dsc_name = "hello_1.0-1.dsc";
|
|
||||||
let dsc = "\
|
|
||||||
Format: 3.0 (native)
|
|
||||||
Source: hello
|
|
||||||
Binary: hello
|
|
||||||
Architecture: any
|
|
||||||
Version: 1.0-1
|
|
||||||
Maintainer: A B <a@b.c>
|
|
||||||
Checksums-Sha1:
|
|
||||||
aaa111 12 hello_1.0.orig.tar.xz
|
|
||||||
Checksums-Sha256:
|
|
||||||
bbb222 12 bogus hello_1.0-1.debian.tar.xz
|
|
||||||
Files:
|
|
||||||
ddd333 12 hello_1.0.orig.tar.xz
|
|
||||||
";
|
|
||||||
let base = tempfile::tempdir().expect("tempdir");
|
|
||||||
std::fs::write(base.path().join(dsc_name), dsc).expect("write dsc");
|
|
||||||
|
|
||||||
let ctx = Arc::new(
|
|
||||||
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
|
||||||
);
|
|
||||||
let mut checksums = FileChecksums::new();
|
|
||||||
let err = include_dsc_artifacts(&ctx, base.path(), dsc_name, &mut checksums)
|
|
||||||
.expect_err("malformed Checksums-Sha256 line must fail the build");
|
|
||||||
let err = err.to_string();
|
|
||||||
assert!(err.contains("Checksums-Sha256"), "{err}");
|
|
||||||
assert!(err.contains("hello_1.0-1.debian.tar.xz"), "{err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Happy path: tarball entries are assembled from the Checksums fields
|
|
||||||
/// (sha1/sha256) and merged with the legacy 5-column `Files` md5, in
|
|
||||||
/// Checksums-Sha1 order, with the `.dsc` itself hashed fresh first.
|
|
||||||
#[test]
|
|
||||||
fn include_dsc_artifacts_merges_legacy_files_layout() {
|
|
||||||
let dsc_name = "hello_1.0-1.dsc";
|
|
||||||
let tarball = "hello_1.0.orig.tar.xz";
|
|
||||||
let dsc = "\
|
|
||||||
Format: 3.0 (quilt)
|
|
||||||
Source: hello
|
|
||||||
Binary: hello
|
|
||||||
Architecture: any
|
|
||||||
Version: 1.0-1
|
|
||||||
Maintainer: A B <a@b.c>
|
|
||||||
Checksums-Sha1:
|
|
||||||
aaa111 12 hello_1.0.orig.tar.xz
|
|
||||||
Checksums-Sha256:
|
|
||||||
bbb222 12 hello_1.0.orig.tar.xz
|
|
||||||
Files:
|
|
||||||
ddd333 12 devel optional hello_1.0.orig.tar.xz
|
|
||||||
";
|
|
||||||
let base = tempfile::tempdir().expect("tempdir");
|
|
||||||
std::fs::write(base.path().join(dsc_name), dsc).expect("write dsc");
|
|
||||||
std::fs::write(base.path().join(tarball), "tarball bytes").expect("write tarball");
|
|
||||||
|
|
||||||
let ctx = Arc::new(
|
|
||||||
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
|
||||||
);
|
|
||||||
let mut checksums = FileChecksums::new();
|
|
||||||
include_dsc_artifacts(&ctx, base.path(), dsc_name, &mut checksums)
|
|
||||||
.expect("valid dsc must parse");
|
|
||||||
|
|
||||||
let collected: Vec<(String, crate::debian::ChecksumEntry)> = checksums
|
|
||||||
.iter()
|
|
||||||
.map(|(k, e)| (k.clone(), e.clone()))
|
|
||||||
.collect();
|
|
||||||
assert_eq!(
|
|
||||||
collected
|
|
||||||
.iter()
|
|
||||||
.map(|(k, _)| k.as_str())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![dsc_name, tarball],
|
|
||||||
".dsc first, then Checksums-Sha1 order"
|
|
||||||
);
|
|
||||||
|
|
||||||
// The .dsc is hashed fresh from disk.
|
|
||||||
let dsc_entry = &collected[0].1;
|
|
||||||
assert_eq!(dsc_entry.size, dsc.len() as u64);
|
|
||||||
assert_eq!(dsc_entry.md5.len(), 32);
|
|
||||||
assert_eq!(dsc_entry.sha1.len(), 40);
|
|
||||||
assert_eq!(dsc_entry.sha256.len(), 64);
|
|
||||||
|
|
||||||
// The tarball reuses the .dsc-recorded digests, including the
|
|
||||||
// legacy 5-column `Files` md5 (section/priority skipped).
|
|
||||||
let tar_entry = &collected[1].1;
|
|
||||||
assert_eq!(tar_entry.size, 12);
|
|
||||||
assert_eq!(tar_entry.md5, "ddd333");
|
|
||||||
assert_eq!(tar_entry.sha1, "aaa111");
|
|
||||||
assert_eq!(tar_entry.sha256, "bbb222");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A binary-only (binNMU) build whose changelog cannot yield the
|
/// A binary-only (binNMU) build whose changelog cannot yield the
|
||||||
/// previous entry (malformed second header, unbalanced parenthesis) must
|
/// previous entry (malformed second header, unbalanced parenthesis) must
|
||||||
/// fail the metadata generation with a diagnostic naming the problem,
|
/// fail the metadata generation with a diagnostic naming the problem,
|
||||||
/// instead of silently emitting a plain `Source:` `.changes` with no
|
/// instead of silently emitting a plain `Source:` `.changes` with no
|
||||||
/// `Binary-Only-Changes` and no redistributed previous `.dsc`.
|
/// `Binary-Only-Changes` and no previous-version reference.
|
||||||
#[test]
|
#[test]
|
||||||
fn binary_only_prev_version_parse_failure_errors_instead_of_wrong_metadata() {
|
fn binary_only_prev_version_parse_failure_errors_instead_of_wrong_metadata() {
|
||||||
let changelog = "\
|
let changelog = "\
|
||||||
@@ -739,4 +552,104 @@ Description: test package
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
assert!(err.contains("Permission denied"), "{err}");
|
assert!(err.contains("Permission denied"), "{err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A binary-only (binNMU) build references the previous source version
|
||||||
|
/// (`Source: pkg (prev)`, `Binary-Only-Changes`) but must NOT
|
||||||
|
/// redistribute any source file: like dpkg-genchanges/genbuildinfo, the
|
||||||
|
/// previous `.dsc` and its tarballs stay out of both documents even when
|
||||||
|
/// they exist next to the artifacts.
|
||||||
|
#[test]
|
||||||
|
fn binary_only_metadata_references_previous_source_without_redistributing_it() {
|
||||||
|
let changelog = "\
|
||||||
|
hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes
|
||||||
|
|
||||||
|
* Binary-only rebuild.
|
||||||
|
|
||||||
|
-- A B <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
hello (1.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
-- A B <a@b.c> Sun, 31 Dec 2023 00:00:00 +0000
|
||||||
|
";
|
||||||
|
let control = "\
|
||||||
|
Source: hello
|
||||||
|
Section: devel
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: A B <a@b.c>
|
||||||
|
|
||||||
|
Package: hello
|
||||||
|
Architecture: all
|
||||||
|
Description: test package
|
||||||
|
";
|
||||||
|
let base = tempfile::tempdir().expect("tempdir");
|
||||||
|
let tree = base.path().join("hello-1.0");
|
||||||
|
std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree");
|
||||||
|
std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
|
||||||
|
std::fs::write(tree.join("debian/control"), control).expect("write control");
|
||||||
|
std::fs::write(
|
||||||
|
tree.join("debian/files"),
|
||||||
|
"hello_1.0-1+b1_all.deb devel optional\n",
|
||||||
|
)
|
||||||
|
.expect("write files");
|
||||||
|
std::fs::write(base.path().join("hello_1.0-1+b1_all.deb"), "deb payload")
|
||||||
|
.expect("write deb");
|
||||||
|
|
||||||
|
// The trap: the previous source artifacts sit right next to the
|
||||||
|
// binaries, as they would after a source build. dpkg does not
|
||||||
|
// redistribute them for a binary-only upload, and neither must we.
|
||||||
|
std::fs::write(
|
||||||
|
base.path().join("hello_1.0-1.dsc"),
|
||||||
|
"Format: 3.0 (quilt)\nSource: hello\nBinary: hello\nArchitecture: any\nVersion: \
|
||||||
|
1.0-1\nMaintainer: A B <a@b.c>\nChecksums-Sha1:\n aaa111 12 \
|
||||||
|
hello_1.0.orig.tar.xz\n",
|
||||||
|
)
|
||||||
|
.expect("write previous dsc");
|
||||||
|
std::fs::write(base.path().join("hello_1.0.orig.tar.xz"), "tarball bytes")
|
||||||
|
.expect("write previous tarball");
|
||||||
|
|
||||||
|
let ctx = Arc::new(
|
||||||
|
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
||||||
|
);
|
||||||
|
let opts = BinaryMetadataOptions {
|
||||||
|
profiles: Vec::new(),
|
||||||
|
vendor: "debian".to_string(),
|
||||||
|
exported_env: BTreeMap::new(),
|
||||||
|
build_arch: "amd64".to_string(),
|
||||||
|
host_arch: "amd64".to_string(),
|
||||||
|
};
|
||||||
|
let (buildinfo_path, changes_path) =
|
||||||
|
generate_binary_metadata(&ctx, &tree, base.path(), &opts)
|
||||||
|
.expect("binNMU metadata generation must succeed");
|
||||||
|
|
||||||
|
let changes = std::fs::read_to_string(&changes_path).expect("read changes");
|
||||||
|
let buildinfo = std::fs::read_to_string(&buildinfo_path).expect("read buildinfo");
|
||||||
|
|
||||||
|
// The previous version is referenced textually.
|
||||||
|
assert!(
|
||||||
|
changes.contains("Source: hello (1.0-1)"),
|
||||||
|
"changes must reference the previous version: {changes}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
buildinfo.contains("Binary-Only-Changes"),
|
||||||
|
"buildinfo must record the binary-only entry: {buildinfo}"
|
||||||
|
);
|
||||||
|
// ... but no source file is distributed, on either side.
|
||||||
|
for (doc, text) in [("changes", &changes), ("buildinfo", &buildinfo)] {
|
||||||
|
assert!(
|
||||||
|
!text.contains("hello_1.0-1.dsc"),
|
||||||
|
"{doc} must not redistribute the previous .dsc: {text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!text.contains("hello_1.0.orig.tar.xz"),
|
||||||
|
"{doc} must not redistribute the previous tarball: {text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The distributed set is exactly the binary artifacts + buildinfo.
|
||||||
|
assert!(
|
||||||
|
changes.contains("hello_1.0-1+b1_all.deb") && changes.contains(".buildinfo"),
|
||||||
|
"changes must distribute the deb and the buildinfo: {changes}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ pub struct ChangesInput {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
/// Sorted binary package names with artifacts (empty for source-only).
|
/// Sorted binary package names with artifacts (empty for source-only).
|
||||||
pub binaries: Vec<String>,
|
pub binaries: Vec<String>,
|
||||||
|
/// Whether the changelog entry is a binary-only (binNMU) upload
|
||||||
|
/// (`Binary-Only: yes` field).
|
||||||
|
pub binary_only: bool,
|
||||||
/// Active build profiles (`Built-For-Profiles`); omitted when empty.
|
/// Active build profiles (`Built-For-Profiles`); omitted when empty.
|
||||||
pub built_for_profiles: Vec<String>,
|
pub built_for_profiles: Vec<String>,
|
||||||
/// `Architecture` field value in encounter order (e.g. `source`,
|
/// `Architecture` field value in encounter order (e.g. `source`,
|
||||||
@@ -142,6 +145,9 @@ pub fn render_changes(input: &ChangesInput) -> Paragraph {
|
|||||||
let joined = input.binaries.join(" ");
|
let joined = input.binaries.join(" ");
|
||||||
p.set("Binary", &wrap_long(&joined));
|
p.set("Binary", &wrap_long(&joined));
|
||||||
}
|
}
|
||||||
|
if input.binary_only {
|
||||||
|
p.set("Binary-Only", "yes");
|
||||||
|
}
|
||||||
if !input.built_for_profiles.is_empty() {
|
if !input.built_for_profiles.is_empty() {
|
||||||
p.set("Built-For-Profiles", &input.built_for_profiles.join(" "));
|
p.set("Built-For-Profiles", &input.built_for_profiles.join(" "));
|
||||||
}
|
}
|
||||||
@@ -329,6 +335,7 @@ mod tests {
|
|||||||
date: "Sat, 22 Aug 2026 10:00:00 +0000".to_string(),
|
date: "Sat, 22 Aug 2026 10:00:00 +0000".to_string(),
|
||||||
source: "pkg".to_string(),
|
source: "pkg".to_string(),
|
||||||
binaries: vec![],
|
binaries: vec![],
|
||||||
|
binary_only: false,
|
||||||
built_for_profiles: vec![],
|
built_for_profiles: vec![],
|
||||||
architecture: "source".to_string(),
|
architecture: "source".to_string(),
|
||||||
version: "1.0".to_string(),
|
version: "1.0".to_string(),
|
||||||
|
|||||||
+75
-16
@@ -638,6 +638,7 @@ pub fn run_source_build(
|
|||||||
date: entry.date_raw.clone(),
|
date: entry.date_raw.clone(),
|
||||||
source: source_display.clone(),
|
source: source_display.clone(),
|
||||||
binaries: Vec::new(), // source-only upload
|
binaries: Vec::new(), // source-only upload
|
||||||
|
binary_only: entry.binary_only,
|
||||||
built_for_profiles: profiles.clone(),
|
built_for_profiles: profiles.clone(),
|
||||||
architecture: "source".to_string(),
|
architecture: "source".to_string(),
|
||||||
version: entry.version.full(),
|
version: entry.version.full(),
|
||||||
@@ -1863,23 +1864,50 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
#[test]
|
#[test]
|
||||||
fn diff_binary_build_metadata() {
|
fn diff_binary_build_metadata() {
|
||||||
const NAME: &str = "pkh-diff-m";
|
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!(
|
let changelog = format!(
|
||||||
"{NAME} (1.0-1) unstable; urgency=medium\n\n * Binary build test.\n\n -- {MAINTAINER} {DATE}\n"
|
"{NAME} (1.0-1) unstable; urgency=medium\n\n * Binary build test.\n\n -- {MAINTAINER} {DATE}\n"
|
||||||
);
|
);
|
||||||
let rules = format!(
|
diff_binary_metadata_case(NAME, &changelog, "1.0-1", false);
|
||||||
"#!/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"
|
}
|
||||||
);
|
|
||||||
|
/// debian/control shared by the binary-metadata differential cases: one
|
||||||
|
/// arch:any deb and one arch:all udeb.
|
||||||
|
fn binary_test_control(name: &str) -> String {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// debian/rules driving dpkg-gencontrol/dpkg-deb directly (no debhelper).
|
||||||
|
fn binary_test_rules(name: &str) -> String {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both-sides binary metadata comparison for one changelog: golden
|
||||||
|
/// `dpkg-buildpackage -b` against the pkh deb flow (rules targets with a
|
||||||
|
/// dpkg-buildpackage-like environment) plus native metadata generation.
|
||||||
|
/// `artifact_version` is the full version the artifacts are named after;
|
||||||
|
/// `with_prev_source` additionally places the previous version's `.dsc`
|
||||||
|
/// and tarball next to the tree on both sides (the binNMU trap: they must
|
||||||
|
/// not be redistributed).
|
||||||
|
fn diff_binary_metadata_case(
|
||||||
|
name: &str,
|
||||||
|
changelog: &str,
|
||||||
|
artifact_version: &str,
|
||||||
|
with_prev_source: bool,
|
||||||
|
) {
|
||||||
|
let control = binary_test_control(name);
|
||||||
|
let rules = binary_test_rules(name);
|
||||||
|
|
||||||
let write_tree = |root: &Path| {
|
let write_tree = |root: &Path| {
|
||||||
fs::create_dir_all(root.join(format!("{NAME}/debian/source"))).expect("mkdir tree");
|
fs::create_dir_all(root.join(format!("{name}/debian/source"))).expect("mkdir tree");
|
||||||
let tree = root.join(NAME);
|
let tree = root.join(name);
|
||||||
fs::write(tree.join("debian/control"), &control).expect("write control");
|
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/changelog"), changelog).expect("write changelog");
|
||||||
fs::write(tree.join("debian/source/format"), "3.0 (native)\n").expect("write format");
|
fs::write(tree.join("debian/source/format"), "3.0 (native)\n").expect("write format");
|
||||||
fs::write(tree.join("debian/rules"), &rules).expect("write rules");
|
fs::write(tree.join("debian/rules"), &rules).expect("write rules");
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -1900,6 +1928,20 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
let golden_tree = write_tree(&golden_root);
|
let golden_tree = write_tree(&golden_root);
|
||||||
let ours_tree = write_tree(&ours_root);
|
let ours_tree = write_tree(&ours_root);
|
||||||
|
|
||||||
|
if with_prev_source {
|
||||||
|
for root in [&golden_root, &ours_root] {
|
||||||
|
let dsc = format!(
|
||||||
|
"Format: 3.0 (native)\nSource: {name}\nBinary: {name}\nArchitecture: all\n\
|
||||||
|
Version: 1.0-1\nMaintainer: {MAINTAINER}\nChecksums-Sha1:\n aaa111 12 \
|
||||||
|
{name}_1.0.tar.xz\nChecksums-Sha256:\n bbb222 12 {name}_1.0.tar.xz\nFiles:\n \
|
||||||
|
ddd333 12 utils optional {name}_1.0.tar.xz\n"
|
||||||
|
);
|
||||||
|
fs::write(root.join(format!("{name}_1.0-1.dsc")), dsc).expect("write previous dsc");
|
||||||
|
fs::write(root.join(format!("{name}_1.0.tar.xz")), "tarball byte")
|
||||||
|
.expect("write previous tarball");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Golden side: real dpkg-buildpackage binary build.
|
// Golden side: real dpkg-buildpackage binary build.
|
||||||
let status = crate::test_support::run_logged(
|
let status = crate::test_support::run_logged(
|
||||||
Command::new("dpkg-buildpackage")
|
Command::new("dpkg-buildpackage")
|
||||||
@@ -1915,7 +1957,7 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
// rules targets directly by default (missing Rules-Requires-Root is
|
// rules targets directly by default (missing Rules-Requires-Root is
|
||||||
// treated as 'no'), so no fakeroot wrapper here either.
|
// treated as 'no'), so no fakeroot wrapper here either.
|
||||||
let entry =
|
let entry =
|
||||||
crate::debian::parse_changelog_entry_from_str(&changelog).expect("parse changelog");
|
crate::debian::parse_changelog_entry_from_str(changelog).expect("parse changelog");
|
||||||
let vendor = env::current_vendor();
|
let vendor = env::current_vendor();
|
||||||
let profiles = env::resolve_build_profiles(&[], &vendor);
|
let profiles = env::resolve_build_profiles(&[], &vendor);
|
||||||
let parallel = env::num_parallel();
|
let parallel = env::num_parallel();
|
||||||
@@ -1959,15 +2001,32 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
|
|
||||||
// Compare artifacts.
|
// Compare artifacts.
|
||||||
assert_changes_equivalent(
|
assert_changes_equivalent(
|
||||||
&golden_root.join(format!("{NAME}_1.0-1_amd64.changes")),
|
&golden_root.join(format!("{name}_{artifact_version}_amd64.changes")),
|
||||||
&ours_root.join(format!("{NAME}_1.0-1_amd64.changes")),
|
&ours_root.join(format!("{name}_{artifact_version}_amd64.changes")),
|
||||||
);
|
);
|
||||||
assert_buildinfo_equivalent(
|
assert_buildinfo_equivalent(
|
||||||
&golden_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
|
&golden_root.join(format!("{name}_{artifact_version}_amd64.buildinfo")),
|
||||||
&ours_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
|
&ours_root.join(format!("{name}_{artifact_version}_amd64.buildinfo")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Differential check of the binary-only (binNMU) metadata against real
|
||||||
|
/// `dpkg-buildpackage -b`: with the previous source artifacts sitting
|
||||||
|
/// next to the tree (as after a source build), the `.changes` must
|
||||||
|
/// distribute only the binaries and the `.buildinfo`, both documents
|
||||||
|
/// referencing the previous version textually only. Regression guard for
|
||||||
|
/// the previous-source redistribution pkh used to emit.
|
||||||
|
#[test]
|
||||||
|
fn diff_binmu_binary_metadata() {
|
||||||
|
const NAME: &str = "pkh-diff-r";
|
||||||
|
let changelog = format!(
|
||||||
|
"{NAME} (1.0-1+b1) unstable; urgency=medium, binary-only=yes\n\n * Binary-only \
|
||||||
|
rebuild.\n\n -- {MAINTAINER} {DATE}\n\n{NAME} (1.0-1) unstable; urgency=medium\n\n \
|
||||||
|
* Initial release.\n\n -- {MAINTAINER} {DATE}\n"
|
||||||
|
);
|
||||||
|
diff_binary_metadata_case(NAME, &changelog, "1.0-1+b1", true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diff_native_minimal() {
|
fn diff_native_minimal() {
|
||||||
differential_case(&FixtureSpec::new("pkh-diff-a", "1.0-1", "unstable"));
|
differential_case(&FixtureSpec::new("pkh-diff-a", "1.0-1", "unstable"));
|
||||||
|
|||||||
Reference in New Issue
Block a user