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