diff --git a/Cargo.lock b/Cargo.lock index a7ecb101c26db..f3e16586e5229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4308,6 +4308,7 @@ dependencies = [ "tokio", "toml_edit", "tracing", + "uv-cache", "uv-configuration", "uv-distribution", "uv-distribution-types", @@ -5270,6 +5271,7 @@ dependencies = [ "pathdiff", "thiserror", "tracing", + "uv-cache", "uv-fs", "uv-platform-tags", "uv-pypi-types", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index a0e8fd89a5606..07694337eda69 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -17,6 +17,7 @@ doctest = false workspace = true [dependencies] +uv-cache = { workspace = true } uv-configuration = { workspace = true } uv-distribution = { workspace = true } uv-distribution-types = { workspace = true } diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index ca7eded393483..58b8d9f339763 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -26,7 +26,7 @@ use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{debug, info_span, instrument, Instrument}; - +use uv_cache::Cache; use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, LowerBound, SourceStrategy}; use uv_distribution::RequiresDist; use uv_distribution_types::{IndexLocations, Resolution}; @@ -260,6 +260,7 @@ impl SourceBuild { mut environment_variables: FxHashMap, level: BuildOutput, concurrent_builds: usize, + cache: &Cache, ) -> Result { let temp_dir = build_context.cache().environment()?; @@ -306,6 +307,7 @@ impl SourceBuild { false, false, false, + cache, )? }; diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 6fc529a2864bf..49879f4ebc8ee 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -368,6 +368,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.build_extra_env_vars.clone(), build_output, self.concurrency.builds, + self.cache, ) .boxed_local() .await?; diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 54941df435408..8c080370f6e8c 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::io; use std::path::{Component, Path, PathBuf}; use std::sync::LazyLock; @@ -236,7 +237,7 @@ pub fn normalize_path(path: &Path) -> PathBuf { } /// Like `fs_err::canonicalize`, but avoids attempting to resolve symlinks on Windows. -pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result { +pub fn canonicalize_executable(path: impl AsRef) -> io::Result { let path = path.as_ref(); debug_assert!( path.is_absolute(), @@ -250,6 +251,24 @@ pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result Result, io::Error> { + let Ok(link) = executable.read_link() else { + return Ok(None); + }; + if link.is_absolute() { + Ok(Some(link)) + } else { + executable + .parent() + .map(|parent| parent.join(link)) + .as_deref() + .map(normalize_absolute_path) + .transpose() + } +} + /// Compute a path describing `path` relative to `base`. /// /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index caa95fc13fbf7..679e824c066a3 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -241,6 +241,7 @@ impl InstalledTools { &self, name: &PackageName, interpreter: Interpreter, + cache: &Cache, ) -> Result { let environment_path = self.tool_dir(name); @@ -270,6 +271,7 @@ impl InstalledTools { false, false, false, + cache, )?; Ok(venv) diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 959a17e203615..4b7ee31089b8b 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -20,6 +20,7 @@ doctest = false workspace = true [dependencies] +uv-cache = { workspace = true } uv-fs = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index 2dd164993e0c0..ef1651a1ae67c 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -2,7 +2,7 @@ use std::io; use std::path::Path; use thiserror::Error; - +use uv_cache::Cache; use uv_platform_tags::PlatformError; use uv_python::{Interpreter, PythonEnvironment}; @@ -12,6 +12,8 @@ mod virtualenv; pub enum Error { #[error(transparent)] Io(#[from] io::Error), + #[error(transparent)] + Interpreter(#[from] uv_python::InterpreterError), #[error("Failed to determine Python interpreter to use")] Discovery(#[from] uv_python::DiscoveryError), #[error("Failed to determine Python interpreter to use")] @@ -55,6 +57,7 @@ pub fn create_venv( allow_existing: bool, relocatable: bool, seed: bool, + cache: &Cache, ) -> Result { // Create the virtualenv at the given location. let virtualenv = virtualenv::create( @@ -65,6 +68,7 @@ pub fn create_venv( allow_existing, relocatable, seed, + cache, )?; // Create the corresponding `PythonEnvironment`. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 70f0ca22e06dc..65a7dc31a59c9 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -3,14 +3,14 @@ use std::env::consts::EXE_SUFFIX; use std::io; use std::io::{BufWriter, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use fs_err as fs; use fs_err::File; use itertools::Itertools; use tracing::debug; - -use uv_fs::{cachedir, Simplified, CWD}; +use uv_cache::Cache; +use uv_fs::{cachedir, normalize_absolute_path, Simplified, CWD}; use uv_pypi_types::Scheme; use uv_python::{Interpreter, VirtualEnvironment}; use uv_version::version; @@ -52,15 +52,40 @@ pub(crate) fn create( allow_existing: bool, relocatable: bool, seed: bool, + cache: &Cache, ) -> Result { // Determine the base Python executable; that is, the Python executable that should be // considered the "base" for the virtual environment. This is typically the Python executable // from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then // the base Python executable is the Python executable of the interpreter's base interpreter. let base_python = if cfg!(unix) { - // On Unix, follow symlinks to resolve the base interpreter, since the Python executable in - // a virtual environment is a symlink to the base interpreter. - uv_fs::canonicalize_executable(interpreter.sys_executable())? + // If we're in virtual environment, resolve symlinks until we find a non-virtual interpreter. + if interpreter.is_virtualenv() { + if let Some(base_executable) = + uv_fs::read_executable_link(interpreter.sys_executable())? + { + let mut base_interpreter = Interpreter::query(base_executable, cache)?; + while base_interpreter.is_virtualenv() { + let Some(base_executable) = + uv_fs::read_executable_link(base_interpreter.sys_executable())? + else { + break; + }; + base_interpreter = Interpreter::query(base_executable, cache)?; + } + base_interpreter.sys_executable().to_path_buf() + } else { + // If the interpreter isn't a symlink, use `sys._base_executable` or, as a last + // resort, `sys.executable`. + if let Some(base_executable) = interpreter.sys_base_executable() { + base_executable.to_path_buf() + } else { + interpreter.sys_executable().to_path_buf() + } + } + } else { + interpreter.sys_executable().to_path_buf() + } } else if cfg!(windows) { // On Windows, follow `virtualenv`. If we're in a virtual environment, use // `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index c49711576174d..f3b144ca9c5c4 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -105,6 +105,7 @@ impl CachedEnvironment { false, true, false, + cache, )?; sync_environment( diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index ed6fd4a01ab59..771677e8a2e7d 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -593,6 +593,7 @@ pub(crate) async fn get_or_init_environment( false, false, false, + cache, )?) } } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 38894f4c55de6..649ff9e33d7ac 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -313,6 +313,7 @@ pub(crate) async fn run( false, false, false, + cache, )?; Some(environment.into_interpreter()) @@ -511,6 +512,7 @@ pub(crate) async fn run( false, false, false, + cache, )? } else { // If we're not isolating the environment, reuse the base environment for the @@ -658,6 +660,7 @@ pub(crate) async fn run( false, false, false, + cache, )?; venv.into_interpreter() } else { @@ -707,6 +710,7 @@ pub(crate) async fn run( false, false, false, + cache, )? } Some(spec) => { diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index adb6f884ef3e0..5b4722cb3eeb7 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -382,7 +382,7 @@ pub(crate) async fn install( ) .await?; - let environment = installed_tools.create_environment(&from.name, interpreter)?; + let environment = installed_tools.create_environment(&from.name, interpreter, &cache)?; // At this point, we removed any existing environment, so we should remove any of its // executables. diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 2296c62f88b6c..42580db5e5b6a 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -265,7 +265,7 @@ async fn upgrade_tool( ) .await?; - let environment = installed_tools.create_environment(name, interpreter.clone())?; + let environment = installed_tools.create_environment(name, interpreter.clone(), cache)?; let environment = sync_environment( environment, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 0900eacbaf36f..a1ff9fcaefcdf 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -270,6 +270,7 @@ async fn venv_impl( allow_existing, relocatable, seed, + cache, ) .map_err(VenvError::Creation)?;