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