Improve cache initialization failure error message (#17469)
Adds an error chain to improve clarity when we fail to initialize the cache directory. Related to https://github.com/astral-sh/uv/issues/17465 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,8 @@ pub const ARCHIVE_VERSION: u8 = 0;
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Failed to initialize cache at `{}`", _0.user_display())]
|
||||
Init(PathBuf, #[source] io::Error),
|
||||
#[error("Could not make the path absolute")]
|
||||
Absolute(#[source] io::Error),
|
||||
#[error("Could not acquire lock")]
|
||||
@@ -455,7 +457,7 @@ impl Cache {
|
||||
pub async fn init(self) -> Result<Self, Error> {
|
||||
let root = &self.root;
|
||||
|
||||
Self::create_base_files(root)?;
|
||||
Self::create_base_files(root).map_err(|err| Error::Init(root.clone(), err))?;
|
||||
|
||||
// Block cache removal operations from interfering.
|
||||
let lock_file = match LockedFile::acquire(
|
||||
@@ -491,7 +493,7 @@ impl Cache {
|
||||
pub fn init_no_wait(self) -> Result<Option<Self>, Error> {
|
||||
let root = &self.root;
|
||||
|
||||
Self::create_base_files(root)?;
|
||||
Self::create_base_files(root).map_err(|err| Error::Init(root.clone(), err))?;
|
||||
|
||||
// Block cache removal operations from interfering.
|
||||
let Some(lock_file) = LockedFile::acquire_no_wait(
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#[cfg(unix)]
|
||||
use anyhow::Result;
|
||||
#[cfg(unix)]
|
||||
use assert_fs::prelude::*;
|
||||
#[cfg(unix)]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(unix)]
|
||||
use crate::common::{TestContext, get_bin, uv_snapshot};
|
||||
|
||||
/// When the cache directory cannot be created (e.g., due to permissions), we should show a
|
||||
/// chained error message that indicates we failed to initialize the cache.
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn cache_init_failure() -> Result<()> {
|
||||
use crate::common::ReadOnlyDirectoryGuard;
|
||||
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a read-only directory that will serve as the parent of the cache.
|
||||
// The guard sets it to read-only and restores original permissions on drop (including panic).
|
||||
let cache_parent = context.temp_dir.child("cache_parent");
|
||||
fs_err::create_dir(&cache_parent)?;
|
||||
let _guard = ReadOnlyDirectoryGuard::new(cache_parent.path())?;
|
||||
|
||||
// Point the cache to a subdirectory within the read-only parent
|
||||
let cache_dir = cache_parent.child("cache");
|
||||
|
||||
let mut filters = context.filters();
|
||||
// Filter both the relative path (in the first line) and absolute path (in the cause)
|
||||
filters.push((r"cache_parent/cache", "[CACHE_DIR]"));
|
||||
filters.push((
|
||||
r"failed to create directory `.*`",
|
||||
"failed to create directory `[CACHE_DIR]`",
|
||||
));
|
||||
|
||||
// Build the sync command manually to use our custom cache directory.
|
||||
// We can't use context.sync() because it adds --cache-dir with the default cache.
|
||||
let mut command = Command::new(get_bin());
|
||||
command
|
||||
.arg("sync")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.current_dir(context.temp_dir.path());
|
||||
context.add_shared_env(&mut command, false);
|
||||
|
||||
// Running a command should fail with a chained error about cache initialization
|
||||
uv_snapshot!(&filters, command, @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to initialize cache at `[CACHE_DIR]`
|
||||
Caused by: failed to create directory `[CACHE_DIR]`: Permission denied (os error 13)
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2151,6 +2151,46 @@ pub async fn download_to_disk(url: &str, path: &Path) {
|
||||
file.sync_all().await.unwrap();
|
||||
}
|
||||
|
||||
/// A guard that sets a directory to read-only and restores original permissions when dropped.
|
||||
///
|
||||
/// This is useful for tests that need to make a directory read-only and ensure
|
||||
/// the permissions are restored even if the test panics.
|
||||
#[cfg(unix)]
|
||||
pub struct ReadOnlyDirectoryGuard {
|
||||
path: PathBuf,
|
||||
original_mode: u32,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl ReadOnlyDirectoryGuard {
|
||||
/// Sets the directory to read-only (removes write permission) and returns a guard
|
||||
/// that will restore the original permissions when dropped.
|
||||
pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let path = path.into();
|
||||
let metadata = fs_err::metadata(&path)?;
|
||||
let original_mode = metadata.permissions().mode();
|
||||
// Remove write permissions (keep read and execute)
|
||||
let readonly_mode = original_mode & !0o222;
|
||||
fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
|
||||
Ok(Self {
|
||||
path,
|
||||
original_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for ReadOnlyDirectoryGuard {
|
||||
fn drop(&mut self) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs_err::set_permissions(
|
||||
&self.path,
|
||||
std::fs::Permissions::from_mode(self.original_mode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility macro to return the name of the current function.
|
||||
///
|
||||
/// https://stackoverflow.com/a/40234666/3549270
|
||||
|
||||
@@ -13,6 +13,9 @@ mod build;
|
||||
#[cfg(feature = "python")]
|
||||
mod build_backend;
|
||||
|
||||
#[cfg(all(feature = "python", feature = "pypi"))]
|
||||
mod cache;
|
||||
|
||||
#[cfg(all(feature = "python", feature = "pypi"))]
|
||||
mod cache_clean;
|
||||
|
||||
|
||||
@@ -1379,7 +1379,8 @@ fn path_with_trailing_space_gives_proper_error() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: failed to open file `[CACHE_DIR]/ /CACHEDIR.TAG`: The system cannot find the path specified. (os error 3)
|
||||
error: Failed to initialize cache at `[CACHE_DIR]/ `
|
||||
Caused by: failed to open file `[CACHE_DIR]/ /CACHEDIR.TAG`: The system cannot find the path specified. (os error 3)
|
||||
"###
|
||||
);
|
||||
// Note the extra trailing `/` in the snapshot is due to the filters, not the actual output.
|
||||
|
||||
Reference in New Issue
Block a user