diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index 8a5645e33..d81d01471 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 982b8cc09..61cbd2908 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 2074d9633..6e9e82772 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 26cc11a4a..ec1e6c531 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