diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index a90b26d65..7d352f1e1 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2799,7 +2799,8 @@ mod tests { arch: None, os: None, libc: None, - prereleases: None + prereleases: None, + arch_mode: None, }) ); assert_eq!( @@ -2818,7 +2819,8 @@ mod tests { }), os: Some(Os(target_lexicon::OperatingSystem::Darwin(None))), libc: Some(Libc::None), - prereleases: None + prereleases: None, + arch_mode: None, }) ); assert_eq!( @@ -2834,7 +2836,8 @@ mod tests { arch: None, os: None, libc: None, - prereleases: None + prereleases: None, + arch_mode: None, }) ); assert_eq!( @@ -2853,7 +2856,8 @@ mod tests { }), os: None, libc: None, - prereleases: None + prereleases: None, + arch_mode: None, }) ); diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 57dafd8db..f2f3863bd 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -36,7 +36,7 @@ use crate::implementation::{ use crate::installation::PythonInstallationKey; use crate::libc::LibcDetectionError; use crate::managed::ManagedPythonInstallation; -use crate::platform::{self, Arch, Libc, Os}; +use crate::platform::{self, Arch, ArchMode, Libc, Os}; use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest}; #[derive(Error, Debug)] @@ -123,6 +123,8 @@ pub struct PythonDownloadRequest { /// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is /// not None, and false otherwise. pub(crate) prereleases: Option, + + pub(crate) arch_mode: Option, } impl PythonDownloadRequest { @@ -133,6 +135,7 @@ impl PythonDownloadRequest { os: Option, libc: Option, prereleases: Option, + arch_mode: Option, ) -> Self { Self { version, @@ -141,6 +144,7 @@ impl PythonDownloadRequest { os, libc, prereleases, + arch_mode, } } @@ -186,6 +190,12 @@ impl PythonDownloadRequest { self } + #[must_use] + pub fn with_arch_mode(mut self, arch_mode: ArchMode) -> Self { + self.arch_mode = Some(arch_mode); + self + } + /// Construct a new [`PythonDownloadRequest`] from a [`PythonRequest`] if possible. /// /// Returns [`None`] if the request kind is not compatible with a download, e.g., it is @@ -247,6 +257,7 @@ impl PythonDownloadRequest { Some(Os::from_env()), Some(Libc::from_env()?), None, + None, )) } @@ -291,6 +302,11 @@ impl PythonDownloadRequest { if !arch.supports(key.arch) { return false; } + if let Some(mode) = self.arch_mode { + if !mode.allows(arch, &key.arch) { + return false; + } + } } if let Some(libc) = &self.libc { @@ -412,6 +428,7 @@ impl From<&ManagedPythonInstallation> for PythonDownloadRequest { Some(key.os), Some(key.libc), Some(key.prerelease.is_some()), + None, ) } } @@ -483,7 +500,15 @@ impl FromStr for PythonDownloadRequest { _ => return Err(Error::TooManyParts(s.to_string())), } } - Ok(Self::new(version, implementation, arch, os, libc, None)) + Ok(Self::new( + version, + implementation, + arch, + os, + libc, + None, + None, + )) } } diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index 0e64a0fea..6fc84c725 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -31,6 +31,17 @@ pub enum ArchVariant { V4, } +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Default)] +pub enum ArchMode { + /// Select the most precise architecture matching the current platform, e.g., x86-64-v4 + #[default] + BestNative, + /// Select the most compatible architecture matching the current platform, e.g., x86-64-v1 + CompatibleNative, + /// Select an emulated architecture, e.g., x86-64 on aarch64 macOS. + Emulated, +} + #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] pub struct Arch { pub(crate) family: target_lexicon::Architecture, @@ -95,6 +106,37 @@ impl Os { } } +impl ArchMode { + pub fn allows(self, current: &Arch, other: &Arch) -> bool { + match self { + Self::CompatibleNative | Self::BestNative => { + // The architecture is native if the family is equal + if current.family != other.family { + return false; + } + + // There is only a compatibility nuance for x86_64 here + if current.family != target_lexicon::Architecture::X86_64 { + return true; + } + + if matches!(self, Self::CompatibleNative) { + // Only allow x86_64 without a variant + return other.variant.is_none(); + } else if matches!(self, Self::BestNative) { + // Only allow the variant matching the current architecture + return current.variant == other.variant; + } + + // We should handle all cases above + unreachable!(); + } + // The architecture is emulated if the family differs + Self::Emulated => current.family != other.family, + } + } +} + impl Arch { pub fn from_env() -> Self { Self { diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 7e190d31d..a1068a4c9 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -17,7 +17,7 @@ use uv_python::downloads::{self, DownloadResult, ManagedPythonDownload, PythonDo use uv_python::managed::{ ManagedPythonInstallation, ManagedPythonInstallations, python_executable_dir, }; -use uv_python::platform::{Arch, Libc}; +use uv_python::platform::{Arch, ArchMode, Libc}; use uv_python::{ PythonDownloads, PythonInstallationKey, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, @@ -51,8 +51,7 @@ impl InstallRequest { "`{}` is not a valid Python download request; see `uv help python` for supported formats and `uv python list --only-downloads` for available versions", request.to_canonical_string() ) - })? - .fill()?; + })?.with_arch_mode(ArchMode::BestNative).fill()?; // Find a matching download let download = diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index a7f43ccdc..5ef8cea76 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use std::fmt::Write; use uv_cli::PythonListFormat; use uv_pep440::Version; +use uv_python::platform::ArchMode; use anyhow::Result; use itertools::Either; @@ -80,7 +81,10 @@ pub(crate) async fn list( PythonListKinds::Downloads => Some(if all_platforms { base_download_request } else { - base_download_request.fill_platform()? + base_download_request + .fill_platform()? + // Only show the best, native architecture by default + .with_arch_mode(ArchMode::BestNative) }), PythonListKinds::Default => { if python_downloads.is_automatic() { @@ -89,7 +93,11 @@ pub(crate) async fn list( } else if all_arches { base_download_request.fill_platform()?.with_any_arch() } else { - base_download_request.fill_platform()? + base_download_request + .fill_platform()? + // Only show the best, native architecture by default + // TODO(zanieb): We should expose this option to the user + .with_arch_mode(ArchMode::BestNative) }) } else { // If fetching is not automatic, then don't show downloads as available by default