Always use base Python discovery logic for cached environments (#11254)

## Summary

This is attempting to solve the same problem surfaced in #11208 and
#11209. However, those PRs only worked for our own managed Pythons. In
Gentoo, for example, they disable the managed Pythons, which led to
failures in the test suite, because the "base Python" returned after
creating a virtual environment would differ from the "base Python" that
you get after _querying_ an existing virtual environment.

The fix here is to apply our same base Python normalization and
discovery logic, to non-standalone / non-managed Pythons. We continue to
use `sys._base_executable` for such Pythons when creating the
virtualenv, but when _caching_, we perform this second discovery step.

Closes https://github.com/astral-sh/uv/issues/11237.
This commit is contained in:
Charlie Marsh 2025-02-05 15:47:56 -05:00 committed by GitHub
parent c0f6406c76
commit 53d1a7aa6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 54 additions and 27 deletions

View file

@ -121,13 +121,33 @@ impl Interpreter {
})
}
/// Return the base Python executable; that is, the Python executable that should be
/// considered the "base" for the virtual environment. This is typically the Python executable
/// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
/// the base Python executable is the Python executable of the interpreter's base interpreter.
///
/// This routine relies on `sys._base_executable`, falling back to `sys.executable` if unset.
/// Broadly, this routine should be used when attempting to determine the "base Python
/// executable" in a way that is consistent with the CPython standard library, such as when
/// determining the `home` key for a virtual environment.
pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
let base_python = std::path::absolute(base_executable)?;
Ok(base_python)
}
/// Determine the base Python executable; that is, the Python executable that should be
/// considered the "base" for the virtual environment. This is typically the Python executable
/// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
/// the base Python executable is the Python executable of the interpreter's base interpreter.
pub fn to_base_python(&self) -> Result<PathBuf, io::Error> {
///
/// This routine mimics the CPython `getpath.py` logic in order to make a more robust assessment
/// of the appropriate base Python executable. Broadly, this routine should be used when
/// attempting to determine the "true" base executable for a Python interpreter by resolving
/// symlinks until a valid Python installation is found. In particular, we tend to use this
/// routine for our own managed (or standalone) Python installations.
pub fn find_base_python(&self) -> Result<PathBuf, io::Error> {
let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable());
let base_python = if cfg!(unix) && self.is_standalone() {
// In `python-build-standalone`, a symlinked interpreter will return its own executable path
// as `sys._base_executable`. Using the symlinked path as the base Python executable can be
// incorrect, since it could cause `home` to point to something that is _not_ a Python
@ -137,7 +157,7 @@ impl Interpreter {
//
// We emulate CPython's `getpath.py` to ensure that the base executable results in a valid
// Python prefix when converted into the `home` key for `pyvenv.cfg`.
match find_base_python(
let base_python = match find_base_python(
base_executable,
self.python_major(),
self.python_minor(),
@ -148,11 +168,7 @@ impl Interpreter {
warn!("Failed to find base Python executable: {err}");
uv_fs::canonicalize_executable(base_executable)?
}
}
} else {
std::path::absolute(base_executable)?
};
Ok(base_python)
}

View file

@ -56,7 +56,14 @@ pub(crate) fn create(
) -> Result<VirtualEnvironment, Error> {
// Determine the base Python executable; that is, the Python executable that should be
// considered the "base" for the virtual environment.
let base_python = interpreter.to_base_python()?;
//
// For consistency with the standard library, rely on `sys._base_executable`, _unless_ we're
// using a uv-managed Python (in which case, we can do better for symlinked executables).
let base_python = if cfg!(unix) && interpreter.is_standalone() {
interpreter.find_base_python()?
} else {
interpreter.to_base_python()?
};
debug!(
"Using base executable for virtual environment: {}",

View file

@ -260,7 +260,11 @@ impl CachedEnvironment {
interpreter: &Interpreter,
cache: &Cache,
) -> Result<Interpreter, uv_python::Error> {
let base_python = interpreter.to_base_python()?;
let base_python = if cfg!(unix) {
interpreter.find_base_python()?
} else {
interpreter.to_base_python()?
};
if base_python == interpreter.sys_executable() {
debug!(
"Caching via base interpreter: `{}`",