diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 08fb3ac181b..507c2d89b1c 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -141,7 +141,12 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { // Clean fingerprints. for (_, layout) in &layouts_with_host { let dir = escape_glob_path(layout.fingerprint())?; - rm_rf_glob(&Path::new(&dir).join(&pkg_dir), config, &mut progress)?; + rm_rf_package_glob_containing_hash( + &pkg.name(), + &Path::new(&dir).join(&pkg_dir), + config, + &mut progress, + )?; } for target in pkg.targets() { @@ -149,7 +154,12 @@ 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_glob(&Path::new(&dir).join(&pkg_dir), config, &mut progress)?; + rm_rf_package_glob_containing_hash( + &pkg.name(), + &Path::new(&dir).join(&pkg_dir), + config, + &mut progress, + )?; } continue; } @@ -222,6 +232,40 @@ 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; + } + + rm_rf(&path, config, progress)?; + } + Ok(()) +} + fn rm_rf_glob( pattern: &Path, config: &Config, diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 98c1e54dded..8aa633381d4 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -96,26 +96,26 @@ fn clean_multiple_packages_in_glob_char_path() { .build(); let foo_path = &p.build_dir().join("debug").join("deps"); + #[cfg(not(target_env = "msvc"))] + let file_glob = "foo-*"; + + #[cfg(target_env = "msvc")] + let file_glob = "foo.pdb"; + // Assert that build artifacts are produced p.cargo("build").run(); - assert_ne!(get_build_artifacts(foo_path).len(), 0); + assert_ne!(get_build_artifacts(foo_path, file_glob).len(), 0); // Assert that build artifacts are destroyed p.cargo("clean -p foo").run(); - assert_eq!(get_build_artifacts(foo_path).len(), 0); + assert_eq!(get_build_artifacts(foo_path, file_glob).len(), 0); } -fn get_build_artifacts(path: &PathBuf) -> Vec> { +fn get_build_artifacts(path: &PathBuf, file_glob: &str) -> Vec> { let pattern = path.to_str().expect("expected utf-8 path"); let pattern = glob::Pattern::escape(pattern); - #[cfg(not(target_env = "msvc"))] - const FILE: &str = "foo-*"; - - #[cfg(target_env = "msvc")] - const FILE: &str = "foo.pdb"; - - let path = PathBuf::from(pattern).join(FILE); + let path = PathBuf::from(pattern).join(file_glob); let path = path.to_str().expect("expected utf-8 path"); glob::glob(path) .expect("expected glob to run") @@ -123,6 +123,86 @@ fn get_build_artifacts(path: &PathBuf) -> Vec> { .collect::>>() } +#[cargo_test] +fn clean_p_only_cleans_specified_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = [ + "foo", + "foo_core", + "foo-base", + ] + "#, + ) + .file("foo/Cargo.toml", &basic_manifest("foo", "0.1.0")) + .file("foo/src/lib.rs", "//! foo") + .file("foo_core/Cargo.toml", &basic_manifest("foo_core", "0.1.0")) + .file("foo_core/src/lib.rs", "//! foo_core") + .file("foo-base/Cargo.toml", &basic_manifest("foo-base", "0.1.0")) + .file("foo-base/src/lib.rs", "//! foo-base") + .build(); + + let fingerprint_path = &p.build_dir().join("debug").join(".fingerprint"); + + p.cargo("build -p foo -p foo_core -p foo-base").run(); + + let mut fingerprint_names = get_fingerprints_without_hashes(fingerprint_path); + + // Artifacts present for all after building + assert!(fingerprint_names.iter().any(|e| e == "foo")); + let num_foo_core_artifacts = fingerprint_names + .iter() + .filter(|&e| e == "foo_core") + .count(); + assert_ne!(num_foo_core_artifacts, 0); + let num_foo_base_artifacts = fingerprint_names + .iter() + .filter(|&e| e == "foo-base") + .count(); + assert_ne!(num_foo_base_artifacts, 0); + + p.cargo("clean -p foo").run(); + + fingerprint_names = get_fingerprints_without_hashes(fingerprint_path); + + // Cleaning `foo` leaves artifacts for the others + assert!(!fingerprint_names.iter().any(|e| e == "foo")); + assert_eq!( + fingerprint_names + .iter() + .filter(|&e| e == "foo_core") + .count(), + num_foo_core_artifacts, + ); + assert_eq!( + fingerprint_names + .iter() + .filter(|&e| e == "foo-base") + .count(), + num_foo_core_artifacts, + ); +} + +fn get_fingerprints_without_hashes(fingerprint_path: &Path) -> Vec { + std::fs::read_dir(fingerprint_path) + .expect("Build dir should be readable") + .filter_map(|entry| entry.ok()) + .map(|entry| { + let name = entry.file_name(); + let name = name + .into_string() + .expect("fingerprint name should be UTF-8"); + name.rsplit_once('-') + .expect("Name should contain at least one hyphen") + .0 + .to_owned() + }) + .collect() +} + #[cargo_test] fn clean_release() { let p = project()