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");