Filter managed Python distributions by platform before querying when included in request (#13936)

In the case where we have platform information in a Python request, we
should filter managed Python distributions by it prior to querying them.

Closes https://github.com/astral-sh/uv/issues/13935

---------

Co-authored-by: Aria Desires <aria.desires@gmail.com>
This commit is contained in:
Zanie Blue 2025-06-13 08:50:44 -05:00 committed by GitHub
parent b95e66019d
commit 881e17600f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 17 deletions

View file

@ -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<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + '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<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + '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,

View file

@ -131,6 +131,54 @@ pub enum ArchRequest {
Environment(Arch),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PlatformRequest {
pub(crate) os: Option<Os>,
pub(crate) arch: Option<ArchRequest>,
pub(crate) libc: Option<Libc>,
}
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 {

View file

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