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}