Skip to content

Commit fde85a8

Browse files
authored
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.
2 parents 3acf9c9 + 4d3bf1f commit fde85a8

File tree

7 files changed

+352
-56
lines changed

7 files changed

+352
-56
lines changed

src/build_helper/src/fs/mod.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Misc filesystem related helpers for use by bootstrap and tools.
2+
use std::fs::Metadata;
3+
use std::path::Path;
4+
use std::{fs, io};
5+
6+
#[cfg(test)]
7+
mod tests;
8+
9+
/// Helper to ignore [`std::io::ErrorKind::NotFound`], but still propagate other
10+
/// [`std::io::ErrorKind`]s.
11+
pub fn ignore_not_found<Op>(mut op: Op) -> io::Result<()>
12+
where
13+
Op: FnMut() -> io::Result<()>,
14+
{
15+
match op() {
16+
Ok(()) => Ok(()),
17+
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
18+
Err(e) => Err(e),
19+
}
20+
}
21+
22+
/// A wrapper around [`std::fs::remove_dir_all`] that can also be used on *non-directory entries*,
23+
/// including files and symbolic links.
24+
///
25+
/// - This will produce an error if the target path is not found.
26+
/// - Like [`std::fs::remove_dir_all`], this helper does not traverse symbolic links, will remove
27+
/// symbolic link itself.
28+
/// - This helper is **not** robust against races on the underlying filesystem, behavior is
29+
/// unspecified if this helper is called concurrently.
30+
/// - This helper is not robust against TOCTOU problems.
31+
///
32+
/// FIXME: this implementation is insufficiently robust to replace bootstrap's clean `rm_rf`
33+
/// implementation:
34+
///
35+
/// - This implementation currently does not perform retries.
36+
#[track_caller]
37+
pub fn recursive_remove<P: AsRef<Path>>(path: P) -> io::Result<()> {
38+
let path = path.as_ref();
39+
let metadata = fs::symlink_metadata(path)?;
40+
#[cfg(windows)]
41+
let is_dir_like = |meta: &fs::Metadata| {
42+
use std::os::windows::fs::FileTypeExt;
43+
meta.is_dir() || meta.file_type().is_symlink_dir()
44+
};
45+
#[cfg(not(windows))]
46+
let is_dir_like = fs::Metadata::is_dir;
47+
48+
if is_dir_like(&metadata) {
49+
fs::remove_dir_all(path)
50+
} else {
51+
try_remove_op_set_perms(fs::remove_file, path, metadata)
52+
}
53+
}
54+
55+
fn try_remove_op_set_perms<'p, Op>(mut op: Op, path: &'p Path, metadata: Metadata) -> io::Result<()>
56+
where
57+
Op: FnMut(&'p Path) -> io::Result<()>,
58+
{
59+
match op(path) {
60+
Ok(()) => Ok(()),
61+
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
62+
let mut perms = metadata.permissions();
63+
perms.set_readonly(false);
64+
fs::set_permissions(path, perms)?;
65+
op(path)
66+
}
67+
Err(e) => Err(e),
68+
}
69+
}

src/build_helper/src/fs/tests.rs

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#![deny(unused_must_use)]
2+
3+
use std::{env, fs, io};
4+
5+
use super::recursive_remove;
6+
7+
mod recursive_remove_tests {
8+
use super::*;
9+
10+
// Basic cases
11+
12+
#[test]
13+
fn nonexistent_path() {
14+
let tmpdir = env::temp_dir();
15+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_nonexistent_path");
16+
assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
17+
assert!(recursive_remove(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
18+
}
19+
20+
#[test]
21+
fn file() {
22+
let tmpdir = env::temp_dir();
23+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_file");
24+
fs::write(&path, b"").unwrap();
25+
assert!(fs::symlink_metadata(&path).is_ok());
26+
assert!(recursive_remove(&path).is_ok());
27+
assert!(fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound));
28+
}
29+
30+
mod dir_tests {
31+
use super::*;
32+
33+
#[test]
34+
fn dir_empty() {
35+
let tmpdir = env::temp_dir();
36+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_empty");
37+
fs::create_dir_all(&path).unwrap();
38+
assert!(fs::symlink_metadata(&path).is_ok());
39+
assert!(recursive_remove(&path).is_ok());
40+
assert!(
41+
fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
42+
);
43+
}
44+
45+
#[test]
46+
fn dir_recursive() {
47+
let tmpdir = env::temp_dir();
48+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_dir_tests_dir_recursive");
49+
fs::create_dir_all(&path).unwrap();
50+
assert!(fs::symlink_metadata(&path).is_ok());
51+
52+
let file_a = path.join("a.txt");
53+
fs::write(&file_a, b"").unwrap();
54+
assert!(fs::symlink_metadata(&file_a).is_ok());
55+
56+
let dir_b = path.join("b");
57+
fs::create_dir_all(&dir_b).unwrap();
58+
assert!(fs::symlink_metadata(&dir_b).is_ok());
59+
60+
let file_c = dir_b.join("c.rs");
61+
fs::write(&file_c, b"").unwrap();
62+
assert!(fs::symlink_metadata(&file_c).is_ok());
63+
64+
assert!(recursive_remove(&path).is_ok());
65+
66+
assert!(
67+
fs::symlink_metadata(&file_a).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
68+
);
69+
assert!(
70+
fs::symlink_metadata(&dir_b).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
71+
);
72+
assert!(
73+
fs::symlink_metadata(&file_c).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
74+
);
75+
}
76+
}
77+
78+
/// Check that [`recursive_remove`] does not traverse symlinks and only removes symlinks
79+
/// themselves.
80+
///
81+
/// Symlink-to-file versus symlink-to-dir is a distinction that's important on Windows, but not
82+
/// on Unix.
83+
mod symlink_tests {
84+
use super::*;
85+
86+
#[cfg(unix)]
87+
#[test]
88+
fn unix_symlink() {
89+
let tmpdir = env::temp_dir();
90+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_unix_symlink");
91+
let symlink_path =
92+
tmpdir.join("__INTERNAL_BOOTSTRAP__symlink_tests_unix_symlink_symlink");
93+
fs::write(&path, b"").unwrap();
94+
95+
assert!(fs::symlink_metadata(&path).is_ok());
96+
assert!(
97+
fs::symlink_metadata(&symlink_path)
98+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
99+
);
100+
101+
std::os::unix::fs::symlink(&path, &symlink_path).unwrap();
102+
103+
assert!(recursive_remove(&symlink_path).is_ok());
104+
105+
// Check that the symlink got removed...
106+
assert!(
107+
fs::symlink_metadata(&symlink_path)
108+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
109+
);
110+
// ... but pointed-to file still exists.
111+
assert!(fs::symlink_metadata(&path).is_ok());
112+
113+
fs::remove_file(&path).unwrap();
114+
}
115+
116+
#[cfg(windows)]
117+
#[test]
118+
fn windows_symlink_to_file() {
119+
let tmpdir = env::temp_dir();
120+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_file");
121+
let symlink_path = tmpdir
122+
.join("__INTERNAL_BOOTSTRAP_SYMLINK_symlink_tests_windows_symlink_to_file_symlink");
123+
fs::write(&path, b"").unwrap();
124+
125+
assert!(fs::symlink_metadata(&path).is_ok());
126+
assert!(
127+
fs::symlink_metadata(&symlink_path)
128+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
129+
);
130+
131+
std::os::windows::fs::symlink_file(&path, &symlink_path).unwrap();
132+
133+
assert!(recursive_remove(&symlink_path).is_ok());
134+
135+
// Check that the symlink-to-file got removed...
136+
assert!(
137+
fs::symlink_metadata(&symlink_path)
138+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
139+
);
140+
// ... but pointed-to file still exists.
141+
assert!(fs::symlink_metadata(&path).is_ok());
142+
143+
fs::remove_file(&path).unwrap();
144+
}
145+
146+
#[cfg(windows)]
147+
#[test]
148+
fn windows_symlink_to_dir() {
149+
let tmpdir = env::temp_dir();
150+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir");
151+
let symlink_path =
152+
tmpdir.join("__INTERNAL_BOOTSTRAP_symlink_tests_windows_symlink_to_dir_symlink");
153+
fs::create_dir_all(&path).unwrap();
154+
155+
assert!(fs::symlink_metadata(&path).is_ok());
156+
assert!(
157+
fs::symlink_metadata(&symlink_path)
158+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
159+
);
160+
161+
std::os::windows::fs::symlink_dir(&path, &symlink_path).unwrap();
162+
163+
assert!(recursive_remove(&symlink_path).is_ok());
164+
165+
// Check that the symlink-to-dir got removed...
166+
assert!(
167+
fs::symlink_metadata(&symlink_path)
168+
.is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
169+
);
170+
// ... but pointed-to dir still exists.
171+
assert!(fs::symlink_metadata(&path).is_ok());
172+
173+
fs::remove_dir_all(&path).unwrap();
174+
}
175+
}
176+
177+
/// Read-only file and directories only need special handling on Windows.
178+
#[cfg(windows)]
179+
mod readonly_tests {
180+
use super::*;
181+
182+
#[test]
183+
fn overrides_readonly() {
184+
let tmpdir = env::temp_dir();
185+
let path = tmpdir.join("__INTERNAL_BOOTSTRAP_readonly_tests_overrides_readonly");
186+
187+
// In case of a previous failed test:
188+
if let Ok(mut perms) = fs::symlink_metadata(&path).map(|m| m.permissions()) {
189+
perms.set_readonly(false);
190+
fs::set_permissions(&path, perms).unwrap();
191+
fs::remove_file(&path).unwrap();
192+
}
193+
194+
fs::write(&path, b"").unwrap();
195+
196+
let mut perms = fs::symlink_metadata(&path).unwrap().permissions();
197+
perms.set_readonly(true);
198+
fs::set_permissions(&path, perms).unwrap();
199+
200+
// Check that file exists but is read-only, and that normal `std::fs::remove_file` fails
201+
// to delete the file.
202+
assert!(fs::symlink_metadata(&path).is_ok_and(|m| m.permissions().readonly()));
203+
assert!(
204+
fs::remove_file(&path).is_err_and(|e| e.kind() == io::ErrorKind::PermissionDenied)
205+
);
206+
207+
assert!(recursive_remove(&path).is_ok());
208+
209+
assert!(
210+
fs::symlink_metadata(&path).is_err_and(|e| e.kind() == io::ErrorKind::NotFound)
211+
);
212+
}
213+
}
214+
}

src/build_helper/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod ci;
44
pub mod drop_bomb;
5+
pub mod fs;
56
pub mod git;
67
pub mod metrics;
78
pub mod stage0_parser;

src/tools/compiletest/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ indexmap = "2.0.0"
1616
miropt-test-tools = { path = "../miropt-test-tools" }
1717
build_helper = { path = "../../build_helper" }
1818
tracing = "0.1"
19-
tracing-subscriber = { version = "0.3.3", default-features = false, features = ["fmt", "env-filter", "smallvec", "parking_lot", "ansi"] }
19+
tracing-subscriber = { version = "0.3.3", default-features = false, features = ["ansi", "env-filter", "fmt", "parking_lot", "smallvec"] }
2020
regex = "1.0"
2121
semver = { version = "1.0.23", features = ["serde"] }
2222
serde = { version = "1.0", features = ["derive"] }

src/tools/compiletest/src/runtest.rs

-23
Original file line numberDiff line numberDiff line change
@@ -2809,29 +2809,6 @@ impl<'test> TestCx<'test> {
28092809
println!("init_incremental_test: incremental_dir={}", incremental_dir.display());
28102810
}
28112811
}
2812-
2813-
fn aggressive_rm_rf(&self, path: &Path) -> io::Result<()> {
2814-
for e in path.read_dir()? {
2815-
let entry = e?;
2816-
let path = entry.path();
2817-
if entry.file_type()?.is_dir() {
2818-
self.aggressive_rm_rf(&path)?;
2819-
} else {
2820-
// Remove readonly files as well on windows (by default we can't)
2821-
fs::remove_file(&path).or_else(|e| {
2822-
if cfg!(windows) && e.kind() == io::ErrorKind::PermissionDenied {
2823-
let mut meta = entry.metadata()?.permissions();
2824-
meta.set_readonly(false);
2825-
fs::set_permissions(&path, meta)?;
2826-
fs::remove_file(&path)
2827-
} else {
2828-
Err(e)
2829-
}
2830-
})?;
2831-
}
2832-
}
2833-
fs::remove_dir(path)
2834-
}
28352812
}
28362813

28372814
struct ProcArgs {

src/tools/compiletest/src/runtest/run_make.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::path::Path;
22
use std::process::{Command, Output, Stdio};
33
use std::{env, fs};
44

5+
use build_helper::fs::{ignore_not_found, recursive_remove};
6+
57
use super::{ProcRes, TestCx, disable_error_reporting};
68
use crate::util::{copy_dir_all, dylib_env_var};
79

@@ -27,9 +29,8 @@ impl TestCx<'_> {
2729
// are hopefully going away, it seems safer to leave this perilous code
2830
// as-is until it can all be deleted.
2931
let tmpdir = cwd.join(self.output_base_name());
30-
if tmpdir.exists() {
31-
self.aggressive_rm_rf(&tmpdir).unwrap();
32-
}
32+
ignore_not_found(|| recursive_remove(&tmpdir)).unwrap();
33+
3334
fs::create_dir_all(&tmpdir).unwrap();
3435

3536
let host = &self.config.host;
@@ -218,9 +219,8 @@ impl TestCx<'_> {
218219
//
219220
// This setup intentionally diverges from legacy Makefile run-make tests.
220221
let base_dir = self.output_base_dir();
221-
if base_dir.exists() {
222-
self.aggressive_rm_rf(&base_dir).unwrap();
223-
}
222+
ignore_not_found(|| recursive_remove(&base_dir)).unwrap();
223+
224224
let rmake_out_dir = base_dir.join("rmake_out");
225225
fs::create_dir_all(&rmake_out_dir).unwrap();
226226

0 commit comments

Comments
 (0)