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
2 changes: 1 addition & 1 deletion src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/self_update/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
205 changes: 205 additions & 0 deletions src/dist/component/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 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 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"
);

// copy_file should preserve the symlink target
let dest_link = dest_dir.join("link.txt");
utils::copy_file(&link_file, &dest_link).unwrap();

assert!(
fs::symlink_metadata(&dest_link)
.unwrap()
.file_type()
.is_symlink(),
"copy_file should preserve symlinks"
);
assert_eq!(
fs::read_link(&dest_link).unwrap().to_str().unwrap(),
"real_file.txt",
"copy_file should preserve the original symlink target"
);
}

/// Test that utils::copy_file_symlink_to_source follows symlinks and copies content
#[test]
#[cfg(unix)]
fn copy_file_symlink_to_source_follows_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 real_file = src_dir.join("real_file.txt");
utils::write_file("", &real_file, "original 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()
);

// copy_file_symlink_to_source should follow the symlink and copy the content
let dest_file = dest_dir.join("copied.txt");
utils::copy_file_symlink_to_source(&link_file, &dest_file).unwrap();

// Destination should be a regular file, NOT a symlink
assert!(
!fs::symlink_metadata(&dest_file)
.unwrap()
.file_type()
.is_symlink(),
"copy_file_symlink_to_source should follow symlinks and create regular files"
);
assert_eq!(
fs::read_to_string(&dest_file).unwrap(),
"original content",
"copy_file_symlink_to_source should copy the target file's content"
);
}

/// 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"
);
}
20 changes: 18 additions & 2 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,29 @@ pub(crate) fn copy_dir(src: &Path, dest: &Path) -> Result<()> {
})
}

/// Copy a file, preserving symlink targets instead of following them.
/// 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, following symlinks and copying the target content.
/// Used for self-update where we want the actual file content, not symlink structure.
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(|_| ())
if preserve_symlink && metadata.file_type().is_symlink() {
let target = fs::read_link(src).with_context(|| RustupError::ReadingFile {
name: "symlink target for",
path: PathBuf::from(src),
})?;
symlink_file(&target, dest).map(|_| ())
} else {
fs::copy(src, dest)
.with_context(|| {
Expand Down
24 changes: 23 additions & 1 deletion src/utils/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -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);
Expand Down