Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ indoc = { version = "2.0.5" }
itertools = { version = "0.14.0" }
jiff = { version = "0.2.0", features = ["serde"] }
junction = { version = "2.0.0" }
libc = { version = "0.2.0" }
mailparse = { version = "0.16.0" }
md-5 = { version = "0.11.0" }
memchr = { version = "2.7.4" }
Expand Down
84 changes: 61 additions & 23 deletions crates/uv-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub use crate::by_timestamp::CachedByTimestamp;
#[cfg(feature = "clap")]
pub use crate::cli::CacheArgs;
use crate::removal::Remover;
pub use crate::removal::{Removal, rm_rf};
pub use crate::removal::{Removal, RemovalMode};
pub use crate::wheel::WheelCache;
use crate::wheel::WheelCacheKind;
pub use archive::ArchiveId;
Expand Down Expand Up @@ -174,6 +174,8 @@ pub struct Cache {
/// Ensure that `uv cache` operations don't remove items from the cache that are used by another
/// uv process.
lock_file: Option<Arc<LockedFile>>,
/// The storage accounting used when removing cache entries.
removal_mode: RemovalMode,
}

impl Cache {
Expand All @@ -184,6 +186,7 @@ impl Cache {
refresh: Refresh::None(Timestamp::now()),
temp_dir: None,
lock_file: None,
removal_mode: RemovalMode::Logical,
}
}

Expand All @@ -195,6 +198,7 @@ impl Cache {
refresh: Refresh::None(Timestamp::now()),
temp_dir: Some(Arc::new(temp_dir)),
lock_file: None,
removal_mode: RemovalMode::Logical,
})
}

Expand All @@ -204,13 +208,34 @@ impl Cache {
Self { refresh, ..self }
}

/// Set the storage accounting used when removing cache entries.
///
/// Falls back to logical accounting when physical accounting is unsupported.
#[must_use]
pub fn with_removal_mode(self, removal_mode: RemovalMode) -> Self {
let removal_mode = match removal_mode {
RemovalMode::Physical if !uv_fs::supports_physical_space() => RemovalMode::Logical,
removal_mode => removal_mode,
};
Self {
removal_mode,
..self
}
}

/// Create an empty removal summary using the cache's configured accounting mode.
pub fn removal(&self) -> Removal {
Removal::new(self.removal_mode)
}

/// Acquire a lock that allows removing entries from the cache.
pub async fn with_exclusive_lock(self) -> Result<Self, LockedFileError> {
let Self {
root,
refresh,
temp_dir,
lock_file,
removal_mode,
} = self;

// Release the existing lock, avoid deadlocks from a cloned cache.
Expand All @@ -233,6 +258,7 @@ impl Cache {
refresh,
temp_dir,
lock_file: Some(Arc::new(lock_file)),
removal_mode,
})
}

Expand All @@ -245,6 +271,7 @@ impl Cache {
refresh,
temp_dir,
lock_file,
removal_mode,
} = self;

match LockedFile::acquire_no_wait(
Expand All @@ -257,12 +284,14 @@ impl Cache {
refresh,
temp_dir,
lock_file: Some(Arc::new(lock_file)),
removal_mode,
}),
None => Err(Self {
root,
refresh,
temp_dir,
lock_file,
removal_mode,
}),
}
}
Expand Down Expand Up @@ -521,7 +550,9 @@ impl Cache {
/// Clear the cache, removing all entries.
pub fn clear(self, reporter: Box<dyn CleanReporter>) -> Result<Removal, io::Error> {
// Remove everything but `.lock`, Windows does not allow removal of a locked file
let mut removal = Remover::new(reporter).rm_rf(&self.root, true)?;
let mut removal = Remover::new(reporter)
.with_removal_mode(self.removal_mode)
.rm_rf(&self.root, true)?;
let Self {
root, lock_file, ..
} = self;
Expand Down Expand Up @@ -557,7 +588,7 @@ impl Cache {
let references = self.find_archive_references()?;

// Remove any entries for the package from the cache.
let mut summary = Removal::default();
let mut summary = self.removal();
for bucket in CacheBucket::iter() {
summary += bucket.remove(self, name)?;
}
Expand All @@ -574,7 +605,7 @@ impl Cache {
for (target, references) in references {
if target.starts_with(&archive_root) && references.iter().all(|path| !path.exists()) {
debug!("Removing dangling cache entry: {}", target.display());
summary += rm_rf(target)?;
summary += self.remove_path(target)?;
}
}

Expand All @@ -583,7 +614,7 @@ impl Cache {

/// Prune dangling cache entries and cached environments.
pub fn prune(&self, ci: bool) -> Result<Removal, io::Error> {
let mut summary = Removal::default();
let mut summary = self.removal();

// First, remove any top-level directories that are unused. These typically represent
// outdated cache buckets (e.g., `wheels-v0`, when latest is `wheels-v1`).
Expand All @@ -604,13 +635,13 @@ impl Cache {
if CacheBucket::iter().all(|bucket| entry.file_name() != bucket.to_str()) {
let path = entry.path();
debug!("Removing dangling cache bucket: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
} else {
// If the file is not a marker file, remove it.
let path = entry.path();
debug!("Removing dangling cache bucket: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
}

Expand All @@ -622,7 +653,7 @@ impl Cache {
let entry = entry?;
let path = entry.path();
debug!("Removing cached environment: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => (),
Expand All @@ -639,7 +670,7 @@ impl Cache {
let path = entry.path();
if path.is_dir() {
debug!("Removing unzipped wheel entry: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
}
}
Expand Down Expand Up @@ -683,7 +714,7 @@ impl Cache {
}

debug!("Removing unzipped built wheel entry: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
}
}
Expand All @@ -700,7 +731,7 @@ impl Cache {
let target = fs_err::canonicalize(&path)?;
if !references.contains_key(&target) {
debug!("Removing dangling cache archive: {}", path.display());
summary += rm_rf(path)?;
summary += self.remove_path(path)?;
}
}
}
Expand All @@ -711,6 +742,13 @@ impl Cache {
Ok(summary)
}

/// Remove a cache path using the cache's configured storage accounting.
pub fn remove_path(&self, path: impl AsRef<Path>) -> io::Result<Removal> {
Remover::default()
.with_removal_mode(self.removal_mode)
.rm_rf(path, false)
}

/// Find all references to entries in the archive bucket.
///
/// Archive entries are often referenced by symlinks in other cache buckets. This method
Expand Down Expand Up @@ -1233,37 +1271,37 @@ impl CacheBucket {
metadata.name == *name
}

let mut summary = Removal::default();
let mut summary = cache.removal();
match self {
Self::Wheels => {
// For `pypi` wheels, we expect a directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Pypi);
summary += rm_rf(root.join(name.to_string()))?;
summary += cache.remove_path(root.join(name.to_string()))?;

// For alternate indices, we expect a directory for every index (under an `index`
// subdirectory), followed by a directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Index);
for directory in directories(root)? {
summary += rm_rf(directory.join(name.to_string()))?;
summary += cache.remove_path(directory.join(name.to_string()))?;
}

// For direct URLs, we expect a directory for every URL, followed by a
// directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Url);
for directory in directories(root)? {
summary += rm_rf(directory.join(name.to_string()))?;
summary += cache.remove_path(directory.join(name.to_string()))?;
}
}
Self::SourceDistributions => {
// For `pypi` wheels, we expect a directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Pypi);
summary += rm_rf(root.join(name.to_string()))?;
summary += cache.remove_path(root.join(name.to_string()))?;

// For alternate indices, we expect a directory for every index (under an `index`
// subdirectory), followed by a directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Index);
for directory in directories(root)? {
summary += rm_rf(directory.join(name.to_string()))?;
summary += cache.remove_path(directory.join(name.to_string()))?;
}

// For direct URLs, we expect a directory for every URL, followed by a
Expand All @@ -1272,7 +1310,7 @@ impl CacheBucket {
let root = cache.bucket(self).join(WheelCacheKind::Url);
for url in directories(root)? {
if directories(&url)?.any(|version| is_match(&version, name)) {
summary += rm_rf(url)?;
summary += cache.remove_path(url)?;
}
}

Expand All @@ -1282,7 +1320,7 @@ impl CacheBucket {
let root = cache.bucket(self).join(WheelCacheKind::Path);
for path in directories(root)? {
if directories(&path)?.any(|version| is_match(&version, name)) {
summary += rm_rf(path)?;
summary += cache.remove_path(path)?;
}
}

Expand All @@ -1293,28 +1331,28 @@ impl CacheBucket {
for repository in directories(root)? {
for sha in directories(repository)? {
if is_match(&sha, name) {
summary += rm_rf(sha)?;
summary += cache.remove_path(sha)?;
}
}
}
}
Self::Simple => {
// For `pypi` wheels, we expect a rkyv file per package, indexed by name.
let root = cache.bucket(self).join(WheelCacheKind::Pypi);
summary += rm_rf(root.join(format!("{name}.rkyv")))?;
summary += cache.remove_path(root.join(format!("{name}.rkyv")))?;

// For alternate indices, we expect a directory for every index (under an `index`
// subdirectory), followed by a directory per package (indexed by name).
let root = cache.bucket(self).join(WheelCacheKind::Index);
for directory in directories(root)? {
summary += rm_rf(directory.join(format!("{name}.rkyv")))?;
summary += cache.remove_path(directory.join(format!("{name}.rkyv")))?;
}
}
Self::FlatIndex => {
// We can't know if the flat index includes a package, so we just remove the entire
// cache entry.
let root = cache.bucket(self);
summary += rm_rf(root)?;
summary += cache.remove_path(root)?;
}
Self::Git
| Self::Interpreter
Expand Down
Loading