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
+31 -1
View File
@@ -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
})
);