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[-<version>]' 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.
This commit is contained in:
2026-09-28 14:04:22 +02:00
parent d927dc93ab
commit e7cad0b1ca
+113 -20
View File
@@ -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-<version>'.
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[-<version>]' 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[-<version>]' 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)) {
// 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::<Vec<_>>()
.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"));
}
}