Skip to content

Commit 1498e00

Browse files
authored
fix: symlink handling for Python from Windows Store (#326)
1 parent b31be7b commit 1498e00

File tree

3 files changed

+52
-12
lines changed

3 files changed

+52
-12
lines changed

crates/pet-fs/src/path.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
use std::{
55
env,
6-
path::{Path, PathBuf, MAIN_SEPARATOR},
6+
path::{Path, PathBuf},
77
};
88

9+
#[cfg(unix)]
10+
use std::path::MAIN_SEPARATOR;
11+
912
/// Strips trailing path separators from a path, preserving root paths.
1013
///
1114
/// This function removes trailing `/` or `\` from paths while ensuring that root paths

crates/pet-windows-store/src/environments.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ impl PotentialPython {
5656
if let Some(result) = get_package_display_name_and_location(&name, hkcu) {
5757
let env_path = norm_case(PathBuf::from(result.env_path));
5858

59+
// Build the base symlinks list
60+
// parent = WindowsApps folder (e.g., C:\Users\...\AppData\Local\Microsoft\WindowsApps)
61+
// path = Package folder inside WindowsApps (e.g., WindowsApps\PythonSoftwareFoundation.Python.3.12_...)
62+
// env_path = Program Files location (e.g., C:\Program Files\WindowsApps\PythonSoftwareFoundation...)
63+
let mut symlinks = vec![
64+
// Symlinks in the user WindowsApps folder
65+
parent.join(format!("python{}.exe", self.version)),
66+
parent.join("python3.exe"),
67+
parent.join("python.exe"),
68+
// Symlinks in the package subfolder under user WindowsApps
69+
path.join("python.exe"),
70+
path.join("python3.exe"),
71+
path.join(format!("python{}.exe", self.version)),
72+
// Symlinks in Program Files
73+
env_path.join("python.exe"),
74+
env_path.join("python3.exe"),
75+
env_path.join(format!("python{}.exe", self.version)),
76+
];
77+
78+
// Add symlinks discovered by find_symlinks (includes python.exe and python3.exe
79+
// from WindowsApps when there's only one Python version installed)
80+
for symlink in &self.symlinks {
81+
if !symlinks.contains(symlink) {
82+
symlinks.push(symlink.clone());
83+
}
84+
}
85+
5986
Some(
6087
PythonEnvironmentBuilder::new(Some(
6188
pet_core::python_environment::PythonEnvironmentKind::WindowsStore,
@@ -70,14 +97,7 @@ impl PotentialPython {
7097
})
7198
// We only have the partial version, no point returning bogus info.
7299
// .version(Some(self.version.clone()))
73-
.symlinks(Some(vec![
74-
parent.join(format!("python{}.exe", self.version)),
75-
path.join("python.exe"),
76-
path.join("python3.exe"),
77-
path.join(format!("python{}.exe", self.version)),
78-
env_path.join("python.exe"),
79-
env_path.join(format!("python{}.exe", self.version)),
80-
]))
100+
.symlinks(Some(symlinks))
81101
.build(),
82102
)
83103
} else {

crates/pet-windows-store/src/lib.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,42 @@ impl Locator for WindowsStore {
6767
use std::path::PathBuf;
6868

6969
use pet_core::python_environment::PythonEnvironmentBuilder;
70+
use pet_fs::path::norm_case;
7071
use pet_virtualenv::is_virtualenv;
7172

73+
// Helper to normalize paths for comparison by stripping \\?\ prefix
74+
fn normalize_for_comparison(path: &PathBuf) -> PathBuf {
75+
let normalized = norm_case(path);
76+
let path_str = normalized.to_string_lossy();
77+
if path_str.starts_with(r"\\?\") {
78+
PathBuf::from(path_str.trim_start_matches(r"\\?\"))
79+
} else {
80+
normalized
81+
}
82+
}
83+
7284
// Assume we create a virtual env from a python install,
7385
// Then the exe in the virtual env bin will be a symlink to the homebrew python install.
7486
// Hence the first part of the condition will be true, but the second part will be false.
7587
if is_virtualenv(env) {
7688
return None;
7789
}
78-
let list_of_possible_exes = vec![env.executable.clone()]
90+
// Normalize paths to handle \\?\ prefix differences
91+
let list_of_possible_exes: Vec<PathBuf> = vec![env.executable.clone()]
7992
.into_iter()
8093
.chain(env.symlinks.clone().unwrap_or_default())
81-
.collect::<Vec<PathBuf>>();
94+
.map(|p| normalize_for_comparison(&p))
95+
.collect();
8296
if let Some(environments) = self.find_with_cache() {
8397
for found_env in environments {
8498
if let Some(symlinks) = &found_env.symlinks {
99+
// Normalize symlinks for comparison
100+
let normalized_symlinks: Vec<PathBuf> =
101+
symlinks.iter().map(normalize_for_comparison).collect();
85102
// Check if we have found this exe.
86103
if list_of_possible_exes
87104
.iter()
88-
.any(|exe| symlinks.contains(exe))
105+
.any(|exe| normalized_symlinks.contains(exe))
89106
{
90107
// Its possible the env discovery was not aware of the symlink
91108
// E.g. if we are asked to resolve `../WindowsApp/python.exe`

0 commit comments

Comments
 (0)