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