diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index 5d76ad73815d..0f6019fcf19e 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -322,10 +322,62 @@ impl Cache { /// /// Returns the number of entries removed from the cache. pub fn remove(&self, name: &PackageName) -> Result { + // Collect the set of referenced archives. + let before = { + let mut references = FxHashSet::default(); + for bucket in CacheBucket::iter() { + let bucket = self.bucket(bucket); + if bucket.is_dir() { + for entry in walkdir::WalkDir::new(bucket) { + let entry = entry?; + if entry.file_type().is_symlink() { + if let Ok(target) = fs_err::canonicalize(entry.path()) { + references.insert(target); + } + } + } + } + } + references + }; + + // Remove any entries for the package from the cache. let mut summary = Removal::default(); for bucket in CacheBucket::iter() { summary += bucket.remove(self, name)?; } + + // Collect the set of referenced archives after the removal. + let after = { + let mut references = FxHashSet::default(); + for bucket in CacheBucket::iter() { + let bucket = self.bucket(bucket); + if bucket.is_dir() { + for entry in walkdir::WalkDir::new(bucket) { + let entry = entry?; + if entry.file_type().is_symlink() { + if let Ok(target) = fs_err::canonicalize(entry.path()) { + references.insert(target); + } + } + } + } + } + references + }; + + if before != after { + // Remove any archives that are no longer referenced. + for entry in fs::read_dir(self.bucket(CacheBucket::Archive))? { + let entry = entry?; + let path = fs_err::canonicalize(entry.path())?; + if !after.contains(&path) && before.contains(&path) { + debug!("Removing dangling cache entry: {}", path.display()); + summary += rm_rf(path)?; + } + } + } + Ok(summary) } diff --git a/crates/uv/tests/cache_clean.rs b/crates/uv/tests/cache_clean.rs index ca3da3741265..0e1a62b8f810 100644 --- a/crates/uv/tests/cache_clean.rs +++ b/crates/uv/tests/cache_clean.rs @@ -65,14 +65,27 @@ fn clean_package_pypi() -> Result<()> { "Expected the `.rkyv` file to exist for `iniconfig`" ); - uv_snapshot!(context.filters(), context.clean().arg("--verbose").arg("iniconfig"), @r###" + let filters: Vec<_> = context + .filters() + .into_iter() + .chain([ + // The cache entry does not have a stable key, so we filter it out + ( + r"\[CACHE_DIR\](\\|\/)(.+)(\\|\/).*", + "[CACHE_DIR]/$2/[ENTRY]", + ), + ]) + .collect(); + + uv_snapshot!(&filters, context.clean().arg("--verbose").arg("iniconfig"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- DEBUG uv [VERSION] ([COMMIT] DATE) - Removed 4 files for iniconfig ([SIZE]) + DEBUG Removing dangling cache entry: [CACHE_DIR]/archive-v0/[ENTRY] + Removed 13 files for iniconfig ([SIZE]) "###); // Assert that the `.rkyv` file is removed for `iniconfig`. @@ -81,6 +94,18 @@ fn clean_package_pypi() -> Result<()> { "Expected the `.rkyv` file to be removed for `iniconfig`" ); + // Running `uv cache prune` should have no effect. + uv_snapshot!(&filters, context.prune().arg("--verbose"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + DEBUG uv [VERSION] ([COMMIT] DATE) + Pruning cache at: [CACHE_DIR]/ + No unused entries found + "###); + Ok(()) } @@ -113,14 +138,27 @@ fn clean_package_index() -> Result<()> { "Expected the `.rkyv` file to exist for `iniconfig`" ); - uv_snapshot!(context.filters(), context.clean().arg("--verbose").arg("iniconfig"), @r###" + let filters: Vec<_> = context + .filters() + .into_iter() + .chain([ + // The cache entry does not have a stable key, so we filter it out + ( + r"\[CACHE_DIR\](\\|\/)(.+)(\\|\/).*", + "[CACHE_DIR]/$2/[ENTRY]", + ), + ]) + .collect(); + + uv_snapshot!(&filters, context.clean().arg("--verbose").arg("iniconfig"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- DEBUG uv [VERSION] ([COMMIT] DATE) - Removed 4 files for iniconfig ([SIZE]) + DEBUG Removing dangling cache entry: [CACHE_DIR]/archive-v0/[ENTRY] + Removed 13 files for iniconfig ([SIZE]) "###); // Assert that the `.rkyv` file is removed for `iniconfig`. diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 2af46d84b6d0..156c9868f68e 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -1396,7 +1396,7 @@ fn install_url_source_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 13 files for source-distribution ([SIZE]) + Removed 19 files for source-distribution ([SIZE]) "### ); @@ -1591,7 +1591,7 @@ fn install_registry_source_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 14 files for source-distribution ([SIZE]) + Removed 20 files for source-distribution ([SIZE]) "### ); @@ -1687,7 +1687,7 @@ fn install_path_source_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 13 files for source-distribution ([SIZE]) + Removed 19 files for source-distribution ([SIZE]) "### ); @@ -1788,7 +1788,7 @@ fn install_path_built_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 2 files for tomli ([SIZE]) + Removed 11 files for tomli ([SIZE]) "### ); @@ -1876,7 +1876,7 @@ fn install_url_built_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 3 files for tqdm ([SIZE]) + Removed 43 files for tqdm ([SIZE]) "### );