deb: strip VCS metadata from prepared build trees
CI / build (push) Successful in 2m53s
CI / test (push) Skipped
CI / snap (push) Failing after 23s

Copying a git checkout verbatim into /tmp/pkh-build-*/ ships '.git',
flipping autotools' building-from-VCS detection (GNU hello's
BUILD_FROM_GIT): the shipped man page gets cleaned and regenerated
via help2man, which is correctly not in Build-Depends. Skip VCS
dirs (.git/.hg/.svn/.bzr/CVS) in local/unshare/ssh copies, and
prune them after an unshare overlayfs mount.
This commit is contained in:
2026-08-24 17:33:44 +02:00
parent 9f47e7dae8
commit 86612efb00
4 changed files with 121 additions and 0 deletions
+82
View File
@@ -20,6 +20,49 @@ pub fn current() -> Arc<Context> {
manager::MANAGER.current()
}
/// Version-control metadata directories that must never leak into a
/// prepared build tree.
///
/// Their presence flips autotools' "building from VCS" detection (e.g. GNU
/// hello's `BUILD_FROM_GIT`, triggered by a `.git` directory next to
/// `configure.ac`), which activates maintainer-only regeneration rules
/// requiring tools that are deliberately not declared as build-dependencies
/// (e.g. `help2man`). Source packages produced by dpkg-source never contain
/// them, so package builds must not see them either.
pub(crate) fn is_vcs_dir_name(name: &std::ffi::OsStr) -> bool {
matches!(
name.to_str(),
Some(".git") | Some(".hg") | Some(".svn") | Some(".bzr") | Some("CVS")
)
}
/// Recursively remove version-control metadata directories below `root`.
///
/// Used after an overlay mount, where the source tree is exposed verbatim
/// and entries cannot be filtered during the copy.
pub(crate) fn prune_vcs_dirs(root: &std::path::Path) -> std::io::Result<()> {
let mut pending = vec![root.to_path_buf()];
while let Some(dir) = pending.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
// symlink_metadata: never follow symlinks while pruning.
let Ok(meta) = std::fs::symlink_metadata(&path) else {
continue;
};
if meta.is_dir() {
if is_vcs_dir_name(&entry.file_name()) {
log::debug!("Removing VCS metadata from build tree: {}", path.display());
std::fs::remove_dir_all(&path)?;
} else {
pending.push(path);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -178,4 +221,43 @@ mod tests {
std::path::Path::new("/nonexistent/target")
);
}
/// Copying a source tree into a build context must strip version-control
/// metadata directories at any depth: their presence flips autotools
/// "building from git" detection and activates maintainer-only rules
/// needing undeclared tools (e.g. help2man for GNU hello's man page).
#[test]
fn test_ensure_available_strips_vcs_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
let src_root = temp_dir.path().join("pkg");
fs::create_dir_all(src_root.join(".git/objects")).unwrap();
fs::write(src_root.join(".git/HEAD"), "ref: refs/heads/main").unwrap();
fs::create_dir_all(src_root.join("src/.svn")).unwrap();
fs::write(src_root.join("src/hello.c"), "int main() {}").unwrap();
let ctx = Context::new(ContextConfig::Local);
let dest = ctx.ensure_available(&src_root, "/tmp").unwrap();
assert!(dest.join("src/hello.c").exists());
assert!(!dest.join(".git").exists());
assert!(!dest.join("src/.svn").exists());
}
/// The overlay-mount path exposes the tree verbatim, so pruning happens
/// after the fact: nested VCS metadata must be removed recursively.
#[test]
fn test_prune_vcs_dirs_removes_nested_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path().join("tree");
fs::create_dir_all(root.join("a/.git/objects")).unwrap();
fs::create_dir_all(root.join("b/c/CVS")).unwrap();
fs::write(root.join("a/.git/HEAD"), "ref").unwrap();
fs::write(root.join("b/keep.txt"), "x").unwrap();
prune_vcs_dirs(&root).unwrap();
assert!(!root.join("a/.git").exists());
assert!(!root.join("b/c/CVS").exists());
assert!(root.join("b/keep.txt").exists());
}
}