1use crate::core::global_cache_tracker::{self, GlobalCacheTracker};
23use crate::ops::CleanContext;
24use crate::util::cache_lock::{CacheLock, CacheLockMode};
25use crate::util::time_span::maybe_parse_time_span;
26use crate::{CargoResult, GlobalContext};
27use anyhow::{Context as _, format_err};
28use serde::Deserialize;
29use std::time::Duration;
30
31const DEFAULT_MAX_AGE_EXTRACTED: &str = "1 month";
34const DEFAULT_MAX_AGE_DOWNLOADED: &str = "3 months";
37const DEFAULT_AUTO_FREQUENCY: &str = "1 day";
39
40pub fn auto_gc(gctx: &GlobalContext) {
53 if !gctx.network_allowed() {
54 tracing::trace!(target: "gc", "running offline, auto gc disabled");
58 return;
59 }
60
61 if let Err(e) = auto_gc_inner(gctx) {
62 if global_cache_tracker::is_silent_error(&e) && !gctx.extra_verbose() {
63 tracing::warn!(target: "gc", "failed to auto-clean cache data: {e:?}");
64 } else {
65 crate::display_warning_with_error(
66 "failed to auto-clean cache data",
67 &e,
68 &mut gctx.shell(),
69 );
70 }
71 }
72}
73
74fn auto_gc_inner(gctx: &GlobalContext) -> CargoResult<()> {
75 let _lock = match gctx.try_acquire_package_cache_lock(CacheLockMode::MutateExclusive)? {
76 Some(lock) => lock,
77 None => {
78 tracing::debug!(target: "gc", "unable to acquire mutate lock, auto gc disabled");
79 return Ok(());
80 }
81 };
82 let deferred = gctx.deferred_global_last_use()?;
84 debug_assert!(deferred.is_empty());
85 let mut global_cache_tracker = gctx.global_cache_tracker()?;
86 let mut gc = Gc::new(gctx, &mut global_cache_tracker)?;
87 let mut clean_ctx = CleanContext::new(gctx);
88 gc.auto(&mut clean_ctx)?;
89 Ok(())
90}
91
92#[derive(Deserialize, Default)]
99#[serde(rename_all = "kebab-case")]
100struct GlobalCleanConfig {
101 max_src_age: Option<String>,
103 max_crate_age: Option<String>,
105 max_index_age: Option<String>,
107 max_git_co_age: Option<String>,
109 max_git_db_age: Option<String>,
111}
112
113#[derive(Clone, Debug, Default)]
115pub struct GcOpts {
116 pub max_src_age: Option<Duration>,
118 pub max_crate_age: Option<Duration>,
120 pub max_index_age: Option<Duration>,
122 pub max_git_co_age: Option<Duration>,
124 pub max_git_db_age: Option<Duration>,
126 pub max_src_size: Option<u64>,
128 pub max_crate_size: Option<u64>,
130 pub max_git_size: Option<u64>,
132 pub max_download_size: Option<u64>,
134}
135
136impl GcOpts {
137 pub fn is_download_cache_opt_set(&self) -> bool {
139 self.max_src_age.is_some()
140 || self.max_crate_age.is_some()
141 || self.max_index_age.is_some()
142 || self.max_git_co_age.is_some()
143 || self.max_git_db_age.is_some()
144 || self.max_src_size.is_some()
145 || self.max_crate_size.is_some()
146 || self.max_git_size.is_some()
147 || self.max_download_size.is_some()
148 }
149
150 pub fn is_download_cache_size_set(&self) -> bool {
152 self.max_src_size.is_some()
153 || self.max_crate_size.is_some()
154 || self.max_git_size.is_some()
155 || self.max_download_size.is_some()
156 }
157
158 pub fn set_max_download_age(&mut self, max_download_age: Duration) {
162 self.max_src_age = Some(maybe_newer_span(max_download_age, self.max_src_age));
163 self.max_crate_age = Some(maybe_newer_span(max_download_age, self.max_crate_age));
164 self.max_index_age = Some(maybe_newer_span(max_download_age, self.max_index_age));
165 self.max_git_co_age = Some(maybe_newer_span(max_download_age, self.max_git_co_age));
166 self.max_git_db_age = Some(maybe_newer_span(max_download_age, self.max_git_db_age));
167 }
168
169 pub fn update_for_auto_gc(&mut self, gctx: &GlobalContext) -> CargoResult<()> {
172 let config = gctx
173 .get::<Option<GlobalCleanConfig>>("cache.global-clean")?
174 .unwrap_or_default();
175 self.update_for_auto_gc_config(&config, gctx.cli_unstable().gc)
176 }
177
178 fn update_for_auto_gc_config(
179 &mut self,
180 config: &GlobalCleanConfig,
181 unstable_allowed: bool,
182 ) -> CargoResult<()> {
183 macro_rules! config_default {
184 ($config:expr, $field:ident, $default:expr, $unstable_allowed:expr) => {
185 if !unstable_allowed {
186 $default
188 } else {
189 $config.$field.as_deref().unwrap_or($default)
190 }
191 };
192 }
193
194 self.max_src_age = newer_time_span_for_config(
195 self.max_src_age,
196 "gc.auto.max-src-age",
197 config_default!(
198 config,
199 max_src_age,
200 DEFAULT_MAX_AGE_EXTRACTED,
201 unstable_allowed
202 ),
203 )?;
204 self.max_crate_age = newer_time_span_for_config(
205 self.max_crate_age,
206 "gc.auto.max-crate-age",
207 config_default!(
208 config,
209 max_crate_age,
210 DEFAULT_MAX_AGE_DOWNLOADED,
211 unstable_allowed
212 ),
213 )?;
214 self.max_index_age = newer_time_span_for_config(
215 self.max_index_age,
216 "gc.auto.max-index-age",
217 config_default!(
218 config,
219 max_index_age,
220 DEFAULT_MAX_AGE_DOWNLOADED,
221 unstable_allowed
222 ),
223 )?;
224 self.max_git_co_age = newer_time_span_for_config(
225 self.max_git_co_age,
226 "gc.auto.max-git-co-age",
227 config_default!(
228 config,
229 max_git_co_age,
230 DEFAULT_MAX_AGE_EXTRACTED,
231 unstable_allowed
232 ),
233 )?;
234 self.max_git_db_age = newer_time_span_for_config(
235 self.max_git_db_age,
236 "gc.auto.max-git-db-age",
237 config_default!(
238 config,
239 max_git_db_age,
240 DEFAULT_MAX_AGE_DOWNLOADED,
241 unstable_allowed
242 ),
243 )?;
244 Ok(())
245 }
246}
247
248pub struct Gc<'a, 'gctx> {
252 gctx: &'gctx GlobalContext,
253 global_cache_tracker: &'a mut GlobalCacheTracker,
254 #[expect(dead_code, reason = "held for `drop`")]
260 lock: CacheLock<'gctx>,
261}
262
263impl<'a, 'gctx> Gc<'a, 'gctx> {
264 pub fn new(
265 gctx: &'gctx GlobalContext,
266 global_cache_tracker: &'a mut GlobalCacheTracker,
267 ) -> CargoResult<Gc<'a, 'gctx>> {
268 let lock = gctx.acquire_package_cache_lock(CacheLockMode::MutateExclusive)?;
269 Ok(Gc {
270 gctx,
271 global_cache_tracker,
272 lock,
273 })
274 }
275
276 fn auto(&mut self, clean_ctx: &mut CleanContext<'gctx>) -> CargoResult<()> {
281 let freq = self
282 .gctx
283 .get::<Option<String>>("cache.auto-clean-frequency")?;
284 let Some(freq) = parse_frequency(freq.as_deref().unwrap_or(DEFAULT_AUTO_FREQUENCY))? else {
285 tracing::trace!(target: "gc", "auto gc disabled");
286 return Ok(());
287 };
288 if !self.global_cache_tracker.should_run_auto_gc(freq)? {
289 return Ok(());
290 }
291 let config = self
292 .gctx
293 .get::<Option<GlobalCleanConfig>>("cache.global-clean")?
294 .unwrap_or_default();
295
296 let mut gc_opts = GcOpts::default();
297 gc_opts.update_for_auto_gc_config(&config, self.gctx.cli_unstable().gc)?;
298 self.gc(clean_ctx, &gc_opts)?;
299 if !clean_ctx.dry_run {
300 self.global_cache_tracker.set_last_auto_gc()?;
301 }
302 Ok(())
303 }
304
305 pub fn gc(&mut self, clean_ctx: &mut CleanContext<'gctx>, gc_opts: &GcOpts) -> CargoResult<()> {
307 self.global_cache_tracker.clean(clean_ctx, gc_opts)?;
308 Ok(())
310 }
311}
312
313fn newer_time_span_for_config(
323 cur_span: Option<Duration>,
324 config_name: &str,
325 config_span: &str,
326) -> CargoResult<Option<Duration>> {
327 let config_span = parse_time_span_for_config(config_name, config_span)?;
328 Ok(Some(maybe_newer_span(config_span, cur_span)))
329}
330
331fn maybe_newer_span(a: Duration, b: Option<Duration>) -> Duration {
333 match b {
334 Some(b) => {
335 if b < a {
336 b
337 } else {
338 a
339 }
340 }
341 None => a,
342 }
343}
344
345fn parse_frequency(frequency: &str) -> CargoResult<Option<Duration>> {
349 if frequency == "always" {
350 return Ok(Some(Duration::new(0, 0)));
351 } else if frequency == "never" {
352 return Ok(None);
353 }
354 let duration = maybe_parse_time_span(frequency).ok_or_else(|| {
355 format_err!(
356 "config option `cache.auto-clean-frequency` expected a value of \"always\", \"never\", \
357 or \"N seconds/minutes/days/weeks/months\", got: {frequency:?}"
358 )
359 })?;
360 Ok(Some(duration))
361}
362
363fn parse_time_span_for_config(config_name: &str, span: &str) -> CargoResult<Duration> {
368 maybe_parse_time_span(span).ok_or_else(|| {
369 format_err!(
370 "config option `{config_name}` expected a value of the form \
371 \"N seconds/minutes/days/weeks/months\", got: {span:?}"
372 )
373 })
374}
375
376pub fn parse_human_size(input: &str) -> CargoResult<u64> {
378 let re = regex::Regex::new(r"(?i)^([0-9]+(\.[0-9])?) ?(b|kb|mb|gb|kib|mib|gib)?$").unwrap();
379 let cap = re.captures(input).ok_or_else(|| {
380 format_err!(
381 "invalid size `{input}`, \
382 expected a number with an optional B, kB, MB, GB, kiB, MiB, or GiB suffix"
383 )
384 })?;
385 let factor = match cap.get(3) {
386 Some(suffix) => match suffix.as_str().to_lowercase().as_str() {
387 "b" => 1.0,
388 "kb" => 1_000.0,
389 "mb" => 1_000_000.0,
390 "gb" => 1_000_000_000.0,
391 "kib" => 1024.0,
392 "mib" => 1024.0 * 1024.0,
393 "gib" => 1024.0 * 1024.0 * 1024.0,
394 s => unreachable!("suffix `{s}` out of sync with regex"),
395 },
396 None => {
397 return cap[1]
398 .parse()
399 .with_context(|| format!("expected an integer size, got `{}`", &cap[1]));
400 }
401 };
402 let num = cap[1]
403 .parse::<f64>()
404 .with_context(|| format!("expected an integer or float, found `{}`", &cap[1]))?;
405 Ok((num * factor) as u64)
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 #[test]
412 fn time_spans() {
413 let d = |x| Some(Duration::from_secs(x));
414 assert_eq!(parse_frequency("5 seconds").unwrap(), d(5));
415 assert_eq!(parse_frequency("always").unwrap(), d(0));
416 assert_eq!(parse_frequency("never").unwrap(), None);
417 }
418
419 #[test]
420 fn time_span_errors() {
421 let e =
422 parse_time_span_for_config("cache.global-clean.max-src-age", "-1 days").unwrap_err();
423 assert_eq!(
424 e.to_string(),
425 "config option `cache.global-clean.max-src-age` \
426 expected a value of the form \"N seconds/minutes/days/weeks/months\", \
427 got: \"-1 days\""
428 );
429 let e = parse_frequency("abc").unwrap_err();
430 assert_eq!(
431 e.to_string(),
432 "config option `cache.auto-clean-frequency` \
433 expected a value of \"always\", \"never\", or \"N seconds/minutes/days/weeks/months\", \
434 got: \"abc\""
435 );
436 }
437
438 #[test]
439 fn human_sizes() {
440 assert_eq!(parse_human_size("0").unwrap(), 0);
441 assert_eq!(parse_human_size("123").unwrap(), 123);
442 assert_eq!(parse_human_size("123b").unwrap(), 123);
443 assert_eq!(parse_human_size("123B").unwrap(), 123);
444 assert_eq!(parse_human_size("123 b").unwrap(), 123);
445 assert_eq!(parse_human_size("123 B").unwrap(), 123);
446 assert_eq!(parse_human_size("1kb").unwrap(), 1_000);
447 assert_eq!(parse_human_size("5kb").unwrap(), 5_000);
448 assert_eq!(parse_human_size("1mb").unwrap(), 1_000_000);
449 assert_eq!(parse_human_size("1gb").unwrap(), 1_000_000_000);
450 assert_eq!(parse_human_size("1kib").unwrap(), 1_024);
451 assert_eq!(parse_human_size("1mib").unwrap(), 1_048_576);
452 assert_eq!(parse_human_size("1gib").unwrap(), 1_073_741_824);
453 assert_eq!(parse_human_size("1.5kb").unwrap(), 1_500);
454 assert_eq!(parse_human_size("1.7b").unwrap(), 1);
455
456 assert!(parse_human_size("").is_err());
457 assert!(parse_human_size("x").is_err());
458 assert!(parse_human_size("1x").is_err());
459 assert!(parse_human_size("1 2").is_err());
460 assert!(parse_human_size("1.5").is_err());
461 assert!(parse_human_size("+1").is_err());
462 assert!(parse_human_size("123 b").is_err());
463 }
464}