deb: skip quilt patch application for single-debian-patch trees

This commit is contained in:
2026-09-17 02:15:15 +02:00
parent 77420e723a
commit f055b70281
+163
View File
@@ -512,6 +512,23 @@ fn apply_quilt_patches(
return Ok(()); 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
// 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"
);
return Ok(());
}
// Make sure quilt is available in the build context // Make sure quilt is available in the build context
log::debug!("Installing quilt for patch application..."); log::debug!("Installing quilt for patch application...");
let status = cap( let status = cap(
@@ -552,6 +569,39 @@ fn apply_quilt_patches(
Ok(()) Ok(())
} }
/// Whether the source options of the package tree at `package_dir` (inside
/// the build context) declare the `single-debian-patch` mode: either
/// `debian/source/local-options` or `debian/source/options` contains a
/// line whose trimmed content — after stripping a leading `--` long-option
/// dash — is exactly `single-debian-patch` (both spellings exist in the
/// wild). A line merely containing the token as a substring (e.g.
/// `--single-debian-patch-foo`) does not count.
fn uses_single_debian_patch(ctx: &Context, package_dir: &Path) -> bool {
let local_options = ctx
.read_file(&package_dir.join("debian/source/local-options"))
.ok();
let options = ctx
.read_file(&package_dir.join("debian/source/options"))
.ok();
options_declare_single_debian_patch(local_options.as_deref(), options.as_deref())
}
/// Pure decision core of [`uses_single_debian_patch`]: do the (optional)
/// contents of `debian/source/local-options` / `debian/source/options`
/// declare `single-debian-patch`?
fn options_declare_single_debian_patch(local_options: Option<&str>, options: Option<&str>) -> bool {
let declares = |content: Option<&str>| {
content.is_some_and(|content| {
content.lines().any(|line| {
let line = line.trim();
let line = line.strip_prefix("--").unwrap_or(line);
line == "single-debian-patch"
})
})
};
declares(local_options) || declares(options)
}
/// Pin a 'NotAutomatic' pocket (e.g. '-proposed') so apt takes it into /// Pin a 'NotAutomatic' pocket (e.g. '-proposed') so apt takes it into
/// account during dependency resolution. /// account during dependency resolution.
/// ///
@@ -685,3 +735,116 @@ fn dose3_explain_dependencies(
cmd.status()?; cmd.status()?;
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::context::ContextConfig;
#[test]
fn detector_matches_local_options_options_and_dashed_spellings() {
// The pkh scaffold spelling: bare token in local-options.
assert!(options_declare_single_debian_patch(
Some("single-debian-patch\n"),
None
));
// The other common spelling in either file: `--`-prefixed.
assert!(options_declare_single_debian_patch(
Some("--single-debian-patch\n"),
None
));
assert!(options_declare_single_debian_patch(
None,
Some("--single-debian-patch\n")
));
// Trailing whitespace and blank lines around the token.
assert!(options_declare_single_debian_patch(
Some("\n single-debian-patch \n"),
None
));
// Declared in options while local-options carries other options.
assert!(options_declare_single_debian_patch(
Some("--extend-diff-ignore='^vendor/'\n"),
Some("single-debian-patch\n")
));
}
#[test]
fn detector_rejects_substrings_empty_and_absent_files() {
// The token must not match as a substring of another option.
assert!(!options_declare_single_debian_patch(
Some("--single-debian-patch-ignore=^foo\n"),
None
));
assert!(!options_declare_single_debian_patch(
Some("--no-single-debian-patch\n"),
Some("single-debian-patching\n")
));
// Absent or empty files: not single-debian-patch.
assert!(!options_declare_single_debian_patch(None, None));
assert!(!options_declare_single_debian_patch(Some(""), Some("")));
// Unrelated content.
assert!(!options_declare_single_debian_patch(
Some("--extend-diff-ignore='^vendor/'\n"),
Some("tar-ignore = .git\n")
));
}
/// A tree with `single-debian-patch` in its local-options is detected
/// through the real build-context file access; a tree without the
/// declaration is not.
#[test]
fn detector_on_a_real_tree_through_the_context() {
let ctx = Context::new(ContextConfig::Local).unwrap();
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("pkg");
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
std::fs::write(
tree.join("debian/source/local-options"),
"single-debian-patch\n",
)
.unwrap();
assert!(uses_single_debian_patch(&ctx, &tree));
// No source options at all.
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("pkg");
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
assert!(!uses_single_debian_patch(&ctx, &tree));
}
/// Direct call of the patch-application phase on a tree simulating the
/// `dpkg-source -b` state of a single-debian-patch scaffold:
/// 'debian-changes' is registered in the series while its content is
/// already ambient in the working tree (raw `quilt push -a` would
/// refuse it with 'file already exists'). The phase must skip the
/// application entirely and succeed.
#[test]
fn apply_phase_skips_single_debian_patch_trees() {
let ctx = Arc::new(Context::new(ContextConfig::Local).unwrap());
// Dashed spelling through debian/source/options this time.
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("pkg");
std::fs::create_dir_all(tree.join("debian/patches")).unwrap();
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
// The touched file already carries the patched content: applying
// the patch would fail ('README' already exists / differs only in
// the patch's imagination).
std::fs::write(tree.join("README"), "patched\n").unwrap();
std::fs::write(
tree.join("debian/patches/debian-changes"),
"--- a/README\n+++ b/README\n@@ -1 +1 @@\n-orig\n+patched\n",
)
.unwrap();
std::fs::write(tree.join("debian/patches/series"), "debian-changes\n").unwrap();
std::fs::write(
tree.join("debian/source/options"),
"--single-debian-patch\n",
)
.unwrap();
apply_quilt_patches(tree.to_str().unwrap(), &HashMap::new(), ctx, &None, &None).unwrap();
}
}