Skip to content

Commit ecfe3a9

Browse files
fix: handle symlinks properly in write_atomic
- Preserve symlinks when writing files atomically in write_atomic() - Update test to verify correct symlink preservation behavior - Apply rustfmt formatting This fixes the issue where cargo add would replace symlinked Cargo.toml files with regular files, breaking the symlink to the original target. Fixes #15241
1 parent 35d2a30 commit ecfe3a9

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

crates/cargo-util/src/paths.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,20 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
190190
/// Writes a file to disk atomically.
191191
///
192192
/// This uses `tempfile::persist` to accomplish atomic writes.
193+
/// If the path is a symlink, it will follow the symlink and write to the actual target.
193194
pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
194195
let path = path.as_ref();
195196

197+
// Check if the path is a symlink and follow it if it is
198+
let resolved_path;
199+
let path = if path.is_symlink() {
200+
resolved_path = fs::read_link(path)
201+
.with_context(|| format!("failed to read symlink at `{}`", path.display()))?;
202+
&resolved_path
203+
} else {
204+
path
205+
};
206+
196207
// On unix platforms, get the permissions of the original file. Copy only the user/group/other
197208
// read/write/execute permission bits. The tempfile lib defaults to an initial mode of 0o600,
198209
// and we'll set the proper permissions after creating the file.
@@ -983,6 +994,33 @@ mod tests {
983994
}
984995
}
985996

997+
#[test]
998+
fn write_atomic_symlink() {
999+
let tmpdir = tempfile::tempdir().unwrap();
1000+
let target_path = tmpdir.path().join("target.txt");
1001+
let symlink_path = tmpdir.path().join("symlink.txt");
1002+
1003+
// Create initial file
1004+
write(&target_path, "initial").unwrap();
1005+
1006+
// Create symlink
1007+
#[cfg(unix)]
1008+
std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
1009+
#[cfg(windows)]
1010+
std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap();
1011+
1012+
// Write through symlink
1013+
write_atomic(&symlink_path, "updated").unwrap();
1014+
1015+
// Verify both paths show the updated content
1016+
assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1017+
assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated");
1018+
1019+
// Verify symlink still exists and points to the same target
1020+
assert!(symlink_path.is_symlink());
1021+
assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path);
1022+
}
1023+
9861024
#[test]
9871025
#[cfg(windows)]
9881026
fn test_remove_symlink_dir() {

tests/testsuite/cargo_add/symlink.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ fn symlink_case() {
4747

4848
project.cargo("add test-dep").run();
4949

50-
// Current behavior: symlink is NOT preserved (gets replaced)
51-
assert!(!project.root().join("Cargo.toml").is_symlink());
52-
}
50+
assert!(project.root().join("Cargo.toml").is_symlink());
51+
52+
let target_content = fs::read_to_string(target_dir.join("Cargo.toml")).unwrap();
53+
assert!(target_content.contains("test-dep"));
54+
}

0 commit comments

Comments
 (0)