#
cargo/ops/
cargo_clean.rs

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