Skip to content

Commit

Permalink
Discover Microsoft Store Pythons (#6807)
Browse files Browse the repository at this point in the history
Microsoft Store Pythons do not always register themselves in the
registry, so we port
<https://github.com/python/cpython/blob/58ce131037ecb34d506a613f21993cde2056f628/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?
  • Loading branch information
konstin authored Aug 29, 2024
1 parent a39eb61 commit 9814852
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 23 deletions.
47 changes: 29 additions & 18 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 ...`
Expand Down Expand Up @@ -308,30 +312,36 @@ 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(|| {
registry_pythons()
.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)
})
Expand All @@ -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)),
}
}

Expand Down Expand Up @@ -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"),
}
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
118 changes: 118 additions & 0 deletions crates/uv-python/src/microsoft_store.rs
Original file line number Diff line number Diff line change
@@ -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()),
)
}
11 changes: 6 additions & 5 deletions crates/uv-python/src/py_launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
})
Expand Down

0 comments on commit 9814852

Please sign in to comment.