mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Merge c3ed8b4467
into f609e1ddaf
This commit is contained in:
commit
f80582e287
26 changed files with 355 additions and 220 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -4676,6 +4676,7 @@ dependencies = [
|
|||
"uv-git-types",
|
||||
"uv-install-wheel",
|
||||
"uv-installer",
|
||||
"uv-lock",
|
||||
"uv-normalize",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
|
@ -4830,11 +4831,11 @@ dependencies = [
|
|||
"tokio",
|
||||
"toml_edit",
|
||||
"tracing",
|
||||
"uv-cache-key",
|
||||
"uv-configuration",
|
||||
"uv-distribution",
|
||||
"uv-distribution-types",
|
||||
"uv-fs",
|
||||
"uv-lock",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-pypi-types",
|
||||
|
@ -4864,6 +4865,7 @@ dependencies = [
|
|||
"uv-dirs",
|
||||
"uv-distribution-types",
|
||||
"uv-fs",
|
||||
"uv-lock",
|
||||
"uv-normalize",
|
||||
"uv-pypi-types",
|
||||
"uv-redacted",
|
||||
|
@ -5249,7 +5251,6 @@ dependencies = [
|
|||
"either",
|
||||
"encoding_rs_io",
|
||||
"fs-err 3.1.1",
|
||||
"fs2",
|
||||
"junction",
|
||||
"path-slash",
|
||||
"percent-encoding",
|
||||
|
@ -5282,6 +5283,7 @@ dependencies = [
|
|||
"uv-cache-key",
|
||||
"uv-fs",
|
||||
"uv-git-types",
|
||||
"uv-lock",
|
||||
"uv-redacted",
|
||||
"uv-static",
|
||||
"uv-version",
|
||||
|
@ -5392,6 +5394,21 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uv-lock"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"fs-err 3.1.1",
|
||||
"fs2",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uv-cache-key",
|
||||
"uv-fs",
|
||||
"uv-state",
|
||||
"uv-static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uv-macros"
|
||||
version = "0.0.1"
|
||||
|
@ -5625,6 +5642,7 @@ dependencies = [
|
|||
"uv-extract",
|
||||
"uv-fs",
|
||||
"uv-install-wheel",
|
||||
"uv-lock",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-platform-tags",
|
||||
|
@ -5889,6 +5907,7 @@ dependencies = [
|
|||
"uv-fs",
|
||||
"uv-install-wheel",
|
||||
"uv-installer",
|
||||
"uv-lock",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-pypi-types",
|
||||
|
|
|
@ -42,6 +42,7 @@ uv-git-types = { path = "crates/uv-git-types" }
|
|||
uv-globfilter = { path = "crates/uv-globfilter" }
|
||||
uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false }
|
||||
uv-installer = { path = "crates/uv-installer" }
|
||||
uv-lock = { path = "crates/uv-lock" }
|
||||
uv-macros = { path = "crates/uv-macros" }
|
||||
uv-metadata = { path = "crates/uv-metadata" }
|
||||
uv-normalize = { path = "crates/uv-normalize" }
|
||||
|
|
|
@ -17,11 +17,11 @@ doctest = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
uv-cache-key = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-fs = { workspace = true }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
|
|
|
@ -27,13 +27,12 @@ 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;
|
||||
use uv_distribution_types::{IndexLocations, Requirement, Resolution};
|
||||
use uv_fs::LockedFile;
|
||||
use uv_fs::{PythonExt, Simplified};
|
||||
use uv_lock::LockedFile;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::PackageName;
|
||||
use uv_pypi_types::VerbatimParsedUrl;
|
||||
|
@ -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}");
|
||||
|
|
|
@ -22,6 +22,7 @@ uv-cache-key = { workspace = true }
|
|||
uv-dirs = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-fs = { workspace = true, features = ["tokio"] }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
uv-redacted = { workspace = true }
|
||||
|
|
|
@ -11,7 +11,9 @@ use tracing::debug;
|
|||
|
||||
pub use archive::ArchiveId;
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_fs::{LockedFile, cachedir, directories};
|
||||
use uv_cache_key::{CacheKey, CacheKeyHasher};
|
||||
use uv_fs::{cachedir, directories};
|
||||
use uv_lock::LockedFile;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pypi_types::ResolutionMetadata;
|
||||
|
||||
|
@ -81,8 +83,7 @@ impl CacheEntry {
|
|||
|
||||
/// Acquire the [`CacheEntry`] as an exclusive lock.
|
||||
pub async fn lock(&self) -> Result<LockedFile, io::Error> {
|
||||
fs_err::create_dir_all(self.dir())?;
|
||||
LockedFile::acquire(self.path(), self.path().display()).await
|
||||
uv_lock::acquire_path(self.path()).await
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,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<LockedFile, io::Error> {
|
||||
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<LockedFile, io::Error> {
|
||||
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<Path> for CacheShard {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fs2::FileExt;
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use tracing::warn;
|
||||
|
||||
pub use crate::path::*;
|
||||
|
||||
|
@ -599,137 +597,6 @@ pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
|
|||
path.as_ref().join("pyvenv.cfg").is_file()
|
||||
}
|
||||
|
||||
/// A file lock that is automatically released when dropped.
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub struct LockedFile(fs_err::File);
|
||||
|
||||
impl LockedFile {
|
||||
/// Inner implementation for [`LockedFile::acquire_blocking`] and [`LockedFile::acquire`].
|
||||
fn lock_file_blocking(file: fs_err::File, resource: &str) -> Result<Self, std::io::Error> {
|
||||
trace!(
|
||||
"Checking lock for `{resource}` at `{}`",
|
||||
file.path().user_display()
|
||||
);
|
||||
match file.file().try_lock_exclusive() {
|
||||
Ok(()) => {
|
||||
debug!("Acquired lock for `{resource}`");
|
||||
Ok(Self(file))
|
||||
}
|
||||
Err(err) => {
|
||||
// Log error code and enum kind to help debugging more exotic failures.
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
debug!("Try lock error: {err:?}");
|
||||
}
|
||||
info!(
|
||||
"Waiting to acquire lock for `{resource}` at `{}`",
|
||||
file.path().user_display(),
|
||||
);
|
||||
file.file().lock_exclusive().map_err(|err| {
|
||||
// Not an fs_err method, we need to build our own path context
|
||||
std::io::Error::other(format!(
|
||||
"Could not acquire lock for `{resource}` at `{}`: {}",
|
||||
file.path().user_display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
debug!("Acquired lock for `{resource}`");
|
||||
Ok(Self(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
path: impl AsRef<Path>,
|
||||
resource: impl Display,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
let file = Self::create(path)?;
|
||||
let resource = resource.to_string();
|
||||
Self::lock_file_blocking(file, &resource)
|
||||
}
|
||||
|
||||
/// Acquire a cross-process lock for a resource using a file at the provided path.
|
||||
#[cfg(feature = "tokio")]
|
||||
pub async fn acquire(
|
||||
path: impl AsRef<Path>,
|
||||
resource: impl Display,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
let file = Self::create(path)?;
|
||||
let resource = resource.to_string();
|
||||
tokio::task::spawn_blocking(move || Self::lock_file_blocking(file, &resource)).await?
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// If path already exists, return it.
|
||||
if let Ok(file) = fs_err::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path.as_ref())
|
||||
{
|
||||
return Ok(file);
|
||||
}
|
||||
|
||||
// Otherwise, create a temporary file with 777 permissions. We must set
|
||||
// permissions _after_ creating the file, to override the `umask`.
|
||||
let file = if let Some(parent) = path.as_ref().parent() {
|
||||
NamedTempFile::new_in(parent)?
|
||||
} else {
|
||||
NamedTempFile::new()?
|
||||
};
|
||||
if let Err(err) = file
|
||||
.as_file()
|
||||
.set_permissions(std::fs::Permissions::from_mode(0o777))
|
||||
{
|
||||
warn!("Failed to set permissions on temporary file: {err}");
|
||||
}
|
||||
|
||||
// Try to move the file to path, but if path exists now, just open path
|
||||
match file.persist_noclobber(path.as_ref()) {
|
||||
Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())),
|
||||
Err(err) => {
|
||||
if err.error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
fs_err::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path.as_ref())
|
||||
} else {
|
||||
Err(err.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn create(path: impl AsRef<Path>) -> std::io::Result<fs_err::File> {
|
||||
fs_err::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockedFile {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = fs2::FileExt::unlock(self.0.file()) {
|
||||
error!(
|
||||
"Failed to unlock {}; program may be stuck: {}",
|
||||
self.0.path().display(),
|
||||
err
|
||||
);
|
||||
} else {
|
||||
debug!("Released lock at `{}`", self.0.path().display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An asynchronous reader that reports progress as bytes are read.
|
||||
#[cfg(feature = "tokio")]
|
||||
pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
|
||||
|
|
|
@ -20,6 +20,7 @@ uv-auth = { workspace = true }
|
|||
uv-cache-key = { workspace = true }
|
||||
uv-fs = { workspace = true, features = ["tokio"] }
|
||||
uv-git-types = { workspace = true }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-redacted = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
uv-version = { workspace = true }
|
||||
|
|
|
@ -5,12 +5,10 @@ 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_fs::LockedFile;
|
||||
use uv_cache_key::RepositoryUrl;
|
||||
use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl};
|
||||
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 {
|
||||
|
|
32
crates/uv-lock/Cargo.toml
Normal file
32
crates/uv-lock/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "uv-lock"
|
||||
version = "0.0.1"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
uv-fs = { workspace = true }
|
||||
uv-state = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
uv-cache-key = { workspace = true }
|
||||
|
||||
fs-err = { workspace = true }
|
||||
fs2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, optional = true}
|
||||
tracing = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tokio = ["dep:tokio", "fs-err/tokio"]
|
227
crates/uv-lock/src/lib.rs
Normal file
227
crates/uv-lock/src/lib.rs
Normal file
|
@ -0,0 +1,227 @@
|
|||
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 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<Path>) -> Result<LockedFile, std::io::Error> {
|
||||
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<T: CacheKey + Display>(
|
||||
resource: T,
|
||||
) -> Result<LockedFile, std::io::Error> {
|
||||
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<FilesystemLocks> = 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)]
|
||||
struct FilesystemLocks {
|
||||
/// The path to the top-level directory of the filesystem locks.
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl FilesystemLocks {
|
||||
/// 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/locks`
|
||||
/// 3. A directory in the local data directory, e.g., `./.uv/locks`
|
||||
fn init() -> Result<Self, std::io::Error> {
|
||||
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 {
|
||||
StateStore::from_settings(None)?.bucket(StateBucket::Locks)
|
||||
};
|
||||
|
||||
// Create the directory, if it doesn't exist.
|
||||
fs::create_dir_all(&root)?;
|
||||
|
||||
// Add a .gitignore.
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(root.join(".gitignore"))
|
||||
{
|
||||
Ok(mut file) => file.write_all(b"*")?,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => (),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
Ok(Self { root })
|
||||
}
|
||||
|
||||
/// Join a path to the root of the locks directory.
|
||||
fn join(&self, path: impl AsRef<Path>) -> PathBuf {
|
||||
self.root.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// A file lock that is automatically released when dropped.
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub struct LockedFile(fs_err::File);
|
||||
|
||||
impl LockedFile {
|
||||
/// Inner implementation for [`LockedFile::acquire_blocking`] and [`LockedFile::acquire`].
|
||||
fn lock_file_blocking(file: fs_err::File, resource: &str) -> Result<Self, std::io::Error> {
|
||||
trace!(
|
||||
"Checking lock for `{resource}` at `{}`",
|
||||
file.path().user_display()
|
||||
);
|
||||
match file.file().try_lock_exclusive() {
|
||||
Ok(()) => {
|
||||
debug!("Acquired lock for `{resource}`");
|
||||
Ok(Self(file))
|
||||
}
|
||||
Err(err) => {
|
||||
// Log error code and enum kind to help debugging more exotic failures.
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
debug!("Try lock error: {err:?}");
|
||||
}
|
||||
info!(
|
||||
"Waiting to acquire lock for `{resource}` at `{}`",
|
||||
file.path().user_display(),
|
||||
);
|
||||
file.file().lock_exclusive().map_err(|err| {
|
||||
// Not an fs_err method, we need to build our own path context
|
||||
std::io::Error::other(format!(
|
||||
"Could not acquire lock for `{resource}` at `{}`: {}",
|
||||
file.path().user_display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
debug!("Acquired lock for `{resource}`");
|
||||
Ok(Self(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[allow(dead_code)]
|
||||
fn acquire_blocking(
|
||||
path: impl AsRef<Path>,
|
||||
resource: impl Display,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
let file = Self::create(path)?;
|
||||
let resource = resource.to_string();
|
||||
Self::lock_file_blocking(file, &resource)
|
||||
}
|
||||
|
||||
/// Acquire a cross-process lock for a resource using a file at the provided path.
|
||||
#[cfg(feature = "tokio")]
|
||||
async fn acquire(
|
||||
path: impl AsRef<Path>,
|
||||
resource: impl Display,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
let file = Self::create(path)?;
|
||||
let resource = resource.to_string();
|
||||
tokio::task::spawn_blocking(move || Self::lock_file_blocking(file, &resource)).await?
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> {
|
||||
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()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path.as_ref())
|
||||
{
|
||||
return Ok(file);
|
||||
}
|
||||
|
||||
// Otherwise, create a temporary file with 777 permissions. We must set
|
||||
// permissions _after_ creating the file, to override the `umask`.
|
||||
let file = if let Some(parent) = path.as_ref().parent() {
|
||||
NamedTempFile::new_in(parent)?
|
||||
} else {
|
||||
NamedTempFile::new()?
|
||||
};
|
||||
if let Err(err) = file
|
||||
.as_file()
|
||||
.set_permissions(std::fs::Permissions::from_mode(0o777))
|
||||
{
|
||||
warn!("Failed to set permissions on temporary file: {err}");
|
||||
}
|
||||
|
||||
// Try to move the file to path, but if path exists now, just open path
|
||||
match file.persist_noclobber(path.as_ref()) {
|
||||
Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())),
|
||||
Err(err) => {
|
||||
if err.error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
fs_err::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path.as_ref())
|
||||
} else {
|
||||
Err(err.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn create(path: impl AsRef<Path>) -> std::io::Result<fs_err::File> {
|
||||
fs_err::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockedFile {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = fs2::FileExt::unlock(self.0.file()) {
|
||||
error!(
|
||||
"Failed to unlock {}; program may be stuck: {}",
|
||||
self.0.path().display(),
|
||||
err
|
||||
);
|
||||
} else {
|
||||
debug!("Released lock at `{}`", self.0.path().display());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ uv-distribution-filename = { workspace = true }
|
|||
uv-extract = { workspace = true }
|
||||
uv-fs = { workspace = true }
|
||||
uv-install-wheel = { workspace = true }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-platform-tags = { workspace = true }
|
||||
|
|
|
@ -8,7 +8,8 @@ use tracing::debug;
|
|||
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_fs::{LockedFile, Simplified};
|
||||
use uv_fs::Simplified;
|
||||
use uv_lock::LockedFile;
|
||||
use uv_pep440::Version;
|
||||
|
||||
use crate::discovery::find_python_installation;
|
||||
|
|
|
@ -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;
|
||||
|
@ -17,8 +17,9 @@ use tracing::{debug, trace, warn};
|
|||
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_cache_key::cache_digest;
|
||||
use uv_fs::{LockedFile, PythonExt, Simplified, write_atomic_sync};
|
||||
use uv_fs::{PythonExt, Simplified, write_atomic_sync};
|
||||
use uv_install_wheel::Layout;
|
||||
use uv_lock::LockedFile;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerEnvironment, StringVersion};
|
||||
use uv_platform_tags::Platform;
|
||||
|
@ -609,27 +610,20 @@ impl Interpreter {
|
|||
|
||||
/// Grab a file lock for the environment to prevent concurrent writes across processes.
|
||||
pub async fn lock(&self) -> Result<LockedFile, io::Error> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ use uv_configuration::PreviewMode;
|
|||
#[cfg(windows)]
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
||||
|
||||
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
|
||||
use uv_fs::{Simplified, replace_symlink, symlink_or_copy_file};
|
||||
use uv_lock::LockedFile;
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
use uv_static::EnvVars;
|
||||
use uv_trampoline_builder::{Launcher, windows_python_launcher};
|
||||
|
@ -124,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<LockedFile, Error> {
|
||||
Ok(LockedFile::acquire(self.root.join(".lock"), self.root.user_display()).await?)
|
||||
Ok(uv_lock::acquire_path(&self.root).await?)
|
||||
}
|
||||
|
||||
/// Prefer, in order:
|
||||
|
|
|
@ -105,6 +105,8 @@ pub enum StateBucket {
|
|||
ManagedPython,
|
||||
/// Installed tools.
|
||||
Tools,
|
||||
/// File-system locks.
|
||||
Locks,
|
||||
}
|
||||
|
||||
impl StateBucket {
|
||||
|
@ -112,6 +114,7 @@ impl StateBucket {
|
|||
match self {
|
||||
Self::ManagedPython => "python",
|
||||
Self::Tools => "tools",
|
||||
Self::Locks => "locks",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,6 +247,9 @@ impl EnvVars {
|
|||
/// Specifies the directory where uv stores managed tools.
|
||||
pub const UV_TOOL_DIR: &'static str = "UV_TOOL_DIR";
|
||||
|
||||
/// Specifies the directory where uv stores filesystem locks.
|
||||
pub const UV_LOCK_DIR: &'static str = "UV_LOCK_DIR";
|
||||
|
||||
/// Specifies the "bin" directory for installing tool executables.
|
||||
pub const UV_TOOL_BIN_DIR: &'static str = "UV_TOOL_BIN_DIR";
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ uv-distribution-types = { workspace = true }
|
|||
uv-fs = { workspace = true }
|
||||
uv-install-wheel = { workspace = true }
|
||||
uv-installer = { workspace = true }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
|
|
|
@ -19,8 +19,9 @@ use uv_install_wheel::read_record_file;
|
|||
pub use receipt::ToolReceipt;
|
||||
pub use tool::{Tool, ToolEntrypoint};
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::{LockedFile, Simplified};
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_lock::LockedFile;
|
||||
use uv_python::{Interpreter, PythonEnvironment};
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
use uv_static::EnvVars;
|
||||
|
@ -144,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<LockedFile, Error> {
|
||||
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.
|
||||
|
|
|
@ -35,6 +35,7 @@ uv-git = { workspace = true }
|
|||
uv-git-types = { workspace = true }
|
||||
uv-install-wheel = { workspace = true, default-features = false }
|
||||
uv-installer = { workspace = true }
|
||||
uv-lock = { workspace = true, features = ["tokio"] }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
|
|
|
@ -27,9 +27,10 @@ use uv_distribution_types::{
|
|||
Index, IndexName, IndexUrl, IndexUrls, NameRequirementSpecification, Requirement,
|
||||
RequirementSource, UnresolvedRequirement, VersionId,
|
||||
};
|
||||
use uv_fs::{LockedFile, Simplified};
|
||||
use uv_fs::Simplified;
|
||||
use uv_git::GIT_STORE;
|
||||
use uv_git_types::GitReference;
|
||||
use uv_lock::LockedFile;
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, PackageName};
|
||||
use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl};
|
||||
use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl};
|
||||
|
|
|
@ -21,9 +21,10 @@ use uv_distribution_types::{
|
|||
Index, Requirement, RequiresPython, Resolution, UnresolvedRequirement,
|
||||
UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::{CWD, LockedFile, Simplified};
|
||||
use uv_fs::{CWD, Simplified};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_lock::LockedFile;
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
||||
use uv_pep508::MarkerTreeContents;
|
||||
|
@ -744,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<LockedFile, std::io::Error> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1012,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, std::io::Error> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -743,6 +743,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());
|
||||
|
|
|
@ -9982,6 +9982,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(
|
||||
|
@ -9994,7 +9995,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 -----
|
||||
|
@ -10009,12 +10010,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 -----
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue