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