Skip to content
Merged
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 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"
);
}
23 changes: 22 additions & 1 deletion src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
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