diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 274cb51d4..0be8d17d1 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: PlatformRequest, preference: PythonPreference, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { @@ -323,16 +324,19 @@ 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 managed installation `{installation}`: does not satisfy `{version}`"); + return false; } + if !platform.matches(installation.key()) { + debug!("Skipping managed installation `{installation}`: does not satisfy `{platform}`"); + return false; + } + true }) .inspect(|installation| debug!("Found managed installation `{installation}`")) .map(|installation| (PythonSource::Managed, installation.executable(false)))) @@ -415,15 +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: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, ) -> Box> + 'a> { @@ -445,7 +451,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, 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 @@ -630,12 +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`]. +/// +/// 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. fn python_interpreters<'a>( version: &'a VersionRequest, implementation: Option<&'a ImplementationName>, + platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, @@ -644,7 +656,7 @@ 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( + python_executables(version, implementation, platform, environments, preference).filter_ok( move |(source, path)| { source_satisfies_environment_preference(*source, path, environments) }, @@ -971,14 +983,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, + PlatformRequest::default(), + 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, + PlatformRequest::default(), environments, preference, cache, @@ -991,8 +1011,15 @@ pub fn find_python_installations<'a>( } Box::new({ debug!("Searching for {request} in {sources}"); - python_interpreters(version, 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({ @@ -1000,6 +1027,7 @@ pub fn find_python_installations<'a>( python_interpreters( &VersionRequest::Default, Some(implementation), + PlatformRequest::default(), environments, preference, cache, @@ -1020,6 +1048,7 @@ pub fn find_python_installations<'a>( python_interpreters( version, Some(implementation), + PlatformRequest::default(), environments, preference, cache, @@ -1043,6 +1072,7 @@ pub fn find_python_installations<'a>( python_interpreters( request.version().unwrap_or(&VersionRequest::Default), request.implementation(), + request.platform(), environments, preference, cache, diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 9b7fc2825..0b7517960 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -131,6 +131,54 @@ pub enum ArchRequest { Environment(Arch), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformRequest { + pub(crate) os: Option, + pub(crate) arch: Option, + pub(crate) libc: Option, +} + +impl PlatformRequest { + /// 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; + } + } + + 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 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 { @@ -412,6 +460,15 @@ impl PythonDownloadRequest { } true } + + /// Extract the platform components of this request. + pub fn platform(&self) -> PlatformRequest { + PlatformRequest { + os: self.os, + arch: self.arch, + libc: self.libc, + } + } } impl From<&ManagedPythonInstallation> for PythonDownloadRequest { diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 024cd5cbc..0fa2d46ab 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};