diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2c43f7717..87f06378b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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)] diff --git a/crates/uv/src/commands/cache_prune.rs b/crates/uv/src/commands/cache_prune.rs index e5aa69336..1087a0043 100644 --- a/crates/uv/src/commands/cache_prune.rs +++ b/crates/uv/src/commands/cache_prune.rs @@ -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 { +pub(crate) fn cache_prune( + ci: bool, + force: bool, + cache: Cache, + printer: Printer, +) -> Result { if !cache.root().exists() { writeln!( printer.stderr(), @@ -19,7 +25,19 @@ pub(crate) fn cache_prune(ci: bool, cache: Cache, printer: Printer) -> Result cache, + Err(cache) => { + debug!("Cache is currently in use, proceeding due to `--force`"); + cache + } + } + } else { + cache.with_exclusive_lock()? + }; writeln!( printer.stderr(), diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index dcd1d1b11..9dc667b7e 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1023,7 +1023,7 @@ async fn run(mut cli: Cli) -> Result { 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, diff --git a/crates/uv/tests/it/cache_prune.rs b/crates/uv/tests/it/cache_prune.rs index b9a34d865..3650559a8 100644 --- a/crates/uv/tests/it/cache_prune.rs +++ b/crates/uv/tests/it/cache_prune.rs @@ -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<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 98da105b0..9dc523efb 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -6007,6 +6007,8 @@ uv cache prune [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

+
--force

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.

--help, -h

Display the concise help for this command

--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.