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:
Zanie Blue
2025-08-25 16:25:05 -05:00
committed by GitHub
parent b6f1fb7d3f
commit 9b8d6989d4
17 changed files with 5518 additions and 2537 deletions
+58
View File
@@ -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;