diff --git a/src/dist/component/tests.rs b/src/dist/component/tests.rs index 3c1bfcfe33..55a692d5d8 100644 --- a/src/dist/component/tests.rs +++ b/src/dist/component/tests.rs @@ -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" + ); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b4ac6f6856..21557e5b10 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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(|| { diff --git a/src/utils/raw.rs b/src/utils/raw.rs index 53e45813d3..a4ad696641 100644 --- a/src/utils/raw.rs +++ b/src/utils/raw.rs @@ -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(())