Skip to main content

cargo/ops/
cargo_clean.rs

1use crate::core::compiler::{CompileKind, CompileMode, Layout, RustcTargetData};
2use crate::core::profiles::Profiles;
3use crate::core::{PackageIdSpec, PackageIdSpecQuery, TargetKind, Workspace};
4use crate::ops;
5use crate::util::HumanBytes;
6use crate::util::edit_distance;
7use crate::util::errors::CargoResult;
8use crate::util::interning::InternedString;
9use crate::util::{GlobalContext, Progress, ProgressStyle};
10use anyhow::bail;
11use cargo_util::paths;
12use cargo_util_terminal::report::Level;
13use indexmap::{IndexMap, IndexSet};
14
15use std::ffi::OsString;
16use std::io::Read;
17use std::path::{Path, PathBuf};
18use std::rc::Rc;
19use std::{fs, io};
20
21pub struct CleanOptions<'gctx> {
22    pub gctx: &'gctx GlobalContext,
23    /// A list of packages to clean. If empty, everything is cleaned.
24    pub spec: IndexSet<String>,
25    /// The target arch triple to clean, or None for the host arch
26    pub targets: Vec<String>,
27    /// Whether to clean the release directory
28    pub profile_specified: bool,
29    /// Whether to clean the directory of a certain build profile
30    pub requested_profile: InternedString,
31    /// Whether to just clean the doc directory
32    pub doc: bool,
33    /// If set, doesn't delete anything.
34    pub dry_run: bool,
35    /// true if target-dir was was explicitly specified via --target-dir
36    pub explicit_target_dir_arg: bool,
37}
38
39pub struct CleanContext<'gctx> {
40    pub gctx: &'gctx GlobalContext,
41    progress: Box<dyn CleaningProgressBar + 'gctx>,
42    pub dry_run: bool,
43    num_files_removed: u64,
44    num_dirs_removed: u64,
45    total_bytes_removed: u64,
46}
47
48/// Cleans various caches.
49pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
50    let mut target_dir = ws.target_dir();
51    let mut build_dir = ws.build_dir();
52    let gctx = opts.gctx;
53    let mut clean_ctx = CleanContext::new(gctx);
54    clean_ctx.dry_run = opts.dry_run;
55
56    const CLEAN_ABORT_NOTE: &str =
57        "cleaning has been aborted to prevent accidental deletion of unrelated files";
58
59    // make sure target_dir is a directory if it exists so that we don't delete files
60    if let Ok(meta) = fs::symlink_metadata(target_dir.as_path_unlocked()) {
61        // do not error if target_dir is symlink; let cargo delete it
62        if !meta.is_symlink() && !meta.is_dir() {
63            let title = format!("cannot clean `{}`: not a directory", target_dir.display());
64            let report = [Level::ERROR
65                .primary_title(title)
66                .element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
67            gctx.shell().print_report(&report, false)?;
68            return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
69        }
70    }
71
72    // do some validation on target_dir if it was specified via --target-dir
73    if opts.explicit_target_dir_arg {
74        let target_dir_path = target_dir.as_path_unlocked();
75
76        // perform validation on target_dir only if it exists and check if the target directory has a valid CACHEDIR.TAG
77        if target_dir_path.exists()
78            && let Err(err) = validate_target_dir_tag(target_dir_path)
79        {
80            // if target_dir was passed explicitly via --target-dir, then hard error if validation fails
81            let title = format!("cannot clean `{}`: {err}", target_dir_path.display());
82            let report = [Level::ERROR
83                .primary_title(title)
84                .element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
85            gctx.shell().print_report(&report, false)?;
86            return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
87        }
88    }
89
90    if opts.doc {
91        if !opts.spec.is_empty() {
92            // FIXME: https://github.com/rust-lang/cargo/issues/8790
93            // This should support the ability to clean specific packages
94            // within the doc directory. It's a little tricky since it
95            // needs to find all documentable targets, but also consider
96            // the fact that target names might overlap with dependency
97            // names and such.
98            bail!("--doc cannot be used with -p");
99        }
100        // If the doc option is set, we just want to delete the doc directory.
101        target_dir = target_dir.join("doc");
102        clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?;
103    } else {
104        let profiles = Profiles::new(&ws, opts.requested_profile)?;
105
106        if opts.profile_specified {
107            // After parsing profiles we know the dir-name of the profile, if a profile
108            // was passed from the command line. If so, delete only the directory of
109            // that profile.
110            let dir_name = profiles.get_dir_name();
111            target_dir = target_dir.join(dir_name);
112            build_dir = build_dir.join(dir_name);
113        }
114
115        // If we have a spec, then we need to delete some packages, otherwise, just
116        // remove the whole target directory and be done with it!
117        //
118        // Note that we don't bother grabbing a lock here as we're just going to
119        // blow it all away anyway.
120        if opts.spec.is_empty() {
121            let paths: &[PathBuf] = if build_dir != target_dir {
122                &[
123                    target_dir.into_path_unlocked(),
124                    build_dir.into_path_unlocked(),
125                ]
126            } else {
127                &[target_dir.into_path_unlocked()]
128            };
129            clean_ctx.remove_paths(paths)?;
130        } else {
131            clean_specs(
132                &mut clean_ctx,
133                &ws,
134                &profiles,
135                &opts.targets,
136                &opts.spec,
137                opts.dry_run,
138            )?;
139        }
140    }
141
142    clean_ctx.display_summary()?;
143    Ok(())
144}
145
146fn validate_target_dir_tag(target_dir_path: &Path) -> CargoResult<()> {
147    const TAG_SIGNATURE: &[u8] = b"Signature: 8a477f597d28d172789f06886806bc55";
148
149    let tag_path = target_dir_path.join("CACHEDIR.TAG");
150
151    // per https://bford.info/cachedir the tag file must not be a symlink
152    if tag_path.is_symlink() {
153        bail!("expect `CACHEDIR.TAG` to be a regular file, got a symlink");
154    }
155
156    if !tag_path.is_file() {
157        bail!("missing or invalid `CACHEDIR.TAG` file");
158    }
159
160    let mut file = fs::File::open(&tag_path)
161        .map_err(|err| anyhow::anyhow!("failed to open `{}`: {}", tag_path.display(), err))?;
162
163    let mut buf = [0u8; TAG_SIGNATURE.len()];
164    match file.read_exact(&mut buf) {
165        Ok(()) if &buf[..] == TAG_SIGNATURE => {}
166        Err(e) if e.kind() != io::ErrorKind::UnexpectedEof => {
167            bail!("failed to read `{}`: {e}", tag_path.display());
168        }
169        _ => {
170            bail!("invalid signature in `CACHEDIR.TAG` file");
171        }
172    }
173
174    Ok(())
175}
176
177fn clean_specs(
178    clean_ctx: &mut CleanContext<'_>,
179    ws: &Workspace<'_>,
180    profiles: &Profiles,
181    targets: &[String],
182    spec: &IndexSet<String>,
183    dry_run: bool,
184) -> CargoResult<()> {
185    // Clean specific packages.
186    let requested_kinds = CompileKind::from_requested_targets(clean_ctx.gctx, targets)?;
187    let target_data = RustcTargetData::new(ws, &requested_kinds)?;
188    let (pkg_set, resolve) = ops::resolve_ws(ws, dry_run)?;
189    let prof_dir_name = profiles.get_dir_name();
190    let host_layout = Layout::new(ws, None, &prof_dir_name, true, true)?;
191    // Convert requested kinds to a Vec of layouts.
192    let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
193        .into_iter()
194        .filter_map(|kind| match kind {
195            CompileKind::Target(target) => {
196                match Layout::new(ws, Some(target), &prof_dir_name, true, true) {
197                    Ok(layout) => Some(Ok((kind, layout))),
198                    Err(e) => Some(Err(e)),
199                }
200            }
201            CompileKind::Host => None,
202        })
203        .collect::<CargoResult<_>>()?;
204    // A Vec of layouts. This is a little convoluted because there can only be
205    // one host_layout.
206    let layouts = if targets.is_empty() {
207        vec![(CompileKind::Host, &host_layout)]
208    } else {
209        target_layouts
210            .iter()
211            .map(|(kind, layout)| (*kind, layout))
212            .collect()
213    };
214    // Create a Vec that also includes the host for things that need to clean both.
215    let layouts_with_host: Vec<(CompileKind, &Layout)> =
216        std::iter::once((CompileKind::Host, &host_layout))
217            .chain(layouts.iter().map(|(k, l)| (*k, *l)))
218            .collect();
219
220    // Cleaning individual rustdoc crates is currently not supported.
221    // For example, the search index would need to be rebuilt to fully
222    // remove it (otherwise you're left with lots of broken links).
223    // Doc tests produce no output.
224
225    // Get Packages for the specified specs.
226    let mut pkg_ids = Vec::new();
227    for spec_str in spec.iter() {
228        // Translate the spec to a Package.
229        let spec = PackageIdSpec::parse(spec_str)?;
230        if spec.partial_version().is_some() {
231            clean_ctx.gctx.shell().warn(&format!(
232                "version qualifier in `-p {}` is ignored, \
233                cleaning all versions of `{}` found",
234                spec_str,
235                spec.name()
236            ))?;
237        }
238        if spec.url().is_some() {
239            clean_ctx.gctx.shell().warn(&format!(
240                "url qualifier in `-p {}` ignored, \
241                cleaning all versions of `{}` found",
242                spec_str,
243                spec.name()
244            ))?;
245        }
246        let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
247        if matches.is_empty() {
248            let mut suggestion = String::new();
249            suggestion.push_str(&edit_distance::closest_msg(
250                &spec.name(),
251                resolve.iter(),
252                |id| id.name().as_str(),
253                "package",
254            ));
255            anyhow::bail!(
256                "package ID specification `{}` did not match any packages{}",
257                spec,
258                suggestion
259            );
260        }
261        pkg_ids.extend(matches);
262    }
263    let packages = pkg_set.get_many(pkg_ids)?;
264
265    clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
266    let mut dirs_to_clean = DirectoriesToClean::default();
267
268    if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
269        for pkg in packages {
270            clean_ctx.progress.on_cleaning_package(&pkg.name())?;
271
272            // Remove intermediate artifacts
273            for (_compile_kind, layout) in &layouts_with_host {
274                let dir = layout.build_dir().build_unit(&pkg.name());
275                clean_ctx.rm_rf(&dir)?;
276            }
277
278            // Remove the uplifted copy.
279            for target in pkg.targets() {
280                if target.is_custom_build() {
281                    continue;
282                }
283                let crate_name: Rc<str> = target.crate_name().into();
284                for &mode in &[
285                    CompileMode::Build,
286                    CompileMode::Test,
287                    CompileMode::Check { test: false },
288                ] {
289                    for (compile_kind, layout) in &layouts {
290                        let triple = target_data.short_name(compile_kind);
291                        let (file_types, _unsupported) = target_data
292                            .info(*compile_kind)
293                            .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
294                        let artifact_dir = layout
295                            .artifact_dir()
296                            .expect("artifact-dir was not locked during clean");
297                        let uplift_dir = match target.kind() {
298                            TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
299                                Some(artifact_dir.examples())
300                            }
301                            // Tests/benchmarks are never uplifted.
302                            TargetKind::Test | TargetKind::Bench => None,
303                            _ => Some(artifact_dir.dest()),
304                        };
305                        if let Some(uplift_dir) = uplift_dir {
306                            for file_type in file_types {
307                                let uplifted_filename = file_type.uplift_filename(target);
308
309                                // Dep-info generated by Cargo itself.
310                                let dep_info = Path::new(&uplifted_filename)
311                                    .with_extension("d")
312                                    .to_string_lossy()
313                                    .into_owned();
314
315                                dirs_to_clean.mark_utf(uplift_dir, |filename| {
316                                    filename == uplifted_filename || filename == dep_info
317                                });
318                            }
319                        }
320                        let path_dash = format!("{}-", crate_name);
321
322                        dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
323                            filename.starts_with(&path_dash)
324                        });
325                    }
326                }
327            }
328        }
329    } else {
330        for pkg in packages {
331            clean_ctx.progress.on_cleaning_package(&pkg.name())?;
332
333            // Clean fingerprints.
334            for (_, layout) in &layouts_with_host {
335                dirs_to_clean.mark_utf(layout.build_dir().legacy_fingerprint(), |filename| {
336                    let Some((pkg_name, _)) = filename.rsplit_once('-') else {
337                        return false;
338                    };
339
340                    pkg_name == pkg.name().as_str()
341                });
342            }
343
344            for target in pkg.targets() {
345                if target.is_custom_build() {
346                    // Get both the build_script_build and the output directory.
347                    for (_, layout) in &layouts_with_host {
348                        dirs_to_clean.mark_utf(layout.build_dir().build(), |filename| {
349                            let Some((current_name, _)) = filename.rsplit_once('-') else {
350                                return false;
351                            };
352
353                            current_name == pkg.name().as_str()
354                        });
355                    }
356                    continue;
357                }
358                let crate_name: Rc<str> = target.crate_name().into();
359                let path_dot: &str = &format!("{crate_name}.");
360                let path_dash: &str = &format!("{crate_name}-");
361                for &mode in &[
362                    CompileMode::Build,
363                    CompileMode::Test,
364                    CompileMode::Check { test: false },
365                ] {
366                    for (compile_kind, layout) in &layouts {
367                        let triple = target_data.short_name(compile_kind);
368                        let (file_types, _unsupported) = target_data
369                            .info(*compile_kind)
370                            .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
371                        let artifact_dir = layout
372                            .artifact_dir()
373                            .expect("artifact-dir was not locked during clean");
374                        let (dir, uplift_dir) = match target.kind() {
375                            TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
376                                (layout.build_dir().examples(), Some(artifact_dir.examples()))
377                            }
378                            // Tests/benchmarks are never uplifted.
379                            TargetKind::Test | TargetKind::Bench => {
380                                (layout.build_dir().legacy_deps(), None)
381                            }
382                            _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
383                        };
384
385                        for file_type in file_types {
386                            // Some files include a hash in the filename, some don't.
387                            let (prefix, suffix) = file_type.output_prefix_suffix(target);
388                            let unhashed_name = file_type.output_filename(target, None);
389                            dirs_to_clean.mark_utf(&dir, |filename| {
390                                (filename.starts_with(&prefix) && filename.ends_with(&suffix))
391                                    || unhashed_name == filename
392                            });
393
394                            // Remove the uplifted copy.
395                            if let Some(uplift_dir) = uplift_dir {
396                                let uplifted_path =
397                                    uplift_dir.join(file_type.uplift_filename(target));
398                                clean_ctx.rm_rf(&uplifted_path)?;
399                                // Dep-info generated by Cargo itself.
400                                let dep_info = uplifted_path.with_extension("d");
401                                clean_ctx.rm_rf(&dep_info)?;
402                            }
403                        }
404                        let unhashed_dep_info = format!("{}.d", crate_name);
405                        dirs_to_clean.mark_utf(dir, |filename| filename == unhashed_dep_info);
406
407                        dirs_to_clean.mark_utf(dir, |filename| {
408                            if filename.starts_with(&path_dash) {
409                                // Remove dep-info file generated by rustc. It is not tracked in
410                                // file_types. It does not have a prefix.
411                                filename.ends_with(".d")
412                            } else if filename.starts_with(&path_dot) {
413                                // Remove split-debuginfo files generated by rustc.
414                                [".o", ".dwo", ".dwp"]
415                                    .iter()
416                                    .any(|suffix| filename.ends_with(suffix))
417                            } else {
418                                false
419                            }
420                        });
421
422                        // TODO: what to do about build_script_build?
423                        dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
424                            filename.starts_with(path_dash)
425                        });
426                    }
427                }
428            }
429        }
430    }
431    clean_ctx.rm_rf_all(dirs_to_clean)?;
432
433    Ok(())
434}
435
436#[derive(Default)]
437struct DirectoriesToClean {
438    dir_contents: IndexMap<PathBuf, IndexSet<OsString>>,
439    to_remove: IndexSet<PathBuf>,
440}
441
442impl DirectoriesToClean {
443    fn mark(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&OsString) -> bool) {
444        let entry = match self.dir_contents.entry(directory.to_owned()) {
445            indexmap::map::Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
446            indexmap::map::Entry::Vacant(vacant_entry) => {
447                let Ok(dir_entries) = std::fs::read_dir(directory.to_owned()) else {
448                    return;
449                };
450                vacant_entry.insert(
451                    dir_entries
452                        .into_iter()
453                        .flatten()
454                        .map(|entry| entry.file_name())
455                        .collect::<IndexSet<_>>(),
456                )
457            }
458        };
459
460        entry.retain(|path| {
461            let should_remove = should_remove_entry(path);
462            if should_remove {
463                self.to_remove.insert(directory.join(path));
464            }
465            !should_remove
466        });
467    }
468
469    fn mark_utf(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&str) -> bool) {
470        self.mark(directory, move |filename| {
471            let Some(as_utf) = filename.to_str() else {
472                return false;
473            };
474            should_remove_entry(as_utf)
475        });
476    }
477}
478
479impl<'gctx> CleanContext<'gctx> {
480    pub fn new(gctx: &'gctx GlobalContext) -> Self {
481        // This progress bar will get replaced, this is just here to avoid needing
482        // an Option until the actual bar is created.
483        let progress = CleaningFolderBar::new(gctx, 0);
484        CleanContext {
485            gctx,
486            progress: Box::new(progress),
487            dry_run: false,
488            num_files_removed: 0,
489            num_dirs_removed: 0,
490            total_bytes_removed: 0,
491        }
492    }
493
494    fn rm_rf_all(&mut self, dirs: DirectoriesToClean) -> CargoResult<()> {
495        for path in dirs.to_remove {
496            self.rm_rf(&path)?;
497        }
498        Ok(())
499    }
500
501    pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
502        let meta = match fs::symlink_metadata(path) {
503            Ok(meta) => meta,
504            Err(e) => {
505                if e.kind() != std::io::ErrorKind::NotFound {
506                    self.gctx
507                        .shell()
508                        .warn(&format!("cannot access {}: {e}", path.display()))?;
509                }
510                return Ok(());
511            }
512        };
513
514        // dry-run displays paths while walking, so don't print here.
515        if !self.dry_run {
516            self.gctx
517                .shell()
518                .verbose(|shell| shell.status("Removing", path.display()))?;
519        }
520        self.progress.display_now()?;
521
522        let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
523            if let Ok(meta) = meta {
524                // Note: This can over-count bytes removed for hard-linked
525                // files. It also under-counts since it only counts the exact
526                // byte sizes and not the block sizes.
527                self.total_bytes_removed += meta.len();
528            }
529            self.num_files_removed += 1;
530            if !self.dry_run {
531                paths::remove_file(path)?;
532            }
533            Ok(())
534        };
535
536        if !meta.is_dir() {
537            return rm_file(path, Ok(meta));
538        }
539
540        for entry in walkdir::WalkDir::new(path).contents_first(true) {
541            let entry = entry?;
542            self.progress.on_clean()?;
543            if self.dry_run {
544                // This prints the path without the "Removing" status since I feel
545                // like it can be surprising or even frightening if cargo says it
546                // is removing something without actually removing it. And I can't
547                // come up with a different verb to use as the status.
548                self.gctx
549                    .shell()
550                    .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
551            }
552            if entry.file_type().is_dir() {
553                self.num_dirs_removed += 1;
554                // The contents should have been removed by now, but sometimes a race condition is hit
555                // where other files have been added by the OS. `paths::remove_dir_all` also falls back
556                // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in
557                // platform-specific edge cases.
558                if !self.dry_run {
559                    paths::remove_dir_all(entry.path())?;
560                }
561            } else {
562                rm_file(entry.path(), entry.metadata())?;
563            }
564        }
565
566        Ok(())
567    }
568
569    pub fn display_summary(&self) -> CargoResult<()> {
570        let status = if self.dry_run { "Summary" } else { "Removed" };
571        let byte_count = if self.total_bytes_removed == 0 {
572            String::new()
573        } else {
574            let bytes = HumanBytes(self.total_bytes_removed);
575            format!(", {bytes:.1} total")
576        };
577        // I think displaying the number of directories removed isn't
578        // particularly interesting to the user. However, if there are 0
579        // files, and a nonzero number of directories, cargo should indicate
580        // that it did *something*, so directory counts are only shown in that
581        // case.
582        let file_count = match (self.num_files_removed, self.num_dirs_removed) {
583            (0, 0) => format!("0 files"),
584            (0, 1) => format!("1 directory"),
585            (0, 2..) => format!("{} directories", self.num_dirs_removed),
586            (1, _) => format!("1 file"),
587            (2.., _) => format!("{} files", self.num_files_removed),
588        };
589        self.gctx
590            .shell()
591            .status(status, format!("{file_count}{byte_count}"))?;
592        if self.dry_run {
593            self.gctx
594                .shell()
595                .warn("no files deleted due to --dry-run")?;
596        }
597        Ok(())
598    }
599
600    /// Deletes all of the given paths, showing a progress bar as it proceeds.
601    ///
602    /// If any path does not exist, or is not accessible, this will not
603    /// generate an error. This only generates an error for other issues, like
604    /// not being able to write to the console.
605    pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
606        let num_paths = paths
607            .iter()
608            .map(|path| walkdir::WalkDir::new(path).into_iter().count())
609            .sum();
610        self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
611        for path in paths {
612            self.rm_rf(path)?;
613        }
614        Ok(())
615    }
616}
617
618trait CleaningProgressBar {
619    fn display_now(&mut self) -> CargoResult<()>;
620    fn on_clean(&mut self) -> CargoResult<()>;
621    fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
622        Ok(())
623    }
624}
625
626struct CleaningFolderBar<'gctx> {
627    bar: Progress<'gctx>,
628    max: usize,
629    cur: usize,
630}
631
632impl<'gctx> CleaningFolderBar<'gctx> {
633    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
634        Self {
635            bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
636            max,
637            cur: 0,
638        }
639    }
640
641    fn cur_progress(&self) -> usize {
642        std::cmp::min(self.cur, self.max)
643    }
644}
645
646impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
647    fn display_now(&mut self) -> CargoResult<()> {
648        self.bar.tick_now(self.cur_progress(), self.max, "")
649    }
650
651    fn on_clean(&mut self) -> CargoResult<()> {
652        self.cur += 1;
653        self.bar.tick(self.cur_progress(), self.max, "")
654    }
655}
656
657struct CleaningPackagesBar<'gctx> {
658    bar: Progress<'gctx>,
659    max: usize,
660    cur: usize,
661    num_files_folders_cleaned: usize,
662    package_being_cleaned: String,
663}
664
665impl<'gctx> CleaningPackagesBar<'gctx> {
666    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
667        Self {
668            bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
669            max,
670            cur: 0,
671            num_files_folders_cleaned: 0,
672            package_being_cleaned: String::new(),
673        }
674    }
675
676    fn cur_progress(&self) -> usize {
677        std::cmp::min(self.cur, self.max)
678    }
679
680    fn format_message(&self) -> String {
681        format!(
682            ": {}, {} files/folders cleaned",
683            self.package_being_cleaned, self.num_files_folders_cleaned
684        )
685    }
686}
687
688impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
689    fn display_now(&mut self) -> CargoResult<()> {
690        self.bar
691            .tick_now(self.cur_progress(), self.max, &self.format_message())
692    }
693
694    fn on_clean(&mut self) -> CargoResult<()> {
695        self.bar
696            .tick(self.cur_progress(), self.max, &self.format_message())?;
697        self.num_files_folders_cleaned += 1;
698        Ok(())
699    }
700
701    fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
702        self.cur += 1;
703        self.package_being_cleaned = String::from(package);
704        self.bar
705            .tick(self.cur_progress(), self.max, &self.format_message())
706    }
707}