Add --force flag for uv cache prune (#16137)

Matching #15992 

See https://github.com/astral-sh/setup-uv/issues/588
This commit is contained in:
Zanie Blue 2025-10-06 11:36:58 -05:00 committed by GitHub
parent 3bed81866f
commit 7e4edf0fb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 81 additions and 3 deletions

View file

@ -802,6 +802,13 @@ pub struct PruneArgs {
/// that were built from source.
#[arg(long)]
pub ci: bool,
/// Force removal of the cache, ignoring in-use checks.
///
/// By default, `uv cache prune` will block until no process is reading the cache. When
/// `--force` is used, `uv cache prune` will proceed without taking a lock.
#[arg(long)]
pub force: bool,
}
#[derive(Args)]

View file

@ -2,6 +2,7 @@ use std::fmt::Write;
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use tracing::debug;
use uv_cache::{Cache, Removal};
use uv_fs::Simplified;
@ -10,7 +11,12 @@ use crate::commands::{ExitStatus, human_readable_bytes};
use crate::printer::Printer;
/// Prune all unreachable objects from the cache.
pub(crate) fn cache_prune(ci: bool, cache: Cache, printer: Printer) -> Result<ExitStatus> {
pub(crate) fn cache_prune(
ci: bool,
force: bool,
cache: Cache,
printer: Printer,
) -> Result<ExitStatus> {
if !cache.root().exists() {
writeln!(
printer.stderr(),
@ -19,7 +25,19 @@ pub(crate) fn cache_prune(ci: bool, cache: Cache, printer: Printer) -> Result<Ex
)?;
return Ok(ExitStatus::Success);
}
let cache = cache.with_exclusive_lock()?;
let cache = if force {
// If `--force` is used, attempt to acquire the exclusive lock but do not block.
match cache.with_exclusive_lock_no_wait() {
Ok(cache) => cache,
Err(cache) => {
debug!("Cache is currently in use, proceeding due to `--force`");
cache
}
}
} else {
cache.with_exclusive_lock()?
};
writeln!(
printer.stderr(),

View file

@ -1023,7 +1023,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
command: CacheCommand::Prune(args),
}) => {
show_settings!(args);
commands::cache_prune(args.ci, cache, printer)
commands::cache_prune(args.ci, args.force, cache, printer)
}
Commands::Cache(CacheNamespace {
command: CacheCommand::Dir,

View file

@ -196,6 +196,57 @@ fn prune_stale_symlink() -> Result<()> {
Ok(())
}
#[test]
fn prune_force() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("typing-extensions\niniconfig")?;
// Install a requirement, to populate the cache.
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
// When unlocked, `--force` should still take a lock
uv_snapshot!(context.filters(), context.prune().arg("--verbose").arg("--force"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Acquired lock for `[CACHE_DIR]/`
Pruning cache at: [CACHE_DIR]/
No unused entries found
DEBUG Released lock at `[CACHE_DIR]/.lock`
");
// Add a stale directory to the cache.
let simple = context.cache_dir.child("simple-v4");
simple.create_dir_all()?;
// When locked, `--force` should proceed without blocking
let _cache = uv_cache::Cache::from_path(context.cache_dir.path()).with_exclusive_lock();
uv_snapshot!(context.filters(), context.prune().arg("--verbose").arg("--force"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Lock is busy for `[CACHE_DIR]/`
DEBUG Cache is currently in use, proceeding due to `--force`
Pruning cache at: [CACHE_DIR]/
DEBUG Removing dangling cache bucket: [CACHE_DIR]/simple-v4
Removed 1 directory
");
Ok(())
}
/// `cache prune --ci` should remove all unzipped archives.
#[test]
fn prune_unzipped() -> Result<()> {