diff --git a/crates/cargo-util/src/paths.rs b/crates/cargo-util/src/paths.rs index cc73fbad892..5cd72e40090 100644 --- a/crates/cargo-util/src/paths.rs +++ b/crates/cargo-util/src/paths.rs @@ -190,9 +190,20 @@ pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> /// Writes a file to disk atomically. /// /// This uses `tempfile::persist` to accomplish atomic writes. +/// If the path is a symlink, it will follow the symlink and write to the actual target. pub fn write_atomic, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { let path = path.as_ref(); + // Check if the path is a symlink and follow it if it is + let resolved_path; + let path = if path.is_symlink() { + resolved_path = fs::read_link(path) + .with_context(|| format!("failed to read symlink at `{}`", path.display()))?; + &resolved_path + } else { + path + }; + // On unix platforms, get the permissions of the original file. Copy only the user/group/other // read/write/execute permission bits. The tempfile lib defaults to an initial mode of 0o600, // and we'll set the proper permissions after creating the file. @@ -983,6 +994,33 @@ mod tests { } } + #[test] + fn write_atomic_symlink() { + let tmpdir = tempfile::tempdir().unwrap(); + let target_path = tmpdir.path().join("target.txt"); + let symlink_path = tmpdir.path().join("symlink.txt"); + + // Create initial file + write(&target_path, "initial").unwrap(); + + // Create symlink + #[cfg(unix)] + std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap(); + #[cfg(windows)] + std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap(); + + // Write through symlink + write_atomic(&symlink_path, "updated").unwrap(); + + // Verify both paths show the updated content + assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated"); + assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated"); + + // Verify symlink still exists and points to the same target + assert!(symlink_path.is_symlink()); + assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path); + } + #[test] #[cfg(windows)] fn test_remove_symlink_dir() { diff --git a/tests/testsuite/cargo_add/mod.rs b/tests/testsuite/cargo_add/mod.rs index a2881cfbca2..5dea186f2f7 100644 --- a/tests/testsuite/cargo_add/mod.rs +++ b/tests/testsuite/cargo_add/mod.rs @@ -145,6 +145,7 @@ mod script_bare; mod script_frontmatter; mod script_shebang; mod sorted_table_with_dotted_item; +mod symlink; mod target; mod target_cfg; mod unknown_inherited_feature; diff --git a/tests/testsuite/cargo_add/symlink.rs b/tests/testsuite/cargo_add/symlink.rs new file mode 100644 index 00000000000..6b9f92f50fd --- /dev/null +++ b/tests/testsuite/cargo_add/symlink.rs @@ -0,0 +1,54 @@ +use cargo_test_support::prelude::*; +use cargo_test_support::project; +use cargo_test_support::registry; +use std::fs; + +#[cargo_test] +fn symlink_case() { + if !cargo_test_support::symlink_supported() { + return; + } + + registry::init(); + registry::Package::new("test-dep", "1.0.0").publish(); + + let project = project().file("src/lib.rs", "").build(); + + let target_dir = project.root().join("target_dir"); + fs::create_dir_all(&target_dir).unwrap(); + + fs::copy( + project.root().join("Cargo.toml"), + target_dir.join("Cargo.toml"), + ) + .unwrap(); + + fs::remove_file(project.root().join("Cargo.toml")).unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink( + target_dir.join("Cargo.toml"), + project.root().join("Cargo.toml"), + ) + .unwrap(); + } + + #[cfg(windows)] + { + use std::os::windows::fs::symlink_file; + symlink_file( + target_dir.join("Cargo.toml"), + project.root().join("Cargo.toml"), + ) + .unwrap(); + } + + project.cargo("add test-dep").run(); + + assert!(project.root().join("Cargo.toml").is_symlink()); + + let target_content = fs::read_to_string(target_dir.join("Cargo.toml")).unwrap(); + assert!(target_content.contains("test-dep")); +}