diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 7f09e5050b..eec1849f2d 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -842,7 +842,7 @@ fn install_bins(process: &Process) -> Result<()> { if rustup_path.exists() { utils::remove_file("rustup-bin", &rustup_path)?; } - utils::copy_file(&this_exe_path, &rustup_path)?; + utils::copy_file_symlink_to_source(&this_exe_path, &rustup_path)?; utils::make_executable(&rustup_path)?; install_proxies(process) } diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 973fd58d85..9487976337 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -706,7 +706,7 @@ pub(crate) fn delete_rustup_and_cargo_home(process: &Process) -> Result<()> { let numbah: u32 = rand::random(); let gc_exe = work_path.join(format!("rustup-gc-{numbah:x}.exe")); // Copy rustup (probably this process's exe) to the gc exe - utils::copy_file(&rustup_path, &gc_exe)?; + utils::copy_file_symlink_to_source(&rustup_path, &gc_exe)?; let gc_exe_win: Vec<_> = gc_exe.as_os_str().encode_wide().chain(Some(0)).collect(); // Make the sub-process opened by gc exe inherit its attribute. diff --git a/src/dist/component/tests.rs b/src/dist/component/tests.rs index 3c1bfcfe33..b5a5a2b6fc 100644 --- a/src/dist/component/tests.rs +++ b/src/dist/component/tests.rs @@ -492,3 +492,208 @@ 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 src_real_file = src_dir.join("real_file.txt"); + utils::write_file("", &src_real_file, "original content").unwrap(); + + let src_subdir = src_dir.join("subdir"); + fs::create_dir(&src_subdir).unwrap(); + + let src_subdir_link_to_file = src_subdir.join("link_to_file.txt"); + symlink("../real_file.txt", &src_subdir_link_to_file).unwrap(); + + let src_real_dir = src_dir.join("real_dir"); + fs::create_dir(&src_real_dir).unwrap(); + utils::write_file("", &src_real_dir.join("inner.txt"), "inner content").unwrap(); + let src_subdir_link_to_dir = src_subdir.join("link_to_dir"); + symlink("../real_dir", &src_subdir_link_to_dir).unwrap(); + + assert!( + fs::symlink_metadata(&src_subdir_link_to_file) + .unwrap() + .file_type() + .is_symlink(), + "Source file symlink should be a symlink" + ); + assert!( + fs::symlink_metadata(&src_subdir_link_to_dir) + .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 that utils::copy_file preserves symlink targets +#[test] +#[cfg(unix)] +fn copy_file_preserves_symlinks() { + use std::os::unix::fs::symlink; + + let tmp = tempfile::tempdir().unwrap(); + let src_dir = tmp.path().join("src"); + let dest_dir = tmp.path().join("dest"); + fs::create_dir_all(&src_dir).unwrap(); + fs::create_dir_all(&dest_dir).unwrap(); + + let src_real_file = src_dir.join("real_file.txt"); + utils::write_file("", &src_real_file, "content").unwrap(); + + let src_link_file = src_dir.join("link.txt"); + symlink("real_file.txt", &src_link_file).unwrap(); + + assert!( + fs::symlink_metadata(&src_link_file) + .unwrap() + .file_type() + .is_symlink() + ); + assert_eq!( + fs::read_link(&src_link_file).unwrap().to_str().unwrap(), + "real_file.txt" + ); + + // copy_file should preserve the symlink target + let dest_link_file = dest_dir.join("link.txt"); + utils::copy_file(&src_link_file, &dest_link_file).unwrap(); + + assert!( + fs::symlink_metadata(&dest_link_file) + .unwrap() + .file_type() + .is_symlink(), + "copy_file should preserve symlinks" + ); + assert_eq!( + fs::read_link(&dest_link_file).unwrap().to_str().unwrap(), + "real_file.txt", + "copy_file should preserve the original symlink target" + ); +} + +/// Test that utils::copy_file_symlink_to_source creates a symlink pointing to the source path +#[test] +#[cfg(unix)] +fn copy_file_symlink_to_source_creates_symlink_to_source() { + use std::os::unix::fs::symlink; + + let tmp = tempfile::tempdir().unwrap(); + let src_dir = tmp.path().join("src"); + let dest_dir = tmp.path().join("dest"); + fs::create_dir_all(&src_dir).unwrap(); + fs::create_dir_all(&dest_dir).unwrap(); + + let src_real_file = src_dir.join("real_file.txt"); + utils::write_file("", &src_real_file, "original content").unwrap(); + + let src_link_file = src_dir.join("link.txt"); + symlink("real_file.txt", &src_link_file).unwrap(); + + assert!( + fs::symlink_metadata(&src_link_file) + .unwrap() + .file_type() + .is_symlink() + ); + + // copy_file_symlink_to_source should create a symlink pointing to the source path + let dest_link_file = dest_dir.join("copied.txt"); + utils::copy_file_symlink_to_source(&src_link_file, &dest_link_file).unwrap(); + + // Destination should be a symlink pointing to the source path + assert!( + fs::symlink_metadata(&dest_link_file) + .unwrap() + .file_type() + .is_symlink(), + "copy_file_symlink_to_source should create a symlink" + ); + assert_eq!( + fs::read_link(&dest_link_file).unwrap(), + src_link_file, + "copy_file_symlink_to_source should create a symlink pointing to the source path" + ); +} + +/// Test that Transaction::copy_file (which uses utils::copy_file) preserves symlinks +#[test] +#[cfg(unix)] +fn transaction_copy_file_preserves_symlinks() { + 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(); + + 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(), + "Transaction::copy_file should preserve symlinks" + ); + assert_eq!( + fs::read_link(&dest_link).unwrap().to_str().unwrap(), + "real_file.txt", + "Transaction::copy_file should preserve symlink target" + ); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b4ac6f6856..8bfbe01f7c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -226,13 +226,34 @@ pub(crate) fn copy_dir(src: &Path, dest: &Path) -> Result<()> { }) } +/// Copy a file from `src` to `dst`, preserving the symlink target if `src` is a symlink. +/// This is the default behavior for component installation. pub(crate) fn copy_file(src: &Path, dest: &Path) -> Result<()> { + copy_file_impl(src, dest, true) +} + +/// Copy a file from `src` to `dst`, or if `src` is a symlink, create a new symlink +/// at `dst` pointing to it. +/// Used for self-update where we want to preserve the symlink to the original location. +pub(crate) fn copy_file_symlink_to_source(src: &Path, dest: &Path) -> Result<()> { + copy_file_impl(src, dest, false) +} + +fn copy_file_impl(src: &Path, dest: &Path, preserve_symlink: bool) -> Result<()> { let metadata = fs::symlink_metadata(src).with_context(|| RustupError::ReadingFile { name: "metadata for", path: PathBuf::from(src), })?; if metadata.file_type().is_symlink() { - symlink_file(src, dest).map(|_| ()) + let target = if preserve_symlink { + &fs::read_link(src).with_context(|| RustupError::ReadingFile { + name: "symlink target for", + path: PathBuf::from(src), + })? + } else { + src + }; + symlink_file(target, dest).map(|_| ()) } else { fs::copy(src, dest) .with_context(|| { diff --git a/src/utils/raw.rs b/src/utils/raw.rs index 7a26b74063..c54fcdee4b 100644 --- a/src/utils/raw.rs +++ b/src/utils/raw.rs @@ -284,7 +284,10 @@ pub(crate) fn copy_dir(src: &Path, dest: &Path) -> io::Result<()> { let kind = entry.file_type()?; let src = entry.path(); let dest = dest.join(entry.file_name()); - if kind.is_dir() { + // Check for symlinks first - is_dir() follows symlinks + if kind.is_symlink() { + copy_symlink(&src, &dest)?; + } else if kind.is_dir() { copy_dir(&src, &dest)?; } else { fs::copy(&src, &dest)?; @@ -293,6 +296,25 @@ pub(crate) fn copy_dir(src: &Path, dest: &Path) -> io::Result<()> { Ok(()) } +/// Copy a symlink, preserving its target +fn copy_symlink(src: &Path, dest: &Path) -> io::Result<()> { + let target = fs::read_link(src)?; + #[cfg(unix)] + { + std::os::unix::fs::symlink(&target, dest) + } + #[cfg(windows)] + { + // Determine symlink type by checking what the source symlink points to + let meta = fs::metadata(src); + if meta.map(|m| m.is_dir()).unwrap_or(false) { + std::os::windows::fs::symlink_dir(&target, dest) + } else { + std::os::windows::fs::symlink_file(&target, dest) + } + } +} + #[cfg(not(windows))] fn has_cmd(cmd: &str, process: &Process) -> bool { let cmd = format!("{}{}", cmd, env::consts::EXE_SUFFIX);