From d2cd6cbd007e10b88f88a742280d99b676bf0ae5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 22 Oct 2024 19:15:57 -0400 Subject: [PATCH 1/2] Use base executable to set virtualenv Python path --- crates/uv-fs/src/path.rs | 18 ++++++++++ .../uv-python/python/get_interpreter_info.py | 1 + crates/uv-python/src/interpreter.rs | 21 +++++++++++ crates/uv-virtualenv/src/virtualenv.rs | 35 +++++++------------ 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 54941df43540..db651278e84f 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -250,6 +250,24 @@ pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result Result, std::io::Error> { + let Ok(link) = executable.read_link() else { + return Ok(None); + }; + if link.is_absolute() { + Ok(Some(link)) + } else { + executable + .parent() + .map(|parent| parent.join(link)) + .as_deref() + .map(normalize_absolute_path) + .transpose() + } +} + /// Compute a path describing `path` relative to `base`. /// /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` diff --git a/crates/uv-python/python/get_interpreter_info.py b/crates/uv-python/python/get_interpreter_info.py index d056d3851eb7..f39506bfebd9 100644 --- a/crates/uv-python/python/get_interpreter_info.py +++ b/crates/uv-python/python/get_interpreter_info.py @@ -563,6 +563,7 @@ def main() -> None: "sys_executable": sys.executable, "sys_path": sys.path, "stdlib": sysconfig.get_path("stdlib"), + "sysconfig_prefix": sysconfig.get_config_var("prefix"), "scheme": get_scheme(), "virtualenv": get_virtualenv(), "platform": os_and_arch, diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index cbf7950f6d9d..c19031084d15 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -45,6 +45,7 @@ pub struct Interpreter { sys_executable: PathBuf, sys_path: Vec, stdlib: PathBuf, + sysconfig_prefix: Option, tags: OnceLock, target: Option, prefix: Option, @@ -78,6 +79,7 @@ impl Interpreter { sys_executable: info.sys_executable, sys_path: info.sys_path, stdlib: info.stdlib, + sysconfig_prefix: info.sysconfig_prefix, tags: OnceLock::new(), target: None, prefix: None, @@ -365,6 +367,11 @@ impl Interpreter { &self.stdlib } + /// Return the `prefix` path for this Python interpreter, as returned by `sysconfig.get_config_var("prefix")`. + pub fn sysconfig_prefix(&self) -> Option<&Path> { + self.sysconfig_prefix.as_deref() + } + /// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`. pub fn purelib(&self) -> &Path { &self.scheme.purelib @@ -424,6 +431,19 @@ impl Interpreter { self.prefix.as_ref() } + /// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter. + /// + /// This method may return false positives, but it should not return false negatives. In other + /// words, if this method returns `true`, the interpreter _may_ be from + /// `python-build-standalone`; if it returns `false`, the interpreter is definitely _not_ from + /// `python-build-standalone`. + /// + /// See: https://github.com/indygreg/python-build-standalone/issues/382 + pub fn is_standalone(&self) -> bool { + self.sysconfig_prefix() + .is_some_and(|prefix| prefix == Path::new("/install")) + } + /// Return the [`Layout`] environment used to install wheels into this interpreter. pub fn layout(&self) -> Layout { Layout { @@ -605,6 +625,7 @@ struct InterpreterInfo { sys_executable: PathBuf, sys_path: Vec, stdlib: PathBuf, + sysconfig_prefix: Option, pointer_size: PointerSize, gil_disabled: bool, } diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 70f0ca22e06d..d11ecc1fb134 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -57,31 +57,20 @@ pub(crate) fn create( // 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. - let base_python = if cfg!(unix) { - // On Unix, follow symlinks to resolve the base interpreter, since the Python executable in - // a virtual environment is a symlink to the base interpreter. - uv_fs::canonicalize_executable(interpreter.sys_executable())? - } else if cfg!(windows) { - // On Windows, follow `virtualenv`. If we're in a virtual environment, use - // `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with - // Python installed from the Windows Store, `sys.base_prefix` is slightly "incorrect". + let base_python = if cfg!(unix) && interpreter.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 is + // incorrect, since it will cause `home` to point to something that is _not_ a Python + // installation. // - // If we're _not_ in a virtual environment, use the interpreter's executable, since it's - // already a "system Python". We canonicalize the path to ensure that it's real and - // consistent, though we don't expect any symlinks on Windows. - if interpreter.is_virtualenv() { - if let Some(base_executable) = interpreter.sys_base_executable() { - base_executable.to_path_buf() - } else { - // Assume `python.exe`, though the exact executable name is never used (below) on - // Windows, only its parent directory. - interpreter.sys_base_prefix().join("python.exe") - } - } else { - interpreter.sys_executable().to_path_buf() - } + // Instead, we want to fully resolve the symlink to the actual Python executable. + uv_fs::canonicalize_executable(interpreter.sys_executable())? } else { - unimplemented!("Only Windows and Unix are supported") + std::path::absolute( + interpreter + .sys_base_executable() + .unwrap_or(interpreter.sys_executable()), + )? }; // Validate the existing location. From ac27720049669ac9e2a22a1a88fb6cccfd55644b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 24 Oct 2024 21:13:37 -0400 Subject: [PATCH 2/2] Remove read_executable_link --- crates/uv-fs/src/path.rs | 18 ------------------ crates/uv-python/src/interpreter.rs | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index db651278e84f..54941df43540 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -250,24 +250,6 @@ pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result Result, std::io::Error> { - let Ok(link) = executable.read_link() else { - return Ok(None); - }; - if link.is_absolute() { - Ok(Some(link)) - } else { - executable - .parent() - .map(|parent| parent.join(link)) - .as_deref() - .map(normalize_absolute_path) - .transpose() - } -} - /// Compute a path describing `path` relative to `base`. /// /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index c19031084d15..0f7f5a7616b6 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -438,7 +438,7 @@ impl Interpreter { /// `python-build-standalone`; if it returns `false`, the interpreter is definitely _not_ from /// `python-build-standalone`. /// - /// See: https://github.com/indygreg/python-build-standalone/issues/382 + /// See: pub fn is_standalone(&self) -> bool { self.sysconfig_prefix() .is_some_and(|prefix| prefix == Path::new("/install"))