Skip to main content

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