1use crate::core::global_cache_tracker::{self, GlobalCacheTracker};
23use crate::ops::CleanContext;
24use crate::util::cache_lock::{CacheLock, CacheLockMode};
25use crate::{CargoResult, GlobalContext};
26use anyhow::{format_err, Context as _};
27use serde::Deserialize;
28use std::time::Duration;
29
30const DEFAULT_MAX_AGE_EXTRACTED: &str = "1 month";
33const DEFAULT_MAX_AGE_DOWNLOADED: &str = "3 months";
36const DEFAULT_AUTO_FREQUENCY: &str = "1 day";
38
39pub fn auto_gc(gctx: &GlobalContext) {
52 if !gctx.cli_unstable().gc {
53 return;
54 }
55 if !gctx.network_allowed() {
56 tracing::trace!(target: "gc", "running offline, auto gc disabled");
60 return;
61 }
62
63 if let Err(e) = auto_gc_inner(gctx) {
64 if global_cache_tracker::is_silent_error(&e) && !gctx.extra_verbose() {
65 tracing::warn!(target: "gc", "failed to auto-clean cache data: {e:?}");
66 } else {
67 crate::display_warning_with_error(
68 "failed to auto-clean cache data",
69 &e,
70 &mut gctx.shell(),
71 );
72 }
73 }
74}
75
76fn auto_gc_inner(gctx: &GlobalContext) -> CargoResult<()> {
77 let _lock = match gctx.try_acquire_package_cache_lock(CacheLockMode::MutateExclusive)? {
78 Some(lock) => lock,
79 None => {
80 tracing::debug!(target: "gc", "unable to acquire mutate lock, auto gc disabled");
81 return Ok(());
82 }
83 };
84 let deferred = gctx.deferred_global_last_use()?;
86 debug_assert!(deferred.is_empty());
87 let mut global_cache_tracker = gctx.global_cache_tracker()?;
88 let mut gc = Gc::new(gctx, &mut global_cache_tracker)?;
89 let mut clean_ctx = CleanContext::new(gctx);
90 gc.auto(&mut clean_ctx)?;
91 Ok(())
92}
93
94#[derive(Deserialize, Default)]
101#[serde(rename_all = "kebab-case")]
102struct AutoConfig {
103 frequency: Option<String>,
105 max_src_age: Option<String>,
107 max_crate_age: Option<String>,
109 max_index_age: Option<String>,
111 max_git_co_age: Option<String>,
113 max_git_db_age: Option<String>,
115}
116
117#[derive(Clone, Debug, Default)]
119pub struct GcOpts {
120 pub max_src_age: Option<Duration>,
122 pub max_crate_age: Option<Duration>,
124 pub max_index_age: Option<Duration>,
126 pub max_git_co_age: Option<Duration>,
128 pub max_git_db_age: Option<Duration>,
130 pub max_src_size: Option<u64>,
132 pub max_crate_size: Option<u64>,
134 pub max_git_size: Option<u64>,
136 pub max_download_size: Option<u64>,
138}
139
140impl GcOpts {
141 pub fn is_download_cache_opt_set(&self) -> bool {
143 self.max_src_age.is_some()
144 || self.max_crate_age.is_some()
145 || self.max_index_age.is_some()
146 || self.max_git_co_age.is_some()
147 || self.max_git_db_age.is_some()
148 || self.max_src_size.is_some()
149 || self.max_crate_size.is_some()
150 || self.max_git_size.is_some()
151 || self.max_download_size.is_some()
152 }
153
154 pub fn is_download_cache_size_set(&self) -> bool {
156 self.max_src_size.is_some()
157 || self.max_crate_size.is_some()
158 || self.max_git_size.is_some()
159 || self.max_download_size.is_some()
160 }
161
162 pub fn set_max_download_age(&mut self, max_download_age: Duration) {
166 self.max_src_age = Some(maybe_newer_span(max_download_age, self.max_src_age));
167 self.max_crate_age = Some(maybe_newer_span(max_download_age, self.max_crate_age));
168 self.max_index_age = Some(maybe_newer_span(max_download_age, self.max_index_age));
169 self.max_git_co_age = Some(maybe_newer_span(max_download_age, self.max_git_co_age));
170 self.max_git_db_age = Some(maybe_newer_span(max_download_age, self.max_git_db_age));
171 }
172
173 pub fn update_for_auto_gc(&mut self, gctx: &GlobalContext) -> CargoResult<()> {
176 let auto_config = gctx
177 .get::<Option<AutoConfig>>("gc.auto")?
178 .unwrap_or_default();
179 self.update_for_auto_gc_config(&auto_config)
180 }
181
182 fn update_for_auto_gc_config(&mut self, auto_config: &AutoConfig) -> CargoResult<()> {
183 self.max_src_age = newer_time_span_for_config(
184 self.max_src_age,
185 "gc.auto.max-src-age",
186 auto_config
187 .max_src_age
188 .as_deref()
189 .unwrap_or(DEFAULT_MAX_AGE_EXTRACTED),
190 )?;
191 self.max_crate_age = newer_time_span_for_config(
192 self.max_crate_age,
193 "gc.auto.max-crate-age",
194 auto_config
195 .max_crate_age
196 .as_deref()
197 .unwrap_or(DEFAULT_MAX_AGE_DOWNLOADED),
198 )?;
199 self.max_index_age = newer_time_span_for_config(
200 self.max_index_age,
201 "gc.auto.max-index-age",
202 auto_config
203 .max_index_age
204 .as_deref()
205 .unwrap_or(DEFAULT_MAX_AGE_DOWNLOADED),
206 )?;
207 self.max_git_co_age = newer_time_span_for_config(
208 self.max_git_co_age,
209 "gc.auto.max-git-co-age",
210 auto_config
211 .max_git_co_age
212 .as_deref()
213 .unwrap_or(DEFAULT_MAX_AGE_EXTRACTED),
214 )?;
215 self.max_git_db_age = newer_time_span_for_config(
216 self.max_git_db_age,
217 "gc.auto.max-git-db-age",
218 auto_config
219 .max_git_db_age
220 .as_deref()
221 .unwrap_or(DEFAULT_MAX_AGE_DOWNLOADED),
222 )?;
223 Ok(())
224 }
225}
226
227pub struct Gc<'a, 'gctx> {
231 gctx: &'gctx GlobalContext,
232 global_cache_tracker: &'a mut GlobalCacheTracker,
233 #[allow(dead_code)] lock: CacheLock<'gctx>,
240}
241
242impl<'a, 'gctx> Gc<'a, 'gctx> {
243 pub fn new(
244 gctx: &'gctx GlobalContext,
245 global_cache_tracker: &'a mut GlobalCacheTracker,
246 ) -> CargoResult<Gc<'a, 'gctx>> {
247 let lock = gctx.acquire_package_cache_lock(CacheLockMode::MutateExclusive)?;
248 Ok(Gc {
249 gctx,
250 global_cache_tracker,
251 lock,
252 })
253 }
254
255 fn auto(&mut self, clean_ctx: &mut CleanContext<'gctx>) -> CargoResult<()> {
260 if !self.gctx.cli_unstable().gc {
261 return Ok(());
262 }
263 let auto_config = self
264 .gctx
265 .get::<Option<AutoConfig>>("gc.auto")?
266 .unwrap_or_default();
267 let Some(freq) = parse_frequency(
268 auto_config
269 .frequency
270 .as_deref()
271 .unwrap_or(DEFAULT_AUTO_FREQUENCY),
272 )?
273 else {
274 tracing::trace!(target: "gc", "auto gc disabled");
275 return Ok(());
276 };
277 if !self.global_cache_tracker.should_run_auto_gc(freq)? {
278 return Ok(());
279 }
280 let mut gc_opts = GcOpts::default();
281 gc_opts.update_for_auto_gc_config(&auto_config)?;
282 self.gc(clean_ctx, &gc_opts)?;
283 if !clean_ctx.dry_run {
284 self.global_cache_tracker.set_last_auto_gc()?;
285 }
286 Ok(())
287 }
288
289 pub fn gc(&mut self, clean_ctx: &mut CleanContext<'gctx>, gc_opts: &GcOpts) -> CargoResult<()> {
291 self.global_cache_tracker.clean(clean_ctx, gc_opts)?;
292 Ok(())
294 }
295}
296
297fn newer_time_span_for_config(
307 cur_span: Option<Duration>,
308 config_name: &str,
309 config_span: &str,
310) -> CargoResult<Option<Duration>> {
311 let config_span = parse_time_span_for_config(config_name, config_span)?;
312 Ok(Some(maybe_newer_span(config_span, cur_span)))
313}
314
315fn maybe_newer_span(a: Duration, b: Option<Duration>) -> Duration {
317 match b {
318 Some(b) => {
319 if b < a {
320 b
321 } else {
322 a
323 }
324 }
325 None => a,
326 }
327}
328
329fn parse_frequency(frequency: &str) -> CargoResult<Option<Duration>> {
333 if frequency == "always" {
334 return Ok(Some(Duration::new(0, 0)));
335 } else if frequency == "never" {
336 return Ok(None);
337 }
338 let duration = maybe_parse_time_span(frequency).ok_or_else(|| {
339 format_err!(
340 "config option `gc.auto.frequency` expected a value of \"always\", \"never\", \
341 or \"N seconds/minutes/days/weeks/months\", got: {frequency:?}"
342 )
343 })?;
344 Ok(Some(duration))
345}
346
347fn parse_time_span_for_config(config_name: &str, span: &str) -> CargoResult<Duration> {
352 maybe_parse_time_span(span).ok_or_else(|| {
353 format_err!(
354 "config option `{config_name}` expected a value of the form \
355 \"N seconds/minutes/days/weeks/months\", got: {span:?}"
356 )
357 })
358}
359
360fn maybe_parse_time_span(span: &str) -> Option<Duration> {
365 let Some(right_i) = span.find(|c: char| !c.is_ascii_digit()) else {
366 return None;
367 };
368 let (left, mut right) = span.split_at(right_i);
369 if right.starts_with(' ') {
370 right = &right[1..];
371 }
372 let count: u64 = left.parse().ok()?;
373 let factor = match right {
374 "second" | "seconds" => 1,
375 "minute" | "minutes" => 60,
376 "hour" | "hours" => 60 * 60,
377 "day" | "days" => 24 * 60 * 60,
378 "week" | "weeks" => 7 * 24 * 60 * 60,
379 "month" | "months" => 2_629_746, _ => return None,
381 };
382 Some(Duration::from_secs(factor * count))
383}
384
385pub fn parse_time_span(span: &str) -> CargoResult<Duration> {
387 maybe_parse_time_span(span).ok_or_else(|| {
388 format_err!(
389 "expected a value of the form \
390 \"N seconds/minutes/days/weeks/months\", got: {span:?}"
391 )
392 })
393}
394
395pub fn parse_human_size(input: &str) -> CargoResult<u64> {
397 let re = regex::Regex::new(r"(?i)^([0-9]+(\.[0-9])?) ?(b|kb|mb|gb|kib|mib|gib)?$").unwrap();
398 let cap = re.captures(input).ok_or_else(|| {
399 format_err!(
400 "invalid size `{input}`, \
401 expected a number with an optional B, kB, MB, GB, kiB, MiB, or GiB suffix"
402 )
403 })?;
404 let factor = match cap.get(3) {
405 Some(suffix) => match suffix.as_str().to_lowercase().as_str() {
406 "b" => 1.0,
407 "kb" => 1_000.0,
408 "mb" => 1_000_000.0,
409 "gb" => 1_000_000_000.0,
410 "kib" => 1024.0,
411 "mib" => 1024.0 * 1024.0,
412 "gib" => 1024.0 * 1024.0 * 1024.0,
413 s => unreachable!("suffix `{s}` out of sync with regex"),
414 },
415 None => {
416 return cap[1]
417 .parse()
418 .with_context(|| format!("expected an integer size, got `{}`", &cap[1]))
419 }
420 };
421 let num = cap[1]
422 .parse::<f64>()
423 .with_context(|| format!("expected an integer or float, found `{}`", &cap[1]))?;
424 Ok((num * factor) as u64)
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 #[test]
431 fn time_spans() {
432 let d = |x| Some(Duration::from_secs(x));
433 assert_eq!(maybe_parse_time_span("0 seconds"), d(0));
434 assert_eq!(maybe_parse_time_span("1second"), d(1));
435 assert_eq!(maybe_parse_time_span("23 seconds"), d(23));
436 assert_eq!(maybe_parse_time_span("5 minutes"), d(60 * 5));
437 assert_eq!(maybe_parse_time_span("2 hours"), d(60 * 60 * 2));
438 assert_eq!(maybe_parse_time_span("1 day"), d(60 * 60 * 24));
439 assert_eq!(maybe_parse_time_span("2 weeks"), d(60 * 60 * 24 * 14));
440 assert_eq!(maybe_parse_time_span("6 months"), d(2_629_746 * 6));
441
442 assert_eq!(parse_frequency("5 seconds").unwrap(), d(5));
443 assert_eq!(parse_frequency("always").unwrap(), d(0));
444 assert_eq!(parse_frequency("never").unwrap(), None);
445 }
446
447 #[test]
448 fn time_span_errors() {
449 assert_eq!(maybe_parse_time_span(""), None);
450 assert_eq!(maybe_parse_time_span("1"), None);
451 assert_eq!(maybe_parse_time_span("second"), None);
452 assert_eq!(maybe_parse_time_span("+2 seconds"), None);
453 assert_eq!(maybe_parse_time_span("day"), None);
454 assert_eq!(maybe_parse_time_span("-1 days"), None);
455 assert_eq!(maybe_parse_time_span("1.5 days"), None);
456 assert_eq!(maybe_parse_time_span("1 dayz"), None);
457 assert_eq!(maybe_parse_time_span("always"), None);
458 assert_eq!(maybe_parse_time_span("never"), None);
459 assert_eq!(maybe_parse_time_span("1 day "), None);
460 assert_eq!(maybe_parse_time_span(" 1 day"), None);
461 assert_eq!(maybe_parse_time_span("1 second"), None);
462
463 let e = parse_time_span_for_config("gc.auto.max-src-age", "-1 days").unwrap_err();
464 assert_eq!(
465 e.to_string(),
466 "config option `gc.auto.max-src-age` \
467 expected a value of the form \"N seconds/minutes/days/weeks/months\", \
468 got: \"-1 days\""
469 );
470 let e = parse_frequency("abc").unwrap_err();
471 assert_eq!(
472 e.to_string(),
473 "config option `gc.auto.frequency` \
474 expected a value of \"always\", \"never\", or \"N seconds/minutes/days/weeks/months\", \
475 got: \"abc\""
476 );
477 }
478
479 #[test]
480 fn human_sizes() {
481 assert_eq!(parse_human_size("0").unwrap(), 0);
482 assert_eq!(parse_human_size("123").unwrap(), 123);
483 assert_eq!(parse_human_size("123b").unwrap(), 123);
484 assert_eq!(parse_human_size("123B").unwrap(), 123);
485 assert_eq!(parse_human_size("123 b").unwrap(), 123);
486 assert_eq!(parse_human_size("123 B").unwrap(), 123);
487 assert_eq!(parse_human_size("1kb").unwrap(), 1_000);
488 assert_eq!(parse_human_size("5kb").unwrap(), 5_000);
489 assert_eq!(parse_human_size("1mb").unwrap(), 1_000_000);
490 assert_eq!(parse_human_size("1gb").unwrap(), 1_000_000_000);
491 assert_eq!(parse_human_size("1kib").unwrap(), 1_024);
492 assert_eq!(parse_human_size("1mib").unwrap(), 1_048_576);
493 assert_eq!(parse_human_size("1gib").unwrap(), 1_073_741_824);
494 assert_eq!(parse_human_size("1.5kb").unwrap(), 1_500);
495 assert_eq!(parse_human_size("1.7b").unwrap(), 1);
496
497 assert!(parse_human_size("").is_err());
498 assert!(parse_human_size("x").is_err());
499 assert!(parse_human_size("1x").is_err());
500 assert!(parse_human_size("1 2").is_err());
501 assert!(parse_human_size("1.5").is_err());
502 assert!(parse_human_size("+1").is_err());
503 assert!(parse_human_size("123 b").is_err());
504 }
505}