From 61e8ef30d93daed853d1886e220c700f479d9c58 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Wed, 6 Sep 2023 20:30:00 -0700 Subject: [PATCH 1/9] Wrap clean operations in a CleanContext. This refactors some of the `cargo clean` code to wrap the "cleaning" operation in a `CleanContext` so that the context can be passed to other parts of cargo which can perform their own cleaning operations. There are some minor changes in the error messages to prepare for cleaning operations that aren't directly related to the build directory. --- src/cargo/ops/cargo_clean.rs | 303 ++++++++++++++++++++--------------- 1 file changed, 173 insertions(+), 130 deletions(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index e05ffecba95..982b8cc092a 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -6,14 +6,13 @@ use crate::util::edit_distance; use crate::util::errors::CargoResult; use crate::util::interning::InternedString; use crate::util::{Config, Progress, ProgressStyle}; - -use anyhow::{bail, Context as _}; +use anyhow::bail; use cargo_util::paths; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; -pub struct CleanOptions<'a> { - pub config: &'a Config, +pub struct CleanOptions<'cfg> { + pub config: &'cfg Config, /// A list of packages to clean. If empty, everything is cleaned. pub spec: Vec, /// The target arch triple to clean, or None for the host arch @@ -26,12 +25,17 @@ pub struct CleanOptions<'a> { pub doc: bool, } -/// Cleans the package's build artifacts. +pub struct CleanContext<'cfg> { + pub config: &'cfg Config, + progress: Box, +} + +/// Cleans various caches. pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let mut target_dir = ws.target_dir(); - let config = ws.config(); + let config = opts.config; + let mut ctx = CleanContext::new(config); - // If the doc option is set, we just want to delete the doc directory. if opts.doc { if !opts.spec.is_empty() { // FIXME: https://github.com/rust-lang/cargo/issues/8790 @@ -42,31 +46,44 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // names and such. bail!("--doc cannot be used with -p"); } + // If the doc option is set, we just want to delete the doc directory. target_dir = target_dir.join("doc"); - return clean_entire_folder(&target_dir.into_path_unlocked(), config); - } - - let profiles = Profiles::new(ws, opts.requested_profile)?; + ctx.remove_paths(&[target_dir.into_path_unlocked()])?; + } else { + let profiles = Profiles::new(&ws, opts.requested_profile)?; + + if opts.profile_specified { + // After parsing profiles we know the dir-name of the profile, if a profile + // was passed from the command line. If so, delete only the directory of + // that profile. + let dir_name = profiles.get_dir_name(); + target_dir = target_dir.join(dir_name); + } - if opts.profile_specified { - // After parsing profiles we know the dir-name of the profile, if a profile - // was passed from the command line. If so, delete only the directory of - // that profile. - let dir_name = profiles.get_dir_name(); - target_dir = target_dir.join(dir_name); + // If we have a spec, then we need to delete some packages, otherwise, just + // remove the whole target directory and be done with it! + // + // Note that we don't bother grabbing a lock here as we're just going to + // blow it all away anyway. + if opts.spec.is_empty() { + ctx.remove_paths(&[target_dir.into_path_unlocked()])?; + } else { + clean_specs(&mut ctx, &ws, &profiles, &opts.targets, &opts.spec)?; + } } - // If we have a spec, then we need to delete some packages, otherwise, just - // remove the whole target directory and be done with it! - // - // Note that we don't bother grabbing a lock here as we're just going to - // blow it all away anyway. - if opts.spec.is_empty() { - return clean_entire_folder(&target_dir.into_path_unlocked(), config); - } + Ok(()) +} +fn clean_specs( + ctx: &mut CleanContext<'_>, + ws: &Workspace<'_>, + profiles: &Profiles, + targets: &[String], + spec: &[String], +) -> CargoResult<()> { // Clean specific packages. - let requested_kinds = CompileKind::from_requested_targets(config, &opts.targets)?; + let requested_kinds = CompileKind::from_requested_targets(ctx.config, targets)?; let target_data = RustcTargetData::new(ws, &requested_kinds)?; let (pkg_set, resolve) = ops::resolve_ws(ws)?; let prof_dir_name = profiles.get_dir_name(); @@ -84,7 +101,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { .collect::>()?; // A Vec of layouts. This is a little convoluted because there can only be // one host_layout. - let layouts = if opts.targets.is_empty() { + let layouts = if targets.is_empty() { vec![(CompileKind::Host, &host_layout)] } else { target_layouts @@ -105,11 +122,11 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // Get Packages for the specified specs. let mut pkg_ids = Vec::new(); - for spec_str in opts.spec.iter() { + for spec_str in spec.iter() { // Translate the spec to a Package. let spec = PackageIdSpec::parse(spec_str)?; if spec.partial_version().is_some() { - config.shell().warn(&format!( + ctx.config.shell().warn(&format!( "version qualifier in `-p {}` is ignored, \ cleaning all versions of `{}` found", spec_str, @@ -117,7 +134,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { ))?; } if spec.url().is_some() { - config.shell().warn(&format!( + ctx.config.shell().warn(&format!( "url qualifier in `-p {}` ignored, \ cleaning all versions of `{}` found", spec_str, @@ -142,20 +159,16 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { } let packages = pkg_set.get_many(pkg_ids)?; - let mut progress = CleaningPackagesBar::new(config, packages.len()); + ctx.progress = Box::new(CleaningPackagesBar::new(ctx.config, packages.len())); + for pkg in packages { let pkg_dir = format!("{}-*", pkg.name()); - progress.on_cleaning_package(&pkg.name())?; + ctx.progress.on_cleaning_package(&pkg.name())?; // Clean fingerprints. for (_, layout) in &layouts_with_host { let dir = escape_glob_path(layout.fingerprint())?; - rm_rf_package_glob_containing_hash( - &pkg.name(), - &Path::new(&dir).join(&pkg_dir), - config, - &mut progress, - )?; + ctx.rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?; } for target in pkg.targets() { @@ -163,11 +176,9 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // Get both the build_script_build and the output directory. for (_, layout) in &layouts_with_host { let dir = escape_glob_path(layout.build())?; - rm_rf_package_glob_containing_hash( + ctx.rm_rf_package_glob_containing_hash( &pkg.name(), &Path::new(&dir).join(&pkg_dir), - config, - &mut progress, )?; } continue; @@ -199,35 +210,35 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let dir_glob = escape_glob_path(dir)?; let dir_glob = Path::new(&dir_glob); - rm_rf_glob(&dir_glob.join(&hashed_name), config, &mut progress)?; - rm_rf(&dir.join(&unhashed_name), config, &mut progress)?; + ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?; + ctx.rm_rf(&dir.join(&unhashed_name))?; // Remove dep-info file generated by rustc. It is not tracked in // file_types. It does not have a prefix. let hashed_dep_info = dir_glob.join(format!("{}-*.d", crate_name)); - rm_rf_glob(&hashed_dep_info, config, &mut progress)?; + ctx.rm_rf_glob(&hashed_dep_info)?; let unhashed_dep_info = dir.join(format!("{}.d", crate_name)); - rm_rf(&unhashed_dep_info, config, &mut progress)?; + ctx.rm_rf(&unhashed_dep_info)?; // Remove split-debuginfo files generated by rustc. let split_debuginfo_obj = dir_glob.join(format!("{}.*.o", crate_name)); - rm_rf_glob(&split_debuginfo_obj, config, &mut progress)?; + ctx.rm_rf_glob(&split_debuginfo_obj)?; let split_debuginfo_dwo = dir_glob.join(format!("{}.*.dwo", crate_name)); - rm_rf_glob(&split_debuginfo_dwo, config, &mut progress)?; + ctx.rm_rf_glob(&split_debuginfo_dwo)?; let split_debuginfo_dwp = dir_glob.join(format!("{}.*.dwp", crate_name)); - rm_rf_glob(&split_debuginfo_dwp, config, &mut progress)?; + ctx.rm_rf_glob(&split_debuginfo_dwp)?; // Remove the uplifted copy. if let Some(uplift_dir) = uplift_dir { let uplifted_path = uplift_dir.join(file_type.uplift_filename(target)); - rm_rf(&uplifted_path, config, &mut progress)?; + ctx.rm_rf(&uplifted_path)?; // Dep-info generated by Cargo itself. let dep_info = uplifted_path.with_extension("d"); - rm_rf(&dep_info, config, &mut progress)?; + ctx.rm_rf(&dep_info)?; } } // TODO: what to do about build_script_build? let dir = escape_glob_path(layout.incremental())?; let incremental = Path::new(&dir).join(format!("{}-*", crate_name)); - rm_rf_glob(&incremental, config, &mut progress)?; + ctx.rm_rf_glob(&incremental)?; } } } @@ -243,92 +254,124 @@ fn escape_glob_path(pattern: &Path) -> CargoResult { Ok(glob::Pattern::escape(pattern)) } -/// Glob remove artifacts for the provided `package` -/// -/// Make sure the artifact is for `package` and not another crate that is prefixed by -/// `package` by getting the original name stripped of the trailing hash and possible -/// extension -fn rm_rf_package_glob_containing_hash( - package: &str, - pattern: &Path, - config: &Config, - progress: &mut dyn CleaningProgressBar, -) -> CargoResult<()> { - // TODO: Display utf8 warning to user? Or switch to globset? - let pattern = pattern - .to_str() - .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?; - for path in glob::glob(pattern)? { - let path = path?; - - let pkg_name = path - .file_name() - .and_then(std::ffi::OsStr::to_str) - .and_then(|artifact| artifact.rsplit_once('-')) - .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))? - .0; - - if pkg_name != package { - continue; +impl<'cfg> CleanContext<'cfg> { + pub fn new(config: &'cfg Config) -> CleanContext<'cfg> { + // This progress bar will get replaced, this is just here to avoid needing + // an Option until the actual bar is created. + let progress = CleaningFolderBar::new(config, 0); + CleanContext { + config, + progress: Box::new(progress), } - - rm_rf(&path, config, progress)?; } - Ok(()) -} -fn rm_rf_glob( - pattern: &Path, - config: &Config, - progress: &mut dyn CleaningProgressBar, -) -> CargoResult<()> { - // TODO: Display utf8 warning to user? Or switch to globset? - let pattern = pattern - .to_str() - .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?; - for path in glob::glob(pattern)? { - rm_rf(&path?, config, progress)?; - } - Ok(()) -} + /// Glob remove artifacts for the provided `package` + /// + /// Make sure the artifact is for `package` and not another crate that is prefixed by + /// `package` by getting the original name stripped of the trailing hash and possible + /// extension + fn rm_rf_package_glob_containing_hash( + &mut self, + package: &str, + pattern: &Path, + ) -> CargoResult<()> { + // TODO: Display utf8 warning to user? Or switch to globset? + let pattern = pattern + .to_str() + .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?; + for path in glob::glob(pattern)? { + let path = path?; + + let pkg_name = path + .file_name() + .and_then(std::ffi::OsStr::to_str) + .and_then(|artifact| artifact.rsplit_once('-')) + .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))? + .0; + + if pkg_name != package { + continue; + } -fn rm_rf(path: &Path, config: &Config, progress: &mut dyn CleaningProgressBar) -> CargoResult<()> { - if fs::symlink_metadata(path).is_err() { - return Ok(()); + self.rm_rf(&path)?; + } + Ok(()) } - config - .shell() - .verbose(|shell| shell.status("Removing", path.display()))?; - progress.display_now()?; - - for entry in walkdir::WalkDir::new(path).contents_first(true) { - let entry = entry?; - progress.on_clean()?; - if entry.file_type().is_dir() { - // The contents should have been removed by now, but sometimes a race condition is hit - // where other files have been added by the OS. `paths::remove_dir_all` also falls back - // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in - // platform-specific edge cases. - paths::remove_dir_all(entry.path()) - .with_context(|| "could not remove build directory")?; - } else { - paths::remove_file(entry.path()).with_context(|| "failed to remove build artifact")?; + fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> { + // TODO: Display utf8 warning to user? Or switch to globset? + let pattern = pattern + .to_str() + .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?; + for path in glob::glob(pattern)? { + self.rm_rf(&path?)?; } + Ok(()) } - Ok(()) -} + pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> { + let meta = match fs::symlink_metadata(path) { + Ok(meta) => meta, + Err(e) => { + if e.kind() != std::io::ErrorKind::NotFound { + self.config + .shell() + .warn(&format!("cannot access {}: {e}", path.display()))?; + } + return Ok(()); + } + }; + + self.config + .shell() + .verbose(|shell| shell.status("Removing", path.display()))?; + self.progress.display_now()?; -fn clean_entire_folder(path: &Path, config: &Config) -> CargoResult<()> { - let num_paths = walkdir::WalkDir::new(path).into_iter().count(); - let mut progress = CleaningFolderBar::new(config, num_paths); - rm_rf(path, config, &mut progress) + if !meta.is_dir() { + return paths::remove_file(path); + } + + for entry in walkdir::WalkDir::new(path).contents_first(true) { + let entry = entry?; + self.progress.on_clean()?; + if entry.file_type().is_dir() { + // The contents should have been removed by now, but sometimes a race condition is hit + // where other files have been added by the OS. `paths::remove_dir_all` also falls back + // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in + // platform-specific edge cases. + paths::remove_dir_all(entry.path())?; + } else { + paths::remove_file(entry.path())?; + } + } + + Ok(()) + } + + /// Deletes all of the given paths, showing a progress bar as it proceeds. + /// + /// If any path does not exist, or is not accessible, this will not + /// generate an error. This only generates an error for other issues, like + /// not being able to write to the console. + pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> { + let num_paths = paths + .iter() + .map(|path| walkdir::WalkDir::new(path).into_iter().count()) + .sum(); + self.progress = Box::new(CleaningFolderBar::new(self.config, num_paths)); + for path in paths { + self.rm_rf(path)?; + } + Ok(()) + } } trait CleaningProgressBar { fn display_now(&mut self) -> CargoResult<()>; fn on_clean(&mut self) -> CargoResult<()>; + fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> { + Ok(()) + } } struct CleaningFolderBar<'cfg> { @@ -381,13 +424,6 @@ impl<'cfg> CleaningPackagesBar<'cfg> { } } - fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> { - self.cur += 1; - self.package_being_cleaned = String::from(package); - self.bar - .tick(self.cur_progress(), self.max, &self.format_message()) - } - fn cur_progress(&self) -> usize { std::cmp::min(self.cur, self.max) } @@ -412,4 +448,11 @@ impl<'cfg> CleaningProgressBar for CleaningPackagesBar<'cfg> { self.num_files_folders_cleaned += 1; Ok(()) } + + fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> { + self.cur += 1; + self.package_being_cleaned = String::from(package); + self.bar + .tick(self.cur_progress(), self.max, &self.format_message()) + } } From 45c5394703a2cf206e1dfe0516579ba394ddc2ab Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Wed, 6 Sep 2023 20:37:47 -0700 Subject: [PATCH 2/9] Add a --dry-run option to `cargo clean`. This adds a `--dry-run` option to have `cargo clean` display what it would delete without actually deleting it. --- src/bin/cargo/commands/clean.rs | 8 ++++ src/cargo/ops/cargo_clean.rs | 39 +++++++++++++++--- tests/testsuite/cargo_clean/help/stdout.log | 1 + tests/testsuite/clean.rs | 44 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index 8a5645e3310..d81d0147150 100644 --- a/src/bin/cargo/commands/clean.rs +++ b/src/bin/cargo/commands/clean.rs @@ -14,6 +14,13 @@ pub fn cli() -> Command { .arg_target_triple("Target triple to clean output for") .arg_target_dir() .arg_manifest_path() + .arg( + flag( + "dry-run", + "Display what would be deleted without deleting anything", + ) + .short('n'), + ) .after_help(color_print::cstr!( "Run `cargo help clean` for more detailed information.\n" )) @@ -33,6 +40,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { requested_profile: args.get_profile_name(config, "dev", ProfileChecking::Custom)?, profile_specified: args.contains_id("profile") || args.flag("release"), doc: args.flag("doc"), + dry_run: args.flag("dry-run"), }; ops::clean(&ws, &opts)?; Ok(()) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 982b8cc092a..61cbd29084e 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -23,11 +23,14 @@ pub struct CleanOptions<'cfg> { pub requested_profile: InternedString, /// Whether to just clean the doc directory pub doc: bool, + /// If set, doesn't delete anything. + pub dry_run: bool, } pub struct CleanContext<'cfg> { pub config: &'cfg Config, progress: Box, + pub dry_run: bool, } /// Cleans various caches. @@ -35,6 +38,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let mut target_dir = ws.target_dir(); let config = opts.config; let mut ctx = CleanContext::new(config); + ctx.dry_run = opts.dry_run; if opts.doc { if !opts.spec.is_empty() { @@ -262,6 +266,7 @@ impl<'cfg> CleanContext<'cfg> { CleanContext { config, progress: Box::new(progress), + dry_run: false, } } @@ -322,26 +327,48 @@ impl<'cfg> CleanContext<'cfg> { } }; - self.config - .shell() - .verbose(|shell| shell.status("Removing", path.display()))?; + if self.dry_run { + // Concise because if in verbose mode, the path will be written in + // the loop below. + self.config + .shell() + .concise(|shell| Ok(writeln!(shell.out(), "{}", path.display())?))?; + } else { + self.config + .shell() + .verbose(|shell| shell.status("Removing", path.display()))?; + } self.progress.display_now()?; + let rm_file = |path: &Path| { + if !self.dry_run { + paths::remove_file(path)?; + } + Ok(()) + }; + if !meta.is_dir() { - return paths::remove_file(path); + return rm_file(path); } for entry in walkdir::WalkDir::new(path).contents_first(true) { let entry = entry?; self.progress.on_clean()?; + if self.dry_run { + self.config + .shell() + .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?; + } if entry.file_type().is_dir() { // The contents should have been removed by now, but sometimes a race condition is hit // where other files have been added by the OS. `paths::remove_dir_all` also falls back // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in // platform-specific edge cases. - paths::remove_dir_all(entry.path())?; + if !self.dry_run { + paths::remove_dir_all(entry.path())?; + } } else { - paths::remove_file(entry.path())?; + rm_file(entry.path())?; } } diff --git a/tests/testsuite/cargo_clean/help/stdout.log b/tests/testsuite/cargo_clean/help/stdout.log index 2074d9633a3..6e9e82772f1 100644 --- a/tests/testsuite/cargo_clean/help/stdout.log +++ b/tests/testsuite/cargo_clean/help/stdout.log @@ -5,6 +5,7 @@ Usage: cargo[EXE] clean [OPTIONS] Options: --doc Whether or not to clean just the documentation directory -q, --quiet Do not print cargo log messages + -n, --dry-run Display what would be deleted without deleting anything -v, --verbose... Use verbose output (-vv very verbose/build.rs output) --color Coloring: auto, always, never --config Override a configuration value diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 26cc11a4a2a..ec1e6c5319f 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -782,6 +782,50 @@ fn clean_spec_reserved() { .run(); } +#[cargo_test] +fn clean_dry_run() { + // Basic `clean --dry-run` test. + Package::new("bar", "1.0.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + let ls_r = || -> Vec<_> { + let mut file_list: Vec<_> = walkdir::WalkDir::new(p.build_dir()) + .into_iter() + .filter_map(|e| e.map(|e| e.path().to_owned()).ok()) + .collect(); + file_list.sort(); + file_list + }; + + // Start with no files. + p.cargo("clean --dry-run").with_stdout("").run(); + p.cargo("check").run(); + let before = ls_r(); + p.cargo("clean --dry-run").with_stdout("[CWD]/target").run(); + // Verify it didn't delete anything. + let after = ls_r(); + assert_eq!(before, after); + let expected = cargo::util::iter_join(before.iter().map(|p| p.to_str().unwrap()), "\n"); + eprintln!("{expected}"); + // Verify the verbose output. + p.cargo("clean --dry-run -v") + .with_stdout_unordered(expected) + .run(); +} + #[cargo_test] fn doc_with_package_selection() { // --doc with -p From 495ed7ebe2b0779c9ff80327871e4cb07b9ff04b Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Wed, 6 Sep 2023 20:42:35 -0700 Subject: [PATCH 3/9] Add a summary to `cargo clean`. This adds a summary at the end when `cargo clean` finishes that displays how many files and bytes were removed. --- crates/cargo-test-support/src/compare.rs | 1 + src/cargo/ops/cargo_clean.rs | 43 +++++++++++++++++++++--- tests/testsuite/clean.rs | 25 ++++++++++---- tests/testsuite/profile_custom.rs | 4 ++- tests/testsuite/script.rs | 1 + 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index 21eb64d2809..09e3a5a0ca5 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -206,6 +206,7 @@ fn substitute_macros(input: &str) -> String { ("[UPDATING]", " Updating"), ("[ADDING]", " Adding"), ("[REMOVING]", " Removing"), + ("[REMOVED]", " Removed"), ("[DOCTEST]", " Doc-tests"), ("[PACKAGING]", " Packaging"), ("[PACKAGED]", " Packaged"), diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 61cbd29084e..6bc60766bbb 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -5,7 +5,7 @@ use crate::ops; use crate::util::edit_distance; use crate::util::errors::CargoResult; use crate::util::interning::InternedString; -use crate::util::{Config, Progress, ProgressStyle}; +use crate::util::{human_readable_bytes, Config, Progress, ProgressStyle}; use anyhow::bail; use cargo_util::paths; use std::fs; @@ -31,6 +31,8 @@ pub struct CleanContext<'cfg> { pub config: &'cfg Config, progress: Box, pub dry_run: bool, + num_files_folders_cleaned: u64, + total_bytes_removed: u64, } /// Cleans various caches. @@ -76,6 +78,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { } } + ctx.display_summary()?; Ok(()) } @@ -267,6 +270,8 @@ impl<'cfg> CleanContext<'cfg> { config, progress: Box::new(progress), dry_run: false, + num_files_folders_cleaned: 0, + total_bytes_removed: 0, } } @@ -340,7 +345,13 @@ impl<'cfg> CleanContext<'cfg> { } self.progress.display_now()?; - let rm_file = |path: &Path| { + let mut rm_file = |path: &Path, meta: Result| { + if let Ok(meta) = meta { + // Note: This can over-count bytes removed for hard-linked + // files. It also under-counts since it only counts the exact + // byte sizes and not the block sizes. + self.total_bytes_removed += meta.len(); + } if !self.dry_run { paths::remove_file(path)?; } @@ -348,12 +359,14 @@ impl<'cfg> CleanContext<'cfg> { }; if !meta.is_dir() { - return rm_file(path); + self.num_files_folders_cleaned += 1; + return rm_file(path, Ok(meta)); } for entry in walkdir::WalkDir::new(path).contents_first(true) { let entry = entry?; self.progress.on_clean()?; + self.num_files_folders_cleaned += 1; if self.dry_run { self.config .shell() @@ -368,13 +381,35 @@ impl<'cfg> CleanContext<'cfg> { paths::remove_dir_all(entry.path())?; } } else { - rm_file(entry.path())?; + rm_file(entry.path(), entry.metadata())?; } } Ok(()) } + fn display_summary(&self) -> CargoResult<()> { + let status = if self.dry_run { "Summary" } else { "Removed" }; + let byte_count = if self.total_bytes_removed == 0 { + String::new() + } else { + // Don't show a fractional number of bytes. + if self.total_bytes_removed < 1024 { + format!(", {}B total", self.total_bytes_removed) + } else { + let (bytes, unit) = human_readable_bytes(self.total_bytes_removed); + format!(", {bytes:.1}{unit} total") + } + }; + self.config.shell().status( + status, + format!( + "{} files/directories{byte_count}", + self.num_files_folders_cleaned + ), + ) + } + /// Deletes all of the given paths, showing a progress bar as it proceeds. /// /// If any path does not exist, or is not accessible, this will not diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index ec1e6c5319f..fd66fbb139e 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -272,7 +272,7 @@ fn clean_doc() { assert!(doc_path.is_dir()); - p.cargo("clean --doc").run(); + p.cargo("clean --doc").with_stderr("[REMOVED] [..]").run(); assert!(!doc_path.is_dir()); assert!(p.build_dir().is_dir()); @@ -414,9 +414,10 @@ fn clean_verbose() { if cfg!(target_os = "macos") { // Rust 1.69 has changed so that split-debuginfo=unpacked includes unpacked for rlibs. for obj in p.glob("target/debug/deps/bar-*.o") { - expected.push_str(&format!("[REMOVING] [..]{}", obj.unwrap().display())); + expected.push_str(&format!("[REMOVING] [..]{}\n", obj.unwrap().display())); } } + expected.push_str("[REMOVED] [..] files/directories, [..] total\n"); p.cargo("clean -p bar --verbose") .with_stderr_unordered(&expected) .run(); @@ -607,7 +608,8 @@ error: package ID specification `baz` did not match any packages p.cargo("clean -p bar:0.1.0") .with_stderr( "warning: version qualifier in `-p bar:0.1.0` is ignored, \ - cleaning all versions of `bar` found", + cleaning all versions of `bar` found\n\ + [REMOVED] [..] files/directories, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -661,7 +663,8 @@ error: package ID specification `baz` did not match any packages p.cargo("clean -p bar:0.1") .with_stderr( "warning: version qualifier in `-p bar:0.1` is ignored, \ - cleaning all versions of `bar` found", + cleaning all versions of `bar` found\n\ + [REMOVED] [..] files/directories, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -715,7 +718,8 @@ error: package ID specification `baz` did not match any packages p.cargo("clean -p bar:0") .with_stderr( "warning: version qualifier in `-p bar:0` is ignored, \ - cleaning all versions of `bar` found", + cleaning all versions of `bar` found\n\ + [REMOVED] [..] files/directories, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -811,10 +815,16 @@ fn clean_dry_run() { }; // Start with no files. - p.cargo("clean --dry-run").with_stdout("").run(); + p.cargo("clean --dry-run") + .with_stdout("") + .with_stderr("[SUMMARY] 0 files/directories") + .run(); p.cargo("check").run(); let before = ls_r(); - p.cargo("clean --dry-run").with_stdout("[CWD]/target").run(); + p.cargo("clean --dry-run") + .with_stdout("[CWD]/target") + .with_stderr("[SUMMARY] [..] files/directories, [..] total") + .run(); // Verify it didn't delete anything. let after = ls_r(); assert_eq!(before, after); @@ -823,6 +833,7 @@ fn clean_dry_run() { // Verify the verbose output. p.cargo("clean --dry-run -v") .with_stdout_unordered(expected) + .with_stderr("[SUMMARY] [..] files/directories, [..] total") .run(); } diff --git a/tests/testsuite/profile_custom.rs b/tests/testsuite/profile_custom.rs index ea6b54c9514..32e471796a4 100644 --- a/tests/testsuite/profile_custom.rs +++ b/tests/testsuite/profile_custom.rs @@ -543,7 +543,9 @@ fn clean_custom_dirname() { assert!(!p.build_dir().join("release").is_dir()); // This should clean 'other' - p.cargo("clean --profile=other").with_stderr("").run(); + p.cargo("clean --profile=other") + .with_stderr("[REMOVED] [..] files/directories, [..] total") + .run(); assert!(p.build_dir().join("debug").is_dir()); assert!(!p.build_dir().join("other").is_dir()); } diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index 75cafc2b2f6..fe1e33c00cc 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -980,6 +980,7 @@ fn cmd_clean_with_embedded() { .with_stderr( "\ [WARNING] `package.edition` is unspecified, defaulting to `2021` +[REMOVED] [..] files/directories, [..] total ", ) .run(); From e7e354c85bdfe188038aac8bf76674a5df0a2a4d Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Thu, 7 Sep 2023 14:38:47 -0700 Subject: [PATCH 4/9] Use existing arg_dry_run function. And drop the `-n` short flag until we decide to commit to using it generally. --- src/bin/cargo/commands/clean.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index d81d0147150..8596561c90c 100644 --- a/src/bin/cargo/commands/clean.rs +++ b/src/bin/cargo/commands/clean.rs @@ -14,13 +14,7 @@ pub fn cli() -> Command { .arg_target_triple("Target triple to clean output for") .arg_target_dir() .arg_manifest_path() - .arg( - flag( - "dry-run", - "Display what would be deleted without deleting anything", - ) - .short('n'), - ) + .arg_dry_run("Display what would be deleted without deleting anything") .after_help(color_print::cstr!( "Run `cargo help clean` for more detailed information.\n" )) @@ -40,7 +34,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { requested_profile: args.get_profile_name(config, "dev", ProfileChecking::Custom)?, profile_specified: args.contains_id("profile") || args.flag("release"), doc: args.flag("doc"), - dry_run: args.flag("dry-run"), + dry_run: args.dry_run(), }; ops::clean(&ws, &opts)?; Ok(()) From e9110aac54d01335d817613aef27dc6b913e1900 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Thu, 7 Sep 2023 14:57:38 -0700 Subject: [PATCH 5/9] Use `Self` type. --- src/cargo/ops/cargo_clean.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 6bc60766bbb..6cba612c237 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -262,7 +262,7 @@ fn escape_glob_path(pattern: &Path) -> CargoResult { } impl<'cfg> CleanContext<'cfg> { - pub fn new(config: &'cfg Config) -> CleanContext<'cfg> { + pub fn new(config: &'cfg Config) -> Self { // This progress bar will get replaced, this is just here to avoid needing // an Option until the actual bar is created. let progress = CleaningFolderBar::new(config, 0); From ebee726d8f4105dca185ddb6253508ca09b7a8f0 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 10 Sep 2023 12:02:17 -0700 Subject: [PATCH 6/9] Separately track files and directories removed. The previous status line was a little awkward in the way it combined both counts. I don't think showing the directories is particularly interesting, so they are only displayed when no files are deleted. --- src/cargo/ops/cargo_clean.rs | 32 ++++++++++++++++++++----------- tests/testsuite/clean.rs | 14 +++++++------- tests/testsuite/profile_custom.rs | 2 +- tests/testsuite/script.rs | 2 +- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 6cba612c237..84ecc5dbfa9 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -31,7 +31,8 @@ pub struct CleanContext<'cfg> { pub config: &'cfg Config, progress: Box, pub dry_run: bool, - num_files_folders_cleaned: u64, + num_files_removed: u64, + num_dirs_removed: u64, total_bytes_removed: u64, } @@ -270,7 +271,8 @@ impl<'cfg> CleanContext<'cfg> { config, progress: Box::new(progress), dry_run: false, - num_files_folders_cleaned: 0, + num_files_removed: 0, + num_dirs_removed: 0, total_bytes_removed: 0, } } @@ -352,6 +354,7 @@ impl<'cfg> CleanContext<'cfg> { // byte sizes and not the block sizes. self.total_bytes_removed += meta.len(); } + self.num_files_removed += 1; if !self.dry_run { paths::remove_file(path)?; } @@ -359,20 +362,19 @@ impl<'cfg> CleanContext<'cfg> { }; if !meta.is_dir() { - self.num_files_folders_cleaned += 1; return rm_file(path, Ok(meta)); } for entry in walkdir::WalkDir::new(path).contents_first(true) { let entry = entry?; self.progress.on_clean()?; - self.num_files_folders_cleaned += 1; if self.dry_run { self.config .shell() .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?; } if entry.file_type().is_dir() { + self.num_dirs_removed += 1; // The contents should have been removed by now, but sometimes a race condition is hit // where other files have been added by the OS. `paths::remove_dir_all` also falls back // to `std::fs::remove_dir_all`, which may be more reliable than a simple walk in @@ -401,13 +403,21 @@ impl<'cfg> CleanContext<'cfg> { format!(", {bytes:.1}{unit} total") } }; - self.config.shell().status( - status, - format!( - "{} files/directories{byte_count}", - self.num_files_folders_cleaned - ), - ) + // I think displaying the number of directories removed isn't + // particularly interesting to the user. However, if there are 0 + // files, and a nonzero number of directories, cargo should indicate + // that it did *something*, so directory counts are only shown in that + // case. + let file_count = match (self.num_files_removed, self.num_dirs_removed) { + (0, 0) => format!("0 files"), + (0, 1) => format!("1 directory"), + (0, 2..) => format!("{} directories", self.num_dirs_removed), + (1, _) => format!("1 file"), + (2.., _) => format!("{} files", self.num_files_removed), + }; + self.config + .shell() + .status(status, format!("{file_count}{byte_count}")) } /// Deletes all of the given paths, showing a progress bar as it proceeds. diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index fd66fbb139e..9474e39965e 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -417,7 +417,7 @@ fn clean_verbose() { expected.push_str(&format!("[REMOVING] [..]{}\n", obj.unwrap().display())); } } - expected.push_str("[REMOVED] [..] files/directories, [..] total\n"); + expected.push_str("[REMOVED] [..] files, [..] total\n"); p.cargo("clean -p bar --verbose") .with_stderr_unordered(&expected) .run(); @@ -609,7 +609,7 @@ error: package ID specification `baz` did not match any packages .with_stderr( "warning: version qualifier in `-p bar:0.1.0` is ignored, \ cleaning all versions of `bar` found\n\ - [REMOVED] [..] files/directories, [..] total", + [REMOVED] [..] files, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -664,7 +664,7 @@ error: package ID specification `baz` did not match any packages .with_stderr( "warning: version qualifier in `-p bar:0.1` is ignored, \ cleaning all versions of `bar` found\n\ - [REMOVED] [..] files/directories, [..] total", + [REMOVED] [..] files, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -719,7 +719,7 @@ error: package ID specification `baz` did not match any packages .with_stderr( "warning: version qualifier in `-p bar:0` is ignored, \ cleaning all versions of `bar` found\n\ - [REMOVED] [..] files/directories, [..] total", + [REMOVED] [..] files, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -817,13 +817,13 @@ fn clean_dry_run() { // Start with no files. p.cargo("clean --dry-run") .with_stdout("") - .with_stderr("[SUMMARY] 0 files/directories") + .with_stderr("[SUMMARY] 0 files") .run(); p.cargo("check").run(); let before = ls_r(); p.cargo("clean --dry-run") .with_stdout("[CWD]/target") - .with_stderr("[SUMMARY] [..] files/directories, [..] total") + .with_stderr("[SUMMARY] [..] files, [..] total") .run(); // Verify it didn't delete anything. let after = ls_r(); @@ -833,7 +833,7 @@ fn clean_dry_run() { // Verify the verbose output. p.cargo("clean --dry-run -v") .with_stdout_unordered(expected) - .with_stderr("[SUMMARY] [..] files/directories, [..] total") + .with_stderr("[SUMMARY] [..] files, [..] total") .run(); } diff --git a/tests/testsuite/profile_custom.rs b/tests/testsuite/profile_custom.rs index 32e471796a4..f7139e55267 100644 --- a/tests/testsuite/profile_custom.rs +++ b/tests/testsuite/profile_custom.rs @@ -544,7 +544,7 @@ fn clean_custom_dirname() { // This should clean 'other' p.cargo("clean --profile=other") - .with_stderr("[REMOVED] [..] files/directories, [..] total") + .with_stderr("[REMOVED] [..] files, [..] total") .run(); assert!(p.build_dir().join("debug").is_dir()); assert!(!p.build_dir().join("other").is_dir()); diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index fe1e33c00cc..c869b43f7d6 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -980,7 +980,7 @@ fn cmd_clean_with_embedded() { .with_stderr( "\ [WARNING] `package.edition` is unspecified, defaulting to `2021` -[REMOVED] [..] files/directories, [..] total +[REMOVED] [..] files, [..] total ", ) .run(); From f61d42d5ef97fe1b0fdaacc7603e465bdc2bbb06 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 10 Sep 2023 12:07:49 -0700 Subject: [PATCH 7/9] Add a warning to `cargo clean --dry-run` This makes it more consistent with other `--dry-run` commands, and makes it clearer to the user that cargo did not do anything. --- src/cargo/ops/cargo_clean.rs | 8 +++++++- tests/testsuite/clean.rs | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 84ecc5dbfa9..2a7d359f940 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -417,7 +417,13 @@ impl<'cfg> CleanContext<'cfg> { }; self.config .shell() - .status(status, format!("{file_count}{byte_count}")) + .status(status, format!("{file_count}{byte_count}"))?; + if self.dry_run { + self.config + .shell() + .warn("no files deleted due to --dry-run")?; + } + Ok(()) } /// Deletes all of the given paths, showing a progress bar as it proceeds. diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 9474e39965e..14a89136813 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -817,13 +817,19 @@ fn clean_dry_run() { // Start with no files. p.cargo("clean --dry-run") .with_stdout("") - .with_stderr("[SUMMARY] 0 files") + .with_stderr( + "[SUMMARY] 0 files\n\ + [WARNING] no files deleted due to --dry-run", + ) .run(); p.cargo("check").run(); let before = ls_r(); p.cargo("clean --dry-run") .with_stdout("[CWD]/target") - .with_stderr("[SUMMARY] [..] files, [..] total") + .with_stderr( + "[SUMMARY] [..] files, [..] total\n\ + [WARNING] no files deleted due to --dry-run", + ) .run(); // Verify it didn't delete anything. let after = ls_r(); @@ -833,7 +839,10 @@ fn clean_dry_run() { // Verify the verbose output. p.cargo("clean --dry-run -v") .with_stdout_unordered(expected) - .with_stderr("[SUMMARY] [..] files, [..] total") + .with_stderr( + "[SUMMARY] [..] files, [..] total\n\ + [WARNING] no files deleted due to --dry-run", + ) .run(); } From c2047345bd4fa870c56014ecc6fa97bbcbff1dbf Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 10 Sep 2023 12:57:00 -0700 Subject: [PATCH 8/9] Don't display paths with `cargo clean --dry-run` without `--verbose` The paths themselves aren't particularly interesting. --- src/cargo/ops/cargo_clean.rs | 13 ++++++------- tests/testsuite/clean.rs | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 2a7d359f940..6f58b8bdc83 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -334,13 +334,8 @@ impl<'cfg> CleanContext<'cfg> { } }; - if self.dry_run { - // Concise because if in verbose mode, the path will be written in - // the loop below. - self.config - .shell() - .concise(|shell| Ok(writeln!(shell.out(), "{}", path.display())?))?; - } else { + // dry-run displays paths while walking, so don't print here. + if !self.dry_run { self.config .shell() .verbose(|shell| shell.status("Removing", path.display()))?; @@ -369,6 +364,10 @@ impl<'cfg> CleanContext<'cfg> { let entry = entry?; self.progress.on_clean()?; if self.dry_run { + // This prints the path without the "Removing" status since I feel + // like it can be surprising or even frightening if cargo says it + // is removing something without actually removing it. And I can't + // come up with a different verb to use as the status. self.config .shell() .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?; diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 14a89136813..fbb4d3e5b40 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -825,7 +825,6 @@ fn clean_dry_run() { p.cargo("check").run(); let before = ls_r(); p.cargo("clean --dry-run") - .with_stdout("[CWD]/target") .with_stderr( "[SUMMARY] [..] files, [..] total\n\ [WARNING] no files deleted due to --dry-run", From 655f75db0abdaf2cb30ac7b0386fd6fcdc893497 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 10 Sep 2023 12:57:57 -0700 Subject: [PATCH 9/9] Add `cargo --dry-run` to the documentation. --- src/doc/man/cargo-clean.md | 5 +++++ src/doc/man/generated_txt/cargo-clean.txt | 5 +++++ src/doc/src/commands/cargo-clean.md | 5 +++++ src/etc/man/cargo-clean.1 | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/src/doc/man/cargo-clean.md b/src/doc/man/cargo-clean.md index 8b9ef8cb6b0..ee920b22369 100644 --- a/src/doc/man/cargo-clean.md +++ b/src/doc/man/cargo-clean.md @@ -36,6 +36,11 @@ multiple times. See {{man "cargo-pkgid" 1}} for the SPEC format. {{#options}} +{{#option "`--dry-run`" }} +Displays a summary of what would be deleted without deleting anything. +Use with `--verbose` to display the actual files that would be deleted. +{{/option}} + {{#option "`--doc`" }} This option will cause `cargo clean` to remove only the `doc` directory in the target directory. diff --git a/src/doc/man/generated_txt/cargo-clean.txt b/src/doc/man/generated_txt/cargo-clean.txt index 33cebb719db..d6b7facf4e1 100644 --- a/src/doc/man/generated_txt/cargo-clean.txt +++ b/src/doc/man/generated_txt/cargo-clean.txt @@ -22,6 +22,11 @@ OPTIONS multiple times. See cargo-pkgid(1) for the SPEC format. Clean Options + --dry-run + Displays a summary of what would be deleted without deleting + anything. Use with --verbose to display the actual files that would + be deleted. + --doc This option will cause cargo clean to remove only the doc directory in the target directory. diff --git a/src/doc/src/commands/cargo-clean.md b/src/doc/src/commands/cargo-clean.md index df43479749a..b1415828b1f 100644 --- a/src/doc/src/commands/cargo-clean.md +++ b/src/doc/src/commands/cargo-clean.md @@ -34,6 +34,11 @@ multiple times. See cargo-pkgid(1) for the SPEC f
+
--dry-run
+
Displays a summary of what would be deleted without deleting anything. +Use with --verbose to display the actual files that would be deleted.
+ +
--doc
This option will cause cargo clean to remove only the doc directory in the target directory.
diff --git a/src/etc/man/cargo-clean.1 b/src/etc/man/cargo-clean.1 index 3cb321f05f0..d71b0e0270a 100644 --- a/src/etc/man/cargo-clean.1 +++ b/src/etc/man/cargo-clean.1 @@ -25,6 +25,12 @@ multiple times. See \fBcargo\-pkgid\fR(1) for the SPEC format. .RE .SS "Clean Options" .sp +\fB\-\-dry\-run\fR +.RS 4 +Displays a summary of what would be deleted without deleting anything. +Use with \fB\-\-verbose\fR to display the actual files that would be deleted. +.RE +.sp \fB\-\-doc\fR .RS 4 This option will cause \fBcargo clean\fR to remove only the \fBdoc\fR directory in