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 36b713a6141e..c7bd1740a360 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: + 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.