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
+9
View File
@@ -173,6 +173,15 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
for entry in std::fs::read_dir(src)? { for entry in std::fs::read_dir(src)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
// Never ship VCS metadata into the build tree: its presence
// flips autotools 'building from VCS' detection (see
// is_vcs_dir_name) and activates maintainer-only regeneration
// rules requiring undeclared tools (e.g. help2man).
if path.symlink_metadata().map(|m| m.is_dir()).unwrap_or(false)
&& super::is_vcs_dir_name(&entry.file_name())
{
continue;
}
let dest_path = dest.join(entry.file_name()); let dest_path = dest.join(entry.file_name());
copy_dir_recursive(&path, &dest_path)?; copy_dir_recursive(&path, &dest_path)?;
} }
+82
View File
@@ -20,6 +20,49 @@ pub fn current() -> Arc<Context> {
manager::MANAGER.current() 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -178,4 +221,43 @@ mod tests {
std::path::Path::new("/nonexistent/target") 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());
}
} }
+8
View File
@@ -315,6 +315,14 @@ impl SshDriver {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
let name = entry.file_name(); let name = entry.file_name();
// Skip VCS metadata (see is_vcs_dir_name): shipping it into
// the remote build tree would flip autotools 'building from
// VCS' detection and activate maintainer-only rules.
if path.symlink_metadata().map(|m| m.is_dir()).unwrap_or(false)
&& super::is_vcs_dir_name(&name)
{
continue;
}
let dest_path = dest.join(name); let dest_path = dest.join(name);
Self::upload_recursive(sftp, &path, &dest_path)?; Self::upload_recursive(sftp, &path, &dest_path)?;
} }
+22
View File
@@ -55,6 +55,14 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
} }
}; };
// Never ship VCS metadata into the build tree: its presence flips
// autotools 'building from VCS' detection (see is_vcs_dir_name) and
// activates maintainer-only regeneration rules requiring tools that
// are not declared build-dependencies (e.g. help2man).
if metadata.is_dir() && super::is_vcs_dir_name(&entry.file_name()) {
continue;
}
if metadata.file_type().is_symlink() { if metadata.file_type().is_symlink() {
let target = std::fs::read_link(&src_path).map_err(|e| { let target = std::fs::read_link(&src_path).map_err(|e| {
io::Error::new( io::Error::new(
@@ -428,6 +436,20 @@ impl UnshareDriver {
.lock() .lock()
.unwrap() .unwrap()
.push(dest_path.to_path_buf()); .push(dest_path.to_path_buf());
// The overlay exposes the source tree verbatim: strip VCS metadata
// (e.g. '.git') whose presence would flip autotools 'building from
// git' detection and activate maintainer-only regeneration rules
// (see is_vcs_dir_name). Removals land as whiteouts in the upper
// layer, leaving the lowerdir untouched.
if let Err(e) = super::prune_vcs_dirs(dest_path) {
log::warn!(
"Failed to prune VCS metadata from '{}': {}",
dest_path.display(),
e
);
}
Ok(()) Ok(())
} }