Skip to content

Commit f8035fd

Browse files
authored
fix: Preserve symlinked manifest paths (#4912)
1 parent 1b232c1 commit f8035fd

File tree

1 file changed

+76
-1
lines changed

1 file changed

+76
-1
lines changed

crates/pixi_manifest/src/manifests/provenance.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,16 @@ impl ManifestProvenance {
6161
}
6262

6363
/// Returns the absolute path to the manifest file.
64+
///
65+
/// This method canonicalizes the parent directory but preserves the original
66+
/// filename, which allows symlinked manifest files to be treated correctly.
6467
pub fn absolute_path(&self) -> PathBuf {
65-
dunce::canonicalize(self.path.clone()).unwrap_or(self.path.to_path_buf())
68+
match (self.path.parent(), self.path.file_name()) {
69+
(Some(parent), Some(file_name)) => dunce::canonicalize(parent)
70+
.map(|canonical_parent| canonical_parent.join(file_name))
71+
.unwrap_or_else(|_| self.path.to_path_buf()),
72+
_ => self.path.to_path_buf(),
73+
}
6674
}
6775
}
6876

@@ -142,3 +150,70 @@ impl<T> AssociateProvenance for T {
142150
WithProvenance::new(self, provenance)
143151
}
144152
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
158+
#[test]
159+
fn absolute_path_canonicalizes_parent_only() {
160+
let temp_dir = tempfile::tempdir().unwrap();
161+
let dotfiles_dir = temp_dir.path().join("dotfiles");
162+
let home_dir = temp_dir.path().join("home");
163+
fs_err::create_dir_all(&dotfiles_dir).unwrap();
164+
fs_err::create_dir_all(&home_dir).unwrap();
165+
166+
// Real manifest lives inside the dotfiles directory.
167+
let real_manifest = dotfiles_dir.join("pixi.toml");
168+
fs_err::write(&real_manifest, "[workspace]\nname = \"test\"\n").unwrap();
169+
170+
// Home directory contains a symlink that points at the real manifest.
171+
let symlink_manifest = home_dir.join("pixi.toml");
172+
#[cfg(unix)]
173+
std::os::unix::fs::symlink(&real_manifest, &symlink_manifest).unwrap();
174+
175+
let canonical_real_path = dunce::canonicalize(&real_manifest).unwrap();
176+
let cases = [
177+
(
178+
"real file",
179+
real_manifest.clone(),
180+
dotfiles_dir.clone(),
181+
true,
182+
),
183+
(
184+
"symlinked file",
185+
symlink_manifest.clone(),
186+
home_dir.clone(),
187+
false,
188+
),
189+
];
190+
191+
for (label, manifest_path, expected_parent, should_match_real) in cases {
192+
let provenance = ManifestProvenance::new(manifest_path.clone(), ManifestKind::Pixi);
193+
let absolute = provenance.absolute_path();
194+
195+
assert_eq!(
196+
absolute.file_name(),
197+
manifest_path.file_name(),
198+
"filename changed for {label}"
199+
);
200+
assert_eq!(
201+
absolute.parent().unwrap(),
202+
dunce::canonicalize(&expected_parent).unwrap(),
203+
"parent directory mismatch for {label}"
204+
);
205+
206+
if should_match_real {
207+
assert_eq!(
208+
absolute, canonical_real_path,
209+
"real file should resolve exactly"
210+
);
211+
} else {
212+
assert_ne!(
213+
absolute, canonical_real_path,
214+
"symlink should not resolve to the real file path"
215+
);
216+
}
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)