Discover Microsoft Store Pythons (#6807)

Microsoft Store Pythons do not always register themselves in the
registry, so we port
<58ce131037/PC/launcher2.c (L1744)>
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?
This commit is contained in:
konsti 2024-08-29 22:56:41 +02:00 committed by GitHub
parent a39eb61ade
commit 9814852295
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 155 additions and 23 deletions

View file

@ -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"),
}

View file

@ -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;

View file

@ -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 <https://github.com/python/cpython/blob/58ce131037ecb34d506a613f21993cde2056f628/PC/launcher2.c#L1744>
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 <https://github.com/python/cpython/blob/58ce131037ecb34d506a613f21993cde2056f628/PC/launcher2.c#L1963-L1985>,
/// 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 <https://github.com/python/cpython/blob/58ce131037ecb34d506a613f21993cde2056f628/PC/launcher2.c#L1744>
pub(crate) fn find_microsoft_store_pythons() -> impl Iterator<Item = WindowsPython> {
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()),
)
}

View file

@ -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<PythonVersion>,
}
@ -24,7 +25,7 @@ fn value_to_string(value: Value) -> Option<String> {
}
/// Find all Pythons registered in the Windows registry following PEP 514.
pub(crate) fn registry_pythons() -> Result<Vec<RegistryPython>, windows_result::Error> {
pub(crate) fn registry_pythons() -> Result<Vec<WindowsPython>, 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<Vec<RegistryPython>, windows_result::
Ok(registry_pythons)
}
fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option<RegistryPython> {
fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option<WindowsPython> {
// `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<Regist
}
});
Some(RegistryPython {
Some(WindowsPython {
path: PathBuf::from(executable_path),
version,
})