Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 48f978b

Browse files
committed
Auto merge of rust-lang#130427 - jieyouxu:rmake-symlink, r=<try>
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. r? `@ghost` try-job: x86_64-msvc try-job: aarch64-apple
2 parents 39b7669 + 1fd1378 commit 48f978b

File tree

7 files changed

+225
-75
lines changed

7 files changed

+225
-75
lines changed

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

Lines changed: 67 additions & 49 deletions
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::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.
Lines changed: 16 additions & 0 deletions
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, link) {
10+
panic!(
11+
"failed to create symlink: original=`{}`, link=`{}`: {e}",
12+
original.as_ref().display(),
13+
link.as_ref().display()
14+
);
15+
}
16+
}
Lines changed: 49 additions & 0 deletions
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};

tests/run-make/invalid-symlink-search-path/rmake.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// In this test, the symlink created is invalid (valid relative to the root, but not
2-
// relatively to where it is located), and used to cause an internal
3-
// compiler error (ICE) when passed as a library search path. This was fixed in #26044,
4-
// and this test checks that the invalid symlink is instead simply ignored.
1+
// In this test, the symlink created is invalid (valid relative to the root, but not relatively to
2+
// where it is located), and used to cause an internal compiler error (ICE) when passed as a library
3+
// search path. This was fixed in #26044, and this test checks that the invalid symlink is instead
4+
// simply ignored.
5+
//
56
// See https://github.com/rust-lang/rust/issues/26006
67

78
//@ needs-symlink
@@ -22,7 +23,22 @@ fn main() {
2223
.run();
2324
rfs::create_dir("out/bar");
2425
rfs::create_dir("out/bar/deps");
25-
rfs::create_symlink("out/foo/libfoo.rlib", "out/bar/deps/libfoo.rlib");
26+
#[cfg(unix)]
27+
{
28+
rfs::unix::symlink("out/foo/libfoo.rlib", "out/bar/deps/libfoo.rlib");
29+
}
30+
#[cfg(windows)]
31+
{
32+
rfs::windows::symlink_file("out/foo/libfoo.rlib", "out/bar/deps/libfoo.rlib");
33+
}
34+
#[cfg(not(any(unix, windows)))]
35+
{
36+
// FIXME(jieyouxu): this is not supported, but we really should implement a only-* directive
37+
// that accepts multiple targets, like e.g. `//@ only-target-families: unix, windows`. That
38+
// way, it properly shows up as an ignored test instead of silently passing.
39+
return;
40+
}
41+
2642
// Check that the invalid symlink does not cause an ICE
2743
rustc()
2844
.input("in/bar/lib.rs")
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
// Crates that are resolved normally have their path canonicalized and all
2-
// symlinks resolved. This did not happen for paths specified
3-
// using the --extern option to rustc, which could lead to rustc thinking
4-
// that it encountered two different versions of a crate, when it's
5-
// actually the same version found through different paths.
6-
// See https://github.com/rust-lang/rust/pull/16505
7-
8-
// This test checks that --extern and symlinks together
9-
// can result in successful compilation.
1+
// Crates that are resolved normally have their path canonicalized and all symlinks resolved. This
2+
// did not happen for paths specified using the `--extern` option to rustc, which could lead to
3+
// rustc thinking that it encountered two different versions of a crate, when it's actually the same
4+
// version found through different paths.
5+
//
6+
// This test checks that `--extern` and symlinks together can result in successful compilation.
7+
//
8+
// See <https://github.com/rust-lang/rust/pull/16505>.
109

1110
//@ ignore-cross-compile
1211
//@ needs-symlink
@@ -16,7 +15,26 @@ use run_make_support::{cwd, rfs, rustc};
1615
fn main() {
1716
rustc().input("foo.rs").run();
1817
rfs::create_dir_all("other");
19-
rfs::create_symlink("libfoo.rlib", "other");
18+
#[cfg(unix)]
19+
{
20+
rfs::unix::symlink(cwd().join("libfoo.rlib"), cwd().join("other").join("libfoo.rlib"));
21+
}
22+
#[cfg(windows)]
23+
{
24+
rfs::windows::symlink_file(
25+
cwd().join("libfoo.rlib"),
26+
cwd().join("other").join("libfoo.rlib"),
27+
);
28+
}
29+
#[cfg(not(any(unix, windows)))]
30+
{
31+
// FIXME(jieyouxu): this is not supported, but we really should implement a only-* directive
32+
// that accepts multiple targets, like e.g. `//@ only-target-families: unix, windows`. That
33+
// way, it properly shows up as an ignored test instead of silently passing.
34+
return;
35+
}
36+
2037
rustc().input("bar.rs").library_search_path(cwd()).run();
21-
rustc().input("baz.rs").extern_("foo", "other").library_search_path(cwd()).run();
38+
39+
rustc().input("baz.rs").extern_("foo", "other/libfoo.rlib").library_search_path(cwd()).run();
2240
}
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
1-
// When a directory and a symlink simultaneously exist with the same name,
2-
// setting that name as the library search path should not cause rustc
3-
// to avoid looking in the symlink and cause an error. This test creates
4-
// a directory and a symlink named "other", and places the library in the symlink.
5-
// If it succeeds, the library was successfully found.
6-
// See https://github.com/rust-lang/rust/issues/12459
1+
// Avoid erroring on symlinks pointing to the same file that are present in the library search path.
2+
//
3+
// See <https://github.com/rust-lang/rust/issues/12459>.
74

85
//@ ignore-cross-compile
96
//@ needs-symlink
107

11-
use run_make_support::{dynamic_lib_name, rfs, rustc};
8+
use run_make_support::{cwd, dynamic_lib_name, rfs, rustc};
129

1310
fn main() {
1411
rustc().input("foo.rs").arg("-Cprefer-dynamic").run();
1512
rfs::create_dir_all("other");
16-
rfs::create_symlink(dynamic_lib_name("foo"), "other");
17-
rustc().input("bar.rs").library_search_path("other").run();
13+
14+
#[cfg(unix)]
15+
{
16+
rfs::unix::symlink(
17+
cwd().join(dynamic_lib_name("foo")),
18+
cwd().join("other").join(dynamic_lib_name("foo")),
19+
);
20+
}
21+
#[cfg(windows)]
22+
{
23+
rfs::windows::symlink_file(
24+
cwd().join(dynamic_lib_name("foo")),
25+
cwd().join("other").join(dynamic_lib_name("foo")),
26+
);
27+
}
28+
#[cfg(not(any(unix, windows)))]
29+
{
30+
// FIXME(jieyouxu): this is not supported, but we really should implement a only-* directive
31+
// that accepts multiple targets, like e.g. `//@ only-target-families: unix, windows`. That
32+
// way, it properly shows up as an ignored test instead of silently passing.
33+
return;
34+
}
35+
36+
rustc().input("bar.rs").library_search_path(cwd()).library_search_path("other").run();
1837
}

tests/run-make/symlinked-rlib/rmake.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ use run_make_support::{cwd, rfs, rustc};
1212

1313
fn main() {
1414
rustc().input("foo.rs").crate_type("rlib").output("foo.xxx").run();
15-
rfs::create_symlink("foo.xxx", "libfoo.rlib");
15+
#[cfg(unix)]
16+
{
17+
rfs::unix::symlink("foo.xxx", "libfoo.rlib");
18+
}
19+
#[cfg(windows)]
20+
{
21+
rfs::windows::symlink_file("foo.xxx", "libfoo.rlib");
22+
}
23+
#[cfg(not(any(unix, windows)))]
24+
{
25+
// FIXME(jieyouxu): this is not supported, but we really should implement a only-* directive
26+
// that accepts multiple targets, like e.g. `//@ only-target-families: unix, windows`. That
27+
// way, it properly shows up as an ignored test instead of silently passing.
28+
return;
29+
}
1630
rustc().input("bar.rs").library_search_path(cwd()).run();
1731
}

0 commit comments

Comments
 (0)