Skip to content

Commit 4476099

Browse files
charliermarshzanieb
authored andcommitted
Use base executable to set virtualenv Python path (#8481)
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.
1 parent 86ac93b commit 4476099

File tree

3 files changed

+34
-23
lines changed

3 files changed

+34
-23
lines changed

crates/uv-python/python/get_interpreter_info.py

+1
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ def main() -> None:
563563
"sys_executable": sys.executable,
564564
"sys_path": sys.path,
565565
"stdlib": sysconfig.get_path("stdlib"),
566+
"sysconfig_prefix": sysconfig.get_config_var("prefix"),
566567
"scheme": get_scheme(),
567568
"virtualenv": get_virtualenv(),
568569
"platform": os_and_arch,

crates/uv-python/src/interpreter.rs

+21
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub struct Interpreter {
4545
sys_executable: PathBuf,
4646
sys_path: Vec<PathBuf>,
4747
stdlib: PathBuf,
48+
sysconfig_prefix: Option<PathBuf>,
4849
tags: OnceLock<Tags>,
4950
target: Option<Target>,
5051
prefix: Option<Prefix>,
@@ -78,6 +79,7 @@ impl Interpreter {
7879
sys_executable: info.sys_executable,
7980
sys_path: info.sys_path,
8081
stdlib: info.stdlib,
82+
sysconfig_prefix: info.sysconfig_prefix,
8183
tags: OnceLock::new(),
8284
target: None,
8385
prefix: None,
@@ -365,6 +367,11 @@ impl Interpreter {
365367
&self.stdlib
366368
}
367369

370+
/// Return the `prefix` path for this Python interpreter, as returned by `sysconfig.get_config_var("prefix")`.
371+
pub fn sysconfig_prefix(&self) -> Option<&Path> {
372+
self.sysconfig_prefix.as_deref()
373+
}
374+
368375
/// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
369376
pub fn purelib(&self) -> &Path {
370377
&self.scheme.purelib
@@ -424,6 +431,19 @@ impl Interpreter {
424431
self.prefix.as_ref()
425432
}
426433

434+
/// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
435+
///
436+
/// This method may return false positives, but it should not return false negatives. In other
437+
/// words, if this method returns `true`, the interpreter _may_ be from
438+
/// `python-build-standalone`; if it returns `false`, the interpreter is definitely _not_ from
439+
/// `python-build-standalone`.
440+
///
441+
/// See: <https://github.com/indygreg/python-build-standalone/issues/382>
442+
pub fn is_standalone(&self) -> bool {
443+
self.sysconfig_prefix()
444+
.is_some_and(|prefix| prefix == Path::new("/install"))
445+
}
446+
427447
/// Return the [`Layout`] environment used to install wheels into this interpreter.
428448
pub fn layout(&self) -> Layout {
429449
Layout {
@@ -605,6 +625,7 @@ struct InterpreterInfo {
605625
sys_executable: PathBuf,
606626
sys_path: Vec<PathBuf>,
607627
stdlib: PathBuf,
628+
sysconfig_prefix: Option<PathBuf>,
608629
pointer_size: PointerSize,
609630
gil_disabled: bool,
610631
}

crates/uv-virtualenv/src/virtualenv.rs

+12-23
Original file line numberDiff line numberDiff line change
@@ -57,31 +57,20 @@ pub(crate) fn create(
5757
// considered the "base" for the virtual environment. This is typically the Python executable
5858
// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
5959
// the base Python executable is the Python executable of the interpreter's base interpreter.
60-
let base_python = if cfg!(unix) {
61-
// On Unix, follow symlinks to resolve the base interpreter, since the Python executable in
62-
// a virtual environment is a symlink to the base interpreter.
63-
uv_fs::canonicalize_executable(interpreter.sys_executable())?
64-
} else if cfg!(windows) {
65-
// On Windows, follow `virtualenv`. If we're in a virtual environment, use
66-
// `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with
67-
// Python installed from the Windows Store, `sys.base_prefix` is slightly "incorrect".
60+
let base_python = if cfg!(unix) && interpreter.is_standalone() {
61+
// In `python-build-standalone`, a symlinked interpreter will return its own executable path
62+
// as `sys._base_executable`. Using the symlinked path as the base Python executable is
63+
// incorrect, since it will cause `home` to point to something that is _not_ a Python
64+
// installation.
6865
//
69-
// If we're _not_ in a virtual environment, use the interpreter's executable, since it's
70-
// already a "system Python". We canonicalize the path to ensure that it's real and
71-
// consistent, though we don't expect any symlinks on Windows.
72-
if interpreter.is_virtualenv() {
73-
if let Some(base_executable) = interpreter.sys_base_executable() {
74-
base_executable.to_path_buf()
75-
} else {
76-
// Assume `python.exe`, though the exact executable name is never used (below) on
77-
// Windows, only its parent directory.
78-
interpreter.sys_base_prefix().join("python.exe")
79-
}
80-
} else {
81-
interpreter.sys_executable().to_path_buf()
82-
}
66+
// Instead, we want to fully resolve the symlink to the actual Python executable.
67+
uv_fs::canonicalize_executable(interpreter.sys_executable())?
8368
} else {
84-
unimplemented!("Only Windows and Unix are supported")
69+
std::path::absolute(
70+
interpreter
71+
.sys_base_executable()
72+
.unwrap_or(interpreter.sys_executable()),
73+
)?
8574
};
8675

8776
// Validate the existing location.

0 commit comments

Comments
 (0)