Rollup merge of #134659 - jieyouxu:recursive-remove, r=ChrisDenton

test-infra: improve compiletest and run-make-support symlink handling

I was trying to implement #134656 to port `tests/run-make/incr-add-rust-src-component.rs`, but found some blockers related to symlink handling, so in this PR I tried to resolve them by improving symlink handling in compiletest and run-make-support (particularly for native windows msvc environment).

Key changes:

- I needed to copy symlinks (duplicate a symlink pointing to the same file), so I pulled out the copy symlink logic and re-exposed it as `run_make_support::rfs::copy_symlink`. This helper correctly accounts for the Windows symlink-to-file vs symlink-to-dir distinction (hereafter "Windows symlinks") when copying symlinks.
- `recursive_remove`:
    - I needed a way to remove symlinks themselves (no symlink traversal). `std::fs::remove_dir_all` handles them, but only if the root path is a directory. So I wrapped `std::fs::remove_dir_all` to also handle when the root path is a non-directory entity (e.g. file or symlink). Again, this properly accounts for Windows symlinks.
    - I wanted to use this for both compiletest and run-make-support, so I put the implementation and accompanying tests in `build_helper`.
    - In this sense, it's a reland of #129302 with proper test coverage.
    - It's a thin wrapper around `std::fs::remove_dir_all` (`remove_dir_all` correctly handles read-only entries on Windows). The helper has additional permission-setting logic for when the root path is a non-dir entry on Windows to handle read-only non-dir entry.

Fixes #126334.
This commit is contained in:
Trevor Gross 2024-12-23 02:07:31 -05:00 committed by GitHub
commit fde85a8e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 352 additions and 56 deletions

View file

@ -16,7 +16,7 @@ indexmap = "2.0.0"
miropt-test-tools = { path = "../miropt-test-tools" }
build_helper = { path = "../../build_helper" }
tracing = "0.1"
tracing-subscriber = { version = "0.3.3", default-features = false, features = ["fmt", "env-filter", "smallvec", "parking_lot", "ansi"] }
tracing-subscriber = { version = "0.3.3", default-features = false, features = ["ansi", "env-filter", "fmt", "parking_lot", "smallvec"] }
regex = "1.0"
semver = { version = "1.0.23", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }

View file

@ -2809,29 +2809,6 @@ impl<'test> TestCx<'test> {
println!("init_incremental_test: incremental_dir={}", incremental_dir.display());
}
}
fn aggressive_rm_rf(&self, path: &Path) -> io::Result<()> {
for e in path.read_dir()? {
let entry = e?;
let path = entry.path();
if entry.file_type()?.is_dir() {
self.aggressive_rm_rf(&path)?;
} else {
// Remove readonly files as well on windows (by default we can't)
fs::remove_file(&path).or_else(|e| {
if cfg!(windows) && e.kind() == io::ErrorKind::PermissionDenied {
let mut meta = entry.metadata()?.permissions();
meta.set_readonly(false);
fs::set_permissions(&path, meta)?;
fs::remove_file(&path)
} else {
Err(e)
}
})?;
}
}
fs::remove_dir(path)
}
}
struct ProcArgs {

View file

@ -2,6 +2,8 @@ use std::path::Path;
use std::process::{Command, Output, Stdio};
use std::{env, fs};
use build_helper::fs::{ignore_not_found, recursive_remove};
use super::{ProcRes, TestCx, disable_error_reporting};
use crate::util::{copy_dir_all, dylib_env_var};
@ -27,9 +29,8 @@ impl TestCx<'_> {
// are hopefully going away, it seems safer to leave this perilous code
// as-is until it can all be deleted.
let tmpdir = cwd.join(self.output_base_name());
if tmpdir.exists() {
self.aggressive_rm_rf(&tmpdir).unwrap();
}
ignore_not_found(|| recursive_remove(&tmpdir)).unwrap();
fs::create_dir_all(&tmpdir).unwrap();
let host = &self.config.host;
@ -218,9 +219,8 @@ impl TestCx<'_> {
//
// This setup intentionally diverges from legacy Makefile run-make tests.
let base_dir = self.output_base_dir();
if base_dir.exists() {
self.aggressive_rm_rf(&base_dir).unwrap();
}
ignore_not_found(|| recursive_remove(&base_dir)).unwrap();
let rmake_out_dir = base_dir.join("rmake_out");
fs::create_dir_all(&rmake_out_dir).unwrap();

View file

@ -1,7 +1,51 @@
use std::fs::FileType;
use std::io;
use std::path::{Path, PathBuf};
/// Copy a directory into another.
/// Given a symlink at `src`, read its target, then create a new symlink at `dst` also pointing to
/// target.
pub fn copy_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
let src = src.as_ref();
let dst = dst.as_ref();
let metadata = symlink_metadata(src);
if let Err(e) = copy_symlink_raw(metadata.file_type(), src, dst) {
panic!("failed to copy symlink from `{}` to `{}`: {e}", src.display(), dst.display(),);
}
}
fn copy_symlink_raw(ty: FileType, src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
// Traverse symlink once to find path of target entity.
let target_path = std::fs::read_link(src)?;
let new_symlink_path = dst.as_ref();
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
if ty.is_symlink_dir() {
std::os::windows::fs::symlink_dir(&target_path, new_symlink_path)?;
} else {
// Target may be a file or another symlink, in any case we can use
// `symlink_file` here.
std::os::windows::fs::symlink_file(&target_path, new_symlink_path)?;
}
}
#[cfg(unix)]
{
let _ = ty;
std::os::unix::fs::symlink(target_path, new_symlink_path)?;
}
#[cfg(not(any(windows, unix)))]
{
let _ = ty;
// Technically there's also wasi, but I have no clue about wasi symlink
// semantics and which wasi targets / environment support symlinks.
unimplemented!("unsupported target");
}
Ok(())
}
/// Copy a directory into another. This will not traverse symlinks; instead, it will create new
/// symlinks pointing at target paths that symlinks in the original directory points to.
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
fn copy_dir_all_inner(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
let dst = dst.as_ref();
@ -14,31 +58,7 @@ pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
if ty.is_dir() {
copy_dir_all_inner(entry.path(), dst.join(entry.file_name()))?;
} else if ty.is_symlink() {
// Traverse symlink once to find path of target entity.
let target_path = std::fs::read_link(entry.path())?;
let new_symlink_path = dst.join(entry.file_name());
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
if ty.is_symlink_dir() {
std::os::windows::fs::symlink_dir(&target_path, new_symlink_path)?;
} else {
// Target may be a file or another symlink, in any case we can use
// `symlink_file` here.
std::os::windows::fs::symlink_file(&target_path, new_symlink_path)?;
}
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(target_path, new_symlink_path)?;
}
#[cfg(not(any(windows, unix)))]
{
// Technically there's also wasi, but I have no clue about wasi symlink
// semantics and which wasi targets / environment support symlinks.
unimplemented!("unsupported target");
}
copy_symlink_raw(ty, entry.path(), dst.join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
@ -64,6 +84,21 @@ pub fn read_dir_entries<P: AsRef<Path>, F: FnMut(&Path)>(dir: P, mut callback: F
}
}
/// A wrapper around [`build_helper::fs::recursive_remove`] which includes the file path in the
/// panic message.
///
/// This handles removing symlinks on Windows (e.g. symlink-to-file will be removed via
/// [`std::fs::remove_file`] while symlink-to-dir will be removed via [`std::fs::remove_dir`]).
#[track_caller]
pub fn recursive_remove<P: AsRef<Path>>(path: P) {
if let Err(e) = build_helper::fs::recursive_remove(path.as_ref()) {
panic!(
"failed to recursive remove filesystem entities at `{}`: {e}",
path.as_ref().display()
);
}
}
/// A wrapper around [`std::fs::remove_file`] which includes the file path in the panic message.
#[track_caller]
pub fn remove_file<P: AsRef<Path>>(path: P) {