From bedaf5803a4e9be8656774dcb47c60fccecd7f0c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Feb 2025 14:10:48 -0500 Subject: [PATCH] Always use base Python discovery logic for cached environments --- crates/uv-python/src/interpreter.rs | 66 ++++++++++++------- crates/uv-virtualenv/src/virtualenv.rs | 9 ++- crates/uv/src/commands/project/environment.rs | 6 +- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 8c6cc22a3281..0689aaff68c8 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -121,38 +121,54 @@ impl Interpreter { }) } - /// Determine the base Python executable; that is, the Python executable that should be + /// 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 { 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 - // installation. Specifically, if the interpreter _itself_ is symlinked to an arbitrary - // location, we need to fully resolve it to the actual Python executable; however, if the - // entire standalone interpreter is symlinked, then we can use the symlinked path. - // - // 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( - base_executable, - self.python_major(), - self.python_minor(), - self.variant().suffix(), - ) { - Ok(path) => path, - Err(err) => { - warn!("Failed to find base Python executable: {err}"); - uv_fs::canonicalize_executable(base_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. + /// + /// 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 { + let base_executable = self.sys_base_executable().unwrap_or(self.sys_executable()); + // 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 + // installation. Specifically, if the interpreter _itself_ is symlinked to an arbitrary + // location, we need to fully resolve it to the actual Python executable; however, if the + // entire standalone interpreter is symlinked, then we can use the symlinked path. + // + // 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`. + let base_python = match find_base_python( + base_executable, + self.python_major(), + self.python_minor(), + self.variant().suffix(), + ) { + Ok(path) => path, + Err(err) => { + warn!("Failed to find base Python executable: {err}"); + uv_fs::canonicalize_executable(base_executable)? } - } else { - std::path::absolute(base_executable)? }; - Ok(base_python) } diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 890142bf1a60..162d4a79c310 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -56,7 +56,14 @@ pub(crate) fn create( ) -> Result { // 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: {}", diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 8cfee7998b35..d11ebda87b27 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -260,7 +260,11 @@ impl CachedEnvironment { interpreter: &Interpreter, cache: &Cache, ) -> Result { - 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: `{}`",