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