diff --git a/Cargo.lock b/Cargo.lock index 0d1654dc8..a47c0cfbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4831,7 +4831,6 @@ dependencies = [ "tokio", "toml_edit", "tracing", - "uv-cache-key", "uv-configuration", "uv-distribution", "uv-distribution-types", @@ -5252,7 +5251,6 @@ dependencies = [ "either", "encoding_rs_io", "fs-err 3.1.1", - "fs2", "junction", "path-slash", "percent-encoding", @@ -5405,6 +5403,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "uv-cache-key", "uv-fs", "uv-state", "uv-static", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index b47abef42..765bea4b5 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -17,7 +17,6 @@ doctest = false workspace = true [dependencies] -uv-cache-key = { 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 1d3f02e74..83c35a10c 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -27,7 +27,6 @@ use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, warn}; -use uv_cache_key::cache_digest; use uv_configuration::PreviewMode; use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution::BuildRequires; @@ -451,12 +450,7 @@ impl SourceBuild { let mut source_tree_lock = None; if self.pep517_backend.is_setuptools() { debug!("Locking the source tree for setuptools"); - let canonical_source_path = self.source_tree.canonicalize()?; - let lock_path = env::temp_dir().join(format!( - "uv-setuptools-{}.lock", - cache_digest(&canonical_source_path) - )); - source_tree_lock = LockedFile::acquire(lock_path, self.source_tree.to_string_lossy()) + source_tree_lock = uv_lock::acquire_path(&self.source_tree) .await .inspect_err(|err| { warn!("Failed to acquire build lock: {err}"); diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index f6db6b86d..14362cdc0 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -11,6 +11,7 @@ use tracing::debug; pub use archive::ArchiveId; use uv_cache_info::Timestamp; +use uv_cache_key::{CacheKey, CacheKeyHasher}; use uv_fs::{cachedir, directories}; use uv_lock::LockedFile; use uv_normalize::PackageName; @@ -82,8 +83,7 @@ impl CacheEntry { /// Acquire the [`CacheEntry`] as an exclusive lock. pub async fn lock(&self) -> Result { - fs_err::create_dir_all(self.dir())?; - LockedFile::acquire(self.path(), self.path().display()).await + uv_lock::acquire_path(self.path()).await } } @@ -109,16 +109,27 @@ impl CacheShard { Self(self.0.join(dir.as_ref())) } - /// Acquire the cache entry as an exclusive lock. - pub async fn lock(&self) -> Result { - fs_err::create_dir_all(self.as_ref())?; - LockedFile::acquire(self.join(".lock"), self.display()).await + /// Return the path to the [`CacheShard`]. + #[inline] + pub fn path(&self) -> &Path { + &self.0 } /// Return the [`CacheShard`] as a [`PathBuf`]. pub fn into_path_buf(self) -> PathBuf { self.0 } + + /// Acquire the cache entry as an exclusive lock. + pub async fn lock(&self) -> Result { + uv_lock::acquire_path(self.path()).await + } +} + +impl CacheKey for CacheShard { + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.0.cache_key(state); + } } impl AsRef for CacheShard { diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index fba4910e6..66903bdb8 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -20,7 +20,6 @@ dunce = { workspace = true } either = { workspace = true } encoding_rs_io = { workspace = true } fs-err = { workspace = true } -fs2 = { workspace = true } path-slash = { workspace = true } percent-encoding = { workspace = true } same-file = { workspace = true } diff --git a/crates/uv-git/src/resolver.rs b/crates/uv-git/src/resolver.rs index 823edd2c6..4683d3cd3 100644 --- a/crates/uv-git/src/resolver.rs +++ b/crates/uv-git/src/resolver.rs @@ -5,13 +5,11 @@ use std::sync::Arc; use dashmap::DashMap; use dashmap::mapref::one::Ref; -use fs_err::tokio as fs; use reqwest_middleware::ClientWithMiddleware; use tracing::debug; -use uv_cache_key::{RepositoryUrl, cache_digest}; +use uv_cache_key::RepositoryUrl; use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; -use uv_lock::LockedFile; use uv_static::EnvVars; use uv_version::version; @@ -164,14 +162,8 @@ impl GitResolver { }; // Avoid races between different processes, too. - let lock_dir = cache.join("locks"); - fs::create_dir_all(&lock_dir).await?; let repository_url = RepositoryUrl::new(url.repository()); - let _lock = LockedFile::acquire( - lock_dir.join(cache_digest(&repository_url)), - &repository_url, - ) - .await?; + let _lock = uv_lock::acquire_resource(repository_url).await?; // Fetch the Git repository. let source = if let Some(reporter) = reporter { diff --git a/crates/uv-lock/Cargo.toml b/crates/uv-lock/Cargo.toml index 2ac86abb0..b719a2d26 100644 --- a/crates/uv-lock/Cargo.toml +++ b/crates/uv-lock/Cargo.toml @@ -19,6 +19,7 @@ workspace = true uv-fs = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } +uv-cache-key = { workspace = true } fs-err = { workspace = true } fs2 = { workspace = true } diff --git a/crates/uv-lock/src/lib.rs b/crates/uv-lock/src/lib.rs index ff4b798ea..c4b2d59c1 100644 --- a/crates/uv-lock/src/lib.rs +++ b/crates/uv-lock/src/lib.rs @@ -1,60 +1,76 @@ -use fs_err as fs; use std::fmt::Display; use std::io::Write; - use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +use fs_err as fs; use fs2::FileExt; -use tempfile::NamedTempFile; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, error, info, trace}; +use uv_cache_key::{CacheKey, cache_digest}; use uv_fs::Simplified; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; +/// Acquire a cross-process lock for files at the provided path. +#[cfg(feature = "tokio")] +pub async fn acquire_path(path: impl AsRef) -> Result { + let locks = get_or_init_locks()?; + let path = path.as_ref(); + LockedFile::acquire(locks.join(cache_digest(&path)), path.display()).await +} + +/// Acquire a cross-process lock for an arbitrary hashable resource (like a URL). +#[cfg(feature = "tokio")] +pub async fn acquire_resource( + resource: T, +) -> Result { + let locks = get_or_init_locks()?; + LockedFile::acquire(locks.join(cache_digest(&resource)), resource).await +} + +/// Get or initialize the global filesystem locks. +fn get_or_init_locks() -> std::io::Result<&'static FilesystemLocks> { + static FILESYSTEM_LOCKS: OnceLock = OnceLock::new(); + + // Return the existing filesystem locks, if they are already initialized. + if let Some(locks) = FILESYSTEM_LOCKS.get() { + return Ok(locks); + } + + // Initialize the filesystem locks. + let locks = FilesystemLocks::init()?; + _ = FILESYSTEM_LOCKS.set(locks); + + Ok(FILESYSTEM_LOCKS.get().unwrap()) +} + /// Filesystem locks used to synchronize access to shared resources across processes. #[derive(Debug, Clone)] -pub struct FilesystemLocks { +struct FilesystemLocks { /// The path to the top-level directory of the filesystem locks. root: PathBuf, } impl FilesystemLocks { - /// A directory for filesystem locks at `root`. - fn from_path(root: impl Into) -> Self { - Self { root: root.into() } - } - - /// Create a new [`FilesystemLocks`] from settings. + /// Create a new [`FilesystemLocks`]. /// /// Prefer, in order: /// /// 1. The specific tool directory specified by the user, i.e., `UV_LOCK_DIR` - /// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/tools` - /// 3. A directory in the local data directory, e.g., `./.uv/tools` - pub fn from_settings() -> Result { - if let Some(lock_dir) = std::env::var_os(EnvVars::UV_LOCK_DIR).filter(|s| !s.is_empty()) { - Ok(Self::from_path(std::path::absolute(lock_dir)?)) + /// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/locks` + /// 3. A directory in the local data directory, e.g., `./.uv/locks` + fn init() -> Result { + let root = if let Some(lock_dir) = + std::env::var_os(EnvVars::UV_LOCK_DIR).filter(|s| !s.is_empty()) + { + std::path::absolute(lock_dir)? } else { - Ok(Self::from_path( - StateStore::from_settings(None)?.bucket(StateBucket::Locks), - )) - } - } + StateStore::from_settings(None)?.bucket(StateBucket::Locks) + }; - /// Create a temporary directory. - pub fn temp() -> Result { - Ok(Self::from_path( - StateStore::temp()?.bucket(StateBucket::Locks), - )) - } - - /// Initialize the directory. - pub fn init(self) -> Result { - let root = &self.root; - - // Create the tools directory, if it doesn't exist. - fs::create_dir_all(root)?; + // Create the directory, if it doesn't exist. + fs::create_dir_all(&root)?; // Add a .gitignore. match fs::OpenOptions::new() @@ -67,12 +83,12 @@ impl FilesystemLocks { Err(err) => return Err(err), } - Ok(self) + Ok(Self { root }) } - /// Return the path of the directory. - pub fn root(&self) -> &Path { - &self.root + /// Join a path to the root of the locks directory. + fn join(&self, path: impl AsRef) -> PathBuf { + self.root.join(path) } } @@ -120,7 +136,8 @@ impl LockedFile { /// The same as [`LockedFile::acquire`], but for synchronous contexts. Do not use from an async /// context, as this can block the runtime while waiting for another process to release the /// lock. - pub fn acquire_blocking( + #[allow(dead_code)] + fn acquire_blocking( path: impl AsRef, resource: impl Display, ) -> Result { @@ -131,7 +148,7 @@ impl LockedFile { /// Acquire a cross-process lock for a resource using a file at the provided path. #[cfg(feature = "tokio")] - pub async fn acquire( + async fn acquire( path: impl AsRef, resource: impl Display, ) -> Result { @@ -143,6 +160,8 @@ impl LockedFile { #[cfg(unix)] fn create(path: impl AsRef) -> Result { use std::os::unix::fs::PermissionsExt; + use tempfile::NamedTempFile; + use tracing::warn; // If path already exists, return it. if let Ok(file) = fs_err::OpenOptions::new() diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 73c4a1812..cfea78a29 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -1,10 +1,10 @@ use std::borrow::Cow; use std::env::consts::ARCH; use std::fmt::{Display, Formatter}; +use std::io; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::sync::OnceLock; -use std::{env, io}; use configparser::ini::Ini; use fs_err as fs; @@ -610,27 +610,20 @@ impl Interpreter { /// Grab a file lock for the environment to prevent concurrent writes across processes. pub async fn lock(&self) -> Result { - if let Some(target) = self.target() { + let path = if let Some(target) = self.target() { // If we're installing into a `--target`, use a target-specific lockfile. - LockedFile::acquire(target.root().join(".lock"), target.root().user_display()).await + target.root() } else if let Some(prefix) = self.prefix() { // Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile. - LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display()).await + prefix.root() } else if self.is_virtualenv() { // If the environment a virtualenv, use a virtualenv-specific lockfile. - LockedFile::acquire( - self.sys_prefix.join(".lock"), - self.sys_prefix.user_display(), - ) - .await + &self.sys_prefix } else { // Otherwise, use a global lockfile. - LockedFile::acquire( - env::temp_dir().join(format!("uv-{}.lock", cache_digest(&self.sys_executable))), - self.sys_prefix.user_display(), - ) - .await - } + &self.sys_executable + }; + uv_lock::acquire_path(path).await } } diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index f7640f60a..18ab9a43f 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -125,7 +125,7 @@ impl ManagedPythonInstallations { /// Grab a file lock for the managed Python distribution directory to prevent concurrent access /// across processes. pub async fn lock(&self) -> Result { - Ok(LockedFile::acquire(self.root.join(".lock"), self.root.user_display()).await?) + Ok(uv_lock::acquire_path(&self.root).await?) } /// Prefer, in order: diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 71974cfc4..e4cde0b8b 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -145,7 +145,7 @@ impl InstalledTools { /// Grab a file lock for the tools directory to prevent concurrent access across processes. pub async fn lock(&self) -> Result { - Ok(LockedFile::acquire(self.root.join(".lock"), self.root.user_display()).await?) + Ok(uv_lock::acquire_path(&self.root).await?) } /// Add a receipt for a tool. diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 8a0a8222c..b555bb102 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -745,27 +745,9 @@ impl ScriptInterpreter { /// Grab a file lock for the script to prevent concurrent writes across processes. pub(crate) async fn lock(script: Pep723ItemRef<'_>) -> Result { match script { - Pep723ItemRef::Script(script) => { - LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&script.path))), - script.path.simplified_display(), - ) - .await - } - Pep723ItemRef::Remote(.., url) => { - LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(url))), - url.to_string(), - ) - .await - } - Pep723ItemRef::Stdin(metadata) => { - LockedFile::acquire( - std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&metadata.raw))), - "stdin".to_string(), - ) - .await - } + Pep723ItemRef::Script(script) => uv_lock::acquire_path(&script.path).await, + Pep723ItemRef::Remote(.., url) => uv_lock::acquire_resource(&url).await, + Pep723ItemRef::Stdin(..) => uv_lock::acquire_resource("stdin").await, } } } @@ -1013,14 +995,7 @@ impl ProjectInterpreter { /// Grab a file lock for the environment to prevent concurrent writes across processes. pub(crate) async fn lock(workspace: &Workspace) -> Result { - LockedFile::acquire( - std::env::temp_dir().join(format!( - "uv-{}.lock", - cache_digest(workspace.install_path()) - )), - workspace.install_path().simplified_display(), - ) - .await + uv_lock::acquire_path(workspace.install_path()).await } } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4c411899c..071fa3e1e 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -741,6 +741,7 @@ impl TestContext { .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1") .env_remove(EnvVars::UV_CACHE_DIR) .env_remove(EnvVars::UV_TOOL_BIN_DIR) + .env_remove(EnvVars::UV_LOCK_DIR) .env_remove(EnvVars::XDG_CONFIG_HOME) .env_remove(EnvVars::XDG_DATA_HOME) .current_dir(self.temp_dir.path()); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index a97775d9f..804b08088 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9997,6 +9997,7 @@ fn read_only() -> Result<()> { use std::os::unix::fs::PermissionsExt; let context = TestContext::new("3.12"); + let lock_dir = context.temp_dir.child("locks"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -10009,7 +10010,7 @@ fn read_only() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_LOCK_DIR, lock_dir.as_os_str()), @r###" success: true exit_code: 0 ----- stdout ----- @@ -10024,12 +10025,13 @@ fn read_only() -> Result<()> { assert!(context.temp_dir.child("uv.lock").exists()); // Remove the flock. - fs_err::remove_file(context.venv.child(".lock"))?; + fs_err::remove_dir_all(&lock_dir)?; + fs_err::create_dir_all(&lock_dir)?; - // Make the virtual environment read and execute (but not write). - fs_err::set_permissions(&context.venv, std::fs::Permissions::from_mode(0o555))?; + // Make the lock directory read and execute (but not write). + fs_err::set_permissions(&lock_dir, std::fs::Permissions::from_mode(0o555))?; - uv_snapshot!(context.filters(), context.sync(), @r" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_LOCK_DIR, lock_dir.as_os_str()), @r" success: true exit_code: 0 ----- stdout ----- diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 61889ddb3..63a188219 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -173,6 +173,10 @@ a link mode. Equivalent to the `--locked` command-line argument. If set, uv will assert that the `uv.lock` remains unchanged. +### `UV_LOCK_DIR` + +Specifies the directory where uv stores filesystem locks. + ### `UV_LOG_CONTEXT` Add additional context and structure to log messages.