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                for &mode in &[
213                    CompileMode::Build,
214                    CompileMode::Test,
215                    CompileMode::Check { test: false },
216                ] {
217                    for (compile_kind, layout) in &layouts {
218                        let triple = target_data.short_name(compile_kind);
219                        let (file_types, _unsupported) = target_data
220                            .info(*compile_kind)
221                            .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
222                        let artifact_dir = layout
223                            .artifact_dir()
224                            .expect("artifact-dir was not locked during clean");
225                        let uplift_dir = match target.kind() {
226                            TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
227                                Some(artifact_dir.examples())
228                            }
229                            // Tests/benchmarks are never uplifted.
230                            TargetKind::Test | TargetKind::Bench => None,
231                            _ => Some(artifact_dir.dest()),
232                        };
233                        if let Some(uplift_dir) = uplift_dir {
234                            for file_type in file_types {
235                                let uplifted_path =
236                                    uplift_dir.join(file_type.uplift_filename(target));
237                                clean_ctx.rm_rf(&uplifted_path)?;
238                                // Dep-info generated by Cargo itself.
239                                let dep_info = uplifted_path.with_extension("d");
240                                clean_ctx.rm_rf(&dep_info)?;
241                            }
242                        }
243                    }
244                }
245            }
246        }
247    } else {
248        // Try to reduce the amount of times we iterate over the same target directory by storing away
249        // the directories we've iterated over (and cleaned for a given package).
250        let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
251        for pkg in packages {
252            let pkg_dir = format!("{}-*", pkg.name());
253            clean_ctx.progress.on_cleaning_package(&pkg.name())?;
254
255            // Clean fingerprints.
256            for (_, layout) in &layouts_with_host {
257                let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
258                clean_ctx.rm_rf_package_glob_containing_hash(
259                    &pkg.name(),
260                    &Path::new(&dir).join(&pkg_dir),
261                )?;
262            }
263
264            for target in pkg.targets() {
265                if target.is_custom_build() {
266                    // Get both the build_script_build and the output directory.
267                    for (_, layout) in &layouts_with_host {
268                        let dir = escape_glob_path(layout.build_dir().build())?;
269                        clean_ctx.rm_rf_package_glob_containing_hash(
270                            &pkg.name(),
271                            &Path::new(&dir).join(&pkg_dir),
272                        )?;
273                    }
274                    continue;
275                }
276                let crate_name: Rc<str> = target.crate_name().into();
277                let path_dot: &str = &format!("{crate_name}.");
278                let path_dash: &str = &format!("{crate_name}-");
279                for &mode in &[
280                    CompileMode::Build,
281                    CompileMode::Test,
282                    CompileMode::Check { test: false },
283                ] {
284                    for (compile_kind, layout) in &layouts {
285                        let triple = target_data.short_name(compile_kind);
286                        let (file_types, _unsupported) = target_data
287                            .info(*compile_kind)
288                            .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
289                        let artifact_dir = layout
290                            .artifact_dir()
291                            .expect("artifact-dir was not locked during clean");
292                        let (dir, uplift_dir) = match target.kind() {
293                            TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
294                                (layout.build_dir().examples(), Some(artifact_dir.examples()))
295                            }
296                            // Tests/benchmarks are never uplifted.
297                            TargetKind::Test | TargetKind::Bench => {
298                                (layout.build_dir().legacy_deps(), None)
299                            }
300                            _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
301                        };
302                        let mut dir_glob_str = escape_glob_path(dir)?;
303                        let dir_glob = Path::new(&dir_glob_str);
304                        for file_type in file_types {
305                            // Some files include a hash in the filename, some don't.
306                            let hashed_name = file_type.output_filename(target, Some("*"));
307                            let unhashed_name = file_type.output_filename(target, None);
308
309                            clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
310                            clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
311
312                            // Remove the uplifted copy.
313                            if let Some(uplift_dir) = uplift_dir {
314                                let uplifted_path =
315                                    uplift_dir.join(file_type.uplift_filename(target));
316                                clean_ctx.rm_rf(&uplifted_path)?;
317                                // Dep-info generated by Cargo itself.
318                                let dep_info = uplifted_path.with_extension("d");
319                                clean_ctx.rm_rf(&dep_info)?;
320                            }
321                        }
322                        let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
323                        clean_ctx.rm_rf(&unhashed_dep_info)?;
324
325                        if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
326                            dir_glob_str.push(std::path::MAIN_SEPARATOR);
327                        }
328                        dir_glob_str.push('*');
329                        let dir_glob_str: Rc<str> = dir_glob_str.into();
330                        if cleaned_packages
331                            .entry(dir_glob_str.clone())
332                            .or_default()
333                            .insert(crate_name.clone())
334                        {
335                            let paths = [
336                                // Remove dep-info file generated by rustc. It is not tracked in
337                                // file_types. It does not have a prefix.
338                                (path_dash, ".d"),
339                                // Remove split-debuginfo files generated by rustc.
340                                (path_dot, ".o"),
341                                (path_dot, ".dwo"),
342                                (path_dot, ".dwp"),
343                            ];
344                            clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
345                        }
346
347                        // TODO: what to do about build_script_build?
348                        let dir = escape_glob_path(layout.build_dir().incremental())?;
349                        let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
350                        clean_ctx.rm_rf_glob(&incremental)?;
351                    }
352                }
353            }
354        }
355    }
356
357    Ok(())
358}
359
360fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
361    let pattern = pattern
362        .to_str()
363        .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
364    Ok(glob::Pattern::escape(pattern))
365}
366
367impl<'gctx> CleanContext<'gctx> {
368    pub fn new(gctx: &'gctx GlobalContext) -> Self {
369        // This progress bar will get replaced, this is just here to avoid needing
370        // an Option until the actual bar is created.
371        let progress = CleaningFolderBar::new(gctx, 0);
372        CleanContext {
373            gctx,
374            progress: Box::new(progress),
375            dry_run: false,
376            num_files_removed: 0,
377            num_dirs_removed: 0,
378            total_bytes_removed: 0,
379        }
380    }
381
382    /// Glob remove artifacts for the provided `package`
383    ///
384    /// Make sure the artifact is for `package` and not another crate that is prefixed by
385    /// `package` by getting the original name stripped of the trailing hash and possible
386    /// extension
387    fn rm_rf_package_glob_containing_hash(
388        &mut self,
389        package: &str,
390        pattern: &Path,
391    ) -> CargoResult<()> {
392        // TODO: Display utf8 warning to user?  Or switch to globset?
393        let pattern = pattern
394            .to_str()
395            .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
396        for path in glob::glob(pattern)? {
397            let path = path?;
398
399            let pkg_name = path
400                .file_name()
401                .and_then(std::ffi::OsStr::to_str)
402                .and_then(|artifact| artifact.rsplit_once('-'))
403                .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
404                .0;
405
406            if pkg_name != package {
407                continue;
408            }
409
410            self.rm_rf(&path)?;
411        }
412        Ok(())
413    }
414
415    fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
416        // TODO: Display utf8 warning to user?  Or switch to globset?
417        let pattern = pattern
418            .to_str()
419            .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
420        for path in glob::glob(pattern)? {
421            self.rm_rf(&path?)?;
422        }
423        Ok(())
424    }
425
426    /// Removes files matching a glob and any of the provided filename patterns (prefix/suffix pairs).
427    ///
428    /// This function iterates over files matching a glob (`pattern`) and removes those whose
429    /// filenames start and end with specific prefix/suffix pairs. It should be more efficient for
430    /// operations involving multiple prefix/suffix pairs, as it iterates over the directory
431    /// only once, unlike making multiple calls to [`Self::rm_rf_glob`].
432    fn rm_rf_prefix_list(
433        &mut self,
434        pattern: &str,
435        path_matchers: &[(&str, &str)],
436    ) -> CargoResult<()> {
437        for path in glob::glob(pattern)? {
438            let path = path?;
439            let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
440            if path_matchers
441                .iter()
442                .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
443            {
444                self.rm_rf(&path)?;
445            }
446        }
447        Ok(())
448    }
449
450    pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
451        let meta = match fs::symlink_metadata(path) {
452            Ok(meta) => meta,
453            Err(e) => {
454                if e.kind() != std::io::ErrorKind::NotFound {
455                    self.gctx
456                        .shell()
457                        .warn(&format!("cannot access {}: {e}", path.display()))?;
458                }
459                return Ok(());
460            }
461        };
462
463        // dry-run displays paths while walking, so don't print here.
464        if !self.dry_run {
465            self.gctx
466                .shell()
467                .verbose(|shell| shell.status("Removing", path.display()))?;
468        }
469        self.progress.display_now()?;
470
471        let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
472            if let Ok(meta) = meta {
473                // Note: This can over-count bytes removed for hard-linked
474                // files. It also under-counts since it only counts the exact
475                // byte sizes and not the block sizes.
476                self.total_bytes_removed += meta.len();
477            }
478            self.num_files_removed += 1;
479            if !self.dry_run {
480                paths::remove_file(path)?;
481            }
482            Ok(())
483        };
484
485        if !meta.is_dir() {
486            return rm_file(path, Ok(meta));
487        }
488
489        for entry in walkdir::WalkDir::new(path).contents_first(true) {
490            let entry = entry?;
491            self.progress.on_clean()?;
492            if self.dry_run {
493                // This prints the path without the "Removing" status since I feel
494                // like it can be surprising or even frightening if cargo says it
495                // is removing something without actually removing it. And I can't
496                // come up with a different verb to use as the status.
497                self.gctx
498                    .shell()
499                    .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
500            }
501            if entry.file_type().is_dir() {
502                self.num_dirs_removed += 1;
503                // The contents should have been removed by now, but sometimes a race condition is hit
504                // where other files have been added by the OS. `paths::remove_dir_all` also falls back
505                // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in
506                // platform-specific edge cases.
507                if !self.dry_run {
508                    paths::remove_dir_all(entry.path())?;
509                }
510            } else {
511                rm_file(entry.path(), entry.metadata())?;
512            }
513        }
514
515        Ok(())
516    }
517
518    pub fn display_summary(&self) -> CargoResult<()> {
519        let status = if self.dry_run { "Summary" } else { "Removed" };
520        let byte_count = if self.total_bytes_removed == 0 {
521            String::new()
522        } else {
523            let bytes = HumanBytes(self.total_bytes_removed);
524            format!(", {bytes:.1} total")
525        };
526        // I think displaying the number of directories removed isn't
527        // particularly interesting to the user. However, if there are 0
528        // files, and a nonzero number of directories, cargo should indicate
529        // that it did *something*, so directory counts are only shown in that
530        // case.
531        let file_count = match (self.num_files_removed, self.num_dirs_removed) {
532            (0, 0) => format!("0 files"),
533            (0, 1) => format!("1 directory"),
534            (0, 2..) => format!("{} directories", self.num_dirs_removed),
535            (1, _) => format!("1 file"),
536            (2.., _) => format!("{} files", self.num_files_removed),
537        };
538        self.gctx
539            .shell()
540            .status(status, format!("{file_count}{byte_count}"))?;
541        if self.dry_run {
542            self.gctx
543                .shell()
544                .warn("no files deleted due to --dry-run")?;
545        }
546        Ok(())
547    }
548
549    /// Deletes all of the given paths, showing a progress bar as it proceeds.
550    ///
551    /// If any path does not exist, or is not accessible, this will not
552    /// generate an error. This only generates an error for other issues, like
553    /// not being able to write to the console.
554    pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
555        let num_paths = paths
556            .iter()
557            .map(|path| walkdir::WalkDir::new(path).into_iter().count())
558            .sum();
559        self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
560        for path in paths {
561            self.rm_rf(path)?;
562        }
563        Ok(())
564    }
565}
566
567trait CleaningProgressBar {
568    fn display_now(&mut self) -> CargoResult<()>;
569    fn on_clean(&mut self) -> CargoResult<()>;
570    fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
571        Ok(())
572    }
573}
574
575struct CleaningFolderBar<'gctx> {
576    bar: Progress<'gctx>,
577    max: usize,
578    cur: usize,
579}
580
581impl<'gctx> CleaningFolderBar<'gctx> {
582    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
583        Self {
584            bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
585            max,
586            cur: 0,
587        }
588    }
589
590    fn cur_progress(&self) -> usize {
591        std::cmp::min(self.cur, self.max)
592    }
593}
594
595impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
596    fn display_now(&mut self) -> CargoResult<()> {
597        self.bar.tick_now(self.cur_progress(), self.max, "")
598    }
599
600    fn on_clean(&mut self) -> CargoResult<()> {
601        self.cur += 1;
602        self.bar.tick(self.cur_progress(), self.max, "")
603    }
604}
605
606struct CleaningPackagesBar<'gctx> {
607    bar: Progress<'gctx>,
608    max: usize,
609    cur: usize,
610    num_files_folders_cleaned: usize,
611    package_being_cleaned: String,
612}
613
614impl<'gctx> CleaningPackagesBar<'gctx> {
615    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
616        Self {
617            bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
618            max,
619            cur: 0,
620            num_files_folders_cleaned: 0,
621            package_being_cleaned: String::new(),
622        }
623    }
624
625    fn cur_progress(&self) -> usize {
626        std::cmp::min(self.cur, self.max)
627    }
628
629    fn format_message(&self) -> String {
630        format!(
631            ": {}, {} files/folders cleaned",
632            self.package_being_cleaned, self.num_files_folders_cleaned
633        )
634    }
635}
636
637impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
638    fn display_now(&mut self) -> CargoResult<()> {
639        self.bar
640            .tick_now(self.cur_progress(), self.max, &self.format_message())
641    }
642
643    fn on_clean(&mut self) -> CargoResult<()> {
644        self.bar
645            .tick(self.cur_progress(), self.max, &self.format_message())?;
646        self.num_files_folders_cleaned += 1;
647        Ok(())
648    }
649
650    fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
651        self.cur += 1;
652        self.package_being_cleaned = String::from(package);
653        self.bar
654            .tick(self.cur_progress(), self.max, &self.format_message())
655    }
656}