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 anyhow::bail;
11use cargo_util::paths;
12use cargo_util_terminal::report::Level;
13use indexmap::{IndexMap, IndexSet};
14
15use std::ffi::OsString;
16use std::io::Read;
17use std::path::{Path, PathBuf};
18use std::rc::Rc;
19use std::{fs, io};
20
21pub struct CleanOptions<'gctx> {
22 pub gctx: &'gctx GlobalContext,
23 pub spec: IndexSet<String>,
25 pub targets: Vec<String>,
27 pub profile_specified: bool,
29 pub requested_profile: InternedString,
31 pub doc: bool,
33 pub dry_run: bool,
35 pub explicit_target_dir_arg: bool,
37}
38
39pub struct CleanContext<'gctx> {
40 pub gctx: &'gctx GlobalContext,
41 progress: Box<dyn CleaningProgressBar + 'gctx>,
42 pub dry_run: bool,
43 num_files_removed: u64,
44 num_dirs_removed: u64,
45 total_bytes_removed: u64,
46}
47
48pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
50 let mut target_dir = ws.target_dir();
51 let mut build_dir = ws.build_dir();
52 let gctx = opts.gctx;
53 let mut clean_ctx = CleanContext::new(gctx);
54 clean_ctx.dry_run = opts.dry_run;
55
56 const CLEAN_ABORT_NOTE: &str =
57 "cleaning has been aborted to prevent accidental deletion of unrelated files";
58
59 if let Ok(meta) = fs::symlink_metadata(target_dir.as_path_unlocked()) {
61 if !meta.is_symlink() && !meta.is_dir() {
63 let title = format!("cannot clean `{}`: not a directory", target_dir.display());
64 let report = [Level::ERROR
65 .primary_title(title)
66 .element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
67 gctx.shell().print_report(&report, false)?;
68 return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
69 }
70 }
71
72 if opts.explicit_target_dir_arg {
74 let target_dir_path = target_dir.as_path_unlocked();
75
76 if target_dir_path.exists()
78 && let Err(err) = validate_target_dir_tag(target_dir_path)
79 {
80 let title = format!("cannot clean `{}`: {err}", target_dir_path.display());
82 let report = [Level::ERROR
83 .primary_title(title)
84 .element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
85 gctx.shell().print_report(&report, false)?;
86 return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
87 }
88 }
89
90 if opts.doc {
91 if !opts.spec.is_empty() {
92 bail!("--doc cannot be used with -p");
99 }
100 target_dir = target_dir.join("doc");
102 clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?;
103 } else {
104 let profiles = Profiles::new(&ws, opts.requested_profile)?;
105
106 if opts.profile_specified {
107 let dir_name = profiles.get_dir_name();
111 target_dir = target_dir.join(dir_name);
112 build_dir = build_dir.join(dir_name);
113 }
114
115 if opts.spec.is_empty() {
121 let paths: &[PathBuf] = if build_dir != target_dir {
122 &[
123 target_dir.into_path_unlocked(),
124 build_dir.into_path_unlocked(),
125 ]
126 } else {
127 &[target_dir.into_path_unlocked()]
128 };
129 clean_ctx.remove_paths(paths)?;
130 } else {
131 clean_specs(
132 &mut clean_ctx,
133 &ws,
134 &profiles,
135 &opts.targets,
136 &opts.spec,
137 opts.dry_run,
138 )?;
139 }
140 }
141
142 clean_ctx.display_summary()?;
143 Ok(())
144}
145
146fn validate_target_dir_tag(target_dir_path: &Path) -> CargoResult<()> {
147 const TAG_SIGNATURE: &[u8] = b"Signature: 8a477f597d28d172789f06886806bc55";
148
149 let tag_path = target_dir_path.join("CACHEDIR.TAG");
150
151 if tag_path.is_symlink() {
153 bail!("expect `CACHEDIR.TAG` to be a regular file, got a symlink");
154 }
155
156 if !tag_path.is_file() {
157 bail!("missing or invalid `CACHEDIR.TAG` file");
158 }
159
160 let mut file = fs::File::open(&tag_path)
161 .map_err(|err| anyhow::anyhow!("failed to open `{}`: {}", tag_path.display(), err))?;
162
163 let mut buf = [0u8; TAG_SIGNATURE.len()];
164 match file.read_exact(&mut buf) {
165 Ok(()) if &buf[..] == TAG_SIGNATURE => {}
166 Err(e) if e.kind() != io::ErrorKind::UnexpectedEof => {
167 bail!("failed to read `{}`: {e}", tag_path.display());
168 }
169 _ => {
170 bail!("invalid signature in `CACHEDIR.TAG` file");
171 }
172 }
173
174 Ok(())
175}
176
177fn clean_specs(
178 clean_ctx: &mut CleanContext<'_>,
179 ws: &Workspace<'_>,
180 profiles: &Profiles,
181 targets: &[String],
182 spec: &IndexSet<String>,
183 dry_run: bool,
184) -> CargoResult<()> {
185 let requested_kinds = CompileKind::from_requested_targets(clean_ctx.gctx, targets)?;
187 let target_data = RustcTargetData::new(ws, &requested_kinds)?;
188 let (pkg_set, resolve) = ops::resolve_ws(ws, dry_run)?;
189 let prof_dir_name = profiles.get_dir_name();
190 let host_layout = Layout::new(ws, None, &prof_dir_name, true, true)?;
191 let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
193 .into_iter()
194 .filter_map(|kind| match kind {
195 CompileKind::Target(target) => {
196 match Layout::new(ws, Some(target), &prof_dir_name, true, true) {
197 Ok(layout) => Some(Ok((kind, layout))),
198 Err(e) => Some(Err(e)),
199 }
200 }
201 CompileKind::Host => None,
202 })
203 .collect::<CargoResult<_>>()?;
204 let layouts = if targets.is_empty() {
207 vec![(CompileKind::Host, &host_layout)]
208 } else {
209 target_layouts
210 .iter()
211 .map(|(kind, layout)| (*kind, layout))
212 .collect()
213 };
214 let layouts_with_host: Vec<(CompileKind, &Layout)> =
216 std::iter::once((CompileKind::Host, &host_layout))
217 .chain(layouts.iter().map(|(k, l)| (*k, *l)))
218 .collect();
219
220 let mut pkg_ids = Vec::new();
227 for spec_str in spec.iter() {
228 let spec = PackageIdSpec::parse(spec_str)?;
230 if spec.partial_version().is_some() {
231 clean_ctx.gctx.shell().warn(&format!(
232 "version qualifier in `-p {}` is ignored, \
233 cleaning all versions of `{}` found",
234 spec_str,
235 spec.name()
236 ))?;
237 }
238 if spec.url().is_some() {
239 clean_ctx.gctx.shell().warn(&format!(
240 "url qualifier in `-p {}` ignored, \
241 cleaning all versions of `{}` found",
242 spec_str,
243 spec.name()
244 ))?;
245 }
246 let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
247 if matches.is_empty() {
248 let mut suggestion = String::new();
249 suggestion.push_str(&edit_distance::closest_msg(
250 &spec.name(),
251 resolve.iter(),
252 |id| id.name().as_str(),
253 "package",
254 ));
255 anyhow::bail!(
256 "package ID specification `{}` did not match any packages{}",
257 spec,
258 suggestion
259 );
260 }
261 pkg_ids.extend(matches);
262 }
263 let packages = pkg_set.get_many(pkg_ids)?;
264
265 clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
266 let mut dirs_to_clean = DirectoriesToClean::default();
267
268 if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
269 for pkg in packages {
270 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
271
272 for (_compile_kind, layout) in &layouts_with_host {
274 let dir = layout.build_dir().build_unit(&pkg.name());
275 clean_ctx.rm_rf(&dir)?;
276 }
277
278 for target in pkg.targets() {
280 if target.is_custom_build() {
281 continue;
282 }
283 let crate_name: Rc<str> = target.crate_name().into();
284 for &mode in &[
285 CompileMode::Build,
286 CompileMode::Test,
287 CompileMode::Check { test: false },
288 ] {
289 for (compile_kind, layout) in &layouts {
290 let triple = target_data.short_name(compile_kind);
291 let (file_types, _unsupported) = target_data
292 .info(*compile_kind)
293 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
294 let artifact_dir = layout
295 .artifact_dir()
296 .expect("artifact-dir was not locked during clean");
297 let uplift_dir = match target.kind() {
298 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
299 Some(artifact_dir.examples())
300 }
301 TargetKind::Test | TargetKind::Bench => None,
303 _ => Some(artifact_dir.dest()),
304 };
305 if let Some(uplift_dir) = uplift_dir {
306 for file_type in file_types {
307 let uplifted_filename = file_type.uplift_filename(target);
308
309 let dep_info = Path::new(&uplifted_filename)
311 .with_extension("d")
312 .to_string_lossy()
313 .into_owned();
314
315 dirs_to_clean.mark_utf(uplift_dir, |filename| {
316 filename == uplifted_filename || filename == dep_info
317 });
318 }
319 }
320 let path_dash = format!("{}-", crate_name);
321
322 dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
323 filename.starts_with(&path_dash)
324 });
325 }
326 }
327 }
328 }
329 } else {
330 for pkg in packages {
331 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
332
333 for (_, layout) in &layouts_with_host {
335 dirs_to_clean.mark_utf(layout.build_dir().legacy_fingerprint(), |filename| {
336 let Some((pkg_name, _)) = filename.rsplit_once('-') else {
337 return false;
338 };
339
340 pkg_name == pkg.name().as_str()
341 });
342 }
343
344 for target in pkg.targets() {
345 if target.is_custom_build() {
346 for (_, layout) in &layouts_with_host {
348 dirs_to_clean.mark_utf(layout.build_dir().build(), |filename| {
349 let Some((current_name, _)) = filename.rsplit_once('-') else {
350 return false;
351 };
352
353 current_name == pkg.name().as_str()
354 });
355 }
356 continue;
357 }
358 let crate_name: Rc<str> = target.crate_name().into();
359 let path_dot: &str = &format!("{crate_name}.");
360 let path_dash: &str = &format!("{crate_name}-");
361 for &mode in &[
362 CompileMode::Build,
363 CompileMode::Test,
364 CompileMode::Check { test: false },
365 ] {
366 for (compile_kind, layout) in &layouts {
367 let triple = target_data.short_name(compile_kind);
368 let (file_types, _unsupported) = target_data
369 .info(*compile_kind)
370 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
371 let artifact_dir = layout
372 .artifact_dir()
373 .expect("artifact-dir was not locked during clean");
374 let (dir, uplift_dir) = match target.kind() {
375 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
376 (layout.build_dir().examples(), Some(artifact_dir.examples()))
377 }
378 TargetKind::Test | TargetKind::Bench => {
380 (layout.build_dir().legacy_deps(), None)
381 }
382 _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
383 };
384
385 for file_type in file_types {
386 let (prefix, suffix) = file_type.output_prefix_suffix(target);
388 let unhashed_name = file_type.output_filename(target, None);
389 dirs_to_clean.mark_utf(&dir, |filename| {
390 (filename.starts_with(&prefix) && filename.ends_with(&suffix))
391 || unhashed_name == filename
392 });
393
394 if let Some(uplift_dir) = uplift_dir {
396 let uplifted_path =
397 uplift_dir.join(file_type.uplift_filename(target));
398 clean_ctx.rm_rf(&uplifted_path)?;
399 let dep_info = uplifted_path.with_extension("d");
401 clean_ctx.rm_rf(&dep_info)?;
402 }
403 }
404 let unhashed_dep_info = format!("{}.d", crate_name);
405 dirs_to_clean.mark_utf(dir, |filename| filename == unhashed_dep_info);
406
407 dirs_to_clean.mark_utf(dir, |filename| {
408 if filename.starts_with(&path_dash) {
409 filename.ends_with(".d")
412 } else if filename.starts_with(&path_dot) {
413 [".o", ".dwo", ".dwp"]
415 .iter()
416 .any(|suffix| filename.ends_with(suffix))
417 } else {
418 false
419 }
420 });
421
422 dirs_to_clean.mark_utf(layout.build_dir().incremental(), |filename| {
424 filename.starts_with(path_dash)
425 });
426 }
427 }
428 }
429 }
430 }
431 clean_ctx.rm_rf_all(dirs_to_clean)?;
432
433 Ok(())
434}
435
436#[derive(Default)]
437struct DirectoriesToClean {
438 dir_contents: IndexMap<PathBuf, IndexSet<OsString>>,
439 to_remove: IndexSet<PathBuf>,
440}
441
442impl DirectoriesToClean {
443 fn mark(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&OsString) -> bool) {
444 let entry = match self.dir_contents.entry(directory.to_owned()) {
445 indexmap::map::Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
446 indexmap::map::Entry::Vacant(vacant_entry) => {
447 let Ok(dir_entries) = std::fs::read_dir(directory.to_owned()) else {
448 return;
449 };
450 vacant_entry.insert(
451 dir_entries
452 .into_iter()
453 .flatten()
454 .map(|entry| entry.file_name())
455 .collect::<IndexSet<_>>(),
456 )
457 }
458 };
459
460 entry.retain(|path| {
461 let should_remove = should_remove_entry(path);
462 if should_remove {
463 self.to_remove.insert(directory.join(path));
464 }
465 !should_remove
466 });
467 }
468
469 fn mark_utf(&mut self, directory: &Path, mut should_remove_entry: impl FnMut(&str) -> bool) {
470 self.mark(directory, move |filename| {
471 let Some(as_utf) = filename.to_str() else {
472 return false;
473 };
474 should_remove_entry(as_utf)
475 });
476 }
477}
478
479impl<'gctx> CleanContext<'gctx> {
480 pub fn new(gctx: &'gctx GlobalContext) -> Self {
481 let progress = CleaningFolderBar::new(gctx, 0);
484 CleanContext {
485 gctx,
486 progress: Box::new(progress),
487 dry_run: false,
488 num_files_removed: 0,
489 num_dirs_removed: 0,
490 total_bytes_removed: 0,
491 }
492 }
493
494 fn rm_rf_all(&mut self, dirs: DirectoriesToClean) -> CargoResult<()> {
495 for path in dirs.to_remove {
496 self.rm_rf(&path)?;
497 }
498 Ok(())
499 }
500
501 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
502 let meta = match fs::symlink_metadata(path) {
503 Ok(meta) => meta,
504 Err(e) => {
505 if e.kind() != std::io::ErrorKind::NotFound {
506 self.gctx
507 .shell()
508 .warn(&format!("cannot access {}: {e}", path.display()))?;
509 }
510 return Ok(());
511 }
512 };
513
514 if !self.dry_run {
516 self.gctx
517 .shell()
518 .verbose(|shell| shell.status("Removing", path.display()))?;
519 }
520 self.progress.display_now()?;
521
522 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
523 if let Ok(meta) = meta {
524 self.total_bytes_removed += meta.len();
528 }
529 self.num_files_removed += 1;
530 if !self.dry_run {
531 paths::remove_file(path)?;
532 }
533 Ok(())
534 };
535
536 if !meta.is_dir() {
537 return rm_file(path, Ok(meta));
538 }
539
540 for entry in walkdir::WalkDir::new(path).contents_first(true) {
541 let entry = entry?;
542 self.progress.on_clean()?;
543 if self.dry_run {
544 self.gctx
549 .shell()
550 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
551 }
552 if entry.file_type().is_dir() {
553 self.num_dirs_removed += 1;
554 if !self.dry_run {
559 paths::remove_dir_all(entry.path())?;
560 }
561 } else {
562 rm_file(entry.path(), entry.metadata())?;
563 }
564 }
565
566 Ok(())
567 }
568
569 pub fn display_summary(&self) -> CargoResult<()> {
570 let status = if self.dry_run { "Summary" } else { "Removed" };
571 let byte_count = if self.total_bytes_removed == 0 {
572 String::new()
573 } else {
574 let bytes = HumanBytes(self.total_bytes_removed);
575 format!(", {bytes:.1} total")
576 };
577 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
583 (0, 0) => format!("0 files"),
584 (0, 1) => format!("1 directory"),
585 (0, 2..) => format!("{} directories", self.num_dirs_removed),
586 (1, _) => format!("1 file"),
587 (2.., _) => format!("{} files", self.num_files_removed),
588 };
589 self.gctx
590 .shell()
591 .status(status, format!("{file_count}{byte_count}"))?;
592 if self.dry_run {
593 self.gctx
594 .shell()
595 .warn("no files deleted due to --dry-run")?;
596 }
597 Ok(())
598 }
599
600 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
606 let num_paths = paths
607 .iter()
608 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
609 .sum();
610 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
611 for path in paths {
612 self.rm_rf(path)?;
613 }
614 Ok(())
615 }
616}
617
618trait CleaningProgressBar {
619 fn display_now(&mut self) -> CargoResult<()>;
620 fn on_clean(&mut self) -> CargoResult<()>;
621 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
622 Ok(())
623 }
624}
625
626struct CleaningFolderBar<'gctx> {
627 bar: Progress<'gctx>,
628 max: usize,
629 cur: usize,
630}
631
632impl<'gctx> CleaningFolderBar<'gctx> {
633 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
634 Self {
635 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
636 max,
637 cur: 0,
638 }
639 }
640
641 fn cur_progress(&self) -> usize {
642 std::cmp::min(self.cur, self.max)
643 }
644}
645
646impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
647 fn display_now(&mut self) -> CargoResult<()> {
648 self.bar.tick_now(self.cur_progress(), self.max, "")
649 }
650
651 fn on_clean(&mut self) -> CargoResult<()> {
652 self.cur += 1;
653 self.bar.tick(self.cur_progress(), self.max, "")
654 }
655}
656
657struct CleaningPackagesBar<'gctx> {
658 bar: Progress<'gctx>,
659 max: usize,
660 cur: usize,
661 num_files_folders_cleaned: usize,
662 package_being_cleaned: String,
663}
664
665impl<'gctx> CleaningPackagesBar<'gctx> {
666 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
667 Self {
668 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
669 max,
670 cur: 0,
671 num_files_folders_cleaned: 0,
672 package_being_cleaned: String::new(),
673 }
674 }
675
676 fn cur_progress(&self) -> usize {
677 std::cmp::min(self.cur, self.max)
678 }
679
680 fn format_message(&self) -> String {
681 format!(
682 ": {}, {} files/folders cleaned",
683 self.package_being_cleaned, self.num_files_folders_cleaned
684 )
685 }
686}
687
688impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
689 fn display_now(&mut self) -> CargoResult<()> {
690 self.bar
691 .tick_now(self.cur_progress(), self.max, &self.format_message())
692 }
693
694 fn on_clean(&mut self) -> CargoResult<()> {
695 self.bar
696 .tick(self.cur_progress(), self.max, &self.format_message())?;
697 self.num_files_folders_cleaned += 1;
698 Ok(())
699 }
700
701 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
702 self.cur += 1;
703 self.package_being_cleaned = String::from(package);
704 self.bar
705 .tick(self.cur_progress(), self.max, &self.format_message())
706 }
707}