Skip to content

Commit 264072b

Browse files
committed
run_make_support: rectify symlink handling
Avoid confusing Unix symlinks and Windows symlinks, and since their semantics are quite different we should avoid trying to make it to automagic in how symlinks are created and deleted. Notably, the tests should reflect what type of symlinks are to be created to match what std does to make it less surprising for test readers.
1 parent 39b7669 commit 264072b

File tree

3 files changed

+132
-49
lines changed

3 files changed

+132
-49
lines changed

src/tools/run-make-support/src/fs.rs renamed to src/tools/run-make-support/src/fs/mod.rs

+67-49
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,10 @@
11
use std::io;
22
use std::path::{Path, PathBuf};
33

4-
// FIXME(jieyouxu): modify create_symlink to panic on windows.
5-
6-
/// Creates a new symlink to a path on the filesystem, adjusting for Windows or Unix.
7-
#[cfg(target_family = "windows")]
8-
pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) {
9-
if link.as_ref().exists() {
10-
std::fs::remove_dir(link.as_ref()).unwrap();
11-
}
12-
if original.as_ref().is_file() {
13-
std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref()).expect(&format!(
14-
"failed to create symlink {:?} for {:?}",
15-
link.as_ref().display(),
16-
original.as_ref().display(),
17-
));
18-
} else {
19-
std::os::windows::fs::symlink_dir(original.as_ref(), link.as_ref()).expect(&format!(
20-
"failed to create symlink {:?} for {:?}",
21-
link.as_ref().display(),
22-
original.as_ref().display(),
23-
));
24-
}
25-
}
26-
27-
/// Creates a new symlink to a path on the filesystem, adjusting for Windows or Unix.
28-
#[cfg(target_family = "unix")]
29-
pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) {
30-
if link.as_ref().exists() {
31-
std::fs::remove_dir(link.as_ref()).unwrap();
32-
}
33-
std::os::unix::fs::symlink(original.as_ref(), link.as_ref()).expect(&format!(
34-
"failed to create symlink {:?} for {:?}",
35-
link.as_ref().display(),
36-
original.as_ref().display(),
37-
));
38-
}
4+
#[cfg(unix)]
5+
pub mod unix;
6+
#[cfg(windows)]
7+
pub mod windows;
398

409
/// Copy a directory into another.
4110
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
@@ -50,7 +19,34 @@ pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
5019
if ty.is_dir() {
5120
copy_dir_all_inner(entry.path(), dst.join(entry.file_name()))?;
5221
} else if ty.is_symlink() {
53-
copy_symlink(entry.path(), dst.join(entry.file_name()))?;
22+
// Traverse symlink once to find path of target entity.
23+
let target_path = std::fs::read_link(entry.path())?;
24+
25+
let new_symlink_path = dst.join(entry.file_name());
26+
#[cfg(windows)]
27+
{
28+
// Find metadata about target entity (shallow, no further symlink traversal in
29+
// case target is also a symlink).
30+
let target_metadata = std::fs::symlink_metadata(&target_path)?;
31+
let target_file_type = target_metadata.file_type();
32+
if target_file_type.is_dir() {
33+
std::os::windows::fs::symlink_dir(&target_path, new_symlink_path)?;
34+
} else {
35+
// Target may be a file or another symlink, in any case we can use
36+
// `symlink_file` here.
37+
std::os::windows::fs::symlink_file(&target_path, new_symlink_path)?;
38+
}
39+
}
40+
#[cfg(unix)]
41+
{
42+
std::os::unix::fs::symlink(target_path, new_symlink_path)?;
43+
}
44+
#[cfg(not(any(windows, unix)))]
45+
{
46+
// Technically there's also wasi, but I have no clue about wasi symlink
47+
// semantics and which wasi targets / environment support symlinks.
48+
unimplemented!("unsupported target");
49+
}
5450
} else {
5551
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
5652
}
@@ -69,12 +65,6 @@ pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
6965
}
7066
}
7167

72-
fn copy_symlink<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
73-
let target_path = std::fs::read_link(from).unwrap();
74-
create_symlink(target_path, to);
75-
Ok(())
76-
}
77-
7868
/// Helper for reading entries in a given directory.
7969
pub fn read_dir_entries<P: AsRef<Path>, F: FnMut(&Path)>(dir: P, mut callback: F) {
8070
for entry in read_dir(dir) {
@@ -85,8 +75,17 @@ pub fn read_dir_entries<P: AsRef<Path>, F: FnMut(&Path)>(dir: P, mut callback: F
8575
/// A wrapper around [`std::fs::remove_file`] which includes the file path in the panic message.
8676
#[track_caller]
8777
pub fn remove_file<P: AsRef<Path>>(path: P) {
88-
std::fs::remove_file(path.as_ref())
89-
.expect(&format!("the file in path \"{}\" could not be removed", path.as_ref().display()));
78+
if let Err(e) = std::fs::remove_file(path.as_ref()) {
79+
panic!("failed to remove file at `{}`: {e}", path.as_ref().display());
80+
}
81+
}
82+
83+
/// A wrapper around [`std::fs::remove_dir`] which includes the directory path in the panic message.
84+
#[track_caller]
85+
pub fn remove_dir<P: AsRef<Path>>(path: P) {
86+
if let Err(e) = std::fs::remove_dir(path.as_ref()) {
87+
panic!("failed to remove directory at `{}`: {e}", path.as_ref().display());
88+
}
9089
}
9190

9291
/// A wrapper around [`std::fs::copy`] which includes the file path in the panic message.
@@ -165,13 +164,32 @@ pub fn create_dir_all<P: AsRef<Path>>(path: P) {
165164
));
166165
}
167166

168-
/// A wrapper around [`std::fs::metadata`] which includes the file path in the panic message.
167+
/// A wrapper around [`std::fs::metadata`] which includes the file path in the panic message. Note
168+
/// that this will traverse symlinks and will return metadata about the target file. Use
169+
/// [`symlink_metadata`] if you don't want to traverse symlinks.
170+
///
171+
/// See [`std::fs::metadata`] docs for more details.
169172
#[track_caller]
170173
pub fn metadata<P: AsRef<Path>>(path: P) -> std::fs::Metadata {
171-
std::fs::metadata(path.as_ref()).expect(&format!(
172-
"the file's metadata in path \"{}\" could not be read",
173-
path.as_ref().display()
174-
))
174+
match std::fs::metadata(path.as_ref()) {
175+
Ok(m) => m,
176+
Err(e) => panic!("failed to read file metadata at `{}`: {e}", path.as_ref().display()),
177+
}
178+
}
179+
180+
/// A wrapper around [`std::fs::symlink_metadata`] which includes the file path in the panic
181+
/// message. Note that this will not traverse symlinks and will return metadata about the filesystem
182+
/// entity itself. Use [`metadata`] if you want to traverse symlinks.
183+
///
184+
/// See [`std::fs::symlink_metadata`] docs for more details.
185+
#[track_caller]
186+
pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> std::fs::Metadata {
187+
match std::fs::symlink_metadata(path.as_ref()) {
188+
Ok(m) => m,
189+
Err(e) => {
190+
panic!("failed to read file metadata (shallow) at `{}`: {e}", path.as_ref().display())
191+
}
192+
}
175193
}
176194

177195
/// A wrapper around [`std::fs::rename`] which includes the file path in the panic message.
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use std::path::Path;
2+
3+
/// Create a new symbolic link (soft link) at `link` pointing to target `original`. Unix symlinks
4+
/// should be removed via [`fs::remove_file`](crate::fs::remove_file).
5+
///
6+
/// This is an infallible wrapper around [`std::os::unix::fs::symlink`], and will panic if we failed
7+
/// to create a symlink at `link` for target `original`.
8+
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) {
9+
if let Err(e) = std::os::unix::fs::symlink(original.as_ref(), link.as_ref()) {
10+
panic!(
11+
"failed to create symlink: original=`{}`, link=`{}`: {e}",
12+
original.as_ref().display(),
13+
link.as_ref().display()
14+
);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// Windows symbolic link helpers. Unlike Unix symbolic links (soft links), on Windows there is a
2+
/// distinction between symlink-to-file versus symlink-to-directory. In particular, a
3+
/// symlink-to-file need to be deleted with [`fs::remove_file`](crate::fs::remove_file) while a
4+
/// symlink-to-dir need to be deleted with [`fs::remove_dir`](crate::fs::remove_dir). See
5+
/// [`CreateSymbolicLinkW`] for details.
6+
///
7+
/// # Notes
8+
///
9+
/// - Windows symbolic links are distinct from junction points or hard links.
10+
/// - A Unix symlink created via WSL but on the Windows host NTFS filesystem need to be deleted with
11+
/// [`fs::remove_file`](crate::fs::remove_file) even if that symlink may point to a directory.
12+
/// - Hard links and junction points need additional handling, which is currently not implemented by
13+
/// this support library.
14+
///
15+
/// # References
16+
///
17+
/// [`CreateSymbolicLinkW`]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
18+
pub mod symlinks {
19+
use std::path::Path;
20+
21+
/// Create a new symbolic link to a directory. A symlink-to-directory needs to be removed with a
22+
/// corresponding [`fs::remove_dir`](crate::fs::remove_dir) and not
23+
/// [`fs::remove_file`](crate::fs::remove_file).
24+
pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) {
25+
if let Err(e) = std::os::windows::fs::symlink_dir(original.as_ref(), link.as_ref()) {
26+
panic!(
27+
"failed to create symlink-to-directory: original=`{}`, link=`{}`: {e}",
28+
original.as_ref().display(),
29+
link.as_ref().display()
30+
);
31+
}
32+
}
33+
34+
/// Create a new symbolic link to a file. A symlink-to-file needs to be removed with a
35+
/// corresponding [`fs::remove_file`](crate::fs::remove_file) and not
36+
/// [`fs::remove_dir`](crate::fs::remove_dir).
37+
pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) {
38+
if let Err(e) = std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref()) {
39+
panic!(
40+
"failed to create symlink-to-file: original=`{}`, link=`{}`: {e}",
41+
original.as_ref().display(),
42+
link.as_ref().display()
43+
);
44+
}
45+
}
46+
}
47+
48+
// Re-export to flatten module hierarchy, but the module is there for documentation purposes.
49+
pub use symlinks::{symlink_dir, symlink_file};

0 commit comments

Comments
 (0)