From 9814852295bfe87e241932869aa3ae6852d88e74 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 29 Aug 2024 22:56:41 +0200 Subject: [PATCH] Discover Microsoft Store Pythons (#6807) Microsoft Store Pythons do not always register themselves in the registry, so we port and look them up on the filesystem in known locations. ## Test Plan So far I've confirmed that we find a store Python when I use `cargo run python list`, can we make this a part of any of the platform tests maybe? --- crates/uv-python/src/discovery.rs | 47 ++++++---- crates/uv-python/src/lib.rs | 2 + crates/uv-python/src/microsoft_store.rs | 118 ++++++++++++++++++++++++ crates/uv-python/src/py_launcher.rs | 11 ++- 4 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 crates/uv-python/src/microsoft_store.rs diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 0e36010db15b..36d9bb8046c3 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -21,7 +21,9 @@ use crate::installation::PythonInstallation; use crate::interpreter::Error as InterpreterError; use crate::managed::ManagedPythonInstallations; #[cfg(windows)] -use crate::py_launcher::registry_pythons; +use crate::microsoft_store::find_microsoft_store_pythons; +#[cfg(windows)] +use crate::py_launcher::{registry_pythons, WindowsPython}; use crate::virtualenv::{ conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir, virtualenv_python_executable, @@ -167,6 +169,8 @@ pub enum PythonSource { SearchPath, /// An executable was found in the Windows registry via PEP 514 Registry, + /// An executable was found in the known Microsoft Store locations + MicrosoftStore, /// The Python installation was found in the uv managed Python directory Managed, /// The Python installation was found via the invoking interpreter i.e. via `python -m uv ...` @@ -308,9 +312,22 @@ fn python_executables_from_installed<'a>( }) .flatten(); - let from_py_launcher = std::iter::once_with(move || { + let from_windows = std::iter::once_with(move || { #[cfg(windows)] { + // Skip interpreter probing if we already know the version doesn't match. + let version_filter = move |entry: &WindowsPython| { + if let Some(version_request) = version { + if let Some(version) = &entry.version { + version_request.matches_version(version) + } else { + true + } + } else { + true + } + }; + env::var_os("UV_TEST_PYTHON_PATH") .is_none() .then(|| { @@ -318,20 +335,13 @@ fn python_executables_from_installed<'a>( .map(|entries| { entries .into_iter() - .filter(move |entry| { - // Skip interpreter probing if we already know the version - // doesn't match. - if let Some(version_request) = version { - if let Some(version) = &entry.version { - version_request.matches_version(version) - } else { - true - } - } else { - true - } - }) + .filter(version_filter) .map(|entry| (PythonSource::Registry, entry.path)) + .chain( + find_microsoft_store_pythons() + .filter(version_filter) + .map(|entry| (PythonSource::MicrosoftStore, entry.path)), + ) }) .map_err(Error::from) }) @@ -350,14 +360,14 @@ fn python_executables_from_installed<'a>( PythonPreference::Managed => Box::new( from_managed_installations .chain(from_search_path) - .chain(from_py_launcher), + .chain(from_windows), ), PythonPreference::System => Box::new( from_search_path - .chain(from_py_launcher) + .chain(from_windows) .chain(from_managed_installations), ), - PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_py_launcher)), + PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows)), } } @@ -1633,6 +1643,7 @@ impl fmt::Display for PythonSource { Self::DiscoveredEnvironment => f.write_str("virtual environment"), Self::SearchPath => f.write_str("search path"), Self::Registry => f.write_str("registry"), + Self::MicrosoftStore => f.write_str("Microsoft Store"), Self::Managed => f.write_str("managed installations"), Self::ParentInterpreter => f.write_str("parent interpreter"), } diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index a6730e474aae..82fe49bd83ec 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -26,6 +26,8 @@ mod installation; mod interpreter; mod libc; pub mod managed; +#[cfg(windows)] +mod microsoft_store; pub mod platform; mod pointer_size; mod prefix; diff --git a/crates/uv-python/src/microsoft_store.rs b/crates/uv-python/src/microsoft_store.rs new file mode 100644 index 000000000000..aefdbb2af2cc --- /dev/null +++ b/crates/uv-python/src/microsoft_store.rs @@ -0,0 +1,118 @@ +//! Microsoft Store Pythons don't register themselves in the registry, so we have to look for them +//! in known locations. +//! +//! Effectively a port of + +use crate::py_launcher::WindowsPython; +use crate::PythonVersion; +use itertools::Either; +use std::env; +use std::path::PathBuf; +use std::str::FromStr; +use tracing::debug; + +#[derive(Debug)] +struct MicrosoftStorePython { + family_name: &'static str, + version: &'static str, +} + +/// List of known Microsoft Store Pythons. +/// +/// Copied from , +/// please update when upstream changes. +const MICROSOFT_STORE_PYTHONS: &[MicrosoftStorePython] = &[ + // Releases made through the Store + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0", + version: "3.13", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0", + version: "3.12", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0", + version: "3.11", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0", + version: "3.10", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0", + version: "3.9", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0", + version: "3.8", + }, + // Side-loadable releases + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.13_3847v3x7pw1km", + version: "3.13", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.12_3847v3x7pw1km", + version: "3.12", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.11_3847v3x7pw1km", + version: "3.11", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.11_hd69rhyc2wevp", + version: "3.11", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.10_3847v3x7pw1km", + version: "3.10", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.10_hd69rhyc2wevp", + version: "3.10", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.9_3847v3x7pw1km", + version: "3.9", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.9_hd69rhyc2wevp", + version: "3.9", + }, + MicrosoftStorePython { + family_name: "PythonSoftwareFoundation.Python.3.8_hd69rhyc2wevp", + version: "3.8", + }, +]; + +/// Microsoft Store Pythons don't register themselves in the registry, so we have to look for them +/// in known locations. +/// +/// Effectively a port of +pub(crate) fn find_microsoft_store_pythons() -> impl Iterator { + let Ok(local_app_data) = env::var("LOCALAPPDATA") else { + debug!("`LOCALAPPDATA` not set, ignoring Microsoft store Pythons"); + return Either::Left(std::iter::empty()); + }; + + let windows_apps = PathBuf::from(local_app_data) + .join("Microsoft") + .join("WindowsApps"); + + Either::Right( + MICROSOFT_STORE_PYTHONS + .iter() + .map(move |store_python| { + let path = windows_apps + .join(store_python.family_name) + .join("python.exe"); + WindowsPython { + path, + // All versions are constants, we know they are valid. + version: Some(PythonVersion::from_str(store_python.version).unwrap()), + } + }) + .filter(|windows_python| windows_python.path.is_file()), + ) +} diff --git a/crates/uv-python/src/py_launcher.rs b/crates/uv-python/src/py_launcher.rs index ae9fd09eb1f1..dcb125285cc6 100644 --- a/crates/uv-python/src/py_launcher.rs +++ b/crates/uv-python/src/py_launcher.rs @@ -4,12 +4,13 @@ use std::str::FromStr; use tracing::debug; use windows_registry::{Key, Value, CURRENT_USER, LOCAL_MACHINE}; -/// A Python interpreter found in the Windows registry through PEP 514. +/// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft +/// Store path. /// /// There are a lot more (optional) fields defined in PEP 514, but we only care about path and /// version here, for everything else we probe with a Python script. #[derive(Debug, Clone)] -pub(crate) struct RegistryPython { +pub(crate) struct WindowsPython { pub(crate) path: PathBuf, pub(crate) version: Option, } @@ -24,7 +25,7 @@ fn value_to_string(value: Value) -> Option { } /// Find all Pythons registered in the Windows registry following PEP 514. -pub(crate) fn registry_pythons() -> Result, windows_result::Error> { +pub(crate) fn registry_pythons() -> Result, windows_result::Error> { let mut registry_pythons = Vec::new(); for root_key in [CURRENT_USER, LOCAL_MACHINE] { let Ok(key_python) = root_key.open(r"Software\Python") else { @@ -67,7 +68,7 @@ pub(crate) fn registry_pythons() -> Result, windows_result:: Ok(registry_pythons) } -fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option { +fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option { // `ExecutablePath` is mandatory for executable Pythons. let Some(executable_path) = tag_key .open("InstallPath") @@ -101,7 +102,7 @@ fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option