Skip to content

Commit ee4fd37

Browse files
committed
Preserve symlinks when creating virtual environments
1 parent 1b77055 commit ee4fd37

File tree

15 files changed

+75
-11
lines changed

15 files changed

+75
-11
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-build-frontend/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ doctest = false
1717
workspace = true
1818

1919
[dependencies]
20+
uv-cache = { workspace = true }
2021
uv-configuration = { workspace = true }
2122
uv-distribution = { workspace = true }
2223
uv-distribution-types = { workspace = true }

crates/uv-build-frontend/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use tokio::io::AsyncBufReadExt;
2626
use tokio::process::Command;
2727
use tokio::sync::{Mutex, Semaphore};
2828
use tracing::{debug, info_span, instrument, Instrument};
29-
29+
use uv_cache::Cache;
3030
use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, LowerBound, SourceStrategy};
3131
use uv_distribution::RequiresDist;
3232
use uv_distribution_types::{IndexLocations, Resolution};
@@ -260,6 +260,7 @@ impl SourceBuild {
260260
mut environment_variables: FxHashMap<OsString, OsString>,
261261
level: BuildOutput,
262262
concurrent_builds: usize,
263+
cache: &Cache,
263264
) -> Result<Self, Error> {
264265
let temp_dir = build_context.cache().environment()?;
265266

@@ -306,6 +307,7 @@ impl SourceBuild {
306307
false,
307308
false,
308309
false,
310+
cache,
309311
)?
310312
};
311313

crates/uv-dispatch/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
368368
self.build_extra_env_vars.clone(),
369369
build_output,
370370
self.concurrency.builds,
371+
self.cache,
371372
)
372373
.boxed_local()
373374
.await?;

crates/uv-fs/src/path.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::io;
23
use std::path::{Component, Path, PathBuf};
34
use std::sync::LazyLock;
45

@@ -236,7 +237,7 @@ pub fn normalize_path(path: &Path) -> PathBuf {
236237
}
237238

238239
/// Like `fs_err::canonicalize`, but avoids attempting to resolve symlinks on Windows.
239-
pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
240+
pub fn canonicalize_executable(path: impl AsRef<Path>) -> io::Result<PathBuf> {
240241
let path = path.as_ref();
241242
debug_assert!(
242243
path.is_absolute(),
@@ -250,6 +251,24 @@ pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBu
250251
}
251252
}
252253

254+
/// Resolve a symlink for an executable. Unlike `fs_err::canonicalize`, this does not resolve
255+
/// symlinks recursively.
256+
pub fn read_executable_link(executable: &Path) -> Result<Option<PathBuf>, io::Error> {
257+
let Ok(link) = executable.read_link() else {
258+
return Ok(None);
259+
};
260+
if link.is_absolute() {
261+
Ok(Some(link))
262+
} else {
263+
executable
264+
.parent()
265+
.map(|parent| parent.join(link))
266+
.as_deref()
267+
.map(normalize_absolute_path)
268+
.transpose()
269+
}
270+
}
271+
253272
/// Compute a path describing `path` relative to `base`.
254273
///
255274
/// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py`

crates/uv-tool/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ impl InstalledTools {
241241
&self,
242242
name: &PackageName,
243243
interpreter: Interpreter,
244+
cache: &Cache,
244245
) -> Result<PythonEnvironment, Error> {
245246
let environment_path = self.tool_dir(name);
246247

@@ -270,6 +271,7 @@ impl InstalledTools {
270271
false,
271272
false,
272273
false,
274+
cache,
273275
)?;
274276

275277
Ok(venv)

crates/uv-virtualenv/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ doctest = false
2020
workspace = true
2121

2222
[dependencies]
23+
uv-cache = { workspace = true }
2324
uv-fs = { workspace = true }
2425
uv-platform-tags = { workspace = true }
2526
uv-pypi-types = { workspace = true }

crates/uv-virtualenv/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::io;
22
use std::path::Path;
33

44
use thiserror::Error;
5-
5+
use uv_cache::Cache;
66
use uv_platform_tags::PlatformError;
77
use uv_python::{Interpreter, PythonEnvironment};
88

@@ -12,6 +12,8 @@ mod virtualenv;
1212
pub enum Error {
1313
#[error(transparent)]
1414
Io(#[from] io::Error),
15+
#[error(transparent)]
16+
Interpreter(#[from] uv_python::InterpreterError),
1517
#[error("Failed to determine Python interpreter to use")]
1618
Discovery(#[from] uv_python::DiscoveryError),
1719
#[error("Failed to determine Python interpreter to use")]
@@ -55,6 +57,7 @@ pub fn create_venv(
5557
allow_existing: bool,
5658
relocatable: bool,
5759
seed: bool,
60+
cache: &Cache,
5861
) -> Result<PythonEnvironment, Error> {
5962
// Create the virtualenv at the given location.
6063
let virtualenv = virtualenv::create(
@@ -65,6 +68,7 @@ pub fn create_venv(
6568
allow_existing,
6669
relocatable,
6770
seed,
71+
cache,
6872
)?;
6973

7074
// Create the corresponding `PythonEnvironment`.

crates/uv-virtualenv/src/virtualenv.rs

+31-6
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
use std::env::consts::EXE_SUFFIX;
44
use std::io;
55
use std::io::{BufWriter, Write};
6-
use std::path::Path;
6+
use std::path::{Path, PathBuf};
77

88
use fs_err as fs;
99
use fs_err::File;
1010
use itertools::Itertools;
1111
use tracing::debug;
12-
13-
use uv_fs::{cachedir, Simplified, CWD};
12+
use uv_cache::Cache;
13+
use uv_fs::{cachedir, normalize_absolute_path, Simplified, CWD};
1414
use uv_pypi_types::Scheme;
1515
use uv_python::{Interpreter, VirtualEnvironment};
1616
use uv_version::version;
@@ -52,15 +52,40 @@ pub(crate) fn create(
5252
allow_existing: bool,
5353
relocatable: bool,
5454
seed: bool,
55+
cache: &Cache,
5556
) -> Result<VirtualEnvironment, Error> {
5657
// Determine the base Python executable; that is, the Python executable that should be
5758
// considered the "base" for the virtual environment. This is typically the Python executable
5859
// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
5960
// the base Python executable is the Python executable of the interpreter's base interpreter.
6061
let base_python = if cfg!(unix) {
61-
// On Unix, follow symlinks to resolve the base interpreter, since the Python executable in
62-
// a virtual environment is a symlink to the base interpreter.
63-
uv_fs::canonicalize_executable(interpreter.sys_executable())?
62+
// If we're in virtual environment, resolve symlinks until we find a non-virtual interpreter.
63+
if interpreter.is_virtualenv() {
64+
if let Some(base_executable) =
65+
uv_fs::read_executable_link(interpreter.sys_executable())?
66+
{
67+
let mut base_interpreter = Interpreter::query(base_executable, cache)?;
68+
while base_interpreter.is_virtualenv() {
69+
let Some(base_executable) =
70+
uv_fs::read_executable_link(base_interpreter.sys_executable())?
71+
else {
72+
break;
73+
};
74+
base_interpreter = Interpreter::query(base_executable, cache)?;
75+
}
76+
base_interpreter.sys_executable().to_path_buf()
77+
} else {
78+
// If the interpreter isn't a symlink, use `sys._base_executable` or, as a last
79+
// resort, `sys.executable`.
80+
if let Some(base_executable) = interpreter.sys_base_executable() {
81+
base_executable.to_path_buf()
82+
} else {
83+
interpreter.sys_executable().to_path_buf()
84+
}
85+
}
86+
} else {
87+
interpreter.sys_executable().to_path_buf()
88+
}
6489
} else if cfg!(windows) {
6590
// On Windows, follow `virtualenv`. If we're in a virtual environment, use
6691
// `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with

crates/uv/src/commands/project/environment.rs

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ impl CachedEnvironment {
105105
false,
106106
true,
107107
false,
108+
cache,
108109
)?;
109110

110111
sync_environment(

crates/uv/src/commands/project/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ pub(crate) async fn get_or_init_environment(
593593
false,
594594
false,
595595
false,
596+
cache,
596597
)?)
597598
}
598599
}

crates/uv/src/commands/project/run.rs

+4
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ pub(crate) async fn run(
313313
false,
314314
false,
315315
false,
316+
cache,
316317
)?;
317318

318319
Some(environment.into_interpreter())
@@ -511,6 +512,7 @@ pub(crate) async fn run(
511512
false,
512513
false,
513514
false,
515+
cache,
514516
)?
515517
} else {
516518
// If we're not isolating the environment, reuse the base environment for the
@@ -658,6 +660,7 @@ pub(crate) async fn run(
658660
false,
659661
false,
660662
false,
663+
cache,
661664
)?;
662665
venv.into_interpreter()
663666
} else {
@@ -707,6 +710,7 @@ pub(crate) async fn run(
707710
false,
708711
false,
709712
false,
713+
cache,
710714
)?
711715
}
712716
Some(spec) => {

crates/uv/src/commands/tool/install.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ pub(crate) async fn install(
382382
)
383383
.await?;
384384

385-
let environment = installed_tools.create_environment(&from.name, interpreter)?;
385+
let environment = installed_tools.create_environment(&from.name, interpreter, &cache)?;
386386

387387
// At this point, we removed any existing environment, so we should remove any of its
388388
// executables.

crates/uv/src/commands/tool/upgrade.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ async fn upgrade_tool(
265265
)
266266
.await?;
267267

268-
let environment = installed_tools.create_environment(name, interpreter.clone())?;
268+
let environment = installed_tools.create_environment(name, interpreter.clone(), cache)?;
269269

270270
let environment = sync_environment(
271271
environment,

crates/uv/src/commands/venv.rs

+1
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ async fn venv_impl(
270270
allow_existing,
271271
relocatable,
272272
seed,
273+
cache,
273274
)
274275
.map_err(VenvError::Creation)?;
275276

0 commit comments

Comments
 (0)