mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-17 22:07:47 +00:00
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:
parent
586bab32b9
commit
d281f49103
3 changed files with 105 additions and 12 deletions
|
@ -4,6 +4,7 @@ use std::env;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing::debug;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_cache_key::cache_digest;
|
use uv_cache_key::cache_digest;
|
||||||
use uv_fs::{LockedFile, Simplified};
|
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.
|
/// 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> {
|
pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
|
||||||
let venv = match fs_err::canonicalize(root.as_ref()) {
|
debug!(
|
||||||
Ok(venv) => venv,
|
"Checking for Python environment at `{}`",
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
root.as_ref().user_display()
|
||||||
|
);
|
||||||
|
match root.as_ref().try_exists() {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => {
|
||||||
return Err(Error::MissingEnvironment(EnvironmentNotFound {
|
return Err(Error::MissingEnvironment(EnvironmentNotFound {
|
||||||
preference: EnvironmentPreference::Any,
|
preference: EnvironmentPreference::Any,
|
||||||
request: PythonRequest::Directory(root.as_ref().to_owned()),
|
request: PythonRequest::Directory(root.as_ref().to_owned()),
|
||||||
|
@ -172,30 +177,35 @@ impl PythonEnvironment {
|
||||||
Err(err) => return Err(Error::Discovery(err.into())),
|
Err(err) => return Err(Error::Discovery(err.into())),
|
||||||
};
|
};
|
||||||
|
|
||||||
if venv.is_file() {
|
if root.as_ref().is_file() {
|
||||||
return Err(InvalidEnvironment {
|
return Err(InvalidEnvironment {
|
||||||
path: venv,
|
path: root.as_ref().to_path_buf(),
|
||||||
kind: InvalidEnvironmentKind::NotDirectory,
|
kind: InvalidEnvironmentKind::NotDirectory,
|
||||||
}
|
}
|
||||||
.into());
|
.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 {
|
return Err(InvalidEnvironment {
|
||||||
path: venv,
|
path: root.as_ref().to_path_buf(),
|
||||||
kind: InvalidEnvironmentKind::Empty,
|
kind: InvalidEnvironmentKind::Empty,
|
||||||
}
|
}
|
||||||
.into());
|
.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
|
// If we can't find an executable, exit before querying to provide a better 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 !(executable.is_symlink() || executable.is_file()) {
|
if !(executable.is_symlink() || executable.is_file()) {
|
||||||
return Err(InvalidEnvironment {
|
return Err(InvalidEnvironment {
|
||||||
path: venv,
|
path: root.as_ref().to_path_buf(),
|
||||||
kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
|
kind: InvalidEnvironmentKind::MissingExecutable(executable.clone()),
|
||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
|
|
|
@ -14646,6 +14646,7 @@ fn lock_explicit_default_index() -> Result<()> {
|
||||||
DEBUG Found workspace root: `[TEMP_DIR]/`
|
DEBUG Found workspace root: `[TEMP_DIR]/`
|
||||||
DEBUG Adding current workspace member: `[TEMP_DIR]/`
|
DEBUG Adding current workspace member: `[TEMP_DIR]/`
|
||||||
DEBUG Using Python request `>=3.12` from `requires-python` metadata
|
DEBUG Using Python request `>=3.12` from `requires-python` metadata
|
||||||
|
DEBUG Checking for Python environment at `.venv`
|
||||||
DEBUG The virtual environment's Python version satisfies `>=3.12`
|
DEBUG The virtual environment's Python version satisfies `>=3.12`
|
||||||
DEBUG Using request timeout of [TIME]
|
DEBUG Using request timeout of [TIME]
|
||||||
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
|
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
|
||||||
|
|
|
@ -3349,6 +3349,88 @@ fn run_gui_script_explicit_unix() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn run_linked_environment_path() -> Result<()> {
|
||||||
|
use anyhow::Ok;
|
||||||
|
|
||||||
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_virtualenv_bin()
|
||||||
|
.with_filtered_python_names();
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["black"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create a link from `target` -> virtual environment
|
||||||
|
fs_err::os::unix::fs::symlink(&context.venv, context.temp_dir.child("target"))?;
|
||||||
|
|
||||||
|
// Running `uv sync` should use the environment at `target``
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 8 packages in [TIME]
|
||||||
|
Prepared 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ black==24.3.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// `sys.prefix` and `sys.executable` should be from the `target` directory
|
||||||
|
uv_snapshot!(context.filters(), context.run()
|
||||||
|
.env_remove("VIRTUAL_ENV") // Ignore the test context's active virtual environment
|
||||||
|
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target")
|
||||||
|
.arg("python").arg("-c").arg("import sys; print(sys.prefix); print(sys.executable)"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
[TEMP_DIR]/target
|
||||||
|
[TEMP_DIR]/target/[BIN]/python
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 8 packages in [TIME]
|
||||||
|
Audited 6 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// And, similarly, the entrypoint should use `target`
|
||||||
|
let black_entrypoint = context.read("target/bin/black");
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
black_entrypoint, @r###"
|
||||||
|
#![TEMP_DIR]/target/[BIN]/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
from black import patched_main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if sys.argv[0].endswith("-script.pyw"):
|
||||||
|
sys.argv[0] = sys.argv[0][:-11]
|
||||||
|
elif sys.argv[0].endswith(".exe"):
|
||||||
|
sys.argv[0] = sys.argv[0][:-4]
|
||||||
|
sys.exit(patched_main())
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn run_gui_script_explicit_stdin_unix() -> Result<()> {
|
fn run_gui_script_explicit_stdin_unix() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue