diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 6d63db2dbbb..71526a36069 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -7,6 +7,7 @@ use crate::util::edit_distance; use crate::util::errors::CargoResult; use crate::util::interning::InternedString; use crate::util::{GlobalContext, Progress, ProgressStyle}; +use annotate_snippets::Level; use anyhow::bail; use cargo_util::paths; use indexmap::{IndexMap, IndexSet}; @@ -49,6 +50,22 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let mut clean_ctx = CleanContext::new(gctx); clean_ctx.dry_run = opts.dry_run; + const CLEAN_ABORT_NOTE: &str = + "cleaning has been aborted to prevent accidental deletion of unrelated files"; + + // make sure target_dir is a directory if it exists so that we don't delete files + if let Ok(meta) = fs::symlink_metadata(target_dir.as_path_unlocked()) { + // do not error if target_dir is symlink; let cargo delete it + if !meta.is_symlink() && !meta.is_dir() { + let title = format!("cannot clean `{}`: not a directory", target_dir.display()); + let report = [Level::ERROR + .primary_title(title) + .element(Level::NOTE.message(CLEAN_ABORT_NOTE))]; + gctx.shell().print_report(&report, false)?; + return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into()); + } + } + if opts.doc { if !opts.spec.is_empty() { // FIXME: https://github.com/rust-lang/cargo/issues/8790 diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 08819d67270..8bbe18c9dff 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -1058,3 +1058,153 @@ fn quiet_does_not_show_summary() { "#]]) .run(); } + +#[cargo_test] +fn target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("target", "") + .build(); + + p.cargo("clean") + .with_stderr_data(str![[r#" +[ERROR] cannot clean `[ROOT]/foo/target`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files + +"#]]) + .with_status(101) + .run(); + + assert!(p.root().join("target").exists()); +} + +#[cargo_test] +fn explicit_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .build(); + + p.cargo("clean --target-dir bar") + .with_stderr_data(str![[r#" +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files + +"#]]) + .with_status(101) + .run(); + + assert!(p.root().join("bar").exists()); +} + +#[cargo_test] +fn env_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .build(); + + p.cargo("clean") + .env("CARGO_TARGET_DIR", "bar") + .with_stderr_data(str![[r#" +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files + +"#]]) + .with_status(101) + .run(); + + assert!(p.root().join("bar").exists()); +} + +#[cargo_test] +fn config_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .file( + ".cargo/config.toml", + "[build] + target-dir = 'bar'", + ) + .build(); + + p.cargo("clean") + .with_stderr_data(str![[r#" +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files + +"#]]) + .with_status(101) + .run(); + + assert!(p.root().join("bar").exists()); +} + +#[cargo_test] +fn target_dir_is_symlink_dir() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .symlink_dir("bar-dest", "target") + .file("bar-dest/.keep", "") + .build(); + + let mut check = p.cargo("clean"); + #[cfg(windows)] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]); + } + #[cfg(not(windows))] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file, [FILE_SIZE]B total + +"#]]); + } + check.with_status(0).run(); + + // make sure cargo has not deleted the files of the symlinked target dir + assert!(p.root().join("bar-dest/.keep").exists()); +} + +#[cargo_test] +fn target_dir_is_symlink_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .symlink("bar-dest", "target") + .file("bar-dest", "") + .build(); + + let mut check = p.cargo("clean"); + #[cfg(windows)] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]); + } + #[cfg(not(windows))] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file, [FILE_SIZE]B total + +"#]]); + } + check.with_status(0).run(); + + // make sure cargo has not deleted the file of the symlinked target dir + assert!(p.root().join("bar-dest").exists()); +}