From 388078099c91fa56b4042e18ad1abf138d61411a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 24 Jul 2024 14:08:49 +1000 Subject: [PATCH 1/3] Return user friendly exe for Windows Store Python --- crates/pet-core/src/python_environment.rs | 52 +++++++++++++++++++- crates/pet-windows-store/README.md | 43 +++++++++++++++- crates/pet-windows-store/src/environments.rs | 48 ++++++++++++++++++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index 7cb18a44..b967e129 100644 --- a/crates/pet-core/src/python_environment.rs +++ b/crates/pet-core/src/python_environment.rs @@ -208,6 +208,20 @@ impl PythonEnvironmentBuilder { symlinks: None, } } + pub fn from_environment(env: PythonEnvironment) -> Self { + Self { + kind: env.kind, + display_name: env.display_name, + name: env.name, + executable: env.executable, + version: env.version, + prefix: env.prefix, + manager: env.manager, + project: env.project, + arch: env.arch, + symlinks: env.symlinks, + } + } pub fn display_name(mut self, display_name: Option) -> Self { self.display_name = display_name; @@ -269,7 +283,7 @@ impl PythonEnvironmentBuilder { } fn update_symlinks_and_exe(&mut self, symlinks: Option>) { - let mut all = vec![]; + let mut all = self.symlinks.clone().unwrap_or_default(); if let Some(ref exe) = self.executable { all.push(exe.clone()); } @@ -334,6 +348,9 @@ fn get_shortest_executable( exes: &Option>, ) -> Option { // For windows store, the exe should always be the one in the WindowsApps folder. + // & it must be the exe that is of the form Python3.12.exe + // We will never use Python.exe nor Python3.exe as the shortest paths + // See README.md if *kind == Some(PythonEnvironmentKind::WindowsStore) { if let Some(exes) = exes { if let Some(exe) = exes.iter().find(|e| { @@ -341,6 +358,15 @@ fn get_shortest_executable( && e.to_string_lossy().contains("Local") && e.to_string_lossy().contains("Microsoft") && e.to_string_lossy().contains("WindowsApps") + // Exe must be in the WindowsApps directory. + && e.parent() + .map(|p| p.ends_with("WindowsApps")) + .unwrap_or_default() + // Always give preference to the exe Python3.12.exe or the like, + // Over Python.exe and Python3.exe + // This is to be consistent with the exe we choose for the Windows Store env. + // See README.md + && e.file_name().map(|f| f.to_string_lossy().to_lowercase().starts_with("python3.")).unwrap_or_default() }) { return Some(exe.clone()); } @@ -386,3 +412,27 @@ pub fn get_environment_key(env: &PythonEnvironment) -> Option { None } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(windows)] + fn shorted_exe_path_windows_store() { + let exes = vec![ + PathBuf::from("C:\\Users\\user\\AppData\\Local\\Microsoft\\WindowsApps\\Python3.12.exe"), + PathBuf::from("C:\\Users\\user\\AppData\\Local\\Microsoft\\WindowsApps\\Python3.exe"), + PathBuf::from("C:\\Users\\user\\AppData\\Local\\Microsoft\\WindowsApps\\Python.exe"), + PathBuf::from("C:\\Users\\donja\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\python.exe"), + PathBuf::from("C:\\Users\\donja\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\python3.exe"), + PathBuf::from("C:\\Users\\donja\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\python12.exe"), + ]; + assert_eq!( + get_shortest_executable(&Some(PythonEnvironmentKind::WindowsStore), &Some(exes)), + Some(PathBuf::from( + "C:\\Users\\user\\AppData\\Local\\Microsoft\\WindowsApps\\Python3.12.exe" + )) + ); + } +} diff --git a/crates/pet-windows-store/README.md b/crates/pet-windows-store/README.md index 2aa94c5c..995d9803 100644 --- a/crates/pet-windows-store/README.md +++ b/crates/pet-windows-store/README.md @@ -2,7 +2,12 @@ ## Known Issues -- Note possible to get the `version` information, hence not returned +- Note possible to get the `version` information, hence not returned (env will need to be resolved) +- If there are multiple versions of Windows Store Python installed, + none of the environments returned will contain the exes `.../WindowsApps/python.exe` or `.../WindowsApps/python3.exe`. + This is becase we will need to spawn both of these exes to figure out the env it belongs to. + For now, we will avoid that. + Upon resolving `.../WindowsApps/python.exe` or `.../WindowsApps/python3.exe` we will return the right information. ```rust for directory under `/AppData/Local/Microsoft/WindowsApps`: @@ -17,7 +22,41 @@ for directory under `/AppData/Local/Microsoft/WindowsApps`: key = `/Repository/Packages/` env_path = `/(PackageRootFolder)` display_name = `/(DisplayName)` - exe = `python.exe` + + // Get the first 2 parts of the version from the path + // directory = \AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\python.exe + // In this case first 2 parts are `3.9` + // Now look for a file named `python3.9.exe` in the `WindowsApps` directory (parent directory) + // If it exists, then use that as a symlink as well + // As a result that exe will have a shorter path, hence thats what users will see + exe = `python.exe` or `pythonX.Y.exe` + // No way to get the full version information. 👍 track this environment ``` + +## Notes + +### Why will `/WindowsApps/python3.exe` & `/WindowsApps/python.exe` will never be returned as preferred exes + +Assume we have Pythoon 3.10 and Python 3.12 installed from Windows Store. +Now we'll have the following exes in the `WindowsApps` directory: + +- `/WindowsApps/python3.10.exe` +- `/WindowsApps/python3.12.exe` +- `/WindowsApps/python3.exe` +- `/WindowsApps/python.exe`. + +However we will not know what Python3.exe and Python.exe point to. +The only way to determine this is by running the exe and checking the version. +But that will slow discovery, hence we will not spawn those and never return them either during a regular discovery. + +### `/WindowsApps/python3.exe` & `/WindowsApps/python.exe` can get returned as symlinks + +If user has just Python 3.10 installed, then `/WindowsApps/python3.exe` & `/WindowsApps/python3.10.exe` will be returned as symlinks. + +Similarly, if caller of the API attempts to resolve either one of the above exes, then we'll end up spawning the exe and we get the fully qualified path such as the following: + +- `C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\python.exe`. + +From here we know the enviroment details, and the original exe will be returned as a symlink. diff --git a/crates/pet-windows-store/src/environments.rs b/crates/pet-windows-store/src/environments.rs index 267c0f99..355f01ac 100644 --- a/crates/pet-windows-store/src/environments.rs +++ b/crates/pet-windows-store/src/environments.rs @@ -13,6 +13,7 @@ use pet_core::python_environment::PythonEnvironment; use pet_core::{arch::Architecture, python_environment::PythonEnvironmentBuilder}; #[cfg(windows)] use pet_fs::path::norm_case; +use pet_python_utils::executable::find_executables; #[cfg(windows)] use regex::Regex; use std::path::PathBuf; @@ -39,6 +40,8 @@ struct PotentialPython { exe: Option, #[allow(dead_code)] version: String, + #[allow(dead_code)] + symlinks: Vec, } #[cfg(windows)] @@ -121,6 +124,7 @@ pub fn list_store_pythons(environment: &EnvVariables) -> Option Option Option Vec { + let mut symlinks = vec![]; + if let Some(bin_dir) = exe_in_windows_app_path.parent() { + if let Some(windows_app_path) = bin_dir.parent() { + // Ensure we're in the right place + if windows_app_path.ends_with("WindowsApp") { + return vec![]; + } + + let possible_exe = + windows_app_path.join(PathBuf::from(format!("python{}.exe", version))); + if possible_exe.exists() { + symlinks.push(possible_exe); + } + + // How many exes do we have that look like with Python3.x.exe + // If we have Python3.12.exe & Python3.10.exe, then we have absolutely no idea whether + // the exes Python3.exe and Python.exe belong to 3.12 or 3.10 without spawning. + // In those cases we will not bother figuring those out. + // However if we have just one Python exe of the form Python3.x.ex, then python.exe and Python3.exe are symlinks. + let mut number_of_python_exes_with_versions = 0; + let mut exes = vec![]; + find_executables(windows_app_path) + .into_iter() + .for_each(|exe| { + if let Some(name) = exe.file_name().and_then(|s| s.to_str()) { + if name.to_lowercase().starts_with("python3.") { + number_of_python_exes_with_versions += 1; + } + exes.push(exe); + } + }); + + if number_of_python_exes_with_versions == 1 { + symlinks.append(&mut exes); + } + } + } + symlinks +} + #[cfg(windows)] #[derive(Debug)] struct StorePythonInfo { From d29398b75f310e221e59a950428c638429ad2dff Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 24 Jul 2024 14:11:23 +1000 Subject: [PATCH 2/3] FIXES --- crates/pet-windows-store/src/environments.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/pet-windows-store/src/environments.rs b/crates/pet-windows-store/src/environments.rs index 355f01ac..6955dc8b 100644 --- a/crates/pet-windows-store/src/environments.rs +++ b/crates/pet-windows-store/src/environments.rs @@ -13,6 +13,7 @@ use pet_core::python_environment::PythonEnvironment; use pet_core::{arch::Architecture, python_environment::PythonEnvironmentBuilder}; #[cfg(windows)] use pet_fs::path::norm_case; +#[cfg(windows)] use pet_python_utils::executable::find_executables; #[cfg(windows)] use regex::Regex; @@ -184,6 +185,7 @@ pub fn list_store_pythons(environment: &EnvVariables) -> Option Vec { let mut symlinks = vec![]; if let Some(bin_dir) = exe_in_windows_app_path.parent() { From 6440d77ddae2c42f18b8bfed31787b5311bc8d23 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 24 Jul 2024 14:29:45 +1000 Subject: [PATCH 3/3] fixes --- crates/pet-windows-store/src/lib.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/pet-windows-store/src/lib.rs b/crates/pet-windows-store/src/lib.rs index 68ec03b7..4d6f70b4 100644 --- a/crates/pet-windows-store/src/lib.rs +++ b/crates/pet-windows-store/src/lib.rs @@ -61,6 +61,9 @@ impl Locator for WindowsStore { #[cfg(windows)] fn try_from(&self, env: &PythonEnv) -> Option { + use std::path::PathBuf; + + use pet_core::python_environment::PythonEnvironmentBuilder; use pet_virtualenv::is_virtualenv; // Assume we create a virtual env from a python install, @@ -69,11 +72,28 @@ impl Locator for WindowsStore { if is_virtualenv(env) { return None; } + let list_of_possible_exes = vec![env.executable.clone()] + .into_iter() + .chain(env.symlinks.clone().unwrap_or_default().into_iter()) + .collect::>(); if let Some(environments) = self.find_with_cache() { for found_env in environments { - if let Some(ref python_executable_path) = found_env.executable { - if python_executable_path == &env.executable { - return Some(found_env); + if let Some(symlinks) = &found_env.symlinks { + // Check if we have found this exe. + if list_of_possible_exes + .iter() + .any(|exe| symlinks.contains(exe)) + { + // Its possible the env discovery was not aware of the symlink + // E.g. if we are asked to resolve `../WindowsApp/python.exe` + // We will have no idea, hence this will get spawned, and then exe + // might be something like `../WindowsApp/PythonSoftwareFoundation.Python.3.10...` + // However the env found by the locator will almost never contain python.exe nor python3.exe + // See README.md + // As a result, we need to add those symlinks here. + let builder = PythonEnvironmentBuilder::from_environment(found_env.clone()) + .symlinks(env.symlinks.clone()); + return Some(builder.build()); } } }