From 01c761fa52fa3c88dd74aa94be238a2e6ee929f9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 9 Jun 2025 17:43:36 -0500 Subject: [PATCH 1/3] Filter managed Python distributions by platform before querying when included in request --- crates/uv-python/src/discovery.rs | 62 +++++++++++++++++++++++-------- crates/uv-python/src/downloads.rs | 48 ++++++++++++++++++++++++ crates/uv-python/src/lib.rs | 1 + 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 274cb51d4f1dd..7efeec9b5f2ca 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -20,7 +20,7 @@ use uv_pep440::{ use uv_static::EnvVars; use uv_warnings::warn_user_once; -use crate::downloads::PythonDownloadRequest; +use crate::downloads::{PlatformRequest, PythonDownloadRequest}; use crate::implementation::ImplementationName; use crate::installation::PythonInstallation; use crate::interpreter::Error as InterpreterError; @@ -312,6 +312,7 @@ fn python_executables_from_virtual_environments<'a>() fn python_executables_from_installed<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform_filter: Option, preference: PythonPreference, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { @@ -323,16 +324,21 @@ fn python_executables_from_installed<'a>( installed_installations.root().user_display() ); let installations = installed_installations.find_matching_current_platform()?; - // Check that the Python version satisfies the request to avoid unnecessary interpreter queries later + // Check that the Python version and platform satisfy the request to avoid unnecessary interpreter queries later Ok(installations .into_iter() .filter(move |installation| { - if version.matches_version(&installation.version()) { - true - } else { - debug!("Skipping incompatible managed installation `{installation}`"); - false + if !version.matches_version(&installation.version()) { + debug!("Skipping incompatible managed installation `{installation}`: version mismatch"); + return false; + } + if let Some(ref platform_req) = platform_filter { + if !platform_req.could_be_satisfied_by_key(installation.key()) { + debug!("Skipping incompatible managed installation `{installation}`: platform mismatch"); + return false; + } } + true }) .inspect(|installation| debug!("Found managed installation `{installation}`")) .map(|installation| (PythonSource::Managed, installation.executable(false)))) @@ -424,6 +430,7 @@ fn python_executables_from_installed<'a>( fn python_executables<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform_filter: Option, environments: EnvironmentPreference, preference: PythonPreference, ) -> Box> + 'a> { @@ -445,7 +452,8 @@ fn python_executables<'a>( .flatten(); let from_virtual_environments = python_executables_from_virtual_environments(); - let from_installed = python_executables_from_installed(version, implementation, preference); + let from_installed = + python_executables_from_installed(version, implementation, platform_filter, preference); // Limit the search to the relevant environment preference; this avoids unnecessary work like // traversal of the file system. Subsequent filtering should be done by the caller with @@ -633,9 +641,14 @@ fn find_all_minor( /// Note interpreters may be excluded by the given [`EnvironmentPreference`] and [`PythonPreference`]. /// /// See [`python_executables`] for more information on discovery. +/// Lazily iterate over all discoverable Python interpreters with optional platform filtering. +/// +/// This version supports early platform filtering for managed installations to avoid +/// unnecessary interpreter queries when the platform clearly doesn't match. fn python_interpreters<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform_filter: Option, environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, @@ -644,11 +657,16 @@ fn python_interpreters<'a>( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. - python_executables(version, implementation, environments, preference).filter_ok( - move |(source, path)| { - source_satisfies_environment_preference(*source, path, environments) - }, - ), + python_executables( + version, + implementation, + platform_filter, + environments, + preference, + ) + .filter_ok(move |(source, path)| { + source_satisfies_environment_preference(*source, path, environments) + }), cache, ) .filter_ok(move |(source, interpreter)| { @@ -971,14 +989,22 @@ pub fn find_python_installations<'a>( } PythonRequest::Any => Box::new({ debug!("Searching for any Python interpreter in {sources}"); - python_interpreters(&VersionRequest::Any, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + &VersionRequest::Any, + None, + None, + environments, + preference, + cache, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }), PythonRequest::Default => Box::new({ debug!("Searching for default Python interpreter in {sources}"); python_interpreters( &VersionRequest::Default, None, + None, environments, preference, cache, @@ -991,7 +1017,7 @@ pub fn find_python_installations<'a>( } Box::new({ debug!("Searching for {request} in {sources}"); - python_interpreters(version, None, environments, preference, cache) + python_interpreters(version, None, None, environments, preference, cache) .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }) } @@ -1000,6 +1026,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Default, Some(implementation), + None, environments, preference, cache, @@ -1020,6 +1047,7 @@ pub fn find_python_installations<'a>( python_interpreters( version, Some(implementation), + None, environments, preference, cache, @@ -1038,11 +1066,13 @@ pub fn find_python_installations<'a>( return Box::new(iter::once(Err(Error::InvalidVersionRequest(err)))); } } + let platform_filter = request.platform_request(); Box::new({ debug!("Searching for {request} in {sources}"); python_interpreters( request.version().unwrap_or(&VersionRequest::Default), request.implementation(), + platform_filter, environments, preference, cache, diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 9b7fc28256af6..084576cdd8574 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -131,6 +131,41 @@ pub enum ArchRequest { Environment(Arch), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PlatformRequest { + pub(crate) os: Option, + pub(crate) arch: Option, + pub(crate) libc: Option, +} + +impl PlatformRequest { + /// Check if this platform request could potentially be satisfied by an installation key. + /// + /// This is a lightweight check that can be done before querying the interpreter + /// to avoid expensive subprocess calls when the platform clearly doesn't match. + pub fn could_be_satisfied_by_key(&self, key: &PythonInstallationKey) -> bool { + if let Some(os) = self.os { + if key.os != os { + return false; + } + } + + if let Some(arch) = self.arch { + if !arch.satisfied_by(key.arch) { + return false; + } + } + + if let Some(libc) = self.libc { + if key.libc != libc { + return false; + } + } + + true + } +} + impl Display for ArchRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -412,6 +447,19 @@ impl PythonDownloadRequest { } true } + + /// Extract the platform components as a `PlatformRequest` for early filtering. + pub fn platform_request(&self) -> Option { + if self.os.is_some() || self.arch.is_some() || self.libc.is_some() { + Some(PlatformRequest { + os: self.os, + arch: self.arch, + libc: self.libc, + }) + } else { + None + } + } } impl From<&ManagedPythonInstallation> for PythonDownloadRequest { diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 024cd5cbc35e9..0fa2d46ab8d0a 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -9,6 +9,7 @@ pub use crate::discovery::{ PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest, find_python_installations, }; +pub use crate::downloads::PlatformRequest; pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment}; pub use crate::implementation::ImplementationName; pub use crate::installation::{PythonInstallation, PythonInstallationKey}; From 2a45f0cc5650570e36d28b2698477661008612e5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 11 Jun 2025 09:09:20 -0500 Subject: [PATCH 2/3] Review --- crates/uv-python/src/discovery.rs | 72 +++++++++++++++---------------- crates/uv-python/src/downloads.rs | 41 +++++++++++------- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 7efeec9b5f2ca..630ec3a1ad924 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -312,7 +312,7 @@ fn python_executables_from_virtual_environments<'a>() fn python_executables_from_installed<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, - platform_filter: Option, + platform: PlatformRequest, preference: PythonPreference, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { @@ -329,14 +329,12 @@ fn python_executables_from_installed<'a>( .into_iter() .filter(move |installation| { if !version.matches_version(&installation.version()) { - debug!("Skipping incompatible managed installation `{installation}`: version mismatch"); + debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`"); return false; } - if let Some(ref platform_req) = platform_filter { - if !platform_req.could_be_satisfied_by_key(installation.key()) { - debug!("Skipping incompatible managed installation `{installation}`: platform mismatch"); - return false; - } + if !platform.matches(installation.key()) { + debug!("Skipping managed installation `{installation}`: does not satisfy `{platform}`"); + return false; } true }) @@ -421,16 +419,17 @@ fn python_executables_from_installed<'a>( /// Lazily iterate over all discoverable Python executables. /// -/// Note that Python executables may be excluded by the given [`EnvironmentPreference`] and -/// [`PythonPreference`]. However, these filters are only applied for performance. We cannot -/// guarantee that the [`EnvironmentPreference`] is satisfied until we query the interpreter. +/// Note that Python executables may be excluded by the given [`EnvironmentPreference`], +/// [`PythonPreference`], and [`PlatformRequest`]. However, these filters are only applied for +/// performance. We cannot guarantee that the all requests or preferences are satisfied until we +/// query the interpreter. /// /// See [`python_executables_from_installed`] and [`python_executables_from_virtual_environments`] /// for more information on discovery. fn python_executables<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, - platform_filter: Option, + platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, ) -> Box> + 'a> { @@ -453,7 +452,7 @@ fn python_executables<'a>( let from_virtual_environments = python_executables_from_virtual_environments(); let from_installed = - python_executables_from_installed(version, implementation, platform_filter, preference); + python_executables_from_installed(version, implementation, platform, preference); // Limit the search to the relevant environment preference; this avoids unnecessary work like // traversal of the file system. Subsequent filtering should be done by the caller with @@ -638,17 +637,17 @@ fn find_all_minor( /// Lazily iterate over all discoverable Python interpreters. /// -/// Note interpreters may be excluded by the given [`EnvironmentPreference`] and [`PythonPreference`]. +/// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`], +/// [`VersionRequest`], or [`PlatformRequest`]. /// -/// See [`python_executables`] for more information on discovery. -/// Lazily iterate over all discoverable Python interpreters with optional platform filtering. +/// The [`PlatformRequest`] is currently on applied to managed Python installations before querying +/// the interpreter. The caller is responsible for ensuring it is applied otherwise. /// -/// This version supports early platform filtering for managed installations to avoid -/// unnecessary interpreter queries when the platform clearly doesn't match. +/// See [`python_executables`] for more information on discovery. fn python_interpreters<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, - platform_filter: Option, + platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, @@ -657,16 +656,11 @@ fn python_interpreters<'a>( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. - python_executables( - version, - implementation, - platform_filter, - environments, - preference, - ) - .filter_ok(move |(source, path)| { - source_satisfies_environment_preference(*source, path, environments) - }), + python_executables(version, implementation, platform, environments, preference).filter_ok( + move |(source, path)| { + source_satisfies_environment_preference(*source, path, environments) + }, + ), cache, ) .filter_ok(move |(source, interpreter)| { @@ -992,7 +986,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Any, None, - None, + PlatformRequest::default(), environments, preference, cache, @@ -1004,7 +998,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Default, None, - None, + PlatformRequest::default(), environments, preference, cache, @@ -1017,8 +1011,15 @@ pub fn find_python_installations<'a>( } Box::new({ debug!("Searching for {request} in {sources}"); - python_interpreters(version, None, None, environments, preference, cache) - .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) + python_interpreters( + version, + None, + PlatformRequest::default(), + environments, + preference, + cache, + ) + .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))) }) } PythonRequest::Implementation(implementation) => Box::new({ @@ -1026,7 +1027,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Default, Some(implementation), - None, + PlatformRequest::default(), environments, preference, cache, @@ -1047,7 +1048,7 @@ pub fn find_python_installations<'a>( python_interpreters( version, Some(implementation), - None, + PlatformRequest::default(), environments, preference, cache, @@ -1066,13 +1067,12 @@ pub fn find_python_installations<'a>( return Box::new(iter::once(Err(Error::InvalidVersionRequest(err)))); } } - let platform_filter = request.platform_request(); Box::new({ debug!("Searching for {request} in {sources}"); python_interpreters( request.version().unwrap_or(&VersionRequest::Default), request.implementation(), - platform_filter, + request.platform(), environments, preference, cache, diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 084576cdd8574..0b751796049ea 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -131,7 +131,7 @@ pub enum ArchRequest { Environment(Arch), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct PlatformRequest { pub(crate) os: Option, pub(crate) arch: Option, @@ -139,11 +139,8 @@ pub struct PlatformRequest { } impl PlatformRequest { - /// Check if this platform request could potentially be satisfied by an installation key. - /// - /// This is a lightweight check that can be done before querying the interpreter - /// to avoid expensive subprocess calls when the platform clearly doesn't match. - pub fn could_be_satisfied_by_key(&self, key: &PythonInstallationKey) -> bool { + /// Check if this platform request is satisfied by an installation key. + pub fn matches(&self, key: &PythonInstallationKey) -> bool { if let Some(os) = self.os { if key.os != os { return false; @@ -166,6 +163,22 @@ impl PlatformRequest { } } +impl Display for PlatformRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut parts = Vec::new(); + if let Some(os) = &self.os { + parts.push(os.to_string()); + } + if let Some(arch) = &self.arch { + parts.push(arch.to_string()); + } + if let Some(libc) = &self.libc { + parts.push(libc.to_string()); + } + write!(f, "{}", parts.join("-")) + } +} + impl Display for ArchRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -448,16 +461,12 @@ impl PythonDownloadRequest { true } - /// Extract the platform components as a `PlatformRequest` for early filtering. - pub fn platform_request(&self) -> Option { - if self.os.is_some() || self.arch.is_some() || self.libc.is_some() { - Some(PlatformRequest { - os: self.os, - arch: self.arch, - libc: self.libc, - }) - } else { - None + /// Extract the platform components of this request. + pub fn platform(&self) -> PlatformRequest { + PlatformRequest { + os: self.os, + arch: self.arch, + libc: self.libc, } } } From 7b4ff785ae7c13aa52862f9b1cba92f004960b23 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 12 Jun 2025 13:12:39 -0500 Subject: [PATCH 3/3] Update crates/uv-python/src/discovery.rs Co-authored-by: Aria Desires --- crates/uv-python/src/discovery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 630ec3a1ad924..0be8d17d1bf25 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -640,7 +640,7 @@ fn find_all_minor( /// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`], /// [`VersionRequest`], or [`PlatformRequest`]. /// -/// The [`PlatformRequest`] is currently on applied to managed Python installations before querying +/// The [`PlatformRequest`] is currently only applied to managed Python installations before querying /// the interpreter. The caller is responsible for ensuring it is applied otherwise. /// /// See [`python_executables`] for more information on discovery.