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