Skip to content

Commit aa90040

Browse files
committed
Add --force flag for uv cache clean
1 parent 1224f65 commit aa90040

File tree

6 files changed

+136
-2
lines changed

6 files changed

+136
-2
lines changed

crates/uv-cache/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,33 @@ impl Cache {
209209
})
210210
}
211211

212+
/// Acquire a lock that allows removing entries from the cache, if available.
213+
///
214+
/// If the lock is not immediately available, returns [`Err`] with self.
215+
pub fn with_exclusive_lock_no_wait(self) -> Result<Self, Self> {
216+
let Self {
217+
root,
218+
refresh,
219+
temp_dir,
220+
lock_file,
221+
} = self;
222+
223+
match LockedFile::acquire_no_wait(root.join(".lock"), root.simplified_display()) {
224+
Some(lock_file) => Ok(Self {
225+
root,
226+
refresh,
227+
temp_dir,
228+
lock_file: Some(Arc::new(lock_file)),
229+
}),
230+
None => Err(Self {
231+
root,
232+
refresh,
233+
temp_dir,
234+
lock_file,
235+
}),
236+
}
237+
}
238+
212239
/// Return the root of the cache.
213240
pub fn root(&self) -> &Path {
214241
&self.root

crates/uv-cli/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,13 @@ pub enum CacheCommand {
775775
pub struct CleanArgs {
776776
/// The packages to remove from the cache.
777777
pub package: Vec<PackageName>,
778+
779+
/// Force removal of the cache, ignoring in-use checks.
780+
///
781+
/// By default, `uv cache clean` will block until no process is reading the cache. When
782+
/// `--force` is used, `uv cache clean` will proceed without taking a lock.
783+
#[arg(long)]
784+
pub force: bool,
778785
}
779786

780787
#[derive(Args, Debug)]

crates/uv-fs/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,28 @@ impl LockedFile {
693693
}
694694
}
695695

696+
/// Inner implementation for [`LockedFile::acquire_no_wait`].
697+
fn lock_file_no_wait(file: fs_err::File, resource: &str) -> Option<Self> {
698+
trace!(
699+
"Checking lock for `{resource}` at `{}`",
700+
file.path().user_display()
701+
);
702+
match file.file().try_lock_exclusive() {
703+
Ok(()) => {
704+
debug!("Acquired lock for `{resource}`");
705+
Some(Self(file))
706+
}
707+
Err(err) => {
708+
// Log error code and enum kind to help debugging more exotic failures.
709+
if err.kind() != std::io::ErrorKind::WouldBlock {
710+
debug!("Try lock error: {err:?}");
711+
}
712+
debug!("Lock is busy for `{resource}`");
713+
None
714+
}
715+
}
716+
}
717+
696718
/// Inner implementation for [`LockedFile::acquire_shared_blocking`] and
697719
/// [`LockedFile::acquire_blocking`].
698720
fn lock_file_shared_blocking(
@@ -782,6 +804,17 @@ impl LockedFile {
782804
.await?
783805
}
784806

807+
/// Acquire a cross-process lock for a resource using a file at the provided path
808+
///
809+
/// Unlike [`LockedFile::acquire`] this function will not wait for the lock to become available.
810+
///
811+
/// If the lock is not immediately available, [`None`] is returned.
812+
pub fn acquire_no_wait(path: impl AsRef<Path>, resource: impl Display) -> Option<Self> {
813+
let file = Self::create(path).ok()?;
814+
let resource = resource.to_string();
815+
Self::lock_file_no_wait(file, &resource)
816+
}
817+
785818
#[cfg(unix)]
786819
fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> {
787820
use std::os::unix::fs::PermissionsExt;

crates/uv/src/commands/cache_clean.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fmt::Write;
22

33
use anyhow::{Context, Result};
44
use owo_colors::OwoColorize;
5+
use tracing::debug;
56

67
use uv_cache::{Cache, Removal};
78
use uv_fs::Simplified;
@@ -14,6 +15,7 @@ use crate::printer::Printer;
1415
/// Clear the cache, removing all entries or those linked to specific packages.
1516
pub(crate) fn cache_clean(
1617
packages: &[PackageName],
18+
force: bool,
1719
cache: Cache,
1820
printer: Printer,
1921
) -> Result<ExitStatus> {
@@ -25,7 +27,19 @@ pub(crate) fn cache_clean(
2527
)?;
2628
return Ok(ExitStatus::Success);
2729
}
28-
let cache = cache.with_exclusive_lock()?;
30+
31+
let cache = if force {
32+
// If `--force` is used, attempt to acquire the exclusive lock but do not block.
33+
match cache.with_exclusive_lock_no_wait() {
34+
Ok(cache) => cache,
35+
Err(cache) => {
36+
debug!("Cache is currently in use, proceeding due to `--force`");
37+
cache
38+
}
39+
}
40+
} else {
41+
cache.with_exclusive_lock()?
42+
};
2943

3044
let summary = if packages.is_empty() {
3145
writeln!(

crates/uv/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
10101010
})
10111011
| Commands::Clean(args) => {
10121012
show_settings!(args);
1013-
commands::cache_clean(&args.package, cache, printer)
1013+
commands::cache_clean(&args.package, args.force, cache, printer)
10141014
}
10151015
Commands::Cache(CacheNamespace {
10161016
command: CacheCommand::Prune(args),

crates/uv/tests/it/cache_clean.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,59 @@ fn clean_all() -> Result<()> {
3535
Ok(())
3636
}
3737

38+
#[test]
39+
fn clean_force() -> Result<()> {
40+
let context = TestContext::new("3.12").with_filtered_counts();
41+
42+
let requirements_txt = context.temp_dir.child("requirements.txt");
43+
requirements_txt.write_str("typing-extensions\niniconfig")?;
44+
45+
// Install a requirement, to populate the cache.
46+
context
47+
.pip_sync()
48+
.arg("requirements.txt")
49+
.assert()
50+
.success();
51+
52+
// When unlocked, `--force` should still take a lock
53+
uv_snapshot!(context.filters(), context.clean().arg("--verbose").arg("--force"), @r"
54+
success: true
55+
exit_code: 0
56+
----- stdout -----
57+
58+
----- stderr -----
59+
DEBUG uv [VERSION] ([COMMIT] DATE)
60+
DEBUG Acquired lock for `[CACHE_DIR]/`
61+
Clearing cache at: [CACHE_DIR]/
62+
DEBUG Released lock at `[CACHE_DIR]/.lock`
63+
Removed [N] files ([SIZE])
64+
");
65+
66+
// Install a requirement, to re-populate the cache.
67+
context
68+
.pip_sync()
69+
.arg("requirements.txt")
70+
.assert()
71+
.success();
72+
73+
// When locked, `--force` should proceed without blocking
74+
let _cache = uv_cache::Cache::from_path(&context.cache_dir.path()).with_exclusive_lock();
75+
uv_snapshot!(context.filters(), context.clean().arg("--verbose").arg("--force"), @r"
76+
success: true
77+
exit_code: 0
78+
----- stdout -----
79+
80+
----- stderr -----
81+
DEBUG uv [VERSION] ([COMMIT] DATE)
82+
DEBUG Lock is busy for `[CACHE_DIR]/`
83+
DEBUG Cache is currently in use, proceeding due to `--force`
84+
Clearing cache at: [CACHE_DIR]/
85+
Removed [N] files ([SIZE])
86+
");
87+
88+
Ok(())
89+
}
90+
3891
/// `cache clean iniconfig` should remove a single package (`iniconfig`).
3992
#[test]
4093
fn clean_package_pypi() -> Result<()> {

0 commit comments

Comments
 (0)