mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 23:37:24 +00:00
Allow pinning managed Python versions to specific build versions (#15314)
Allows pinning the Python build version via environment variables, e.g., `UV_PYTHON_CPYTHON_BUILD=...`. Each variable is implementation specific, because they use different versioning schemes. Updates the Python download metadata to include a `build` string, so we can filter downloads by the pin. Writes the build version to a file in the managed install, e.g., `cpython-3.10.18-macos-aarch64-none/BUILD`, so we can filter installed versions by the pin. Some important follow-up here: - Include the build version in not found errors (when pinned) - Automatically use a remote list of Python downloads to satisfy build versions not present in the latest embedded download metadata Some less important follow-ups to consider: - Allow using ranges for build version pins
This commit is contained in:
parent
b6f1fb7d3f
commit
9b8d6989d4
17 changed files with 5518 additions and 2537 deletions
File diff suppressed because it is too large
Load diff
|
@ -153,6 +153,7 @@ class PythonDownload:
|
|||
implementation: ImplementationName
|
||||
filename: str
|
||||
url: str
|
||||
build: str
|
||||
sha256: str | None = None
|
||||
build_options: list[str] = field(default_factory=list)
|
||||
variant: Variant | None = None
|
||||
|
@ -397,6 +398,7 @@ class CPythonFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=filename,
|
||||
url=url,
|
||||
build=str(release),
|
||||
build_options=build_options,
|
||||
variant=variant,
|
||||
sha256=sha256,
|
||||
|
@ -507,6 +509,7 @@ class PyPyFinder(Finder):
|
|||
python_version = Version.from_str(version["python_version"])
|
||||
if python_version < (3, 7, 0):
|
||||
continue
|
||||
pypy_version = version["pypy_version"]
|
||||
for file in version["files"]:
|
||||
arch = self._normalize_arch(file["arch"])
|
||||
platform = self._normalize_os(file["platform"])
|
||||
|
@ -523,6 +526,7 @@ class PyPyFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=file["filename"],
|
||||
url=file["download_url"],
|
||||
build=pypy_version,
|
||||
)
|
||||
# Only keep the latest pypy version of each arch/platform
|
||||
if (python_version, arch, platform) not in results:
|
||||
|
@ -612,6 +616,7 @@ class PyodideFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=asset["name"],
|
||||
url=url,
|
||||
build=pyodide_version,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -708,6 +713,7 @@ class GraalPyFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=asset["name"],
|
||||
url=url,
|
||||
build=graalpy_version,
|
||||
sha256=sha256,
|
||||
)
|
||||
# Only keep the latest GraalPy version of each arch/platform
|
||||
|
@ -811,6 +817,7 @@ def render(downloads: list[PythonDownload]) -> None:
|
|||
"url": download.url,
|
||||
"sha256": download.sha256,
|
||||
"variant": download.variant if download.variant else None,
|
||||
"build": download.build,
|
||||
}
|
||||
|
||||
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
@ -29,6 +29,7 @@ use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
|
|||
use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
|
||||
#[cfg(windows)]
|
||||
use crate::microsoft_store::find_microsoft_store_pythons;
|
||||
use crate::python_version::python_build_versions_from_env;
|
||||
use crate::virtualenv::Error as VirtualEnvError;
|
||||
use crate::virtualenv::{
|
||||
CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
|
||||
|
@ -263,6 +264,9 @@ pub enum Error {
|
|||
// TODO(zanieb): Is this error case necessary still? We should probably drop it.
|
||||
#[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
|
||||
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
|
||||
|
||||
#[error(transparent)]
|
||||
BuildVersion(#[from] crate::python_version::BuildVersionError),
|
||||
}
|
||||
|
||||
/// Lazily iterate over Python executables in mutable virtual environments.
|
||||
|
@ -342,6 +346,9 @@ fn python_executables_from_installed<'a>(
|
|||
installed_installations.root().user_display()
|
||||
);
|
||||
let installations = installed_installations.find_matching_current_platform()?;
|
||||
|
||||
let build_versions = python_build_versions_from_env()?;
|
||||
|
||||
// Check that the Python version and platform satisfy the request to avoid
|
||||
// unnecessary interpreter queries later
|
||||
Ok(installations
|
||||
|
@ -355,6 +362,22 @@ fn python_executables_from_installed<'a>(
|
|||
debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(requested_build) = build_versions.get(&installation.implementation()) {
|
||||
let Some(installation_build) = installation.build() else {
|
||||
debug!(
|
||||
"Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
|
||||
);
|
||||
return false;
|
||||
};
|
||||
if installation_build != requested_build {
|
||||
debug!(
|
||||
"Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.inspect(|installation| debug!("Found managed installation `{installation}`"))
|
||||
|
@ -1218,6 +1241,7 @@ pub fn find_python_installations<'a>(
|
|||
return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
|
||||
}
|
||||
}
|
||||
|
||||
Box::new({
|
||||
debug!("Searching for {request} in {sources}");
|
||||
python_interpreters(
|
||||
|
@ -1229,7 +1253,9 @@ pub fn find_python_installations<'a>(
|
|||
cache,
|
||||
preview,
|
||||
)
|
||||
.filter_ok(|(_source, interpreter)| request.satisfied_by_interpreter(interpreter))
|
||||
.filter_ok(move |(_source, interpreter)| {
|
||||
request.satisfied_by_interpreter(interpreter)
|
||||
})
|
||||
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
||||
})
|
||||
}
|
||||
|
@ -3186,6 +3212,7 @@ mod tests {
|
|||
arch: None,
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
@ -3205,6 +3232,7 @@ mod tests {
|
|||
))),
|
||||
os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
|
||||
libc: Some(Libc::None),
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
@ -3221,6 +3249,7 @@ mod tests {
|
|||
arch: None,
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
@ -3240,6 +3269,7 @@ mod tests {
|
|||
))),
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
|
|
@ -36,6 +36,7 @@ use crate::implementation::{
|
|||
};
|
||||
use crate::installation::PythonInstallationKey;
|
||||
use crate::managed::ManagedPythonInstallation;
|
||||
use crate::python_version::{BuildVersionError, python_build_version_from_env};
|
||||
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -110,6 +111,8 @@ pub enum Error {
|
|||
url: Box<Url>,
|
||||
python_builds_dir: PathBuf,
|
||||
},
|
||||
#[error(transparent)]
|
||||
BuildVersion(#[from] BuildVersionError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
@ -144,6 +147,7 @@ pub struct ManagedPythonDownload {
|
|||
key: PythonInstallationKey,
|
||||
url: Cow<'static, str>,
|
||||
sha256: Option<Cow<'static, str>>,
|
||||
build: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
|
||||
|
@ -153,6 +157,7 @@ pub struct PythonDownloadRequest {
|
|||
pub(crate) arch: Option<ArchRequest>,
|
||||
pub(crate) os: Option<Os>,
|
||||
pub(crate) libc: Option<Libc>,
|
||||
pub(crate) build: Option<String>,
|
||||
|
||||
/// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is
|
||||
/// not None, and false otherwise.
|
||||
|
@ -255,6 +260,7 @@ impl PythonDownloadRequest {
|
|||
arch,
|
||||
os,
|
||||
libc,
|
||||
build: None,
|
||||
prereleases,
|
||||
}
|
||||
}
|
||||
|
@ -311,6 +317,12 @@ impl PythonDownloadRequest {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_build(mut self, build: String) -> Self {
|
||||
self.build = Some(build);
|
||||
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
|
||||
|
@ -356,11 +368,25 @@ impl PythonDownloadRequest {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Fill the build field from the environment variable relevant for the [`ImplementationName`].
|
||||
pub fn fill_build_from_env(mut self) -> Result<Self, Error> {
|
||||
if self.build.is_some() {
|
||||
return Ok(self);
|
||||
}
|
||||
let Some(implementation) = self.implementation else {
|
||||
return Ok(self);
|
||||
};
|
||||
|
||||
self.build = python_build_version_from_env(implementation)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn fill(mut self) -> Result<Self, Error> {
|
||||
if self.implementation.is_none() {
|
||||
self.implementation = Some(ImplementationName::CPython);
|
||||
}
|
||||
self = self.fill_platform()?;
|
||||
self = self.fill_build_from_env()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
|
@ -434,7 +460,31 @@ impl PythonDownloadRequest {
|
|||
|
||||
/// Whether this request is satisfied by a Python download.
|
||||
pub fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
|
||||
self.satisfied_by_key(download.key())
|
||||
// First check the key
|
||||
if !self.satisfied_by_key(download.key()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check the build if specified
|
||||
if let Some(ref requested_build) = self.build {
|
||||
let Some(download_build) = download.build() else {
|
||||
debug!(
|
||||
"Skipping download `{}`: a build version was requested but is not available for this download",
|
||||
download
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
if download_build != requested_build {
|
||||
debug!(
|
||||
"Skipping download `{}`: requested build version `{}` does not match download build version `{}`",
|
||||
download, requested_build, download_build
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to pre-release Python versions.
|
||||
|
@ -753,6 +803,7 @@ struct JsonPythonDownload {
|
|||
url: String,
|
||||
sha256: Option<String>,
|
||||
variant: Option<String>,
|
||||
build: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
@ -767,7 +818,25 @@ pub enum DownloadResult {
|
|||
Fetched(PathBuf),
|
||||
}
|
||||
|
||||
/// A wrapper type to display a `ManagedPythonDownload` with its build information.
|
||||
pub struct ManagedPythonDownloadWithBuild<'a>(&'a ManagedPythonDownload);
|
||||
|
||||
impl Display for ManagedPythonDownloadWithBuild<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(build) = self.0.build {
|
||||
write!(f, "{}+{}", self.0.key, build)
|
||||
} else {
|
||||
write!(f, "{}", self.0.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedPythonDownload {
|
||||
/// Return a display type that includes the build information.
|
||||
pub fn to_display_with_build(&self) -> ManagedPythonDownloadWithBuild<'_> {
|
||||
ManagedPythonDownloadWithBuild(self)
|
||||
}
|
||||
|
||||
/// Return the first [`ManagedPythonDownload`] matching a request, if any.
|
||||
///
|
||||
/// If there is no stable version matching the request, a compatible pre-release version will
|
||||
|
@ -852,6 +921,10 @@ impl ManagedPythonDownload {
|
|||
self.sha256.as_ref()
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Option<&'static str> {
|
||||
self.build
|
||||
}
|
||||
|
||||
/// Download and extract a Python distribution, retrying on failure.
|
||||
#[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
|
||||
pub async fn fetch_with_retry(
|
||||
|
@ -1345,6 +1418,9 @@ fn parse_json_downloads(
|
|||
|
||||
let url = Cow::Owned(entry.url);
|
||||
let sha256 = entry.sha256.map(Cow::Owned);
|
||||
let build = entry
|
||||
.build
|
||||
.map(|s| Box::leak(s.into_boxed_str()) as &'static str);
|
||||
|
||||
Some(ManagedPythonDownload {
|
||||
key: PythonInstallationKey::new_from_version(
|
||||
|
@ -1355,6 +1431,7 @@ fn parse_json_downloads(
|
|||
),
|
||||
url,
|
||||
sha256,
|
||||
build,
|
||||
})
|
||||
})
|
||||
.sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
|
||||
|
@ -1513,6 +1590,10 @@ async fn read_url(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::implementation::LenientImplementationName;
|
||||
use crate::installation::PythonInstallationKey;
|
||||
use uv_platform::{Arch, Libc, Os, Platform};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Parse a request with all of its fields.
|
||||
|
@ -1726,4 +1807,90 @@ mod tests {
|
|||
|
||||
assert!(matches!(result, Err(Error::TooManyParts(_))));
|
||||
}
|
||||
|
||||
/// Test that build filtering works correctly
|
||||
#[test]
|
||||
fn test_python_download_request_build_filtering() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_version(VersionRequest::from_str("3.12").unwrap())
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_build("20240814".to_string());
|
||||
|
||||
let downloads: Vec<_> = ManagedPythonDownload::iter_all(None)
|
||||
.unwrap()
|
||||
.filter(|d| request.satisfied_by_download(d))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!downloads.is_empty(),
|
||||
"Should find at least one matching download"
|
||||
);
|
||||
for download in downloads {
|
||||
assert_eq!(download.build(), Some("20240814"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that an invalid build results in no matches
|
||||
#[test]
|
||||
fn test_python_download_request_invalid_build() {
|
||||
// Create a request with a non-existent build
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_version(VersionRequest::from_str("3.12").unwrap())
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_build("99999999".to_string());
|
||||
|
||||
// Should find no matching downloads
|
||||
let downloads: Vec<_> = ManagedPythonDownload::iter_all(None)
|
||||
.unwrap()
|
||||
.filter(|d| request.satisfied_by_download(d))
|
||||
.collect();
|
||||
|
||||
assert_eq!(downloads.len(), 0);
|
||||
}
|
||||
|
||||
/// Test build display
|
||||
#[test]
|
||||
fn test_managed_python_download_build_display() {
|
||||
// Create a test download with a build
|
||||
let key = PythonInstallationKey::new(
|
||||
LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
|
||||
3,
|
||||
12,
|
||||
0,
|
||||
None,
|
||||
Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
),
|
||||
crate::PythonVariant::default(),
|
||||
);
|
||||
|
||||
let download_with_build = ManagedPythonDownload {
|
||||
key,
|
||||
url: Cow::Borrowed("https://example.com/python.tar.gz"),
|
||||
sha256: Some(Cow::Borrowed("abc123")),
|
||||
build: Some("20240101"),
|
||||
};
|
||||
|
||||
// Test display with build
|
||||
assert_eq!(
|
||||
download_with_build.to_display_with_build().to_string(),
|
||||
"cpython-3.12.0-linux-x86_64-gnu+20240101"
|
||||
);
|
||||
|
||||
// Test download without build
|
||||
let download_without_build = ManagedPythonDownload {
|
||||
key: download_with_build.key.clone(),
|
||||
url: Cow::Borrowed("https://example.com/python.tar.gz"),
|
||||
sha256: Some(Cow::Borrowed("abc123")),
|
||||
build: None,
|
||||
};
|
||||
|
||||
// Test display without build
|
||||
assert_eq!(
|
||||
download_without_build.to_display_with_build().to_string(),
|
||||
"cpython-3.12.0-linux-x86_64-gnu"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -252,6 +252,7 @@ impl PythonInstallation {
|
|||
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
|
||||
|
|
|
@ -21,7 +21,7 @@ pub use crate::interpreter::{
|
|||
};
|
||||
pub use crate::pointer_size::PointerSize;
|
||||
pub use crate::prefix::Prefix;
|
||||
pub use crate::python_version::PythonVersion;
|
||||
pub use crate::python_version::{BuildVersionError, PythonVersion};
|
||||
pub use crate::target::Target;
|
||||
pub use crate::version_files::{
|
||||
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
||||
|
|
|
@ -320,6 +320,10 @@ pub struct ManagedPythonInstallation {
|
|||
///
|
||||
/// Empty when self was constructed from a path.
|
||||
sha256: Option<Cow<'static, str>>,
|
||||
/// The build version of the Python installation.
|
||||
///
|
||||
/// Empty when self was constructed from a path without a BUILD file.
|
||||
build: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl ManagedPythonInstallation {
|
||||
|
@ -329,6 +333,7 @@ impl ManagedPythonInstallation {
|
|||
key: download.key().clone(),
|
||||
url: Some(download.url().clone()),
|
||||
sha256: download.sha256().cloned(),
|
||||
build: download.build().map(Cow::Borrowed),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,11 +347,19 @@ impl ManagedPythonInstallation {
|
|||
|
||||
let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
|
||||
|
||||
// Try to read the BUILD file if it exists
|
||||
let build = match fs::read_to_string(path.join("BUILD")) {
|
||||
Ok(content) => Some(Cow::Owned(content.trim().to_string())),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => None,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
key,
|
||||
url: None,
|
||||
sha256: None,
|
||||
build,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -455,6 +468,11 @@ impl ManagedPythonInstallation {
|
|||
self.key.platform()
|
||||
}
|
||||
|
||||
/// The build version of this installation, if available.
|
||||
pub fn build(&self) -> Option<&str> {
|
||||
self.build.as_deref()
|
||||
}
|
||||
|
||||
pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
|
||||
PythonInstallationMinorVersionKey::ref_cast(&self.key)
|
||||
}
|
||||
|
@ -618,6 +636,15 @@ impl ManagedPythonInstallation {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the build version is written to a BUILD file in the installation directory.
|
||||
pub fn ensure_build_file(&self) -> Result<(), Error> {
|
||||
if let Some(ref build) = self.build {
|
||||
let build_file = self.path.join("BUILD");
|
||||
fs::write(&build_file, build.as_ref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a link to this installation's binary, e.g., as created by
|
||||
/// [`create_bin_link`].
|
||||
pub fn is_bin_link(&self, path: &Path) -> bool {
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
#[cfg(feature = "schemars")]
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerEnvironment, StringVersion};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::implementation::ImplementationName;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BuildVersionError {
|
||||
#[error("`{0}` is not valid unicode: {1:?}")]
|
||||
NotUnicode(&'static str, OsString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PythonVersion(StringVersion);
|
||||
|
@ -206,6 +219,51 @@ impl PythonVersion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the environment variable name for the build constraint for a given implementation.
|
||||
pub(crate) fn python_build_version_variable(implementation: ImplementationName) -> &'static str {
|
||||
match implementation {
|
||||
ImplementationName::CPython => EnvVars::UV_PYTHON_CPYTHON_BUILD,
|
||||
ImplementationName::PyPy => EnvVars::UV_PYTHON_PYPY_BUILD,
|
||||
ImplementationName::GraalPy => EnvVars::UV_PYTHON_GRAALPY_BUILD,
|
||||
ImplementationName::Pyodide => EnvVars::UV_PYTHON_PYODIDE_BUILD,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the build version number from the environment variable for a given implementation.
|
||||
pub(crate) fn python_build_version_from_env(
|
||||
implementation: ImplementationName,
|
||||
) -> Result<Option<String>, BuildVersionError> {
|
||||
let variable = python_build_version_variable(implementation);
|
||||
|
||||
let Some(build_os) = env::var_os(variable) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let build = build_os
|
||||
.into_string()
|
||||
.map_err(|raw| BuildVersionError::NotUnicode(variable, raw))?;
|
||||
|
||||
let trimmed = build.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
|
||||
/// Get the build version numbers for all Python implementations.
|
||||
pub(crate) fn python_build_versions_from_env()
|
||||
-> Result<BTreeMap<ImplementationName, String>, BuildVersionError> {
|
||||
let mut versions = BTreeMap::new();
|
||||
for implementation in ImplementationName::iter_all() {
|
||||
let Some(build) = python_build_version_from_env(implementation)? else {
|
||||
continue;
|
||||
};
|
||||
versions.insert(implementation, build);
|
||||
}
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue