mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +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::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());
|
||||
|
|
|
@ -14646,6 +14646,7 @@ fn lock_explicit_default_index() -> Result<()> {
|
|||
DEBUG Found workspace root: `[TEMP_DIR]/`
|
||||
DEBUG Adding current workspace member: `[TEMP_DIR]/`
|
||||
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 Using request timeout of [TIME]
|
||||
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
|
||||
|
|
|
@ -3349,6 +3349,88 @@ fn run_gui_script_explicit_unix() -> Result<()> {
|
|||
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]
|
||||
#[cfg(not(windows))]
|
||||
fn run_gui_script_explicit_stdin_unix() -> Result<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue