Skip to content

Commit

Permalink
Use base executable to set virtualenv Python path (#8481)
Browse files Browse the repository at this point in the history
See extensive discussion in
#8433 (comment).

This PR brings us into alignment with the standard library by using
`sys._base_executable` rather than canonicalizing the executable path.

The benefits are primarily for Homebrew, where we'll now resolve to
paths like `/opt/homebrew/opt/[email protected]/bin` instead of the
undesirable
`/opt/homebrew/Cellar/[email protected]/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`.

Most other users should see no change, though in some cases, nested
virtual environments now have slightly different behavior -- namely,
they _sometimes_ resolve to the virtual environment Python (at least for
Homebrew; not for rtx or uv Pythons though). See
[here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0)
for a breakdown.

Closes #1640.
Closes #1795.
  • Loading branch information
charliermarsh authored and zanieb committed Nov 7, 2024
1 parent 86ac93b commit 4476099
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 23 deletions.
1 change: 1 addition & 0 deletions crates/uv-python/python/get_interpreter_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Interpreter {
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
stdlib: PathBuf,
sysconfig_prefix: Option<PathBuf>,
tags: OnceLock<Tags>,
target: Option<Target>,
prefix: Option<Prefix>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -605,6 +625,7 @@ struct InterpreterInfo {
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
stdlib: PathBuf,
sysconfig_prefix: Option<PathBuf>,
pointer_size: PointerSize,
gil_disabled: bool,
}
Expand Down
35 changes: 12 additions & 23 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 4476099

Please sign in to comment.