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