deb: skip quilt patch application for single-debian-patch trees
This commit is contained in:
@@ -512,6 +512,23 @@ fn apply_quilt_patches(
|
||||
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
|
||||
log::debug!("Installing quilt for patch application...");
|
||||
let status = cap(
|
||||
@@ -552,6 +569,39 @@ fn apply_quilt_patches(
|
||||
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
|
||||
/// account during dependency resolution.
|
||||
///
|
||||
@@ -685,3 +735,116 @@ fn dose3_explain_dependencies(
|
||||
cmd.status()?;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user