Skip to main content

cargo/workspace/
gc.rs

1//! Support for garbage collecting unused files from downloaded files or
2//! artifacts from the target directory.
3//!
4//! The [`Gc`] type provides the high-level interface for the
5//! garbage-collection system.
6//!
7//! Garbage collection can be done "automatically" by cargo, which it does by
8//! default once a day when running any command that does a lot of work (like
9//! `cargo build`). The entry point for this is the [`auto_gc`] function,
10//! which handles some basic setup, creating the [`Gc`], and calling
11//! [`Gc::auto`].
12//!
13//! Garbage collection can also be done manually via the `cargo clean` command
14//! by passing any option that requests deleting unused files. That is
15//! implemented by calling the [`Gc::gc`] method.
16//!
17//! Garbage collection for the global cache is guided by the last-use tracking
18//! implemented in the [`crate::workspace::global_cache_tracker`] module. See that
19//! module documentation for an in-depth explanation of how global cache
20//! tracking works.
21
22use crate::context::{CargoCacheConfig, GlobalCleanConfig};
23use crate::ops::CleanContext;
24use crate::util::cache_lock::{CacheLock, CacheLockMode};
25use crate::util::time_span::maybe_parse_time_span;
26use crate::workspace::global_cache_tracker::{self, GlobalCacheTracker};
27use crate::{CargoResult, GlobalContext};
28use anyhow::{Context as _, format_err};
29use std::time::Duration;
30
31/// Default max age to auto-clean extracted sources, which can be recovered
32/// without downloading anything.
33const DEFAULT_MAX_AGE_EXTRACTED: &str = "1 month";
34/// Default max ago to auto-clean cache data, which must be downloaded to
35/// recover.
36const DEFAULT_MAX_AGE_DOWNLOADED: &str = "3 months";
37/// How often auto-gc will run by default unless overridden in the config.
38const DEFAULT_AUTO_FREQUENCY: &str = "1 day";
39
40/// Performs automatic garbage collection.
41///
42/// This is called in various places in Cargo where garbage collection should
43/// be performed automatically based on the config settings. The default
44/// behavior is to only clean once a day.
45///
46/// This should only be called in code paths for commands that are already
47/// doing a lot of work. It should only be called *after* crates are
48/// downloaded so that the last-use data is updated first.
49///
50/// It should be cheap to call this multiple times (subsequent calls are
51/// ignored), but try not to abuse that.
52pub fn auto_gc(gctx: &GlobalContext) {
53    if !gctx.network_allowed() {
54        // As a conservative choice, auto-gc is disabled when offline. If the
55        // user is indefinitely offline, we don't want to delete things they
56        // may later depend on.
57        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    // This should not be called when there are pending deferred entries, so check that.
83    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/// Options to use for garbage collection.
93#[derive(Clone, Debug, Default)]
94pub struct GcOpts {
95    /// The `--max-src-age` CLI option.
96    pub max_src_age: Option<Duration>,
97    // The `--max-crate-age` CLI option.
98    pub max_crate_age: Option<Duration>,
99    /// The `--max-index-age` CLI option.
100    pub max_index_age: Option<Duration>,
101    /// The `--max-git-co-age` CLI option.
102    pub max_git_co_age: Option<Duration>,
103    /// The `--max-git-db-age` CLI option.
104    pub max_git_db_age: Option<Duration>,
105    /// The `--max-src-size` CLI option.
106    pub max_src_size: Option<u64>,
107    /// The `--max-crate-size` CLI option.
108    pub max_crate_size: Option<u64>,
109    /// The `--max-git-size` CLI option.
110    pub max_git_size: Option<u64>,
111    /// The `--max-download-size` CLI option.
112    pub max_download_size: Option<u64>,
113}
114
115impl GcOpts {
116    /// Returns whether any download cache cleaning options are set.
117    pub fn is_download_cache_opt_set(&self) -> bool {
118        self.max_src_age.is_some()
119            || self.max_crate_age.is_some()
120            || self.max_index_age.is_some()
121            || self.max_git_co_age.is_some()
122            || self.max_git_db_age.is_some()
123            || self.max_src_size.is_some()
124            || self.max_crate_size.is_some()
125            || self.max_git_size.is_some()
126            || self.max_download_size.is_some()
127    }
128
129    /// Returns whether any download cache cleaning options based on size are set.
130    pub fn is_download_cache_size_set(&self) -> bool {
131        self.max_src_size.is_some()
132            || self.max_crate_size.is_some()
133            || self.max_git_size.is_some()
134            || self.max_download_size.is_some()
135    }
136
137    /// Updates the `GcOpts` to incorporate the specified max download age.
138    ///
139    /// "Download" means any cached data that can be re-downloaded.
140    pub fn set_max_download_age(&mut self, max_download_age: Duration) {
141        self.max_src_age = Some(maybe_newer_span(max_download_age, self.max_src_age));
142        self.max_crate_age = Some(maybe_newer_span(max_download_age, self.max_crate_age));
143        self.max_index_age = Some(maybe_newer_span(max_download_age, self.max_index_age));
144        self.max_git_co_age = Some(maybe_newer_span(max_download_age, self.max_git_co_age));
145        self.max_git_db_age = Some(maybe_newer_span(max_download_age, self.max_git_db_age));
146    }
147
148    /// Updates the configuration of this [`GcOpts`] to incorporate the
149    /// settings from config.
150    pub fn update_for_auto_gc(&mut self, gctx: &GlobalContext) -> CargoResult<()> {
151        let config = gctx
152            .get::<CargoCacheConfig>("cache")?
153            .global_clean
154            .unwrap_or_default();
155        self.update_for_auto_gc_config(&config, gctx.cli_unstable().gc)
156    }
157
158    fn update_for_auto_gc_config(
159        &mut self,
160        config: &GlobalCleanConfig,
161        unstable_allowed: bool,
162    ) -> CargoResult<()> {
163        macro_rules! config_default {
164            ($config:expr, $field:ident, $default:expr, $unstable_allowed:expr) => {
165                if !unstable_allowed {
166                    // These config options require -Zgc
167                    $default
168                } else {
169                    $config.$field.as_deref().unwrap_or($default)
170                }
171            };
172        }
173
174        self.max_src_age = newer_time_span_for_config(
175            self.max_src_age,
176            "gc.auto.max-src-age",
177            config_default!(
178                config,
179                max_src_age,
180                DEFAULT_MAX_AGE_EXTRACTED,
181                unstable_allowed
182            ),
183        )?;
184        self.max_crate_age = newer_time_span_for_config(
185            self.max_crate_age,
186            "gc.auto.max-crate-age",
187            config_default!(
188                config,
189                max_crate_age,
190                DEFAULT_MAX_AGE_DOWNLOADED,
191                unstable_allowed
192            ),
193        )?;
194        self.max_index_age = newer_time_span_for_config(
195            self.max_index_age,
196            "gc.auto.max-index-age",
197            config_default!(
198                config,
199                max_index_age,
200                DEFAULT_MAX_AGE_DOWNLOADED,
201                unstable_allowed
202            ),
203        )?;
204        self.max_git_co_age = newer_time_span_for_config(
205            self.max_git_co_age,
206            "gc.auto.max-git-co-age",
207            config_default!(
208                config,
209                max_git_co_age,
210                DEFAULT_MAX_AGE_EXTRACTED,
211                unstable_allowed
212            ),
213        )?;
214        self.max_git_db_age = newer_time_span_for_config(
215            self.max_git_db_age,
216            "gc.auto.max-git-db-age",
217            config_default!(
218                config,
219                max_git_db_age,
220                DEFAULT_MAX_AGE_DOWNLOADED,
221                unstable_allowed
222            ),
223        )?;
224        Ok(())
225    }
226}
227
228/// Garbage collector.
229///
230/// See the module docs at [`crate::workspace::gc`] for more information on GC.
231pub struct Gc<'a, 'gctx> {
232    gctx: &'gctx GlobalContext,
233    global_cache_tracker: &'a mut GlobalCacheTracker,
234    /// A lock on the package cache.
235    ///
236    /// This is important to be held, since we don't want multiple cargos to
237    /// be allowed to write to the cache at the same time, or for others to
238    /// read while we are modifying the cache.
239    #[expect(dead_code, reason = "held for `drop`")]
240    lock: CacheLock<'gctx>,
241}
242
243impl<'a, 'gctx> Gc<'a, 'gctx> {
244    pub fn new(
245        gctx: &'gctx GlobalContext,
246        global_cache_tracker: &'a mut GlobalCacheTracker,
247    ) -> CargoResult<Gc<'a, 'gctx>> {
248        let lock = gctx.acquire_package_cache_lock(CacheLockMode::MutateExclusive)?;
249        Ok(Gc {
250            gctx,
251            global_cache_tracker,
252            lock,
253        })
254    }
255
256    /// Performs automatic garbage cleaning.
257    ///
258    /// This returns immediately without doing work if garbage collection has
259    /// been performed recently (since `cache.auto-clean-frequency`).
260    fn auto(&mut self, clean_ctx: &mut CleanContext<'gctx>) -> CargoResult<()> {
261        let cache_config = self.gctx.get::<CargoCacheConfig>("cache")?;
262        let freq = cache_config.auto_clean_frequency;
263        let Some(freq) = parse_frequency(freq.as_deref().unwrap_or(DEFAULT_AUTO_FREQUENCY))? else {
264            tracing::trace!(target: "gc", "auto gc disabled");
265            return Ok(());
266        };
267        if !self.global_cache_tracker.should_run_auto_gc(freq)? {
268            return Ok(());
269        }
270        let config = cache_config.global_clean.unwrap_or_default();
271
272        let mut gc_opts = GcOpts::default();
273        gc_opts.update_for_auto_gc_config(&config, self.gctx.cli_unstable().gc)?;
274        self.gc(clean_ctx, &gc_opts)?;
275        if !clean_ctx.dry_run {
276            self.global_cache_tracker.set_last_auto_gc()?;
277        }
278        Ok(())
279    }
280
281    /// Performs garbage collection based on the given options.
282    pub fn gc(&mut self, clean_ctx: &mut CleanContext<'gctx>, gc_opts: &GcOpts) -> CargoResult<()> {
283        self.global_cache_tracker.clean(clean_ctx, gc_opts)?;
284        // In the future, other gc operations go here, such as target cleaning.
285        Ok(())
286    }
287}
288
289/// Returns the shorter duration from `cur_span` versus `config_span`.
290///
291/// This is used because the user may specify multiple options which overlap,
292/// and this will pick whichever one is shorter.
293///
294/// * `cur_span` is the span we are comparing against (the value from the CLI
295///   option). If None, just returns the config duration.
296/// * `config_name` is the name of the config option the span is loaded from.
297/// * `config_span` is the span value loaded from config.
298fn newer_time_span_for_config(
299    cur_span: Option<Duration>,
300    config_name: &str,
301    config_span: &str,
302) -> CargoResult<Option<Duration>> {
303    let config_span = parse_time_span_for_config(config_name, config_span)?;
304    Ok(Some(maybe_newer_span(config_span, cur_span)))
305}
306
307/// Returns whichever [`Duration`] is shorter.
308fn maybe_newer_span(a: Duration, b: Option<Duration>) -> Duration {
309    match b {
310        Some(b) => {
311            if b < a {
312                b
313            } else {
314                a
315            }
316        }
317        None => a,
318    }
319}
320
321/// Parses a frequency string.
322///
323/// Returns `Ok(None)` if the frequency is "never".
324fn parse_frequency(frequency: &str) -> CargoResult<Option<Duration>> {
325    if frequency == "always" {
326        return Ok(Some(Duration::new(0, 0)));
327    } else if frequency == "never" {
328        return Ok(None);
329    }
330    let duration = maybe_parse_time_span(frequency).ok_or_else(|| {
331        format_err!(
332            "config option `cache.auto-clean-frequency` expected a value of \"always\", \"never\", \
333             or \"N seconds/minutes/days/weeks/months\", got: {frequency:?}"
334        )
335    })?;
336    Ok(Some(duration))
337}
338
339/// Parses a time span value fetched from config.
340///
341/// This is here to provide better error messages specific to reading from
342/// config.
343fn parse_time_span_for_config(config_name: &str, span: &str) -> CargoResult<Duration> {
344    maybe_parse_time_span(span).ok_or_else(|| {
345        format_err!(
346            "config option `{config_name}` expected a value of the form \
347             \"N seconds/minutes/days/weeks/months\", got: {span:?}"
348        )
349    })
350}
351
352/// Parses a file size using metric or IEC units.
353pub fn parse_human_size(input: &str) -> CargoResult<u64> {
354    let re = regex::Regex::new(r"(?i)^([0-9]+(\.[0-9])?) ?(b|kb|mb|gb|kib|mib|gib)?$").unwrap();
355    let cap = re.captures(input).ok_or_else(|| {
356        format_err!(
357            "invalid size `{input}`, \
358             expected a number with an optional B, kB, MB, GB, kiB, MiB, or GiB suffix"
359        )
360    })?;
361    let factor = match cap.get(3) {
362        Some(suffix) => match suffix.as_str().to_lowercase().as_str() {
363            "b" => 1.0,
364            "kb" => 1_000.0,
365            "mb" => 1_000_000.0,
366            "gb" => 1_000_000_000.0,
367            "kib" => 1024.0,
368            "mib" => 1024.0 * 1024.0,
369            "gib" => 1024.0 * 1024.0 * 1024.0,
370            s => unreachable!("suffix `{s}` out of sync with regex"),
371        },
372        None => {
373            return cap[1]
374                .parse()
375                .with_context(|| format!("expected an integer size, got `{}`", &cap[1]));
376        }
377    };
378    let num = cap[1]
379        .parse::<f64>()
380        .with_context(|| format!("expected an integer or float, found `{}`", &cap[1]))?;
381    Ok((num * factor) as u64)
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    #[test]
388    fn time_spans() {
389        let d = |x| Some(Duration::from_secs(x));
390        assert_eq!(parse_frequency("5 seconds").unwrap(), d(5));
391        assert_eq!(parse_frequency("always").unwrap(), d(0));
392        assert_eq!(parse_frequency("never").unwrap(), None);
393    }
394
395    #[test]
396    fn time_span_errors() {
397        let e =
398            parse_time_span_for_config("cache.global-clean.max-src-age", "-1 days").unwrap_err();
399        assert_eq!(
400            e.to_string(),
401            "config option `cache.global-clean.max-src-age` \
402             expected a value of the form \"N seconds/minutes/days/weeks/months\", \
403             got: \"-1 days\""
404        );
405        let e = parse_frequency("abc").unwrap_err();
406        assert_eq!(
407            e.to_string(),
408            "config option `cache.auto-clean-frequency` \
409             expected a value of \"always\", \"never\", or \"N seconds/minutes/days/weeks/months\", \
410             got: \"abc\""
411        );
412    }
413
414    #[test]
415    fn human_sizes() {
416        assert_eq!(parse_human_size("0").unwrap(), 0);
417        assert_eq!(parse_human_size("123").unwrap(), 123);
418        assert_eq!(parse_human_size("123b").unwrap(), 123);
419        assert_eq!(parse_human_size("123B").unwrap(), 123);
420        assert_eq!(parse_human_size("123 b").unwrap(), 123);
421        assert_eq!(parse_human_size("123 B").unwrap(), 123);
422        assert_eq!(parse_human_size("1kb").unwrap(), 1_000);
423        assert_eq!(parse_human_size("5kb").unwrap(), 5_000);
424        assert_eq!(parse_human_size("1mb").unwrap(), 1_000_000);
425        assert_eq!(parse_human_size("1gb").unwrap(), 1_000_000_000);
426        assert_eq!(parse_human_size("1kib").unwrap(), 1_024);
427        assert_eq!(parse_human_size("1mib").unwrap(), 1_048_576);
428        assert_eq!(parse_human_size("1gib").unwrap(), 1_073_741_824);
429        assert_eq!(parse_human_size("1.5kb").unwrap(), 1_500);
430        assert_eq!(parse_human_size("1.7b").unwrap(), 1);
431
432        assert!(parse_human_size("").is_err());
433        assert!(parse_human_size("x").is_err());
434        assert!(parse_human_size("1x").is_err());
435        assert!(parse_human_size("1 2").is_err());
436        assert!(parse_human_size("1.5").is_err());
437        assert!(parse_human_size("+1").is_err());
438        assert!(parse_human_size("123  b").is_err());
439    }
440}