Avoid resolving symbolic links when querying Python interpreters (#11083)

Closes https://github.com/astral-sh/uv/issues/11048

This brings the `PythonEnvironment::from_root` behavior in-line with the
rest of uv Python discovery behavior (and in-line with pip). It's not
clear why we were canonicalizing the path in the first place here.
This commit is contained in:
Zanie Blue 2025-01-30 10:10:33 -06:00 committed by GitHub
parent 586bab32b9
commit d281f49103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 12 deletions

View file

@ -4,6 +4,7 @@ use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::debug;
use uv_cache::Cache;
use uv_cache_key::cache_digest;
use uv_fs::{LockedFile, Simplified};
@ -161,9 +162,13 @@ impl PythonEnvironment {
///
/// N.B. This function also works for system Python environments and users depend on this.
pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
let venv = match fs_err::canonicalize(root.as_ref()) {
Ok(venv) => venv,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
debug!(
"Checking for Python environment at `{}`",
root.as_ref().user_display()
);
match root.as_ref().try_exists() {
Ok(true) => {}
Ok(false) => {
return Err(Error::MissingEnvironment(EnvironmentNotFound {
preference: EnvironmentPreference::Any,
request: PythonRequest::Directory(root.as_ref().to_owned()),
@ -172,30 +177,35 @@ impl PythonEnvironment {
Err(err) => return Err(Error::Discovery(err.into())),
};
if venv.is_file() {
if root.as_ref().is_file() {
return Err(InvalidEnvironment {
path: venv,
path: root.as_ref().to_path_buf(),
kind: InvalidEnvironmentKind::NotDirectory,
}
.into());
}
if venv.read_dir().is_ok_and(|mut dir| dir.next().is_none()) {
if root
.as_ref()
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none())
{
return Err(InvalidEnvironment {
path: venv,
path: root.as_ref().to_path_buf(),
kind: InvalidEnvironmentKind::Empty,
}
.into());
}
let executable = virtualenv_python_executable(&venv);
// Note we do not canonicalize the root path or the executable path, this is important
// because the path the interpreter is invoked at can determine the value of
// `sys.executable`.
let executable = virtualenv_python_executable(&root);
// Check if the executable exists before querying so we can provide a more specific error
// Note we intentionally don't require a resolved link to exist here, we're just trying to
// tell if this _looks_ like a Python environment.
// If we can't find an executable, exit before querying to provide a better error.
if !(executable.is_symlink() || executable.is_file()) {
return Err(InvalidEnvironment {
path: venv,
path: root.as_ref().to_path_buf(),
kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
}
.into());