1use crate::core::compiler::{CompileKind, CompileMode, Layout, RustcTargetData};
2use crate::core::profiles::Profiles;
3use crate::core::{PackageIdSpec, PackageIdSpecQuery, TargetKind, Workspace};
4use crate::ops;
5use crate::util::HumanBytes;
6use crate::util::edit_distance;
7use crate::util::errors::CargoResult;
8use crate::util::interning::InternedString;
9use crate::util::{GlobalContext, Progress, ProgressStyle};
10use annotate_snippets::Level;
11use anyhow::bail;
12use cargo_util::paths;
13use indexmap::{IndexMap, IndexSet};
14
15use std::ffi::OsString;
16use std::fs;
17use std::path::{Path, PathBuf};
18use std::rc::Rc;
19
20pub struct CleanOptions<'gctx> {
21 pub gctx: &'gctx GlobalContext,
22 pub spec: IndexSet<String>,
24 pub targets: Vec<String>,
26 pub profile_specified: bool,
28 pub requested_profile: InternedString,
30 pub doc: bool,
32 pub dry_run: bool,
34}
35
36pub struct CleanContext<'gctx> {
37 pub gctx: &'gctx GlobalContext,
38 progress: Box<dyn CleaningProgressBar + 'gctx>,
39 pub dry_run: bool,
40 num_files_removed: u64,
41 num_dirs_removed: u64,
42 total_bytes_removed: u64,
43}
44
45pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
47 let mut target_dir = ws.target_dir();
48 let mut build_dir = ws.build_dir();
49 let gctx = opts.gctx;
50 let mut clean_ctx = CleanContext::new(gctx);
51 clean_ctx.dry_run = opts.dry_run;
52
53 const CLEAN_ABORT_NOTE: &str =
54 "cleaning has been aborted to prevent accidental deletion of unrelated files";
55
56 if let Ok(meta) = fs::symlink_metadata(target_dir.as_path_unlocked()) {
58 if !meta.is_symlink() && !meta.is_dir() {
60 let title = format!("cannot clean `{}`: not a directory", target_dir.display());
61 let report = [Level::ERROR
62 .primary_title(title)
63 .element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
64 gctx.shell().print_report(&report, false)?;
65 return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
66 }
67 }
68
69 if opts.doc {
70 if !opts.spec.is_empty() {
71 bail!("--doc cannot be used with -p");
78 }
79 target_dir = target_dir.join("doc");
81 clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?;
82 } else {
83 let profiles = Profiles::new(&ws, opts.requested_profile)?;
84
85 if opts.profile_specified {
86 let dir_name = profiles.get_dir_name();
90 target_dir = target_dir.join(dir_name);
91 build_dir = build_dir.join(dir_name);
92 }
93
94 if opts.spec.is_empty() {
100 let paths: &[PathBuf] = if build_dir != target_dir {
101 &[
102 target_dir.into_path_unlocked(),
103 build_dir.into_path_unlocked(),
104 ]
105 } else {
106 &[target_dir.into_path_unlocked()]
107 };
108 clean_ctx.remove_paths(paths)?;
109 } else {
110 clean_specs(
111 &mut clean_ctx,
112 &ws,
113 &profiles,
114 &opts.targets,
115 &opts.spec,
116 opts.dry_run,
117 )?;
118 }
119 }
120
121 clean_ctx.display_summary()?;
122 Ok(())
123}
124
125fn clean_specs(
126 clean_ctx: &mut CleanContext<'_>,
127 ws: &Workspace<'_>,
128 profiles: &Profiles,
129 targets: &[String],
130 spec: &IndexSet<String>,
131 dry_run: bool,
132) -> CargoResult<()> {
133 let requested_kinds = CompileKind::from_requested_targets(clean_ctx.gctx, targets)?;
135 let target_data = RustcTargetData::new(ws, &requested_kinds)?;
136 let (pkg_set, resolve) = ops::resolve_ws(ws, dry_run)?;
137 let prof_dir_name = profiles.get_dir_name();
138 let host_layout = Layout::new(ws, None, &prof_dir_name, true, true)?;
139 let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
141 .into_iter()
142 .filter_map(|kind| match kind {
143 CompileKind::Target(target) => {
144 match Layout::new(ws, Some(target), &prof_dir_name, true, true) {
145 Ok(layout) => Some(Ok((kind, layout))),
146 Err(e) => Some(Err(e)),
147 }
148 }
149 CompileKind::Host => None,
150 })
151 .collect::<CargoResult<_>>()?;
152 let layouts = if targets.is_empty() {
155 vec![(CompileKind::Host, &host_layout)]
156 } else {
157 target_layouts
158 .iter()
159 .map(|(kind, layout)| (*kind, layout))
160 .collect()
161 };
162 let layouts_with_host: Vec<(CompileKind, &Layout)> =
164 std::iter::once((CompileKind::Host, &host_layout))
165 .chain(layouts.iter().map(|(k, l)| (*k, *l)))
166 .collect();
167
168 let mut pkg_ids = Vec::new();
175 for spec_str in spec.iter() {
176 let spec = PackageIdSpec::parse(spec_str)?;
178 if spec.partial_version().is_some() {
179 clean_ctx.gctx.shell().warn(&format!(
180 "version qualifier in `-p {}` is ignored, \
181 cleaning all versions of `{}` found",
182 spec_str,
183 spec.name()
184 ))?;
185 }
186 if spec.url().is_some() {
187 clean_ctx.gctx.shell().warn(&format!(
188 "url qualifier in `-p {}` ignored, \
189 cleaning all versions of `{}` found",
190 spec_str,
191 spec.name()
192 ))?;
193 }
194 let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
195 if matches.is_empty() {
196 let mut suggestion = String::new();
197 suggestion.push_str(&edit_distance::closest_msg(
198 &spec.name(),
199 resolve.iter(),
200 |id| id.name().as_str(),
201 "package",
202 ));
203 anyhow::bail!(
204 "package ID specification `{}` did not match any packages{}",
205 spec,
206 suggestion
207 );
208 }
209 pkg_ids.extend(matches);
210 }
211 let packages = pkg_set.get_many(pkg_ids)?;
212
213 clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
214 let mut dirs_to_clean = DirectoriesToClean::default();
215
216 if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
217 for pkg in packages {
218 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
219
220 for (_compile_kind, layout) in &layouts_with_host {
222 let dir = layout.build_dir().build_unit(&pkg.name());
223 clean_ctx.rm_rf(&dir)?;
224 }
225
226 for target in pkg.targets() {
228 if target.is_custom_build() {
229 continue;
230 }
231 let crate_name: Rc<str> = target.crate_name().into();
232 for &mode in &[
233 CompileMode::Build,
234 CompileMode::Test,
235 CompileMode::Check { test: false },
236 ] {
237 for (compile_kind, layout) in &layouts {
238 let triple = target_data.short_name(compile_kind);
239 let (file_types, _unsupported) = target_data
240 .info(*compile_kind)
241 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
242 let artifact_dir = layout
243 .artifact_dir()
244 .expect("artifact-dir was not locked during clean");
245 let uplift_dir = match target.kind() {
246 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
247 Some(artifact_dir.examples())
248 }
249 TargetKind::Test | TargetKind::Bench => None,
251 _ => Some(artifact_dir.dest()),
252 };
253 if let Some(uplift_dir) = uplift_dir {
254 for file_type in file_types {
255 let uplifted_filename = file_type.uplift_filename(target);
256
257 let dep_info = Path::new(&uplifted_filename)
259 .with_extension("d")
260 .to_string_lossy()
261 .into_owned();
262
263 dirs_to_clean.mark_utf(uplift_dir, |filename| {
264 filename == uplifted_filename || filename == dep_info
265 });
266 }
267 }
268 let path_dash = format!("{}-", crate_name);
269
270 dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
271 filename.starts_with(&path_dash)
272 });
273 }
274 }
275 }
276 }
277 } else {
278 for pkg in packages {
279 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
280
281 for (_, layout) in &layouts_with_host {
283 dirs_to_clean.mark_utf(layout.build_dir().legacy_fingerprint(), |filename| {
284 let Some((pkg_name, _)) = filename.rsplit_once('-') else {
285 return false;
286 };
287
288 pkg_name == pkg.name().as_str()
289 });
290 }
291
292 for target in pkg.targets() {
293 if target.is_custom_build() {
294 for (_, layout) in &layouts_with_host {
296 dirs_to_clean.mark_utf(layout.build_dir().build(), |filename| {
297 let Some((current_name, _)) = filename.rsplit_once('-') else {
298 return false;
299 };
300
301 current_name == pkg.name().as_str()
302 });
303 }
304 continue;
305 }
306 let crate_name: Rc<str> = target.crate_name().into();
307 let path_dot: &str = &format!("{crate_name}.");
308 let path_dash: &str = &format!("{crate_name}-");
309 for &mode in &[
310 CompileMode::Build,
311 CompileMode::Test,
312 CompileMode::Check { test: false },
313 ] {
314 for (compile_kind, layout) in &layouts {
315 let triple = target_data.short_name(compile_kind);
316 let (file_types, _unsupported) = target_data
317 .info(*compile_kind)
318 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
319 let artifact_dir = layout
320 .artifact_dir()
321 .expect("artifact-dir was not locked during clean");
322 let (dir, uplift_dir) = match target.kind() {
323 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
324 (layout.build_dir().examples(), Some(artifact_dir.examples()))
325 }
326 TargetKind::Test | TargetKind::Bench => {
328 (layout.build_dir().legacy_deps(), None)
329 }
330 _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
331 };
332
333 for file_type in file_types {
334 let (prefix, suffix) = file_type.output_prefix_suffix(target);
336 let unhashed_name = file_type.output_filename(target, None);
337 dirs_to_clean.mark_utf(&dir, |filename| {
338 (filename.starts_with(&prefix) && filename.ends_with(&suffix))
339 || unhashed_name == filename
340 });
341
342 if let Some(uplift_dir) = uplift_dir {
344 let uplifted_path =
345 uplift_dir.join(file_type.uplift_filename(target));
346 clean_ctx.rm_rf(&uplifted_path)?;
347 let dep_info = uplifted_path.with_extension("d");
349 clean_ctx.rm_rf(&dep_info)?;
350 }
351 }
352 let unhashed_dep_info = format!("{}.d", crate_name);
353 dirs_to_clean.mark_utf(dir, |filename| filename == unhashed_dep_info);
354
355 dirs_to_clean.mark_utf(dir, |filename| {
356 if filename.starts_with(&path_dash) {
357 filename.ends_with(".d")
360 } else if filename.starts_with(&path_dot) {
361 [".o", ".dwo", ".dwp"]
363 .iter()
364 .any(|suffix| filename.ends_with(suffix))
365 } else {
366 false
367 }
368 });
369
370 dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
372 filename.starts_with(path_dash)
373 });
374 }
375 }
376 }
377 }
378 }
379 clean_ctx.rm_rf_all(dirs_to_clean)?;
380
381 Ok(())
382}
383
384#[derive(Default)]
385struct DirectoriesToClean {
386 dir_contents: IndexMap<PathBuf, IndexSet<OsString>>,
387 to_remove: IndexSet<PathBuf>,
388}
389
390impl DirectoriesToClean {
391 fn mark(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&OsString) -> bool) {
392 let entry = match self.dir_contents.entry(directory.to_owned()) {
393 indexmap::map::Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
394 indexmap::map::Entry::Vacant(vacant_entry) => {
395 let Ok(dir_entries) = std::fs::read_dir(directory.to_owned()) else {
396 return;
397 };
398 vacant_entry.insert(
399 dir_entries
400 .into_iter()
401 .flatten()
402 .map(|entry| entry.file_name())
403 .collect::<IndexSet<_>>(),
404 )
405 }
406 };
407
408 entry.retain(|path| {
409 let should_remove = should_remove_entry(path);
410 if should_remove {
411 self.to_remove.insert(directory.join(path));
412 }
413 !should_remove
414 });
415 }
416
417 fn mark_utf(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&str) -> bool) {
418 self.mark(directory, move |filename| {
419 let Some(as_utf) = filename.to_str() else {
420 return false;
421 };
422 should_remove_entry(as_utf)
423 });
424 }
425}
426
427impl<'gctx> CleanContext<'gctx> {
428 pub fn new(gctx: &'gctx GlobalContext) -> Self {
429 let progress = CleaningFolderBar::new(gctx, 0);
432 CleanContext {
433 gctx,
434 progress: Box::new(progress),
435 dry_run: false,
436 num_files_removed: 0,
437 num_dirs_removed: 0,
438 total_bytes_removed: 0,
439 }
440 }
441
442 fn rm_rf_all(&mut self, dirs: DirectoriesToClean) -> CargoResult<()> {
443 for path in dirs.to_remove {
444 self.rm_rf(&path)?;
445 }
446 Ok(())
447 }
448
449 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
450 let meta = match fs::symlink_metadata(path) {
451 Ok(meta) => meta,
452 Err(e) => {
453 if e.kind() != std::io::ErrorKind::NotFound {
454 self.gctx
455 .shell()
456 .warn(&format!("cannot access {}: {e}", path.display()))?;
457 }
458 return Ok(());
459 }
460 };
461
462 if !self.dry_run {
464 self.gctx
465 .shell()
466 .verbose(|shell| shell.status("Removing", path.display()))?;
467 }
468 self.progress.display_now()?;
469
470 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
471 if let Ok(meta) = meta {
472 self.total_bytes_removed += meta.len();
476 }
477 self.num_files_removed += 1;
478 if !self.dry_run {
479 paths::remove_file(path)?;
480 }
481 Ok(())
482 };
483
484 if !meta.is_dir() {
485 return rm_file(path, Ok(meta));
486 }
487
488 for entry in walkdir::WalkDir::new(path).contents_first(true) {
489 let entry = entry?;
490 self.progress.on_clean()?;
491 if self.dry_run {
492 self.gctx
497 .shell()
498 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
499 }
500 if entry.file_type().is_dir() {
501 self.num_dirs_removed += 1;
502 if !self.dry_run {
507 paths::remove_dir_all(entry.path())?;
508 }
509 } else {
510 rm_file(entry.path(), entry.metadata())?;
511 }
512 }
513
514 Ok(())
515 }
516
517 pub fn display_summary(&self) -> CargoResult<()> {
518 let status = if self.dry_run { "Summary" } else { "Removed" };
519 let byte_count = if self.total_bytes_removed == 0 {
520 String::new()
521 } else {
522 let bytes = HumanBytes(self.total_bytes_removed);
523 format!(", {bytes:.1} total")
524 };
525 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
531 (0, 0) => format!("0 files"),
532 (0, 1) => format!("1 directory"),
533 (0, 2..) => format!("{} directories", self.num_dirs_removed),
534 (1, _) => format!("1 file"),
535 (2.., _) => format!("{} files", self.num_files_removed),
536 };
537 self.gctx
538 .shell()
539 .status(status, format!("{file_count}{byte_count}"))?;
540 if self.dry_run {
541 self.gctx
542 .shell()
543 .warn("no files deleted due to --dry-run")?;
544 }
545 Ok(())
546 }
547
548 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
554 let num_paths = paths
555 .iter()
556 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
557 .sum();
558 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
559 for path in paths {
560 self.rm_rf(path)?;
561 }
562 Ok(())
563 }
564}
565
566trait CleaningProgressBar {
567 fn display_now(&mut self) -> CargoResult<()>;
568 fn on_clean(&mut self) -> CargoResult<()>;
569 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
570 Ok(())
571 }
572}
573
574struct CleaningFolderBar<'gctx> {
575 bar: Progress<'gctx>,
576 max: usize,
577 cur: usize,
578}
579
580impl<'gctx> CleaningFolderBar<'gctx> {
581 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
582 Self {
583 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
584 max,
585 cur: 0,
586 }
587 }
588
589 fn cur_progress(&self) -> usize {
590 std::cmp::min(self.cur, self.max)
591 }
592}
593
594impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
595 fn display_now(&mut self) -> CargoResult<()> {
596 self.bar.tick_now(self.cur_progress(), self.max, "")
597 }
598
599 fn on_clean(&mut self) -> CargoResult<()> {
600 self.cur += 1;
601 self.bar.tick(self.cur_progress(), self.max, "")
602 }
603}
604
605struct CleaningPackagesBar<'gctx> {
606 bar: Progress<'gctx>,
607 max: usize,
608 cur: usize,
609 num_files_folders_cleaned: usize,
610 package_being_cleaned: String,
611}
612
613impl<'gctx> CleaningPackagesBar<'gctx> {
614 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
615 Self {
616 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
617 max,
618 cur: 0,
619 num_files_folders_cleaned: 0,
620 package_being_cleaned: String::new(),
621 }
622 }
623
624 fn cur_progress(&self) -> usize {
625 std::cmp::min(self.cur, self.max)
626 }
627
628 fn format_message(&self) -> String {
629 format!(
630 ": {}, {} files/folders cleaned",
631 self.package_being_cleaned, self.num_files_folders_cleaned
632 )
633 }
634}
635
636impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
637 fn display_now(&mut self) -> CargoResult<()> {
638 self.bar
639 .tick_now(self.cur_progress(), self.max, &self.format_message())
640 }
641
642 fn on_clean(&mut self) -> CargoResult<()> {
643 self.bar
644 .tick(self.cur_progress(), self.max, &self.format_message())?;
645 self.num_files_folders_cleaned += 1;
646 Ok(())
647 }
648
649 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
650 self.cur += 1;
651 self.package_being_cleaned = String::from(package);
652 self.bar
653 .tick(self.cur_progress(), self.max, &self.format_message())
654 }
655}