From e7cad0b1cae2af8e190826ced4b751ec27342256 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Mon, 28 Sep 2026 14:04:22 +0200 Subject: [PATCH] deb: apply real patches in single-debian-patch trees The f055b70 skip assumed a single-debian-patch tree carries all patch content ambient in the working tree. A tree that declares single-debian-patch while listing real quilt patches in debian/patches/series (e.g. a pkh-scaffolded package that grew patches, like lite-xl) was built from unpatched sources: the binary silently lost the patch content, while plain dpkg-buildpackage - and Launchpad, which never sees debian/source/local-options - applies the series. Mirror dpkg-source --before-build semantics: apply the series minus the ambient 'debian-changes[-]' entries, dropping those entries from the staged series so 'quilt push -a' tolerates the ambient content. A series holding only the ambient entry keeps skipping, as before (covered by apply_phase_skips_single_debian_patch_trees); the classification core is unit-tested in series_patches_to_apply_classification. --- src/deb/local.rs | 141 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 24 deletions(-) diff --git a/src/deb/local.rs b/src/deb/local.rs index 315fe23..b57e55b 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -950,6 +950,29 @@ fn generate_upload_metadata( Ok((buildinfo, changes)) } +/// Is this series entry the ambient single-debian-patch placeholder? +/// `dpkg-source -b` names it 'debian-changes' or 'debian-changes-'. +fn is_debian_changes(entry: &str) -> bool { + entry == "debian-changes" || entry.starts_with("debian-changes-") +} + +/// Pure decision core of the series classification in +/// [`apply_quilt_patches`]: the entries quilt must apply. Blank and comment +/// lines never apply; in a `single-debian-patch` tree the ambient +/// 'debian-changes[-]' entries are dropped (their content stays in +/// the working tree — `quilt push -a` would refuse them). +fn series_patches_to_apply<'a>(series_content: &'a str, single_debian_patch: bool) -> Vec<&'a str> { + series_content + .lines() + .map(str::trim) + .filter(|entry| { + !entry.is_empty() + && !entry.starts_with('#') + && !(single_debian_patch && is_debian_changes(entry)) + }) + .collect() +} + /// Apply quilt patches before building, if the package provides a /// 'debian/patches/series' file. `pop_first` un-applies the previous /// attempt's patches first (resumed session whose patch tree changed). @@ -972,35 +995,56 @@ fn apply_quilt_patches( // Skip patch application if the series file contains no patches let series_content = ctx.read_file(&series_path)?; - let total_patches = series_content - .lines() - .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#')) - .count(); - let has_patches = total_patches > 0; - if !has_patches { - log::debug!( - "'{}' contains no patches, skipping quilt patch application", - series_path.display() - ); - return Ok(()); - } - // A `single-debian-patch` tree is already patched by construction: - // `dpkg-source -b` folds the working-tree delta into - // 'debian/patches/debian-changes' and registers it in the series + // Classify the series entries. In a `single-debian-patch` tree, + // `dpkg-source --before-build` applies every series entry except the + // ambient 'debian-changes[-]' one: `dpkg-source -b` folded + // the working-tree delta into it and registered it in the series // WITHOUT applying it — the content stays ambient in the working tree // (e.g. the vendored rust '.cargo/config.toml'). Raw `quilt push -a` - // would then refuse the patch ('file already exists'); only dpkg's own - // patch(1)-based application tolerates that. Since `dpkg-source -b` - // regenerates the patch from the working tree at source-build time, - // applying patches is wrong in principle here: skip the step. - if uses_single_debian_patch(&ctx, Path::new(package_dir)) { - log::info!( - "Tree uses single-debian-patch: the working tree already \ - carries the patch content, skipping quilt patch application" - ); + // would refuse that entry ('file already exists'); only dpkg's own + // patch(1)-based application tolerates it. Real patches listed + // alongside it — or in a tree that declares `single-debian-patch` + // while carrying an unapplied quilt series, like a pkh-scaffolded + // package that grew real patches — must still be applied. + let single_debian_patch = uses_single_debian_patch(&ctx, Path::new(package_dir)); + let real_patches = series_patches_to_apply(&series_content, single_debian_patch); + let total_patches = real_patches.len(); + if total_patches == 0 { + if single_debian_patch { + log::info!( + "Tree uses single-debian-patch: the working tree already \ + carries the patch content, skipping quilt patch application" + ); + } else { + log::debug!( + "'{}' contains no patches, skipping quilt patch application", + series_path.display() + ); + } return Ok(()); } + if single_debian_patch + && series_content + .lines() + .any(|line| is_debian_changes(line.trim())) + { + // Mirror dpkg-source: drop the ambient debian-changes entries from + // the staged series so `quilt push -a` below only sees the real + // patches. The staged copy is per-session, so the host tree keeps + // the pristine series. + let filtered = series_content + .lines() + .filter(|line| !is_debian_changes(line.trim())) + .collect::>() + .join("\n"); + ctx.write_file(&series_path, &format!("{}\n", filtered))?; + log::info!( + "Tree uses single-debian-patch: dropped the ambient \ + 'debian-changes' entries from the staged series, applying \ + the real patches" + ); + } // Make sure quilt is available in the build context log::debug!("Installing quilt for patch application..."); @@ -1529,4 +1573,53 @@ mod tests { ) .unwrap(); } + + /// Series classification for `apply_quilt_patches`: real patches apply; + /// blank and comment lines never do; a `single-debian-patch` tree's + /// ambient 'debian-changes' entries are dropped while real patches + /// listed alongside them still apply (pkh must mirror dpkg-source's + /// `--before-build` behavior). + #[test] + fn series_patches_to_apply_classification() { + // Plain quilt tree: every entry applies. + assert_eq!( + series_patches_to_apply("0001-a.patch\n0002-b.patch\n", false), + vec!["0001-a.patch", "0002-b.patch"] + ); + + // Comments and blank lines never apply. + assert_eq!( + series_patches_to_apply("# header\n\n0001-a.patch\n \n", false), + vec!["0001-a.patch"] + ); + + // single-debian-patch tree with only the ambient entry: nothing to + // apply — the working tree already carries the content. + assert_eq!( + series_patches_to_apply("debian-changes-2.1.8\n", true), + Vec::<&str>::new() + ); + + // single-debian-patch tree whose series grew real patches: apply + // the real ones, drop the ambient entries. + assert_eq!( + series_patches_to_apply( + "0001-real.patch\ndebian-changes-2.1.8\n0002-real.patch\n", + true + ), + vec!["0001-real.patch", "0002-real.patch"] + ); + + // The same series in a plain tree keeps the debian-changes entry: + // without the single-debian-patch declaration it is a real patch. + assert_eq!( + series_patches_to_apply("debian-changes-2.1.8\n", false), + vec!["debian-changes-2.1.8"] + ); + + // The versioned and unversioned ambient names are both recognized. + assert!(is_debian_changes("debian-changes")); + assert!(is_debian_changes("debian-changes-1.0")); + assert!(!is_debian_changes("0001-real.patch")); + } }