#
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)?;
    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) => match Layout::new(ws, Some(target), &prof_dir_name) {
    125                Ok(layout) => Some(Ok((kind, layout))),
    126                Err(e) => Some(Err(e)),
    127            },
    128            CompileKind::Host => None,
    129        })
    130        .collect::<CargoResult<_>>()?;
    131    // A Vec of layouts. This is a little convoluted because there can only be
    132    // one host_layout.
    133    let layouts = if targets.is_empty() {
    134        vec![(CompileKind::Host, &host_layout)]
    135    } else {
    136        target_layouts
    137            .iter()
    138            .map(|(kind, layout)| (*kind, layout))
    139            .collect()
    140    };
    141    // Create a Vec that also includes the host for things that need to clean both.
    142    let layouts_with_host: Vec<(CompileKind, &Layout)> =
    143        std::iter::once((CompileKind::Host, &host_layout))
    144            .chain(layouts.iter().map(|(k, l)| (*k, *l)))
    145            .collect();
    146
    147    // Cleaning individual rustdoc crates is currently not supported.
    148    // For example, the search index would need to be rebuilt to fully
    149    // remove it (otherwise you're left with lots of broken links).
    150    // Doc tests produce no output.
    151
    152    // Get Packages for the specified specs.
    153    let mut pkg_ids = Vec::new();
    154    for spec_str in spec.iter() {
    155        // Translate the spec to a Package.
    156        let spec = PackageIdSpec::parse(spec_str)?;
    157        if spec.partial_version().is_some() {
    158            clean_ctx.gctx.shell().warn(&format!(
    159                "version qualifier in `-p {}` is ignored, \
    160                cleaning all versions of `{}` found",
    161                spec_str,
    162                spec.name()
    163            ))?;
    164        }
    165        if spec.url().is_some() {
    166            clean_ctx.gctx.shell().warn(&format!(
    167                "url qualifier in `-p {}` ignored, \
    168                cleaning all versions of `{}` found",
    169                spec_str,
    170                spec.name()
    171            ))?;
    172        }
    173        let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
    174        if matches.is_empty() {
    175            let mut suggestion = String::new();
    176            suggestion.push_str(&edit_distance::closest_msg(
    177                &spec.name(),
    178                resolve.iter(),
    179                |id| id.name().as_str(),
    180                "package",
    181            ));
    182            anyhow::bail!(
    183                "package ID specification `{}` did not match any packages{}",
    184                spec,
    185                suggestion
    186            );
    187        }
    188        pkg_ids.extend(matches);
    189    }
    190    let packages = pkg_set.get_many(pkg_ids)?;
    191
    192    clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
    193
    194    // Try to reduce the amount of times we iterate over the same target directory by storing away
    195    // the directories we've iterated over (and cleaned for a given package).
    196    let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
    197    for pkg in packages {
    198        let pkg_dir = format!("{}-*", pkg.name());
    199        clean_ctx.progress.on_cleaning_package(&pkg.name())?;
    200
    201        // Clean fingerprints.
    202        for (_, layout) in &layouts_with_host {
    203            let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
    204            clean_ctx
    205                .rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?;
    206        }
    207
    208        for target in pkg.targets() {
    209            if target.is_custom_build() {
    210                // Get both the build_script_build and the output directory.
    211                for (_, layout) in &layouts_with_host {
    212                    let dir = escape_glob_path(layout.build_dir().build())?;
    213                    clean_ctx.rm_rf_package_glob_containing_hash(
    214                        &pkg.name(),
    215                        &Path::new(&dir).join(&pkg_dir),
    216                    )?;
    217                }
    218                continue;
    219            }
    220            let crate_name: Rc<str> = target.crate_name().into();
    221            let path_dot: &str = &format!("{crate_name}.");
    222            let path_dash: &str = &format!("{crate_name}-");
    223            for &mode in &[
    224                CompileMode::Build,
    225                CompileMode::Test,
    226                CompileMode::Check { test: false },
    227            ] {
    228                for (compile_kind, layout) in &layouts {
    229                    if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
    230                        let dir = layout.build_dir().build_unit(&pkg.name());
    231                        clean_ctx.rm_rf_glob(&dir)?;
    232                        continue;
    233                    }
    234
    235                    let triple = target_data.short_name(compile_kind);
    236                    let (file_types, _unsupported) = target_data
    237                        .info(*compile_kind)
    238                        .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
    239                    let (dir, uplift_dir) = match target.kind() {
    240                        TargetKind::ExampleBin | TargetKind::ExampleLib(..) => (
    241                            layout.build_dir().examples(),
    242                            Some(layout.artifact_dir().examples()),
    243                        ),
    244                        // Tests/benchmarks are never uplifted.
    245                        TargetKind::Test | TargetKind::Bench => {
    246                            (layout.build_dir().legacy_deps(), None)
    247                        }
    248                        _ => (
    249                            layout.build_dir().legacy_deps(),
    250                            Some(layout.artifact_dir().dest()),
    251                        ),
    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}