Skip to content

Commit

Permalink
Add relocatable installs to support concurrency-safe cached environments
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jul 29, 2024
1 parent cb47aed commit ed464d0
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 45 deletions.
3 changes: 1 addition & 2 deletions crates/uv-installer/src/installer.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
10 changes: 10 additions & 0 deletions crates/uv-python/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Interpreter {
prefix: Option<Prefix>,
pointer_size: PointerSize,
gil_disabled: bool,
relocatable: bool,
}

impl Interpreter {
Expand Down Expand Up @@ -75,6 +76,7 @@ impl Interpreter {
tags: OnceLock::new(),
target: None,
prefix: None,
relocatable: false,
})
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -426,6 +437,7 @@ impl Interpreter {
},
}
},
relocatable: self.relocatable,
}
}

Expand Down
57 changes: 14 additions & 43 deletions crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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`].
Expand Down
1 change: 1 addition & 0 deletions crates/uv/tests/cache_prune.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
"###);
}
Expand Down

0 comments on commit ed464d0

Please sign in to comment.