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/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index 8a5645e3310..8596561c90c 100644 --- a/src/bin/cargo/commands/clean.rs +++ b/src/bin/cargo/commands/clean.rs @@ -14,6 +14,7 @@ pub fn cli() -> Command { .arg_target_triple("Target triple to clean output for") .arg_target_dir() .arg_manifest_path() + .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" )) @@ -33,6 +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.dry_run(), }; ops::clean(&ws, &opts)?; Ok(()) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index e05ffecba95..6f58b8bdc83 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -5,15 +5,14 @@ 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 anyhow::{bail, Context as _}; +use crate::util::{human_readable_bytes, Config, Progress, ProgressStyle}; +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 @@ -24,14 +23,26 @@ pub struct CleanOptions<'a> { 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, + num_files_removed: u64, + num_dirs_removed: u64, + total_bytes_removed: u64, } -/// Cleans the package's build artifacts. +/// 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); + ctx.dry_run = opts.dry_run; - // 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 +53,45 @@ 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); - } + ctx.display_summary()?; + 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 +109,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 +130,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 +142,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 +167,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 +184,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 +218,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 +262,193 @@ 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) -> 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); + CleanContext { + config, + progress: Box::new(progress), + dry_run: false, + num_files_removed: 0, + num_dirs_removed: 0, + total_bytes_removed: 0, } + } - rm_rf(&path, config, progress)?; + /// 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; + } + + self.rm_rf(&path)?; + } + Ok(()) } - 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)?; + 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(()) -} -fn rm_rf(path: &Path, config: &Config, progress: &mut dyn CleaningProgressBar) -> CargoResult<()> { - if fs::symlink_metadata(path).is_err() { - return 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(()); + } + }; + + // 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()))?; + } + self.progress.display_now()?; + + 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(); + } + self.num_files_removed += 1; + if !self.dry_run { + paths::remove_file(path)?; + } + Ok(()) + }; + + if !meta.is_dir() { + return rm_file(path, Ok(meta)); + } + + for entry in walkdir::WalkDir::new(path).contents_first(true) { + 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())?))?; + } + 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 + // platform-specific edge cases. + if !self.dry_run { + paths::remove_dir_all(entry.path())?; + } + } else { + rm_file(entry.path(), entry.metadata())?; + } + } + + 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")?; + 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 { - paths::remove_file(entry.path()).with_context(|| "failed to remove build artifact")?; + // 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") + } + }; + // 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}"))?; + if self.dry_run { + self.config + .shell() + .warn("no files deleted due to --dry-run")?; } + Ok(()) } - Ok(()) -} - -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) + /// 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 +501,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 +525,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()) + } } 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 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..fbb4d3e5b40 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, [..] 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, [..] 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, [..] 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, [..] total", ) .run(); let mut walker = walkdir::WalkDir::new(p.build_dir()) @@ -782,6 +786,65 @@ 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("") + .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_stderr( + "[SUMMARY] [..] files, [..] total\n\ + [WARNING] no files deleted due to --dry-run", + ) + .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) + .with_stderr( + "[SUMMARY] [..] files, [..] total\n\ + [WARNING] no files deleted due to --dry-run", + ) + .run(); +} + #[cargo_test] fn doc_with_package_selection() { // --doc with -p diff --git a/tests/testsuite/profile_custom.rs b/tests/testsuite/profile_custom.rs index ea6b54c9514..f7139e55267 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, [..] 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..c869b43f7d6 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, [..] total ", ) .run();