mirror of https://github.com/astral-sh/uv
Support creating lock files on ExFAT on MacOS (#17115)
## Summary Fix #16859 by falling back to simply creating the lock file and then attempting to apply permissions in cases where the temporary lockfile cannot be renamed without overwriting (persist_noclobber) due to lack of underlying support from the filesystem. I've also improved the error handling. ## Test Plan Manually on MacOS with an ExFAT partition. ~~~ bash session $ hdiutil create -size 1g -fs ExFAT -volname EXFATDISK exfat.dmg $ hdiutil attach exfat.dmg $ cd /Volumes/EXFATDISK $ uv init --bare --cache-dir build/uv/cache -v ~~~
This commit is contained in:
parent
a2d64aa224
commit
d20948bec2
|
|
@ -5613,7 +5613,6 @@ dependencies = [
|
||||||
"uv-client",
|
"uv-client",
|
||||||
"uv-distribution-filename",
|
"uv-distribution-filename",
|
||||||
"uv-extract",
|
"uv-extract",
|
||||||
"uv-fs",
|
|
||||||
"uv-pep440",
|
"uv-pep440",
|
||||||
"uv-platform",
|
"uv-platform",
|
||||||
"uv-redacted",
|
"uv-redacted",
|
||||||
|
|
@ -5721,6 +5720,7 @@ dependencies = [
|
||||||
"same-file",
|
"same-file",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror 2.0.17",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uv-cache-info",
|
"uv-cache-info",
|
||||||
"uv-cache-key",
|
"uv-cache-key",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ uv-cache = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-distribution-filename = { workspace = true }
|
uv-distribution-filename = { workspace = true }
|
||||||
uv-extract = { workspace = true }
|
uv-extract = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
|
||||||
uv-pep440 = { workspace = true }
|
uv-pep440 = { workspace = true }
|
||||||
uv-platform = { workspace = true }
|
uv-platform = { workspace = true }
|
||||||
uv-redacted = { workspace = true }
|
uv-redacted = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,9 @@ use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uv_distribution_filename::SourceDistExtension;
|
use uv_distribution_filename::SourceDistExtension;
|
||||||
|
|
||||||
use uv_cache::{Cache, CacheBucket, CacheEntry};
|
use uv_cache::{Cache, CacheBucket, CacheEntry, Error as CacheError};
|
||||||
use uv_client::{BaseClient, is_transient_network_error};
|
use uv_client::{BaseClient, is_transient_network_error};
|
||||||
use uv_extract::{Error as ExtractError, stream};
|
use uv_extract::{Error as ExtractError, stream};
|
||||||
use uv_fs::LockedFileError;
|
|
||||||
use uv_pep440::Version;
|
use uv_pep440::Version;
|
||||||
use uv_platform::Platform;
|
use uv_platform::Platform;
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
|
|
@ -137,7 +136,7 @@ pub enum Error {
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
LockedFile(#[from] LockedFileError),
|
Cache(#[from] CacheError),
|
||||||
|
|
||||||
#[error("Failed to detect platform")]
|
#[error("Failed to detect platform")]
|
||||||
Platform(#[from] uv_platform::Error),
|
Platform(#[from] uv_platform::Error),
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,6 @@ rustc-hash = { workspace = true }
|
||||||
same-file = { workspace = true }
|
same-file = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
walkdir = { workspace = true }
|
walkdir = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,17 @@ mod wheel;
|
||||||
/// Must be kept in-sync with the version in [`CacheBucket::to_str`].
|
/// Must be kept in-sync with the version in [`CacheBucket::to_str`].
|
||||||
pub const ARCHIVE_VERSION: u8 = 0;
|
pub const ARCHIVE_VERSION: u8 = 0;
|
||||||
|
|
||||||
|
/// Error locking a cache entry or shard
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error("Could not make the path absolute")]
|
||||||
|
Absolute(#[source] io::Error),
|
||||||
|
#[error("Could not acquire lock")]
|
||||||
|
Acquire(#[from] LockedFileError),
|
||||||
|
}
|
||||||
|
|
||||||
/// A [`CacheEntry`] which may or may not exist yet.
|
/// A [`CacheEntry`] which may or may not exist yet.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CacheEntry(PathBuf);
|
pub struct CacheEntry(PathBuf);
|
||||||
|
|
@ -80,14 +91,14 @@ impl CacheEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acquire the [`CacheEntry`] as an exclusive lock.
|
/// Acquire the [`CacheEntry`] as an exclusive lock.
|
||||||
pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
|
pub async fn lock(&self) -> Result<LockedFile, Error> {
|
||||||
fs_err::create_dir_all(self.dir())?;
|
fs_err::create_dir_all(self.dir())?;
|
||||||
LockedFile::acquire(
|
Ok(LockedFile::acquire(
|
||||||
self.path(),
|
self.path(),
|
||||||
LockedFileMode::Exclusive,
|
LockedFileMode::Exclusive,
|
||||||
self.path().display(),
|
self.path().display(),
|
||||||
)
|
)
|
||||||
.await
|
.await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,14 +125,14 @@ impl CacheShard {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acquire the cache entry as an exclusive lock.
|
/// Acquire the cache entry as an exclusive lock.
|
||||||
pub async fn lock(&self) -> Result<LockedFile, LockedFileError> {
|
pub async fn lock(&self) -> Result<LockedFile, Error> {
|
||||||
fs_err::create_dir_all(self.as_ref())?;
|
fs_err::create_dir_all(self.as_ref())?;
|
||||||
LockedFile::acquire(
|
Ok(LockedFile::acquire(
|
||||||
self.join(".lock"),
|
self.join(".lock"),
|
||||||
LockedFileMode::Exclusive,
|
LockedFileMode::Exclusive,
|
||||||
self.display(),
|
self.display(),
|
||||||
)
|
)
|
||||||
.await
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`CacheShard`] as a [`PathBuf`].
|
/// Return the [`CacheShard`] as a [`PathBuf`].
|
||||||
|
|
@ -391,7 +402,7 @@ impl Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate the cache scaffold.
|
/// Populate the cache scaffold.
|
||||||
fn create_base_files(root: &PathBuf) -> Result<(), io::Error> {
|
fn create_base_files(root: &PathBuf) -> io::Result<()> {
|
||||||
// Create the cache directory, if it doesn't exist.
|
// Create the cache directory, if it doesn't exist.
|
||||||
fs_err::create_dir_all(root)?;
|
fs_err::create_dir_all(root)?;
|
||||||
|
|
||||||
|
|
@ -441,7 +452,7 @@ impl Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the [`Cache`].
|
/// Initialize the [`Cache`].
|
||||||
pub async fn init(self) -> Result<Self, LockedFileError> {
|
pub async fn init(self) -> Result<Self, Error> {
|
||||||
let root = &self.root;
|
let root = &self.root;
|
||||||
|
|
||||||
Self::create_base_files(root)?;
|
Self::create_base_files(root)?;
|
||||||
|
|
@ -466,18 +477,18 @@ impl Cache {
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root: std::path::absolute(root)?,
|
root: std::path::absolute(root).map_err(Error::Absolute)?,
|
||||||
lock_file,
|
lock_file,
|
||||||
..self
|
..self
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the [`Cache`], assuming that there are no other uv processes running.
|
/// Initialize the [`Cache`], assuming that there are no other uv processes running.
|
||||||
pub fn init_no_wait(self) -> Result<Option<Self>, io::Error> {
|
pub fn init_no_wait(self) -> Result<Option<Self>, Error> {
|
||||||
let root = &self.root;
|
let root = &self.root;
|
||||||
|
|
||||||
Self::create_base_files(root)?;
|
Self::create_base_files(root)?;
|
||||||
|
|
@ -491,7 +502,7 @@ impl Cache {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
root: std::path::absolute(root)?,
|
root: std::path::absolute(root).map_err(Error::Absolute)?,
|
||||||
lock_file: Some(Arc::new(lock_file)),
|
lock_file: Some(Arc::new(lock_file)),
|
||||||
..self
|
..self
|
||||||
}))
|
}))
|
||||||
|
|
@ -531,7 +542,7 @@ impl Cache {
|
||||||
/// Remove a package from the cache.
|
/// Remove a package from the cache.
|
||||||
///
|
///
|
||||||
/// Returns the number of entries removed from the cache.
|
/// Returns the number of entries removed from the cache.
|
||||||
pub fn remove(&self, name: &PackageName) -> Result<Removal, io::Error> {
|
pub fn remove(&self, name: &PackageName) -> io::Result<Removal> {
|
||||||
// Collect the set of referenced archives.
|
// Collect the set of referenced archives.
|
||||||
let references = self.find_archive_references()?;
|
let references = self.find_archive_references()?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use std::fmt::{Display, Formatter};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use uv_cache::Error as CacheError;
|
||||||
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
||||||
use uv_fs::LockedFileError;
|
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
|
|
||||||
|
|
@ -339,7 +339,7 @@ pub enum ErrorKind {
|
||||||
CacheWrite(#[source] std::io::Error),
|
CacheWrite(#[source] std::io::Error),
|
||||||
|
|
||||||
#[error("Failed to acquire lock on the client cache")]
|
#[error("Failed to acquire lock on the client cache")]
|
||||||
CacheLock(#[source] LockedFileError),
|
CacheLock(#[source] CacheError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ use tokio::task::JoinError;
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
|
|
||||||
use crate::metadata::MetadataError;
|
use crate::metadata::MetadataError;
|
||||||
|
use uv_cache::Error as CacheError;
|
||||||
use uv_client::WrappedReqwestError;
|
use uv_client::WrappedReqwestError;
|
||||||
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
||||||
use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError};
|
use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError};
|
||||||
use uv_fs::{LockedFileError, Simplified};
|
use uv_fs::Simplified;
|
||||||
use uv_git::GitError;
|
use uv_git::GitError;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
|
|
@ -42,7 +43,7 @@ pub enum Error {
|
||||||
#[error("Failed to write to the distribution cache")]
|
#[error("Failed to write to the distribution cache")]
|
||||||
CacheWrite(#[source] std::io::Error),
|
CacheWrite(#[source] std::io::Error),
|
||||||
#[error("Failed to acquire lock on the distribution cache")]
|
#[error("Failed to acquire lock on the distribution cache")]
|
||||||
CacheLock(#[source] LockedFileError),
|
CacheLock(#[source] CacheError),
|
||||||
#[error("Failed to deserialize cache entry")]
|
#[error("Failed to deserialize cache entry")]
|
||||||
CacheDecode(#[from] rmp_serde::decode::Error),
|
CacheDecode(#[from] rmp_serde::decode::Error),
|
||||||
#[error("Failed to serialize cache entry")]
|
#[error("Failed to serialize cache entry")]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::convert::Into;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
@ -59,10 +60,18 @@ pub enum LockedFileError {
|
||||||
source: io::Error,
|
source: io::Error,
|
||||||
},
|
},
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] io::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
#[cfg(feature = "tokio")]
|
#[cfg(feature = "tokio")]
|
||||||
JoinError(#[from] tokio::task::JoinError),
|
JoinError(#[from] tokio::task::JoinError),
|
||||||
|
#[error("Could not create temporary file")]
|
||||||
|
CreateTemporary(#[source] io::Error),
|
||||||
|
#[error("Could not persist temporary file `{}`", path.user_display())]
|
||||||
|
PersistTemporary {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockedFileError {
|
impl LockedFileError {
|
||||||
|
|
@ -72,6 +81,8 @@ impl LockedFileError {
|
||||||
#[cfg(feature = "tokio")]
|
#[cfg(feature = "tokio")]
|
||||||
Self::JoinError(_) => None,
|
Self::JoinError(_) => None,
|
||||||
Self::Lock { source, .. } => Some(source),
|
Self::Lock { source, .. } => Some(source),
|
||||||
|
Self::CreateTemporary(err) => Some(err),
|
||||||
|
Self::PersistTemporary { source, .. } => Some(source),
|
||||||
Self::Io(err) => Some(err),
|
Self::Io(err) => Some(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +212,7 @@ impl LockedFile {
|
||||||
mode: LockedFileMode,
|
mode: LockedFileMode,
|
||||||
resource: impl Display,
|
resource: impl Display,
|
||||||
) -> Result<Self, LockedFileError> {
|
) -> Result<Self, LockedFileError> {
|
||||||
let file = Self::create(path)?;
|
let file = Self::create(&path)?;
|
||||||
let resource = resource.to_string();
|
let resource = resource.to_string();
|
||||||
Self::lock_file(file, mode, &resource).await
|
Self::lock_file(file, mode, &resource).await
|
||||||
}
|
}
|
||||||
|
|
@ -222,10 +233,25 @@ impl LockedFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> {
|
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use rustix::io::Errno;
|
||||||
|
#[allow(clippy::disallowed_types)]
|
||||||
|
use std::{fs::File, os::unix::fs::PermissionsExt};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
/// The permissions the lockfile should end up with
|
||||||
|
const DESIRED_MODE: u32 = 0o666;
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_types)]
|
||||||
|
fn try_set_permissions(file: &File, path: &Path) {
|
||||||
|
if let Err(err) = file.set_permissions(std::fs::Permissions::from_mode(DESIRED_MODE)) {
|
||||||
|
warn!(
|
||||||
|
"Failed to set permissions on temporary file `{path}`: {err}",
|
||||||
|
path = path.user_display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If path already exists, return it.
|
// If path already exists, return it.
|
||||||
if let Ok(file) = fs_err::OpenOptions::new()
|
if let Ok(file) = fs_err::OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
|
|
@ -238,16 +264,12 @@ impl LockedFile {
|
||||||
// Otherwise, create a temporary file with 666 permissions. We must set
|
// Otherwise, create a temporary file with 666 permissions. We must set
|
||||||
// permissions _after_ creating the file, to override the `umask`.
|
// permissions _after_ creating the file, to override the `umask`.
|
||||||
let file = if let Some(parent) = path.as_ref().parent() {
|
let file = if let Some(parent) = path.as_ref().parent() {
|
||||||
NamedTempFile::new_in(parent)?
|
NamedTempFile::new_in(parent)
|
||||||
} else {
|
} else {
|
||||||
NamedTempFile::new()?
|
NamedTempFile::new()
|
||||||
};
|
|
||||||
if let Err(err) = file
|
|
||||||
.as_file()
|
|
||||||
.set_permissions(std::fs::Permissions::from_mode(0o666))
|
|
||||||
{
|
|
||||||
warn!("Failed to set permissions on temporary file: {err}");
|
|
||||||
}
|
}
|
||||||
|
.map_err(LockedFileError::CreateTemporary)?;
|
||||||
|
try_set_permissions(file.as_file(), file.path());
|
||||||
|
|
||||||
// Try to move the file to path, but if path exists now, just open path
|
// Try to move the file to path, but if path exists now, just open path
|
||||||
match file.persist_noclobber(path.as_ref()) {
|
match file.persist_noclobber(path.as_ref()) {
|
||||||
|
|
@ -258,20 +280,60 @@ impl LockedFile {
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(path.as_ref())
|
.open(path.as_ref())
|
||||||
|
.map_err(Into::into)
|
||||||
|
} else if matches!(
|
||||||
|
Errno::from_io_error(&err.error),
|
||||||
|
Some(Errno::NOTSUP | Errno::INVAL)
|
||||||
|
) {
|
||||||
|
// Fallback in case `persist_noclobber`, which uses `renameat2` or
|
||||||
|
// `renameatx_np` under the hood, is not supported by the FS. Linux reports this
|
||||||
|
// with `EINVAL` and MacOS with `ENOTSUP`. For these reasons and many others,
|
||||||
|
// there isn't an ErrorKind we can use here, and in fact on MacOS `ENOTSUP` gets
|
||||||
|
// mapped to `ErrorKind::Other`
|
||||||
|
|
||||||
|
// There is a race here where another process has just created the file, and we
|
||||||
|
// try to open it and get permission errors because the other process hasn't set
|
||||||
|
// the permission bits yet. This will lead to a transient failure, but unlike
|
||||||
|
// alternative approaches it won't ever lead to a situation where two processes
|
||||||
|
// are locking two different files. Also, since `persist_noclobber` is more
|
||||||
|
// likely to not be supported on special filesystems which don't have permission
|
||||||
|
// bits, it's less likely to ever matter.
|
||||||
|
let file = fs_err::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(path.as_ref())?;
|
||||||
|
|
||||||
|
// We don't want to `try_set_permissions` in cases where another user's process
|
||||||
|
// has already created the lockfile and changed its permissions because we might
|
||||||
|
// not have permission to change the permissions which would produce a confusing
|
||||||
|
// warning.
|
||||||
|
if file
|
||||||
|
.metadata()
|
||||||
|
.is_ok_and(|metadata| metadata.permissions().mode() != DESIRED_MODE)
|
||||||
|
{
|
||||||
|
try_set_permissions(file.file(), path.as_ref());
|
||||||
|
}
|
||||||
|
Ok(file)
|
||||||
} else {
|
} else {
|
||||||
Err(err.error)
|
let temp_path = err.file.into_temp_path();
|
||||||
|
Err(LockedFileError::PersistTemporary {
|
||||||
|
path: <tempfile::TempPath as AsRef<Path>>::as_ref(&temp_path).to_path_buf(),
|
||||||
|
source: err.error,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
fn create(path: impl AsRef<Path>) -> std::io::Result<fs_err::File> {
|
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
|
||||||
fs_err::OpenOptions::new()
|
fs_err::OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(path.as_ref())
|
.open(path.as_ref())
|
||||||
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue