Avoid canonicalizing user-provided interpreters (#2072)

## Summary

We shouldn't be resolving symlinks on the provided interpreter;
otherwise we break `pyenv`, since running `cargo run pip install mypy
--python .venv/bin/python` will immediately resolve to (e.g.)
`/Users/crmarsh/.pyenv/versions/3.10.2/bin/python3.10`, and pyenv relies
on the path to do its lookups.

Instead, the canonicalizing happens when we query the interpreter
metadata.

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

## Test Plan

Ran `cargo run pip install mypy --python .venv/bin/python -v -n` with a
virtualenv created using a pyenv Python; verified that Mypy was
installed into the virtual environment, rather than into the global
environment.
This commit is contained in:
Charlie Marsh 2024-02-28 21:32:52 -05:00 committed by GitHub
parent ef15098288
commit 1f19ef670b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 35 deletions

View file

@ -1,12 +1,14 @@
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use std::path::{Component, Path, PathBuf};
use std::path::{Path, PathBuf};
use once_cell::sync::Lazy;
use regex::Regex;
use url::{ParseError, Url};
use uv_fs::normalize_path;
/// A wrapper around [`Url`] that preserves the original string.
#[derive(Debug, Clone, Eq, derivative::Derivative)]
#[derivative(PartialEq, Hash)]
@ -60,7 +62,7 @@ impl VerbatimUrl {
};
// Normalize the path.
let path = normalize_path(&path);
let path = normalize_path(path);
// Convert to a URL.
let url = Url::from_file_path(path).expect("path is absolute");
@ -81,7 +83,7 @@ impl VerbatimUrl {
};
// Normalize the path.
let path = normalize_path(&path);
let path = normalize_path(path);
// Convert to a URL.
let url = Url::from_file_path(path).expect("path is absolute");
@ -200,36 +202,6 @@ fn expand_env_vars(s: &str, escape: bool) -> Cow<'_, str> {
})
}
/// Normalize a path, removing things like `.` and `..`.
///
/// Source: <https://github.com/rust-lang/cargo/blob/b48c41aedbd69ee3990d62a0e2006edbb506a480/crates/cargo-util/src/paths.rs#L76C1-L109C2>
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
/// Like [`Url::parse`], but only splits the scheme. Derived from the `url` crate.
pub fn split_scheme(s: &str) -> Option<(&str, &str)> {
/// <https://url.spec.whatwg.org/#c0-controls-and-space>

View file

@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::path::Path;
use std::path::{Component, Path, PathBuf};
pub trait Simplified {
/// Simplify a [`Path`].
@ -46,6 +46,36 @@ pub fn normalize_url_path(path: &str) -> Cow<'_, str> {
}
}
/// Normalize a path, removing things like `.` and `..`.
///
/// Source: <https://github.com/rust-lang/cargo/blob/b48c41aedbd69ee3990d62a0e2006edbb506a480/crates/cargo-util/src/paths.rs#L76C1-L109C2>
pub fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
let mut components = path.as_ref().components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -8,6 +8,7 @@ use tracing::{debug, instrument};
use platform_host::Platform;
use uv_cache::Cache;
use uv_fs::normalize_path;
use crate::{Error, Interpreter};
@ -63,7 +64,8 @@ pub fn find_requested_python(
Interpreter::query(&executable, platform.clone(), cache).map(Some)
} else {
// `-p /home/ferris/.local/bin/python3.10`
let executable = fs_err::canonicalize(request)?;
let executable = normalize_path(request);
Interpreter::query(&executable, platform.clone(), cache).map(Some)
}
}