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)?;
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) => match Layout::new(ws, Some(target), &prof_dir_name) {
126                Ok(layout) => Some(Ok((kind, layout))),
127                Err(e) => Some(Err(e)),
128            },
129            CompileKind::Host => None,
130        })
131        .collect::<CargoResult<_>>()?;
132    // A Vec of layouts. This is a little convoluted because there can only be
133    // one host_layout.
134    let layouts = if targets.is_empty() {
135        vec![(CompileKind::Host, &host_layout)]
136    } else {
137        target_layouts
138            .iter()
139            .map(|(kind, layout)| (*kind, layout))
140            .collect()
141    };
142    // Create a Vec that also includes the host for things that need to clean both.
143    let layouts_with_host: Vec<(CompileKind, &Layout)> =
144        std::iter::once((CompileKind::Host, &host_layout))
145            .chain(layouts.iter().map(|(k, l)| (*k, *l)))
146            .collect();
147
148    // Cleaning individual rustdoc crates is currently not supported.
149    // For example, the search index would need to be rebuilt to fully
150    // remove it (otherwise you're left with lots of broken links).
151    // Doc tests produce no output.
152
153    // Get Packages for the specified specs.
154    let mut pkg_ids = Vec::new();
155    for spec_str in spec.iter() {
156        // Translate the spec to a Package.
157        let spec = PackageIdSpec::parse(spec_str)?;
158        if spec.partial_version().is_some() {
159            clean_ctx.gctx.shell().warn(&format!(
160                "version qualifier in `-p {}` is ignored, \
161                cleaning all versions of `{}` found",
162                spec_str,
163                spec.name()
164            ))?;
165        }
166        if spec.url().is_some() {
167            clean_ctx.gctx.shell().warn(&format!(
168                "url qualifier in `-p {}` ignored, \
169                cleaning all versions of `{}` found",
170                spec_str,
171                spec.name()
172            ))?;
173        }
174        let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
175        if matches.is_empty() {
176            let mut suggestion = String::new();
177            suggestion.push_str(&edit_distance::closest_msg(
178                &spec.name(),
179                resolve.iter(),
180                |id| id.name().as_str(),
181                "package",
182            ));
183            anyhow::bail!(
184                "package ID specification `{}` did not match any packages{}",
185                spec,
186                suggestion
187            );
188        }
189        pkg_ids.extend(matches);
190    }
191    let packages = pkg_set.get_many(pkg_ids)?;
192
193    clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
194
195    // Try to reduce the amount of times we iterate over the same target directory by storing away
196    // the directories we've iterated over (and cleaned for a given package).
197    let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
198    for pkg in packages {
199        let pkg_dir = format!("{}-*", pkg.name());
200        clean_ctx.progress.on_cleaning_package(&pkg.name())?;
201
202        // Clean fingerprints.
203        for (_, layout) in &layouts_with_host {
204            let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
205            clean_ctx
206                .rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?;
207        }
208
209        for target in pkg.targets() {
210            if target.is_custom_build() {
211                // Get both the build_script_build and the output directory.
212                for (_, layout) in &layouts_with_host {
213                    let dir = escape_glob_path(layout.build_dir().build())?;
214                    clean_ctx.rm_rf_package_glob_containing_hash(
215                        &pkg.name(),
216                        &Path::new(&dir).join(&pkg_dir),
217                    )?;
218                }
219                continue;
220            }
221            let crate_name: Rc<str> = target.crate_name().into();
222            let path_dot: &str = &format!("{crate_name}.");
223            let path_dash: &str = &format!("{crate_name}-");
224            for &mode in &[
225                CompileMode::Build,
226                CompileMode::Test,
227                CompileMode::Check { test: false },
228            ] {
229                for (compile_kind, layout) in &layouts {
230                    if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
231                        let dir = layout.build_dir().build_unit(&pkg.name());
232                        clean_ctx.rm_rf_glob(&dir)?;
233                        continue;
234                    }
235
236                    let triple = target_data.short_name(compile_kind);
237                    let (file_types, _unsupported) = target_data
238                        .info(*compile_kind)
239                        .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
240                    let artifact_dir = layout
241                        .artifact_dir()
242                        .expect("artifact-dir was not locked during clean");
243                    let (dir, uplift_dir) = match target.kind() {
244                        TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
245                            (layout.build_dir().examples(), Some(artifact_dir.examples()))
246                        }
247                        // Tests/benchmarks are never uplifted.
248                        TargetKind::Test | TargetKind::Bench => {
249                            (layout.build_dir().legacy_deps(), None)
250                        }
251                        _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
252                    };
253                    let mut dir_glob_str = escape_glob_path(dir)?;
254                    let dir_glob = Path::new(&dir_glob_str);
255                    for file_type in file_types {
256                        // Some files include a hash in the filename, some don't.
257                        let hashed_name = file_type.output_filename(target, Some("*"));
258                        let unhashed_name = file_type.output_filename(target, None);
259
260                        clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
261                        clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
262
263                        // Remove the uplifted copy.
264                        if let Some(uplift_dir) = uplift_dir {
265                            let uplifted_path = uplift_dir.join(file_type.uplift_filename(target));
266                            clean_ctx.rm_rf(&uplifted_path)?;
267                            // Dep-info generated by Cargo itself.
268                            let dep_info = uplifted_path.with_extension("d");
269                            clean_ctx.rm_rf(&dep_info)?;
270                        }
271                    }
272                    let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
273                    clean_ctx.rm_rf(&unhashed_dep_info)?;
274
275                    if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
276                        dir_glob_str.push(std::path::MAIN_SEPARATOR);
277                    }
278                    dir_glob_str.push('*');
279                    let dir_glob_str: Rc<str> = dir_glob_str.into();
280                    if cleaned_packages
281                        .entry(dir_glob_str.clone())
282                        .or_default()
283                        .insert(crate_name.clone())
284                    {
285                        let paths = [
286                            // Remove dep-info file generated by rustc. It is not tracked in
287                            // file_types. It does not have a prefix.
288                            (path_dash, ".d"),
289                            // Remove split-debuginfo files generated by rustc.
290                            (path_dot, ".o"),
291                            (path_dot, ".dwo"),
292                            (path_dot, ".dwp"),
293                        ];
294                        clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
295                    }
296
297                    // TODO: what to do about build_script_build?
298                    let dir = escape_glob_path(layout.build_dir().incremental())?;
299                    let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
300                    clean_ctx.rm_rf_glob(&incremental)?;
301                }
302            }
303        }
304    }
305
306    Ok(())
307}
308
309fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
310    let pattern = pattern
311        .to_str()
312        .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
313    Ok(glob::Pattern::escape(pattern))
314}
315
316impl<'gctx> CleanContext<'gctx> {
317    pub fn new(gctx: &'gctx GlobalContext) -> Self {
318        // This progress bar will get replaced, this is just here to avoid needing
319        // an Option until the actual bar is created.
320        let progress = CleaningFolderBar::new(gctx, 0);
321        CleanContext {
322            gctx,
323            progress: Box::new(progress),
324            dry_run: false,
325            num_files_removed: 0,
326            num_dirs_removed: 0,
327            total_bytes_removed: 0,
328        }
329    }
330
331    /// Glob remove artifacts for the provided `package`
332    ///
333    /// Make sure the artifact is for `package` and not another crate that is prefixed by
334    /// `package` by getting the original name stripped of the trailing hash and possible
335    /// extension
336    fn rm_rf_package_glob_containing_hash(
337        &mut self,
338        package: &str,
339        pattern: &Path,
340    ) -> CargoResult<()> {
341        // TODO: Display utf8 warning to user?  Or switch to globset?
342        let pattern = pattern
343            .to_str()
344            .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
345        for path in glob::glob(pattern)? {
346            let path = path?;
347
348            let pkg_name = path
349                .file_name()
350                .and_then(std::ffi::OsStr::to_str)
351                .and_then(|artifact| artifact.rsplit_once('-'))
352                .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
353                .0;
354
355            if pkg_name != package {
356                continue;
357            }
358
359            self.rm_rf(&path)?;
360        }
361        Ok(())
362    }
363
364    fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
365        // TODO: Display utf8 warning to user?  Or switch to globset?
366        let pattern = pattern
367            .to_str()
368            .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
369        for path in glob::glob(pattern)? {
370            self.rm_rf(&path?)?;
371        }
372        Ok(())
373    }
374
375    /// Removes files matching a glob and any of the provided filename patterns (prefix/suffix pairs).
376    ///
377    /// This function iterates over files matching a glob (`pattern`) and removes those whose
378    /// filenames start and end with specific prefix/suffix pairs. It should be more efficient for
379    /// operations involving multiple prefix/suffix pairs, as it iterates over the directory
380    /// only once, unlike making multiple calls to [`Self::rm_rf_glob`].
381    fn rm_rf_prefix_list(
382        &mut self,
383        pattern: &str,
384        path_matchers: &[(&str, &str)],
385    ) -> CargoResult<()> {
386        for path in glob::glob(pattern)? {
387            let path = path?;
388            let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
389            if path_matchers
390                .iter()
391                .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
392            {
393                self.rm_rf(&path)?;
394            }
395        }
396        Ok(())
397    }
398
399    pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
400        let meta = match fs::symlink_metadata(path) {
401            Ok(meta) => meta,
402            Err(e) => {
403                if e.kind() != std::io::ErrorKind::NotFound {
404                    self.gctx
405                        .shell()
406                        .warn(&format!("cannot access {}: {e}", path.display()))?;
407                }
408                return Ok(());
409            }
410        };
411
412        // dry-run displays paths while walking, so don't print here.
413        if !self.dry_run {
414            self.gctx
415                .shell()
416                .verbose(|shell| shell.status("Removing", path.display()))?;
417        }
418        self.progress.display_now()?;
419
420        let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
421            if let Ok(meta) = meta {
422                // Note: This can over-count bytes removed for hard-linked
423                // files. It also under-counts since it only counts the exact
424                // byte sizes and not the block sizes.
425                self.total_bytes_removed += meta.len();
426            }
427            self.num_files_removed += 1;
428            if !self.dry_run {
429                paths::remove_file(path)?;
430            }
431            Ok(())
432        };
433
434        if !meta.is_dir() {
435            return rm_file(path, Ok(meta));
436        }
437
438        for entry in walkdir::WalkDir::new(path).contents_first(true) {
439            let entry = entry?;
440            self.progress.on_clean()?;
441            if self.dry_run {
442                // This prints the path without the "Removing" status since I feel
443                // like it can be surprising or even frightening if cargo says it
444                // is removing something without actually removing it. And I can't
445                // come up with a different verb to use as the status.
446                self.gctx
447                    .shell()
448                    .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
449            }
450            if entry.file_type().is_dir() {
451                self.num_dirs_removed += 1;
452                // The contents should have been removed by now, but sometimes a race condition is hit
453                // where other files have been added by the OS. `paths::remove_dir_all` also falls back
454                // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in
455                // platform-specific edge cases.
456                if !self.dry_run {
457                    paths::remove_dir_all(entry.path())?;
458                }
459            } else {
460                rm_file(entry.path(), entry.metadata())?;
461            }
462        }
463
464        Ok(())
465    }
466
467    pub fn display_summary(&self) -> CargoResult<()> {
468        let status = if self.dry_run { "Summary" } else { "Removed" };
469        let byte_count = if self.total_bytes_removed == 0 {
470            String::new()
471        } else {
472            let bytes = HumanBytes(self.total_bytes_removed);
473            format!(", {bytes:.1} total")
474        };
475        // I think displaying the number of directories removed isn't
476        // particularly interesting to the user. However, if there are 0
477        // files, and a nonzero number of directories, cargo should indicate
478        // that it did *something*, so directory counts are only shown in that
479        // case.
480        let file_count = match (self.num_files_removed, self.num_dirs_removed) {
481            (0, 0) => format!("0 files"),
482            (0, 1) => format!("1 directory"),
483            (0, 2..) => format!("{} directories", self.num_dirs_removed),
484            (1, _) => format!("1 file"),
485            (2.., _) => format!("{} files", self.num_files_removed),
486        };
487        self.gctx
488            .shell()
489            .status(status, format!("{file_count}{byte_count}"))?;
490        if self.dry_run {
491            self.gctx
492                .shell()
493                .warn("no files deleted due to --dry-run")?;
494        }
495        Ok(())
496    }
497
498    /// Deletes all of the given paths, showing a progress bar as it proceeds.
499    ///
500    /// If any path does not exist, or is not accessible, this will not
501    /// generate an error. This only generates an error for other issues, like
502    /// not being able to write to the console.
503    pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
504        let num_paths = paths
505            .iter()
506            .map(|path| walkdir::WalkDir::new(path).into_iter().count())
507            .sum();
508        self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
509        for path in paths {
510            self.rm_rf(path)?;
511        }
512        Ok(())
513    }
514}
515
516trait CleaningProgressBar {
517    fn display_now(&mut self) -> CargoResult<()>;
518    fn on_clean(&mut self) -> CargoResult<()>;
519    fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
520        Ok(())
521    }
522}
523
524struct CleaningFolderBar<'gctx> {
525    bar: Progress<'gctx>,
526    max: usize,
527    cur: usize,
528}
529
530impl<'gctx> CleaningFolderBar<'gctx> {
531    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
532        Self {
533            bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
534            max,
535            cur: 0,
536        }
537    }
538
539    fn cur_progress(&self) -> usize {
540        std::cmp::min(self.cur, self.max)
541    }
542}
543
544impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
545    fn display_now(&mut self) -> CargoResult<()> {
546        self.bar.tick_now(self.cur_progress(), self.max, "")
547    }
548
549    fn on_clean(&mut self) -> CargoResult<()> {
550        self.cur += 1;
551        self.bar.tick(self.cur_progress(), self.max, "")
552    }
553}
554
555struct CleaningPackagesBar<'gctx> {
556    bar: Progress<'gctx>,
557    max: usize,
558    cur: usize,
559    num_files_folders_cleaned: usize,
560    package_being_cleaned: String,
561}
562
563impl<'gctx> CleaningPackagesBar<'gctx> {
564    fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
565        Self {
566            bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
567            max,
568            cur: 0,
569            num_files_folders_cleaned: 0,
570            package_being_cleaned: String::new(),
571        }
572    }
573
574    fn cur_progress(&self) -> usize {
575        std::cmp::min(self.cur, self.max)
576    }
577
578    fn format_message(&self) -> String {
579        format!(
580            ": {}, {} files/folders cleaned",
581            self.package_being_cleaned, self.num_files_folders_cleaned
582        )
583    }
584}
585
586impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
587    fn display_now(&mut self) -> CargoResult<()> {
588        self.bar
589            .tick_now(self.cur_progress(), self.max, &self.format_message())
590    }
591
592    fn on_clean(&mut self) -> CargoResult<()> {
593        self.bar
594            .tick(self.cur_progress(), self.max, &self.format_message())?;
595        self.num_files_folders_cleaned += 1;
596        Ok(())
597    }
598
599    fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
600        self.cur += 1;
601        self.package_being_cleaned = String::from(package);
602        self.bar
603            .tick(self.cur_progress(), self.max, &self.format_message())
604    }
605}