diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index a94dcbf60..f08100ea2 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -388,13 +388,34 @@ impl Cache { } } - for entry in fs::read_dir(self.bucket(CacheBucket::Archive))? { - let entry = entry?; - let path = entry.path().canonicalize()?; - if !references.contains(&path) { - debug!("Removing dangling cache entry: {}", path.display()); - summary += rm_rf(path)?; + match fs::read_dir(self.bucket(CacheBucket::Archive)) { + Ok(entries) => { + for entry in entries { + let entry = entry?; + let path = entry.path().canonicalize()?; + if !references.contains(&path) { + debug!("Removing dangling cache entry: {}", path.display()); + summary += rm_rf(path)?; + } + } } + Err(err) if err.kind() == io::ErrorKind::NotFound => (), + Err(err) => return Err(err), + } + + // Third, remove any cached environments. These are never referenced by symlinks, so we can + // remove them directly. + match fs::read_dir(self.bucket(CacheBucket::Environments)) { + Ok(entries) => { + for entry in entries { + let entry = entry?; + let path = entry.path().canonicalize()?; + debug!("Removing dangling cache entry: {}", path.display()); + summary += rm_rf(path)?; + } + } + Err(err) if err.kind() == io::ErrorKind::NotFound => (), + Err(err) => return Err(err), } Ok(summary) diff --git a/crates/uv/tests/cache_prune.rs b/crates/uv/tests/cache_prune.rs index fcab2202d..0da22defd 100644 --- a/crates/uv/tests/cache_prune.rs +++ b/crates/uv/tests/cache_prune.rs @@ -83,6 +83,59 @@ fn prune_stale_directory() -> Result<()> { Ok(()) } +/// `cache prune` should remove all cached environments from the cache. +#[test] +fn prune_cached_env() { + let context = TestContext::new("3.12").with_filtered_counts(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + uv_snapshot!(context.filters(), context.tool_run() + .arg("pytest@8.0.0") + .arg("--version") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + pytest 8.0.0 + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning. + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + iniconfig==2.0.0 + + packaging==24.0 + + pluggy==1.4.0 + + pytest==8.0.0 + "###); + + 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, prune_command(&context).arg("--verbose"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + DEBUG uv [VERSION] ([COMMIT] DATE) + Pruning cache at: [CACHE_DIR]/ + DEBUG Removing dangling cache entry: [CACHE_DIR]/environments-v1/[ENTRY] + Removed [N] files ([SIZE]) + "###); +} + /// `cache prune` should remove any stale symlink from the cache. #[test] fn prune_stale_symlink() -> Result<()> { diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 15b33e4a8..e158469ca 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -108,6 +108,10 @@ impl TestContext { format!("{verb} [N] packages"), )); } + self.filters.push(( + "Removed \\d+ files?".to_string(), + "Removed [N] files".to_string(), + )); self }