1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use miette::{Context, IntoDiagnostic};
/// Format the resolved `virtualStoreDir` as a display-ready prefix for
/// `aube list --long` and `aube why --long`, ending with a path
/// separator so callers can concatenate an encoded `dep_path`
/// filename. When `aube_dir` is a subdirectory of `ref_dir` the result
/// is relative (`./node_modules/.aube/`), matching the historical
/// output. For overrides that sit above or outside `ref_dir` (custom
/// `virtualStoreDir` like `~/.my-store/project` or `.vstore-out`) the
/// absolute path is returned so users can still find where packages
/// actually live — `../../../...` would be technically correct but
/// hard to paste into a shell.
pub(crate) fn format_virtual_store_display_prefix(
aube_dir: &std::path::Path,
ref_dir: &std::path::Path,
) -> String {
if let Some(rel) = pathdiff::diff_paths(aube_dir, ref_dir)
&& !rel.as_os_str().is_empty()
&& !rel
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return format!("./{}/", rel.display());
}
format!("{}/", aube_dir.display())
}
/// Whether metadata describes a POSIX symlink, Windows symlink, or NTFS
/// junction. Rust reports junctions as directories rather than symlinks, so
/// Windows callers must also inspect the reparse-point attribute before
/// traversing a path.
pub(crate) fn is_link_or_junction_metadata(metadata: &std::fs::Metadata) -> bool {
if metadata.file_type().is_symlink() {
return true;
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return true;
}
}
false
}
/// Remove an existing file/dir/symlink at the given path, if present.
///
/// Windows quirk: directory symlinks report as symlinks, while NTFS junctions
/// report as directories. `remove_dir_all` handles the latter without
/// traversing the reparse point. For directory symlinks, `remove_file` may
/// return `Access is denied (os error 5)` because Win32 requires
/// `RemoveDirectory` for directory-shaped links, so fall back to
/// `std::fs::remove_dir`.
pub(crate) fn remove_existing(path: &std::path::Path) -> miette::Result<()> {
let Ok(md) = path.symlink_metadata() else {
return Ok(());
};
let file_type = md.file_type();
if file_type.is_dir() {
return std::fs::remove_dir_all(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove {}", path.display()));
}
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(_) if file_type.is_symlink() => std::fs::remove_dir(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove {}", path.display())),
Err(e) => Err(e)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove {}", path.display())),
}
}
/// Create a directory link (symlink on Unix, NTFS junction on
/// Windows). Thin re-export of [`aube_linker::create_dir_link`] —
/// the linker owns the platform-specific implementation so every
/// directory-link call site in the workspace behaves identically,
/// including Windows' "junctions not symlinks" choice that keeps
/// installs working without Developer Mode.
pub(crate) fn symlink_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
aube_linker::create_dir_link(src, dst)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn removes_a_symlink_pointing_at_a_populated_directory_without_touching_target() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target");
std::fs::create_dir(&target).unwrap();
let canary = target.join("keep.txt");
std::fs::write(&canary, b"keep me").unwrap();
let link = dir.path().join("link");
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &link).unwrap();
#[cfg(windows)]
aube_linker::create_dir_link(&target, &link).unwrap();
assert!(is_link_or_junction_metadata(
&link.symlink_metadata().unwrap()
));
remove_existing(&link).unwrap();
assert!(!link.exists());
assert!(
canary.exists(),
"remove_existing must not recurse into the symlink's target"
);
}
#[test]
fn missing_path_is_a_noop() {
let dir = tempfile::tempdir().unwrap();
remove_existing(&dir.path().join("does-not-exist")).unwrap();
}
}