diff --git a/Cargo.lock b/Cargo.lock
index cc30ef3f4..beecd2313 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5043,6 +5043,8 @@ dependencies = [
"uv-state",
"uv-warnings",
"which",
+ "windows-registry",
+ "windows-result",
"windows-sys 0.59.0",
"winsafe 0.0.22",
]
diff --git a/Cargo.toml b/Cargo.toml
index 155da3443..7557cc46c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -157,6 +157,8 @@ url = { version = "2.5.0" }
urlencoding = { version = "2.1.3" }
walkdir = { version = "2.5.0" }
which = { version = "6.0.0", features = ["regex"] }
+windows-registry = { version = "0.2.0" }
+windows-result = { version = "0.2.0" }
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO"] }
winreg = { version = "0.52.0" }
winsafe = { version = "0.0.22", features = ["kernel"] }
diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml
index 57bcd183f..c3c17a6ce 100644
--- a/crates/uv-python/Cargo.toml
+++ b/crates/uv-python/Cargo.toml
@@ -58,6 +58,8 @@ rustix = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { workspace = true }
winsafe = { workspace = true }
+windows-registry = { workspace = true }
+windows-result = { workspace = true }
[dev-dependencies]
anyhow = { version = "1.0.80" }
diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs
index 9158d9837..0e36010db 100644
--- a/crates/uv-python/src/discovery.rs
+++ b/crates/uv-python/src/discovery.rs
@@ -20,7 +20,8 @@ use crate::implementation::ImplementationName;
use crate::installation::PythonInstallation;
use crate::interpreter::Error as InterpreterError;
use crate::managed::ManagedPythonInstallations;
-use crate::py_launcher::{self, py_list_paths};
+#[cfg(windows)]
+use crate::py_launcher::registry_pythons;
use crate::virtualenv::{
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
virtualenv_python_executable,
@@ -164,8 +165,8 @@ pub enum PythonSource {
DiscoveredEnvironment,
/// An executable was found in the search path i.e. `PATH`
SearchPath,
- /// An executable was found via the `py` launcher
- PyLauncher,
+ /// An executable was found in the Windows registry via PEP 514
+ Registry,
/// 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 ...`
@@ -189,9 +190,9 @@ pub enum Error {
#[error(transparent)]
VirtualEnv(#[from] crate::virtualenv::Error),
- /// An error was encountered when using the `py` launcher on Windows.
- #[error(transparent)]
- PyLauncher(#[from] crate::py_launcher::Error),
+ #[cfg(windows)]
+ #[error("Failed to query installed Python versions from the Windows registry")]
+ RegistryError(#[from] windows_result::Error),
/// An invalid version request was given
#[error("Invalid version request: {0}")]
@@ -307,23 +308,40 @@ fn python_executables_from_installed<'a>(
})
.flatten();
- // TODO(konstin): Implement to read python installations from the registry instead.
let from_py_launcher = std::iter::once_with(move || {
- (cfg!(windows) && env::var_os("UV_TEST_PYTHON_PATH").is_none())
- .then(|| {
- py_list_paths()
- .map(|entries|
- // We can avoid querying the interpreter using versions from the py launcher output unless a patch is requested
- entries.into_iter().filter(move |entry|
- version.is_none() || version.is_some_and(|version|
- version.has_patch() || version.matches_major_minor(entry.major, entry.minor)
- )
- )
- .map(|entry| (PythonSource::PyLauncher, entry.executable_path)))
- .map_err(Error::from)
- })
- .into_iter()
- .flatten_ok()
+ #[cfg(windows)]
+ {
+ 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
+ }
+ })
+ .map(|entry| (PythonSource::Registry, entry.path))
+ })
+ .map_err(Error::from)
+ })
+ .into_iter()
+ .flatten_ok()
+ }
+ #[cfg(not(windows))]
+ {
+ Vec::new()
+ }
})
.flatten();
@@ -626,11 +644,6 @@ impl Error {
false
}
},
- // Ignore `py` if it's not installed
- Error::PyLauncher(py_launcher::Error::NotFound) => {
- debug!("The `py` launcher could not be found to query for Python versions");
- false
- }
_ => true,
}
}
@@ -1293,7 +1306,7 @@ impl PythonPreference {
// If not dealing with a system interpreter source, we don't care about the preference
if !matches!(
source,
- PythonSource::Managed | PythonSource::SearchPath | PythonSource::PyLauncher
+ PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
) {
return true;
}
@@ -1302,10 +1315,10 @@ impl PythonPreference {
PythonPreference::OnlyManaged => matches!(source, PythonSource::Managed),
Self::Managed | Self::System => matches!(
source,
- PythonSource::Managed | PythonSource::SearchPath | PythonSource::PyLauncher
+ PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
),
PythonPreference::OnlySystem => {
- matches!(source, PythonSource::SearchPath | PythonSource::PyLauncher)
+ matches!(source, PythonSource::SearchPath | PythonSource::Registry)
}
}
}
@@ -1619,7 +1632,7 @@ impl fmt::Display for PythonSource {
Self::CondaPrefix => f.write_str("conda prefix"),
Self::DiscoveredEnvironment => f.write_str("virtual environment"),
Self::SearchPath => f.write_str("search path"),
- Self::PyLauncher => f.write_str("`py` launcher output"),
+ Self::Registry => f.write_str("registry"),
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 2ba1e90f2..a6730e474 100644
--- a/crates/uv-python/src/lib.rs
+++ b/crates/uv-python/src/lib.rs
@@ -29,6 +29,7 @@ pub mod managed;
pub mod platform;
mod pointer_size;
mod prefix;
+#[cfg(windows)]
mod py_launcher;
mod python_version;
mod target;
@@ -60,9 +61,6 @@ pub enum Error {
#[error(transparent)]
Discovery(#[from] discovery::Error),
- #[error(transparent)]
- PyLauncher(#[from] py_launcher::Error),
-
#[error(transparent)]
ManagedPython(#[from] managed::Error),
diff --git a/crates/uv-python/src/py_launcher.rs b/crates/uv-python/src/py_launcher.rs
index e7cc4a7b7..ae9fd09eb 100644
--- a/crates/uv-python/src/py_launcher.rs
+++ b/crates/uv-python/src/py_launcher.rs
@@ -1,93 +1,108 @@
-use regex::Regex;
-use std::io;
+use crate::PythonVersion;
use std::path::PathBuf;
-use std::process::{Command, ExitStatus};
-use std::sync::LazyLock;
-use thiserror::Error;
-use tracing::info_span;
+use std::str::FromStr;
+use tracing::debug;
+use windows_registry::{Key, Value, CURRENT_USER, LOCAL_MACHINE};
-#[derive(Debug, Clone)]
-pub(crate) struct PyListPath {
- pub(crate) major: u8,
- pub(crate) minor: u8,
- pub(crate) executable_path: PathBuf,
-}
-
-/// An error was encountered when using the `py` launcher on Windows.
-#[derive(Error, Debug)]
-pub enum Error {
- #[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
- StatusCode {
- message: String,
- exit_code: ExitStatus,
- stdout: String,
- stderr: String,
- },
- #[error("Failed to run `py --list-paths` to find Python installations")]
- Io(#[source] io::Error),
- #[error("The `py` launcher could not be found")]
- NotFound,
-}
-
-/// ```text
-/// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe
-/// -V:3.8 C:\Users\Ferris\AppData\Local\Programs\Python\Python38\python.exe
-/// ```
-static PY_LIST_PATHS: LazyLock = LazyLock::new(|| {
- // Without the `R` flag, paths have trailing \r
- Regex::new(r"(?mR)^ -(?:V:)?(\d).(\d+)-?(?:arm)?\d*\s*\*?\s*(.*)$").unwrap()
-});
-
-/// Use the `py` launcher to find installed Python versions.
+/// A Python interpreter found in the Windows registry through PEP 514.
///
-/// Calls `py --list-paths`.
-pub(crate) fn py_list_paths() -> Result, Error> {
- // konstin: The command takes 8ms on my machine.
- let output = info_span!("py_list_paths")
- .in_scope(|| Command::new("py").arg("--list-paths").output())
- .map_err(|err| {
- if err.kind() == std::io::ErrorKind::NotFound {
- Error::NotFound
- } else {
- Error::Io(err)
- }
- })?;
+/// 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) path: PathBuf,
+ pub(crate) version: Option,
+}
- // `py` sometimes prints "Installed Pythons found by py Launcher for Windows" to stderr which we ignore.
- if !output.status.success() {
- return Err(Error::StatusCode {
- message: format!(
- "Running `py --list-paths` failed with status {}",
- output.status
- ),
- exit_code: output.status,
- stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
- stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
- });
+/// Àdding `windows_registry::Value::into_string()`.
+fn value_to_string(value: Value) -> Option {
+ match value {
+ Value::String(string) => Some(string),
+ Value::Bytes(bytes) => String::from_utf8(bytes.clone()).ok(),
+ Value::U32(_) | Value::U64(_) | Value::MultiString(_) | Value::Unknown(_) => None,
+ }
+}
+
+/// Find all Pythons registered in the Windows registry following PEP 514.
+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 {
+ continue;
+ };
+ for company in key_python.keys()? {
+ // Reserved name according to the PEP.
+ if company == "PyLauncher" {
+ continue;
+ }
+ let Ok(company_key) = key_python.open(&company) else {
+ // Ignore invalid entries
+ continue;
+ };
+ for tag in company_key.keys()? {
+ let tag_key = company_key.open(&tag)?;
+
+ if let Some(registry_python) = read_registry_entry(&company, &tag, &tag_key) {
+ registry_pythons.push(registry_python);
+ }
+ }
+ }
}
- // Find the first python of the version we want in the list
- let stdout = String::from_utf8(output.stdout).map_err(|err| Error::StatusCode {
- message: format!("The stdout of `py --list-paths` isn't UTF-8 encoded: {err}"),
- exit_code: output.status,
- stdout: String::from_utf8_lossy(err.as_bytes()).trim().to_string(),
- stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
- })?;
+ // The registry has no natural ordering, so we're processing the latest version first.
+ registry_pythons.sort_by(|a, b| {
+ // Highest version first (reverse), but entries without version at the bottom (regular
+ // order).
+ if let (Some(version_a), Some(version_b)) = (&a.version, &b.version) {
+ version_a.cmp(version_b).reverse().then(a.path.cmp(&b.path))
+ } else {
+ a.version
+ .as_ref()
+ .map(|version| &***version)
+ .cmp(&b.version.as_ref().map(|version| &***version))
+ .then(a.path.cmp(&b.path))
+ }
+ });
- Ok(PY_LIST_PATHS
- .captures_iter(&stdout)
- .filter_map(|captures| {
- let (_, [major, minor, path]) = captures.extract();
- if let (Some(major), Some(minor)) = (major.parse::().ok(), minor.parse::().ok())
- {
- Some(PyListPath {
- major,
- minor,
- executable_path: PathBuf::from(path),
- })
- } else {
+ Ok(registry_pythons)
+}
+
+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")
+ .and_then(|install_path| install_path.get_value("ExecutablePath"))
+ .ok()
+ .and_then(value_to_string)
+ else {
+ debug!(
+ r"Python interpreter in the registry is not executable: `Software\Python\{}\{}",
+ company, tag
+ );
+ return None;
+ };
+
+ // `SysVersion` is optional.
+ let version = tag_key
+ .get_value("SysVersion")
+ .ok()
+ .and_then(|value| match value {
+ Value::String(s) => Some(s),
+ _ => None,
+ })
+ .and_then(|s| match PythonVersion::from_str(&s) {
+ Ok(version) => Some(version),
+ Err(err) => {
+ debug!(
+ "Skipping Python interpreter ({executable_path}) \
+ with invalid registry version {s}: {err}",
+ );
None
}
- })
- .collect())
+ });
+
+ Some(RegistryPython {
+ path: PathBuf::from(executable_path),
+ version,
+ })
}
diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs
index 53ae970ae..a4ead9181 100644
--- a/crates/uv-settings/src/settings.rs
+++ b/crates/uv-settings/src/settings.rs
@@ -591,8 +591,8 @@ pub struct PipOptions {
/// workflows.
///
/// Supported formats:
- /// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
- /// `python3.10` on Linux and macOS.
+ /// - `3.10` looks for an installed Python 3.10 in the registry on Windows (see
+ /// `py --list-paths`), or `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[option(
diff --git a/docs/concepts/python-versions.md b/docs/concepts/python-versions.md
index 8ff9cbfe1..8553795ba 100644
--- a/docs/concepts/python-versions.md
+++ b/docs/concepts/python-versions.md
@@ -169,8 +169,8 @@ When searching for a Python version, the following locations are checked:
- Managed Python installations in the `UV_PYTHON_INSTALL_DIR`.
- A Python interpreter on the `PATH` as `python`, `python3`, or `python3.x` on macOS and Linux, or
`python.exe` on Windows.
-- On Windows, the Python interpreter returned by `py --list-paths` that matches the requested
- version.
+- On Windows, the Python interpreters in the Windows registry and Microsoft Store Python
+ interpreters (see `py --list-paths`) that match the requested version.
In some cases, uv allows using a Python version from a virtual environment. In this case, the
virtual environment's interpreter will be checked for compatibility with the request before
diff --git a/docs/reference/settings.md b/docs/reference/settings.md
index 1f6dd3f66..e35330848 100644
--- a/docs/reference/settings.md
+++ b/docs/reference/settings.md
@@ -2328,8 +2328,8 @@ which is intended for use in continuous integration (CI) environments or other a
workflows.
Supported formats:
-- `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
- `python3.10` on Linux and macOS.
+- `3.10` looks for an installed Python 3.10 in the registry on Windows (see
+ `py --list-paths`), or `python3.10` on Linux and macOS.
- `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
- `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
diff --git a/uv.schema.json b/uv.schema.json
index c9efb1b5e..d983c13e4 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -887,7 +887,7 @@
]
},
"python": {
- "description": "The Python interpreter into which packages should be installed.\n\nBy default, uv installs into the virtual environment in the current working directory or any parent directory. The `--python` option allows you to specify a different interpreter, which is intended for use in continuous integration (CI) environments or other automated workflows.\n\nSupported formats: - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or `python3.10` on Linux and macOS. - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.",
+ "description": "The Python interpreter into which packages should be installed.\n\nBy default, uv installs into the virtual environment in the current working directory or any parent directory. The `--python` option allows you to specify a different interpreter, which is intended for use in continuous integration (CI) environments or other automated workflows.\n\nSupported formats: - `3.10` looks for an installed Python 3.10 in the registry on Windows (see `py --list-paths`), or `python3.10` on Linux and macOS. - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`. - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.",
"type": [
"string",
"null"