diff --git a/crates/uv-installer/src/installer.rs b/crates/uv-installer/src/installer.rs index fb67dbe20113..546bc67278e5 100644 --- a/crates/uv-installer/src/installer.rs +++ b/crates/uv-installer/src/installer.rs @@ -1,8 +1,7 @@ -use std::convert; - use anyhow::{Context, Error, Result}; use install_wheel_rs::{linker::LinkMode, Layout}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use std::convert; use tokio::sync::oneshot; use tracing::instrument; diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index 3449fc5e7532..45d7d3d5373a 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -144,6 +144,16 @@ impl PythonEnvironment { }))) } + /// Create a [`PythonEnvironment`] from an existing [`Interpreter`] with relocatable paths. + #[must_use] + pub fn with_relocatable(self) -> Self { + let inner = Arc::unwrap_or_clone(self.0); + Self(Arc::new(PythonEnvironmentShared { + interpreter: inner.interpreter.with_relocatable(), + ..inner + })) + } + /// Returns the root (i.e., `prefix`) of the Python interpreter. pub fn root(&self) -> &Path { &self.0.root diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index f98b0040aa04..60355c6f81fb 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -45,6 +45,7 @@ pub struct Interpreter { prefix: Option, pointer_size: PointerSize, gil_disabled: bool, + relocatable: bool, } impl Interpreter { @@ -75,6 +76,7 @@ impl Interpreter { tags: OnceLock::new(), target: None, prefix: None, + relocatable: false, }) } @@ -109,6 +111,15 @@ impl Interpreter { }) } + /// Return a new [`Interpreter`] that should be treated as relocatable. + #[must_use] + pub fn with_relocatable(self) -> Self { + Self { + relocatable: true, + ..self + } + } + /// Return the [`Interpreter`] for the base executable, if it's available. /// /// If no such base executable is available, or if the base executable is the same as the @@ -426,6 +437,7 @@ impl Interpreter { }, } }, + relocatable: self.relocatable, } } diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 484bea8d04d8..13f16190bd34 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -5,7 +5,6 @@ use distribution_types::Resolution; use uv_cache::{Cache, CacheBucket}; use uv_client::Connectivity; use uv_configuration::{Concurrency, PreviewMode}; -use uv_fs::{LockedFile, Simplified}; use uv_python::{Interpreter, PythonEnvironment}; use uv_requirements::RequirementsSpecification; @@ -86,57 +85,26 @@ impl CachedEnvironment { // Search in the content-addressed cache. let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash); - // Lock at the interpreter level, to avoid concurrent modification across processes. - fs_err::tokio::create_dir_all(cache_entry.dir()).await?; - let _lock = LockedFile::acquire( - cache_entry.dir().join(".lock"), - cache_entry.dir().user_display(), - )?; - - let ok = cache_entry.path().join(".ok"); - if settings.reinstall.is_none() { - // If the receipt exists, return the environment. - if ok.is_file() { - debug!( - "Reusing cached environment at: `{}`", - cache_entry.path().display() - ); - return Ok(Self(PythonEnvironment::from_root( - cache_entry.path(), - cache, - )?)); - } - } else { - // If the receipt exists, remove it. - match fs_err::tokio::remove_file(&ok).await { - Ok(()) => { - debug!( - "Removed receipt for environment at: `{}`", - cache_entry.path().display() - ); + if let Ok(root) = fs_err::read_link(cache_entry.path()) { + if let Ok(environment) = PythonEnvironment::from_root(root, cache) { + return Ok(Self(environment)); } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), } } - debug!( - "Creating cached environment at: `{}`", - cache_entry.path().display() - ); - + // Create the environment in the cache, then relocate it to its content-addressed location. + let temp_dir = cache.environment()?; let venv = uv_virtualenv::create_venv( - cache_entry.path(), + temp_dir.path(), interpreter, uv_virtualenv::Prompt::None, false, false, false, )?; - - let venv = sync_environment( - venv, + sync_environment( + venv.with_relocatable(), &resolution, settings.as_ref().into(), state, @@ -149,10 +117,13 @@ impl CachedEnvironment { ) .await?; - // Create the receipt, to indicate to future readers that the environment is complete. - fs_err::tokio::File::create(ok).await?; + // Now that the environment is complete, sync it to its content-addressed location. + let id = cache + .persist(temp_dir.into_path(), cache_entry.path()) + .await?; + let root = cache.archive(&id); - Ok(Self(venv)) + Ok(Self(PythonEnvironment::from_root(root, cache)?)) } /// Convert the [`CachedEnvironment`] into an [`Interpreter`]. diff --git a/crates/uv/tests/cache_prune.rs b/crates/uv/tests/cache_prune.rs index fc1c5aefc29b..8ae2600a6523 100644 --- a/crates/uv/tests/cache_prune.rs +++ b/crates/uv/tests/cache_prune.rs @@ -122,6 +122,7 @@ fn prune_cached_env() { DEBUG uv [VERSION] ([COMMIT] DATE) Pruning cache at: [CACHE_DIR]/ DEBUG Removing dangling cache entry: [CACHE_DIR]/environments-v1/[ENTRY] + DEBUG Removing dangling cache entry: [CACHE_DIR]/archive-v0/[ENTRY] Removed [N] files ([SIZE]) "###); }