Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/dist/component/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,128 @@ fn rollback_failure_keeps_going() {
#[test]
#[ignore]
fn intermediate_dir_rollback() {}

#[test]
#[cfg(unix)]
fn copy_dir_preserves_symlinks() {
// copy_dir must preserve symlinks, not follow them
use std::os::unix::fs::symlink;

let cx = DistContext::new(None).unwrap();
let mut tx = cx.transaction();

let src_dir = cx.pkg_dir.path();

let real_file = src_dir.join("real_file.txt");
utils::write_file("", &real_file, "original content").unwrap();

let subdir = src_dir.join("subdir");
fs::create_dir(&subdir).unwrap();

let file_symlink = subdir.join("link_to_file.txt");
symlink("../real_file.txt", &file_symlink).unwrap();

let real_dir = src_dir.join("real_dir");
fs::create_dir(&real_dir).unwrap();
utils::write_file("", &real_dir.join("inner.txt"), "inner content").unwrap();
let dir_symlink = subdir.join("link_to_dir");
symlink("../real_dir", &dir_symlink).unwrap();

assert!(
fs::symlink_metadata(&file_symlink)
.unwrap()
.file_type()
.is_symlink(),
"Source file symlink should be a symlink"
);
assert!(
fs::symlink_metadata(&dir_symlink)
.unwrap()
.file_type()
.is_symlink(),
"Source dir symlink should be a symlink"
);

tx.copy_dir("test-component", PathBuf::from("dest"), src_dir)
.unwrap();
tx.commit();

let dest_file_symlink = cx.prefix.path().join("dest/subdir/link_to_file.txt");
let dest_dir_symlink = cx.prefix.path().join("dest/subdir/link_to_dir");

assert!(
fs::symlink_metadata(&dest_file_symlink)
.unwrap()
.file_type()
.is_symlink(),
"Destination file symlink should be preserved as a symlink"
);
assert!(
fs::symlink_metadata(&dest_dir_symlink)
.unwrap()
.file_type()
.is_symlink(),
"Destination dir symlink should be preserved as a symlink"
);

assert_eq!(
fs::read_link(&dest_file_symlink).unwrap().to_str().unwrap(),
"../real_file.txt",
"File symlink target should be preserved"
);
assert_eq!(
fs::read_link(&dest_dir_symlink).unwrap().to_str().unwrap(),
"../real_dir",
"Dir symlink target should be preserved"
);
}

#[test]
#[cfg(unix)]
fn copy_file_preserves_symlinks() {
// copy_file must preserve symlink targets, not create new symlinks to source
use std::os::unix::fs::symlink;

let cx = DistContext::new(None).unwrap();
let mut tx = cx.transaction();

let src_dir = cx.pkg_dir.path();
let real_file = src_dir.join("real_file.txt");
utils::write_file("", &real_file, "content").unwrap();

let link_file = src_dir.join("link.txt");
symlink("real_file.txt", &link_file).unwrap();

assert!(
fs::symlink_metadata(&link_file)
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(
fs::read_link(&link_file).unwrap().to_str().unwrap(),
"real_file.txt"
);

tx.copy_file(
"test-component",
PathBuf::from("copied_link.txt"),
&link_file,
)
.unwrap();
tx.commit();

let dest_link = cx.prefix.path().join("copied_link.txt");
assert!(
fs::symlink_metadata(&dest_link)
.unwrap()
.file_type()
.is_symlink(),
"Copied file should be a symlink"
);
assert_eq!(
fs::read_link(&dest_link).unwrap().to_str().unwrap(),
"real_file.txt",
"Symlink target should be preserved, not point to original source"
);
}
7 changes: 6 additions & 1 deletion src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,12 @@ pub(crate) fn copy_file(src: &Path, dest: &Path) -> Result<()> {
path: PathBuf::from(src),
})?;
if metadata.file_type().is_symlink() {
symlink_file(src, dest).map(|_| ())
// Read the symlink target and create a new symlink with the same target
let link_target = fs::read_link(src).with_context(|| RustupError::ReadingFile {
name: "symlink target for",
path: PathBuf::from(src),
})?;
symlink_file(&link_target, dest).map(|_| ())
} else {
fs::copy(src, dest)
.with_context(|| {
Expand Down
27 changes: 22 additions & 5 deletions src/utils/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,29 @@ pub(crate) fn copy_dir(src: &Path, dest: &Path) -> io::Result<()> {
for entry in src.read_dir()? {
let entry = entry?;
let kind = entry.file_type()?;
let src = entry.path();
let dest = dest.join(entry.file_name());
if kind.is_dir() {
copy_dir(&src, &dest)?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());

// Check for symlinks first, before is_dir() which follows symlinks
if kind.is_symlink() {
let link_target = fs::read_link(&src_path)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(&link_target, &dest_path)?;
}
#[cfg(windows)]
{
// On Windows, we need to know if it's a file or directory symlink
if src_path.is_dir() {
std::os::windows::fs::symlink_dir(&link_target, &dest_path)?;
} else {
std::os::windows::fs::symlink_file(&link_target, &dest_path)?;
}
}
} else if kind.is_dir() {
copy_dir(&src_path, &dest_path)?;
} else {
fs::copy(&src, &dest)?;
fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
Expand Down