Differentiate between implicit vs explicit architecture requests (#13723)

In https://github.com/astral-sh/uv/pull/13721#issuecomment-2920530601 I
presumed that all the installation problems in
https://github.com/astral-sh/uv/pull/13722 were solved by
https://github.com/astral-sh/uv/pull/13709 but they were not because we
don't differentiate between implicit and explicit architecture requests
so a request for `aarch64` is considered satisfied by an existing
`x86-64` installation even if the user explicitly requested that
architecture.

Now, we track if it was explicit or implicit, requiring an exact match
in the former case, and a `supports` in the latter.

We considered doing this for other items in the request, like the
operating system but we don't have a `supports()` concept there. It
might make sense for libc in the future.
This commit is contained in:
Zanie Blue 2025-05-29 17:04:57 -05:00 committed by GitHub
parent 067b03cc9a
commit 0c5ae1f25c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 48 additions and 30 deletions

View file

@ -2723,7 +2723,7 @@ mod tests {
use crate::{
discovery::{PythonRequest, VersionRequest},
downloads::PythonDownloadRequest,
downloads::{ArchRequest, PythonDownloadRequest},
implementation::ImplementationName,
platform::{Arch, Libc, Os},
};
@ -2813,10 +2813,10 @@ mod tests {
PythonVariant::Default
)),
implementation: Some(ImplementationName::CPython),
arch: Some(Arch {
arch: Some(ArchRequest::Explicit(Arch {
family: Architecture::Aarch64(Aarch64Architecture::Aarch64),
variant: None
}),
})),
os: Some(Os(target_lexicon::OperatingSystem::Darwin(None))),
libc: Some(Libc::None),
prereleases: None
@ -2848,10 +2848,10 @@ mod tests {
PythonVariant::Default
)),
implementation: None,
arch: Some(Arch {
arch: Some(ArchRequest::Explicit(Arch {
family: Architecture::Aarch64(Aarch64Architecture::Aarch64),
variant: None
}),
})),
os: None,
libc: None,
prereleases: None

View file

@ -116,7 +116,7 @@ pub struct ManagedPythonDownload {
pub struct PythonDownloadRequest {
pub(crate) version: Option<VersionRequest>,
pub(crate) implementation: Option<ImplementationName>,
pub(crate) arch: Option<Arch>,
pub(crate) arch: Option<ArchRequest>,
pub(crate) os: Option<Os>,
pub(crate) libc: Option<Libc>,
@ -125,11 +125,40 @@ pub struct PythonDownloadRequest {
pub(crate) prereleases: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchRequest {
Explicit(Arch),
Environment(Arch),
}
impl Display for ArchRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Explicit(arch) | Self::Environment(arch) => write!(f, "{arch}"),
}
}
}
impl ArchRequest {
pub(crate) fn satisfied_by(self, arch: Arch) -> bool {
match self {
Self::Explicit(request) => request == arch,
Self::Environment(env) => env.supports(arch),
}
}
pub fn inner(&self) -> Arch {
match self {
Self::Explicit(arch) | Self::Environment(arch) => *arch,
}
}
}
impl PythonDownloadRequest {
pub fn new(
version: Option<VersionRequest>,
implementation: Option<ImplementationName>,
arch: Option<Arch>,
arch: Option<ArchRequest>,
os: Option<Os>,
libc: Option<Libc>,
prereleases: Option<bool>,
@ -158,7 +187,7 @@ impl PythonDownloadRequest {
#[must_use]
pub fn with_arch(mut self, arch: Arch) -> Self {
self.arch = Some(arch);
self.arch = Some(ArchRequest::Explicit(arch));
self
}
@ -219,7 +248,7 @@ impl PythonDownloadRequest {
/// Platform information is pulled from the environment.
pub fn fill_platform(mut self) -> Result<Self, Error> {
if self.arch.is_none() {
self.arch = Some(Arch::from_env());
self.arch = Some(ArchRequest::Environment(Arch::from_env()));
}
if self.os.is_none() {
self.os = Some(Os::from_env());
@ -238,18 +267,6 @@ impl PythonDownloadRequest {
Ok(self)
}
/// Construct a new [`PythonDownloadRequest`] with platform information from the environment.
pub fn from_env() -> Result<Self, Error> {
Ok(Self::new(
None,
None,
Some(Arch::from_env()),
Some(Os::from_env()),
Some(Libc::from_env()?),
None,
))
}
pub fn implementation(&self) -> Option<&ImplementationName> {
self.implementation.as_ref()
}
@ -258,7 +275,7 @@ impl PythonDownloadRequest {
self.version.as_ref()
}
pub fn arch(&self) -> Option<&Arch> {
pub fn arch(&self) -> Option<&ArchRequest> {
self.arch.as_ref()
}
@ -288,7 +305,7 @@ impl PythonDownloadRequest {
}
if let Some(arch) = &self.arch {
if !arch.supports(key.arch) {
if !arch.satisfied_by(key.arch) {
return false;
}
}
@ -366,7 +383,7 @@ impl PythonDownloadRequest {
}
if let Some(arch) = self.arch() {
let interpreter_arch = Arch::from(&interpreter.platform().arch());
if &interpreter_arch != arch {
if !arch.satisfied_by(interpreter_arch) {
debug!(
"Skipping interpreter at `{executable}`: architecture `{interpreter_arch}` does not match request `{arch}`"
);
@ -408,7 +425,7 @@ impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
"Managed Python installations are expected to always have known implementation names, found {name}"
),
},
Some(key.arch),
Some(ArchRequest::Explicit(key.arch)),
Some(key.os),
Some(key.libc),
Some(key.prerelease.is_some()),
@ -478,7 +495,7 @@ impl FromStr for PythonDownloadRequest {
);
}
3 => os = Some(Os::from_str(part)?),
4 => arch = Some(Arch::from_str(part)?),
4 => arch = Some(ArchRequest::Explicit(Arch::from_str(part)?)),
5 => libc = Some(Libc::from_str(part)?),
_ => return Err(Error::TooManyParts(s.to_string())),
}