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