mirror of https://github.com/astral-sh/uv
798 lines
27 KiB
Rust
798 lines
27 KiB
Rust
use std::borrow::Cow;
|
|
use std::fmt;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::str::FromStr;
|
|
|
|
use indexmap::IndexMap;
|
|
use ref_cast::RefCast;
|
|
use tracing::{debug, info};
|
|
|
|
use uv_cache::Cache;
|
|
use uv_client::BaseClientBuilder;
|
|
use uv_pep440::{Prerelease, Version};
|
|
use uv_platform::{Arch, Libc, Os, Platform};
|
|
use uv_preview::Preview;
|
|
|
|
use crate::discovery::{
|
|
EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
|
|
};
|
|
use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest, Reporter};
|
|
use crate::implementation::LenientImplementationName;
|
|
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
|
|
use crate::{
|
|
Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference, PythonSource,
|
|
PythonVariant, PythonVersion, downloads,
|
|
};
|
|
|
|
/// A Python interpreter and accompanying tools.
|
|
#[derive(Clone, Debug)]
|
|
pub struct PythonInstallation {
|
|
// Public in the crate for test assertions
|
|
pub(crate) source: PythonSource,
|
|
pub(crate) interpreter: Interpreter,
|
|
}
|
|
|
|
impl PythonInstallation {
|
|
/// Create a new [`PythonInstallation`] from a source, interpreter tuple.
|
|
pub(crate) fn from_tuple(tuple: (PythonSource, Interpreter)) -> Self {
|
|
let (source, interpreter) = tuple;
|
|
Self {
|
|
source,
|
|
interpreter,
|
|
}
|
|
}
|
|
|
|
/// Find an installed [`PythonInstallation`].
|
|
///
|
|
/// This is the standard interface for discovering a Python installation for creating
|
|
/// an environment. If interested in finding an existing environment, see
|
|
/// [`PythonEnvironment::find`] instead.
|
|
///
|
|
/// Note we still require an [`EnvironmentPreference`] as this can either bypass virtual environments
|
|
/// or prefer them. In most cases, this should be [`EnvironmentPreference::OnlySystem`]
|
|
/// but if you want to allow an interpreter from a virtual environment if it satisfies the request,
|
|
/// then use [`EnvironmentPreference::Any`].
|
|
///
|
|
/// See [`find_installation`] for implementation details.
|
|
pub fn find(
|
|
request: &PythonRequest,
|
|
environments: EnvironmentPreference,
|
|
preference: PythonPreference,
|
|
cache: &Cache,
|
|
preview: Preview,
|
|
) -> Result<Self, Error> {
|
|
let installation =
|
|
find_python_installation(request, environments, preference, cache, preview)??;
|
|
Ok(installation)
|
|
}
|
|
|
|
/// Find an installed [`PythonInstallation`] that satisfies a requested version, if the request cannot
|
|
/// be satisfied, fallback to the best available Python installation.
|
|
pub fn find_best(
|
|
request: &PythonRequest,
|
|
environments: EnvironmentPreference,
|
|
preference: PythonPreference,
|
|
cache: &Cache,
|
|
preview: Preview,
|
|
) -> Result<Self, Error> {
|
|
Ok(find_best_python_installation(
|
|
request,
|
|
environments,
|
|
preference,
|
|
cache,
|
|
preview,
|
|
)??)
|
|
}
|
|
|
|
/// Find or fetch a [`PythonInstallation`].
|
|
///
|
|
/// Unlike [`PythonInstallation::find`], if the required Python is not installed it will be installed automatically.
|
|
pub async fn find_or_download(
|
|
request: Option<&PythonRequest>,
|
|
environments: EnvironmentPreference,
|
|
preference: PythonPreference,
|
|
python_downloads: PythonDownloads,
|
|
client_builder: &BaseClientBuilder<'_>,
|
|
cache: &Cache,
|
|
reporter: Option<&dyn Reporter>,
|
|
python_install_mirror: Option<&str>,
|
|
pypy_install_mirror: Option<&str>,
|
|
python_downloads_json_url: Option<&str>,
|
|
preview: Preview,
|
|
) -> Result<Self, Error> {
|
|
let request = request.unwrap_or(&PythonRequest::Default);
|
|
|
|
// Search for the installation
|
|
let err = match Self::find(request, environments, preference, cache, preview) {
|
|
Ok(installation) => return Ok(installation),
|
|
Err(err) => err,
|
|
};
|
|
|
|
match err {
|
|
// If Python is missing, we should attempt a download
|
|
Error::MissingPython(..) => {}
|
|
// If we raised a non-critical error, we should attempt a download
|
|
Error::Discovery(ref err) if !err.is_critical() => {}
|
|
// Otherwise, this is fatal
|
|
_ => return Err(err),
|
|
}
|
|
|
|
// If we can't convert the request to a download, throw the original error
|
|
let Some(download_request) = PythonDownloadRequest::from_request(request) else {
|
|
return Err(err);
|
|
};
|
|
|
|
let downloads_enabled = preference.allows_managed()
|
|
&& python_downloads.is_automatic()
|
|
&& client_builder.connectivity.is_online();
|
|
|
|
let download = download_request.clone().fill().map(|request| {
|
|
ManagedPythonDownload::from_request(&request, python_downloads_json_url)
|
|
});
|
|
|
|
// Regardless of whether downloads are enabled, we want to determine if the download is
|
|
// available to power error messages. However, if downloads aren't enabled, we don't want to
|
|
// report any errors related to them.
|
|
let download = match download {
|
|
Ok(Ok(download)) => Some(download),
|
|
// If the download cannot be found, return the _original_ discovery error
|
|
Ok(Err(downloads::Error::NoDownloadFound(_))) => {
|
|
if downloads_enabled {
|
|
debug!("No downloads are available for {request}");
|
|
return Err(err);
|
|
}
|
|
None
|
|
}
|
|
Err(err) | Ok(Err(err)) => {
|
|
if downloads_enabled {
|
|
// We failed to determine the platform information
|
|
return Err(err.into());
|
|
}
|
|
None
|
|
}
|
|
};
|
|
|
|
let Some(download) = download else {
|
|
// N.B. We should only be in this case when downloads are disabled; when downloads are
|
|
// enabled, we should fail eagerly when something goes wrong with the download.
|
|
debug_assert!(!downloads_enabled);
|
|
return Err(err);
|
|
};
|
|
|
|
// If the download is available, but not usable, we attach a hint to the original error.
|
|
if !downloads_enabled {
|
|
let for_request = match request {
|
|
PythonRequest::Default | PythonRequest::Any => String::new(),
|
|
_ => format!(" for {request}"),
|
|
};
|
|
|
|
match python_downloads {
|
|
PythonDownloads::Automatic => {}
|
|
PythonDownloads::Manual => {
|
|
return Err(err.with_missing_python_hint(format!(
|
|
"A managed Python download is available{for_request}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
|
|
request.to_canonical_string(),
|
|
)));
|
|
}
|
|
PythonDownloads::Never => {
|
|
return Err(err.with_missing_python_hint(format!(
|
|
"A managed Python download is available{for_request}, but Python downloads are set to 'never'"
|
|
)));
|
|
}
|
|
}
|
|
|
|
match preference {
|
|
PythonPreference::OnlySystem => {
|
|
return Err(err.with_missing_python_hint(format!(
|
|
"A managed Python download is available{for_request}, but the Python preference is set to 'only system'"
|
|
)));
|
|
}
|
|
PythonPreference::Managed
|
|
| PythonPreference::OnlyManaged
|
|
| PythonPreference::System => {}
|
|
}
|
|
|
|
if !client_builder.connectivity.is_online() {
|
|
return Err(err.with_missing_python_hint(format!(
|
|
"A managed Python download is available{for_request}, but uv is set to offline mode"
|
|
)));
|
|
}
|
|
|
|
return Err(err);
|
|
}
|
|
|
|
Self::fetch(
|
|
download,
|
|
client_builder,
|
|
cache,
|
|
reporter,
|
|
python_install_mirror,
|
|
pypy_install_mirror,
|
|
preview,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Download and install the requested installation.
|
|
pub async fn fetch(
|
|
download: &'static ManagedPythonDownload,
|
|
client_builder: &BaseClientBuilder<'_>,
|
|
cache: &Cache,
|
|
reporter: Option<&dyn Reporter>,
|
|
python_install_mirror: Option<&str>,
|
|
pypy_install_mirror: Option<&str>,
|
|
preview: Preview,
|
|
) -> Result<Self, Error> {
|
|
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
|
|
let installations_dir = installations.root();
|
|
let scratch_dir = installations.scratch();
|
|
let _lock = installations.lock().await?;
|
|
|
|
// Python downloads are performing their own retries to catch stream errors, disable the
|
|
// default retries to avoid the middleware from performing uncontrolled retries.
|
|
let retry_policy = client_builder.retry_policy();
|
|
let client = client_builder.clone().retries(0).build();
|
|
|
|
info!("Fetching requested Python...");
|
|
let result = download
|
|
.fetch_with_retry(
|
|
&client,
|
|
&retry_policy,
|
|
installations_dir,
|
|
&scratch_dir,
|
|
false,
|
|
python_install_mirror,
|
|
pypy_install_mirror,
|
|
reporter,
|
|
)
|
|
.await?;
|
|
|
|
let path = match result {
|
|
DownloadResult::AlreadyAvailable(path) => path,
|
|
DownloadResult::Fetched(path) => path,
|
|
};
|
|
|
|
let installed = ManagedPythonInstallation::new(path, download);
|
|
installed.ensure_externally_managed()?;
|
|
installed.ensure_sysconfig_patched()?;
|
|
installed.ensure_canonical_executables()?;
|
|
installed.ensure_build_file()?;
|
|
|
|
let minor_version = installed.minor_version_key();
|
|
let highest_patch = installations
|
|
.find_all()?
|
|
.filter(|installation| installation.minor_version_key() == minor_version)
|
|
.filter_map(|installation| installation.version().patch())
|
|
.fold(0, std::cmp::max);
|
|
if installed
|
|
.version()
|
|
.patch()
|
|
.is_some_and(|p| p >= highest_patch)
|
|
{
|
|
installed.ensure_minor_version_link(preview)?;
|
|
}
|
|
|
|
if let Err(e) = installed.ensure_dylib_patched() {
|
|
e.warn_user(&installed);
|
|
}
|
|
|
|
Ok(Self {
|
|
source: PythonSource::Managed,
|
|
interpreter: Interpreter::query(installed.executable(false), cache)?,
|
|
})
|
|
}
|
|
|
|
/// Create a [`PythonInstallation`] from an existing [`Interpreter`].
|
|
pub fn from_interpreter(interpreter: Interpreter) -> Self {
|
|
Self {
|
|
source: PythonSource::ProvidedPath,
|
|
interpreter,
|
|
}
|
|
}
|
|
|
|
/// Return the [`PythonSource`] of the Python installation, indicating where it was found.
|
|
pub fn source(&self) -> &PythonSource {
|
|
&self.source
|
|
}
|
|
|
|
pub fn key(&self) -> PythonInstallationKey {
|
|
self.interpreter.key()
|
|
}
|
|
|
|
/// Return the Python [`Version`] of the Python installation as reported by its interpreter.
|
|
pub fn python_version(&self) -> &Version {
|
|
self.interpreter.python_version()
|
|
}
|
|
|
|
/// Return the [`LenientImplementationName`] of the Python installation as reported by its interpreter.
|
|
pub fn implementation(&self) -> LenientImplementationName {
|
|
LenientImplementationName::from(self.interpreter.implementation_name())
|
|
}
|
|
|
|
/// Whether this is a CPython installation.
|
|
///
|
|
/// Returns false if it is an alternative implementation, e.g., PyPy.
|
|
pub(crate) fn is_alternative_implementation(&self) -> bool {
|
|
!matches!(
|
|
self.implementation(),
|
|
LenientImplementationName::Known(ImplementationName::CPython)
|
|
) || self.os().is_emscripten()
|
|
}
|
|
|
|
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
|
|
pub fn arch(&self) -> Arch {
|
|
self.interpreter.arch()
|
|
}
|
|
|
|
/// Return the [`Libc`] of the Python installation as reported by its interpreter.
|
|
pub fn libc(&self) -> Libc {
|
|
self.interpreter.libc()
|
|
}
|
|
|
|
/// Return the [`Os`] of the Python installation as reported by its interpreter.
|
|
pub fn os(&self) -> Os {
|
|
self.interpreter.os()
|
|
}
|
|
|
|
/// Return the [`Interpreter`] for the Python installation.
|
|
pub fn interpreter(&self) -> &Interpreter {
|
|
&self.interpreter
|
|
}
|
|
|
|
/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
|
|
pub fn into_interpreter(self) -> Interpreter {
|
|
self.interpreter
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum PythonInstallationKeyError {
|
|
#[error("Failed to parse Python installation key `{0}`: {1}")]
|
|
ParseError(String, String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct PythonInstallationKey {
|
|
pub(crate) implementation: LenientImplementationName,
|
|
pub(crate) major: u8,
|
|
pub(crate) minor: u8,
|
|
pub(crate) patch: u8,
|
|
pub(crate) prerelease: Option<Prerelease>,
|
|
pub(crate) platform: Platform,
|
|
pub(crate) variant: PythonVariant,
|
|
}
|
|
|
|
impl PythonInstallationKey {
|
|
pub fn new(
|
|
implementation: LenientImplementationName,
|
|
major: u8,
|
|
minor: u8,
|
|
patch: u8,
|
|
prerelease: Option<Prerelease>,
|
|
platform: Platform,
|
|
variant: PythonVariant,
|
|
) -> Self {
|
|
Self {
|
|
implementation,
|
|
major,
|
|
minor,
|
|
patch,
|
|
prerelease,
|
|
platform,
|
|
variant,
|
|
}
|
|
}
|
|
|
|
pub fn new_from_version(
|
|
implementation: LenientImplementationName,
|
|
version: &PythonVersion,
|
|
platform: Platform,
|
|
variant: PythonVariant,
|
|
) -> Self {
|
|
Self {
|
|
implementation,
|
|
major: version.major(),
|
|
minor: version.minor(),
|
|
patch: version.patch().unwrap_or_default(),
|
|
prerelease: version.pre(),
|
|
platform,
|
|
variant,
|
|
}
|
|
}
|
|
|
|
pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
|
|
if self.os().is_emscripten() {
|
|
Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
|
|
} else {
|
|
Cow::Borrowed(&self.implementation)
|
|
}
|
|
}
|
|
|
|
pub fn version(&self) -> PythonVersion {
|
|
PythonVersion::from_str(&format!(
|
|
"{}.{}.{}{}",
|
|
self.major,
|
|
self.minor,
|
|
self.patch,
|
|
self.prerelease
|
|
.map(|pre| pre.to_string())
|
|
.unwrap_or_default()
|
|
))
|
|
.expect("Python installation keys must have valid Python versions")
|
|
}
|
|
|
|
/// The version in `x.y.z` format.
|
|
pub fn sys_version(&self) -> String {
|
|
format!("{}.{}.{}", self.major, self.minor, self.patch)
|
|
}
|
|
|
|
pub fn major(&self) -> u8 {
|
|
self.major
|
|
}
|
|
|
|
pub fn minor(&self) -> u8 {
|
|
self.minor
|
|
}
|
|
|
|
pub fn platform(&self) -> &Platform {
|
|
&self.platform
|
|
}
|
|
|
|
pub fn arch(&self) -> &Arch {
|
|
&self.platform.arch
|
|
}
|
|
|
|
pub fn os(&self) -> &Os {
|
|
&self.platform.os
|
|
}
|
|
|
|
pub fn libc(&self) -> &Libc {
|
|
&self.platform.libc
|
|
}
|
|
|
|
pub fn variant(&self) -> &PythonVariant {
|
|
&self.variant
|
|
}
|
|
|
|
/// Return a canonical name for a minor versioned executable.
|
|
pub fn executable_name_minor(&self) -> String {
|
|
format!(
|
|
"python{maj}.{min}{var}{exe}",
|
|
maj = self.major,
|
|
min = self.minor,
|
|
var = self.variant.suffix(),
|
|
exe = std::env::consts::EXE_SUFFIX
|
|
)
|
|
}
|
|
|
|
/// Return a canonical name for a major versioned executable.
|
|
pub fn executable_name_major(&self) -> String {
|
|
format!(
|
|
"python{maj}{var}{exe}",
|
|
maj = self.major,
|
|
var = self.variant.suffix(),
|
|
exe = std::env::consts::EXE_SUFFIX
|
|
)
|
|
}
|
|
|
|
/// Return a canonical name for an un-versioned executable.
|
|
pub fn executable_name(&self) -> String {
|
|
format!(
|
|
"python{var}{exe}",
|
|
var = self.variant.suffix(),
|
|
exe = std::env::consts::EXE_SUFFIX
|
|
)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PythonInstallationKey {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let variant = match self.variant {
|
|
PythonVariant::Default => String::new(),
|
|
_ => format!("+{}", self.variant),
|
|
};
|
|
write!(
|
|
f,
|
|
"{}-{}.{}.{}{}{}-{}",
|
|
self.implementation(),
|
|
self.major,
|
|
self.minor,
|
|
self.patch,
|
|
self.prerelease
|
|
.map(|pre| pre.to_string())
|
|
.unwrap_or_default(),
|
|
variant,
|
|
self.platform
|
|
)
|
|
}
|
|
}
|
|
|
|
impl FromStr for PythonInstallationKey {
|
|
type Err = PythonInstallationKeyError;
|
|
|
|
fn from_str(key: &str) -> Result<Self, Self::Err> {
|
|
let parts = key.split('-').collect::<Vec<_>>();
|
|
|
|
// We need exactly implementation-version-os-arch-libc
|
|
if parts.len() != 5 {
|
|
return Err(PythonInstallationKeyError::ParseError(
|
|
key.to_string(),
|
|
format!(
|
|
"expected exactly 5 `-`-separated values, got {}",
|
|
parts.len()
|
|
),
|
|
));
|
|
}
|
|
|
|
let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
|
|
unreachable!()
|
|
};
|
|
|
|
let implementation = LenientImplementationName::from(*implementation_str);
|
|
|
|
let (version, variant) = match version_str.split_once('+') {
|
|
Some((version, variant)) => {
|
|
let variant = PythonVariant::from_str(variant).map_err(|()| {
|
|
PythonInstallationKeyError::ParseError(
|
|
key.to_string(),
|
|
format!("invalid Python variant: {variant}"),
|
|
)
|
|
})?;
|
|
(version, variant)
|
|
}
|
|
None => (*version_str, PythonVariant::Default),
|
|
};
|
|
|
|
let version = PythonVersion::from_str(version).map_err(|err| {
|
|
PythonInstallationKeyError::ParseError(
|
|
key.to_string(),
|
|
format!("invalid Python version: {err}"),
|
|
)
|
|
})?;
|
|
|
|
let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
|
|
PythonInstallationKeyError::ParseError(
|
|
key.to_string(),
|
|
format!("invalid platform: {err}"),
|
|
)
|
|
})?;
|
|
|
|
Ok(Self {
|
|
implementation,
|
|
major: version.major(),
|
|
minor: version.minor(),
|
|
patch: version.patch().unwrap_or_default(),
|
|
prerelease: version.pre(),
|
|
platform,
|
|
variant,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for PythonInstallationKey {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for PythonInstallationKey {
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
self.implementation
|
|
.cmp(&other.implementation)
|
|
.then_with(|| self.version().cmp(&other.version()))
|
|
// Platforms are sorted in preferred order for the target
|
|
.then_with(|| self.platform.cmp(&other.platform).reverse())
|
|
// Python variants are sorted in preferred order, with `Default` first
|
|
.then_with(|| self.variant.cmp(&other.variant).reverse())
|
|
}
|
|
}
|
|
|
|
/// A view into a [`PythonInstallationKey`] that excludes the patch and prerelease versions.
|
|
#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
|
|
#[repr(transparent)]
|
|
pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);
|
|
|
|
impl PythonInstallationMinorVersionKey {
|
|
/// Cast a `&PythonInstallationKey` to a `&PythonInstallationMinorVersionKey` using ref-cast.
|
|
#[inline]
|
|
pub fn ref_cast(key: &PythonInstallationKey) -> &Self {
|
|
RefCast::ref_cast(key)
|
|
}
|
|
|
|
/// Takes an [`IntoIterator`] of [`ManagedPythonInstallation`]s and returns an [`FxHashMap`] from
|
|
/// [`PythonInstallationMinorVersionKey`] to the installation with highest [`PythonInstallationKey`]
|
|
/// for that minor version key.
|
|
#[inline]
|
|
pub fn highest_installations_by_minor_version_key<'a, I>(
|
|
installations: I,
|
|
) -> IndexMap<Self, ManagedPythonInstallation>
|
|
where
|
|
I: IntoIterator<Item = &'a ManagedPythonInstallation>,
|
|
{
|
|
let mut minor_versions = IndexMap::default();
|
|
for installation in installations {
|
|
minor_versions
|
|
.entry(installation.minor_version_key().clone())
|
|
.and_modify(|high_installation: &mut ManagedPythonInstallation| {
|
|
if installation.key() >= high_installation.key() {
|
|
*high_installation = installation.clone();
|
|
}
|
|
})
|
|
.or_insert_with(|| installation.clone());
|
|
}
|
|
minor_versions
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PythonInstallationMinorVersionKey {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Display every field on the wrapped key except the patch
|
|
// and prerelease (with special formatting for the variant).
|
|
let variant = match self.0.variant {
|
|
PythonVariant::Default => String::new(),
|
|
_ => format!("+{}", self.0.variant),
|
|
};
|
|
write!(
|
|
f,
|
|
"{}-{}.{}{}-{}",
|
|
self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for PythonInstallationMinorVersionKey {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Display every field on the wrapped key except the patch
|
|
// and prerelease.
|
|
f.debug_struct("PythonInstallationMinorVersionKey")
|
|
.field("implementation", &self.0.implementation)
|
|
.field("major", &self.0.major)
|
|
.field("minor", &self.0.minor)
|
|
.field("variant", &self.0.variant)
|
|
.field("os", &self.0.platform.os)
|
|
.field("arch", &self.0.platform.arch)
|
|
.field("libc", &self.0.platform.libc)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for PythonInstallationMinorVersionKey {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
// Compare every field on the wrapped key except the patch
|
|
// and prerelease.
|
|
self.0.implementation == other.0.implementation
|
|
&& self.0.major == other.0.major
|
|
&& self.0.minor == other.0.minor
|
|
&& self.0.platform == other.0.platform
|
|
&& self.0.variant == other.0.variant
|
|
}
|
|
}
|
|
|
|
impl Hash for PythonInstallationMinorVersionKey {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
// Hash every field on the wrapped key except the patch
|
|
// and prerelease.
|
|
self.0.implementation.hash(state);
|
|
self.0.major.hash(state);
|
|
self.0.minor.hash(state);
|
|
self.0.platform.hash(state);
|
|
self.0.variant.hash(state);
|
|
}
|
|
}
|
|
|
|
impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
|
|
fn from(key: PythonInstallationKey) -> Self {
|
|
Self(key)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use uv_platform::ArchVariant;
|
|
|
|
#[test]
|
|
fn test_python_installation_key_from_str() {
|
|
// Test basic parsing
|
|
let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
|
|
assert_eq!(
|
|
key.implementation,
|
|
LenientImplementationName::Known(ImplementationName::CPython)
|
|
);
|
|
assert_eq!(key.major, 3);
|
|
assert_eq!(key.minor, 12);
|
|
assert_eq!(key.patch, 0);
|
|
assert_eq!(
|
|
key.platform.os,
|
|
Os::new(target_lexicon::OperatingSystem::Linux)
|
|
);
|
|
assert_eq!(
|
|
key.platform.arch,
|
|
Arch::new(target_lexicon::Architecture::X86_64, None)
|
|
);
|
|
assert_eq!(
|
|
key.platform.libc,
|
|
Libc::Some(target_lexicon::Environment::Gnu)
|
|
);
|
|
|
|
// Test with architecture variant
|
|
let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
|
|
assert_eq!(
|
|
key.implementation,
|
|
LenientImplementationName::Known(ImplementationName::CPython)
|
|
);
|
|
assert_eq!(key.major, 3);
|
|
assert_eq!(key.minor, 11);
|
|
assert_eq!(key.patch, 2);
|
|
assert_eq!(
|
|
key.platform.os,
|
|
Os::new(target_lexicon::OperatingSystem::Linux)
|
|
);
|
|
assert_eq!(
|
|
key.platform.arch,
|
|
Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
|
|
);
|
|
assert_eq!(
|
|
key.platform.libc,
|
|
Libc::Some(target_lexicon::Environment::Musl)
|
|
);
|
|
|
|
// Test with Python variant (freethreaded)
|
|
let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
|
|
.unwrap();
|
|
assert_eq!(
|
|
key.implementation,
|
|
LenientImplementationName::Known(ImplementationName::CPython)
|
|
);
|
|
assert_eq!(key.major, 3);
|
|
assert_eq!(key.minor, 13);
|
|
assert_eq!(key.patch, 0);
|
|
assert_eq!(key.variant, PythonVariant::Freethreaded);
|
|
assert_eq!(
|
|
key.platform.os,
|
|
Os::new(target_lexicon::OperatingSystem::Darwin(None))
|
|
);
|
|
assert_eq!(
|
|
key.platform.arch,
|
|
Arch::new(
|
|
target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
|
|
None
|
|
)
|
|
);
|
|
assert_eq!(key.platform.libc, Libc::None);
|
|
|
|
// Test error cases
|
|
assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
|
|
assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
|
|
assert!(PythonInstallationKey::from_str("cpython").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_python_installation_key_display() {
|
|
let key = PythonInstallationKey {
|
|
implementation: LenientImplementationName::from("cpython"),
|
|
major: 3,
|
|
minor: 12,
|
|
patch: 0,
|
|
prerelease: None,
|
|
platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
|
|
variant: PythonVariant::Default,
|
|
};
|
|
assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");
|
|
|
|
let key_with_variant = PythonInstallationKey {
|
|
implementation: LenientImplementationName::from("cpython"),
|
|
major: 3,
|
|
minor: 13,
|
|
patch: 0,
|
|
prerelease: None,
|
|
platform: Platform::from_str("macos-aarch64-none").unwrap(),
|
|
variant: PythonVariant::Freethreaded,
|
|
};
|
|
assert_eq!(
|
|
key_with_variant.to_string(),
|
|
"cpython-3.13.0+freethreaded-macos-aarch64-none"
|
|
);
|
|
}
|
|
}
|