Skip to main content

cargo/ops/
cargo_clean.rs

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