diff --git a/src/build/mod.rs b/src/build/mod.rs index c943c01..fec81d6 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -224,7 +224,8 @@ fn retry_after_revendor( /// Run the full native source-build pipeline in `cwd`. /// /// Steps (mirroring `dpkg-buildpackage -S -I -i -nc -d`): -/// 1. sanity checks and metadata resolution (changelog, control), +/// 1. sanity checks and metadata resolution (changelog, control); a +/// `binary-only=yes` changelog entry is refused, like `dpkg-source -b`, /// 2. environment setup (`SOURCE_DATE_EPOCH`, `DEB_BUILD_OPTIONS`, arch vars), /// 3. signing decision (key discovery, UNRELEASED handling), /// 4. `dpkg-source --before-build` then `dpkg-source -b`, @@ -281,33 +282,25 @@ pub fn run_source_build( let mut entries = crate::debian::changelog::parse_changelog_entries(&changelog_path, Some(2))?; let entry = entries.remove(0); let previous_entry = entries.into_iter().next(); + + // A binary-only (binNMU) changelog entry is a binary publication whose + // source is already in the archive: like dpkg-source, refuse to build + // source for it instead of producing binNMU-style source metadata. + if entry.binary_only { + return Err( + "cannot build source for a binary-only publication: the changelog \ + entry sets binary-only=yes (dpkg-source refuses it too)" + .into(), + ); + } + let ctrl = ControlInfo::parse(&control_path)?; if let Some(u) = &ui { u.set_build_target(&entry.source, &entry.version.full(), &entry.distribution); } - // binNMU builds reference the *previous* (source) version in their - // artifact metadata, like dpkg-genchanges/genbuildinfo do. - let previous_version = if entry.binary_only { - previous_entry.as_ref().map(|e| e.version.full()) - } else { - None - }; - let source_display = if entry.binary_only { - match previous_version.as_deref() { - Some(prev) => format!("{} ({})", entry.source, prev), - None => entry.source.clone(), - } - } else { - entry.source.clone() - }; - let binary_only_changes = entry.binary_only.then(|| { - format!( - "{}\n\n -- {} <{}> {}", - entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw - ) - }); + let source_display = entry.source.clone(); let sversion = entry.version.no_epoch(); let dsc_name = format!("{}_{}.dsc", entry.source, sversion); @@ -431,27 +424,6 @@ pub fn run_source_build( .into()); } - // Binary-only uploads redistribute the *previous* source: metadata - // references the previous version's .dsc (which must already exist in - // the output directory), exactly like dpkg-genchanges/genbuildinfo. - let ref_dsc_name = match previous_version.as_deref().filter(|_| entry.binary_only) { - Some(prev) => { - let prev_version = crate::debian::DebianVersion::parse(prev)?; - let name = format!("{}_{}.dsc", entry.source, prev_version.no_epoch()); - if !parent.join(&name).exists() { - return Err(format!( - "binary-only build requires the previous source '{} \ - {}' to exist next to the package", - entry.source, prev - ) - .into()); - } - name - } - None => dsc_name.clone(), - }; - let ref_dsc_path = parent.join(&ref_dsc_name); - // ------------------------------------------------------------------ // 6. .buildinfo generation (native dpkg-genbuildinfo equivalent) // ------------------------------------------------------------------ @@ -461,7 +433,7 @@ pub fn run_source_build( // What the .buildinfo itself records: like dpkg-genbuildinfo, only the // referenced .dsc — not the tarballs, and never the buildinfo itself. let mut buildinfo_checksums = FileChecksums::new(); - buildinfo_checksums.add_file_as(&ref_dsc_path, &ref_dsc_name)?; + buildinfo_checksums.add_file_as(&dsc_path, &dsc_name)?; let status_path = PathBuf::from("/var/lib/dpkg/status"); let bd_fields = [ctrl.source.get("Build-Depends").unwrap_or("")]; @@ -474,7 +446,9 @@ pub fn run_source_build( binaries: Vec::new(), // source-only build architecture: "source".to_string(), version: entry.version.full(), - binary_only_changes: binary_only_changes.clone(), + // A binary-only entry never reaches a source build (refused + // above), so the buildinfo never carries Binary-Only-Changes. + binary_only_changes: None, build_origin: vendor.clone(), build_architecture: arch_vars .get("DEB_BUILD_ARCH") @@ -515,14 +489,14 @@ pub fn run_source_build( // Pull the tarball checksums out of the referenced .dsc so they are // distributed through the .changes like dpkg-genchanges does, in the // order the .dsc itself lists them. - let dsc_content = std::fs::read_to_string(&ref_dsc_path) - .map_err(|e| format!("cannot read '{}': {}", ref_dsc_path.display(), e))?; + let dsc_content = std::fs::read_to_string(&dsc_path) + .map_err(|e| format!("cannot read '{}': {}", dsc_path.display(), e))?; let dsc_para = parse_paragraphs(crate::debian::control::strip_clearsigned_armour( &dsc_content, )) .into_iter() .next() - .ok_or_else(|| format!("'{}' is empty", ref_dsc_path.display()))?; + .ok_or_else(|| format!("'{}' is empty", dsc_path.display()))?; // Whether the upload redistributes the upstream tarballs (dpkg // -sa/-si/-sd source styles). Stripping only applies to a split source @@ -542,7 +516,7 @@ pub fn run_source_build( continue; }; for cl in parse_checksum_field(field, value) - .map_err(|e| format!("cannot parse '{}': {e}", ref_dsc_path.display()))? + .map_err(|e| format!("cannot parse '{}': {e}", dsc_path.display()))? { if !dsc_files.contains_key(&cl.name) { dsc_file_names.push(cl.name.clone()); @@ -582,7 +556,7 @@ pub fn run_source_build( }; for name in &dsc_file_names { - if name == &ref_dsc_name { + if name == &dsc_name { continue; // already computed directly above } if is_stripped(name) { @@ -600,7 +574,7 @@ pub fn run_source_build( let partial = dsc_files.get(name).ok_or_else(|| { format!( "file '{name}' listed in '{}' has no checksum entry", - ref_dsc_path.display() + dsc_path.display() ) })?; checksums.insert_entry( @@ -627,13 +601,9 @@ pub fn run_source_build( // dsc and tarballs use the source stanza defaults (not persisted into // debian/files, matching dpkg). let mut changes_files = files_list.clone(); - changes_files.add(FilesEntry::new( - &ref_dsc_name, - ctrl.section(), - ctrl.priority(), - )); + changes_files.add(FilesEntry::new(&dsc_name, ctrl.section(), ctrl.priority())); for name in &dsc_file_names { - if name != &ref_dsc_name && !is_stripped(name) { + if name != &dsc_name && !is_stripped(name) { changes_files.add(FilesEntry::new(name, ctrl.section(), ctrl.priority())); } } @@ -689,13 +659,9 @@ pub fn run_source_build( log::info!("Signing {}", dsc_name); crate::utils::gpg::clearsign_file(&dsc_path, &keyid)?; // The freshly built .dsc changed: refresh its digests in both - // checksum sets. For binary-only builds the metadata references the - // *previous* .dsc (untouched by this build), so there is nothing to - // refresh. - if !entry.binary_only { - buildinfo_checksums.add_file_as(&dsc_path, &dsc_name)?; - checksums.add_file_as(&dsc_path, &dsc_name)?; - } + // checksum sets. + buildinfo_checksums.add_file_as(&dsc_path, &dsc_name)?; + checksums.add_file_as(&dsc_path, &dsc_name)?; // Re-render the .buildinfo from its own set (the .dsc only, like // dpkg-genbuildinfo): it must not list the tarballs or itself. buildinfo::save_buildinfo(&buildinfo_path, &render_buildinfo_doc(&buildinfo_checksums))?; @@ -1051,6 +1017,35 @@ mod tests { use super::*; use crate::debian::DebianVersion; + /// A binary-only (binNMU) changelog entry is a binary publication: like + /// `dpkg-source -b`, the source build must refuse it outright instead of + /// producing binNMU-style source metadata referencing the previous + /// version's `.dsc`. + #[test] + fn source_build_refuses_binary_only_changelog() { + 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"), + "hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes\n\n \ + * Binary-only rebuild.\n\n -- A B Mon, 01 Jan 2024 00:00:00 +0000\n", + ) + .expect("write changelog"); + std::fs::write( + tree.join("debian/control"), + "Source: hello\nMaintainer: A B \n", + ) + .expect("write control"); + std::fs::write(tree.join("debian/rules"), "#!/usr/bin/make -f\n").expect("write rules"); + + let err = run_source_build(&tree, &SourceBuildOptions::default(), None) + .expect_err("binary-only entries must not build a source package"); + let err = err.to_string(); + assert!(err.contains("binary-only"), "{err}"); + assert!(err.contains("source"), "{err}"); + } + #[test] fn partial_checksum_defaults() { let p = PartialChecksum::default(); @@ -1277,6 +1272,8 @@ mod differential_tests { extra_source_fields: &'static [(&'static str, &'static str)], /// Version of the previous changelog entry, when the fixture has one. previous_version: Option<&'static str>, + /// Mark the newest changelog entry `binary-only=yes` (binNMU). + binary_only_marker: bool, } impl FixtureSpec { @@ -1294,6 +1291,7 @@ mod differential_tests { patches: &[], extra_source_fields: &[], previous_version: None, + binary_only_marker: false, } } @@ -1306,9 +1304,14 @@ mod differential_tests { } fn changelog(&self) -> String { + let params = if self.binary_only_marker { + format!("urgency={}, binary-only=yes", self.urgency) + } else { + format!("urgency={}", self.urgency) + }; let mut out = format!( - "{} ({}) {}; urgency={}\n\n", - self.name, self.version, self.distribution, self.urgency + "{} ({}) {}; {}\n\n", + self.name, self.version, self.distribution, params ); for line in self.body { out.push_str(" * "); @@ -2108,8 +2111,10 @@ Provides: virtual-thing (= 2.0), plain-virtual spec.body = &["* Binary-only rebuild."]; spec.previous_version = Some("1.0-1"); - // Binary-only metadata references the previous version's .dsc, which - // must already exist next to the package tree. + // A binNMU-style version number (+b1) with a previous entry, but + // WITHOUT the binary-only marker: this is a plain source build, and + // the sibling previous-version .dsc (as left by an earlier source + // build) must not change either side's output. let base = tempfile::tempdir().expect("tempdir"); let tree = write_fixture(base.path(), &spec); let prev_dsc = format!( @@ -2124,6 +2129,42 @@ Provides: virtual-thing (= 2.0), plain-virtual differential_on_tree(&tree, &SourceBuildOptions::default()); } + /// Both implementations must refuse a source build of a changelog entry + /// marked `binary-only=yes`: dpkg-source errors out, and pkh must refuse + /// the same way instead of producing binNMU-style source metadata + /// referencing the previous version. + #[test] + fn diff_source_build_rejects_binary_only_marker() { + let mut spec = FixtureSpec::new("pkh-diff-s", "1.0-1+b1", "unstable"); + spec.body = &["* Binary-only rebuild."]; + spec.previous_version = Some("1.0-1"); + spec.binary_only_marker = true; + let base = tempfile::tempdir().expect("tempdir"); + let tree = write_fixture(base.path(), &spec); + + // Golden side: real dpkg refuses. + let status = crate::test_support::run_logged( + Command::new("dpkg-buildpackage").current_dir(&tree).args([ + "-S", + "-I", + "-i", + "-nc", + "-d", + "--no-sign", + ]), + ) + .expect("run dpkg-buildpackage (is dpkg-dev installed?)"); + assert!( + !status.success(), + "dpkg-buildpackage -S must refuse a binary-only changelog entry" + ); + + // Ours: the native pipeline refuses likewise. + let err = run_source_build(&tree, &SourceBuildOptions::default(), None) + .expect_err("native source build must refuse a binary-only entry"); + assert!(err.to_string().contains("binary-only"), "{err}"); + } + #[test] fn diff_closes_bugs() { let mut spec = FixtureSpec::new("pkh-diff-j", "2.0-1", "unstable");