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 for &mode in &[
213 CompileMode::Build,
214 CompileMode::Test,
215 CompileMode::Check { test: false },
216 ] {
217 for (compile_kind, layout) in &layouts {
218 let triple = target_data.short_name(compile_kind);
219 let (file_types, _unsupported) = target_data
220 .info(*compile_kind)
221 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
222 let artifact_dir = layout
223 .artifact_dir()
224 .expect("artifact-dir was not locked during clean");
225 let uplift_dir = match target.kind() {
226 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
227 Some(artifact_dir.examples())
228 }
229 TargetKind::Test | TargetKind::Bench => None,
231 _ => Some(artifact_dir.dest()),
232 };
233 if let Some(uplift_dir) = uplift_dir {
234 for file_type in file_types {
235 let uplifted_path =
236 uplift_dir.join(file_type.uplift_filename(target));
237 clean_ctx.rm_rf(&uplifted_path)?;
238 let dep_info = uplifted_path.with_extension("d");
240 clean_ctx.rm_rf(&dep_info)?;
241 }
242 }
243 }
244 }
245 }
246 }
247 } else {
248 let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
251 for pkg in packages {
252 let pkg_dir = format!("{}-*", pkg.name());
253 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
254
255 for (_, layout) in &layouts_with_host {
257 let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
258 clean_ctx.rm_rf_package_glob_containing_hash(
259 &pkg.name(),
260 &Path::new(&dir).join(&pkg_dir),
261 )?;
262 }
263
264 for target in pkg.targets() {
265 if target.is_custom_build() {
266 for (_, layout) in &layouts_with_host {
268 let dir = escape_glob_path(layout.build_dir().build())?;
269 clean_ctx.rm_rf_package_glob_containing_hash(
270 &pkg.name(),
271 &Path::new(&dir).join(&pkg_dir),
272 )?;
273 }
274 continue;
275 }
276 let crate_name: Rc<str> = target.crate_name().into();
277 let path_dot: &str = &format!("{crate_name}.");
278 let path_dash: &str = &format!("{crate_name}-");
279 for &mode in &[
280 CompileMode::Build,
281 CompileMode::Test,
282 CompileMode::Check { test: false },
283 ] {
284 for (compile_kind, layout) in &layouts {
285 let triple = target_data.short_name(compile_kind);
286 let (file_types, _unsupported) = target_data
287 .info(*compile_kind)
288 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
289 let artifact_dir = layout
290 .artifact_dir()
291 .expect("artifact-dir was not locked during clean");
292 let (dir, uplift_dir) = match target.kind() {
293 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
294 (layout.build_dir().examples(), Some(artifact_dir.examples()))
295 }
296 TargetKind::Test | TargetKind::Bench => {
298 (layout.build_dir().legacy_deps(), None)
299 }
300 _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
301 };
302 let mut dir_glob_str = escape_glob_path(dir)?;
303 let dir_glob = Path::new(&dir_glob_str);
304 for file_type in file_types {
305 let hashed_name = file_type.output_filename(target, Some("*"));
307 let unhashed_name = file_type.output_filename(target, None);
308
309 clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
310 clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
311
312 if let Some(uplift_dir) = uplift_dir {
314 let uplifted_path =
315 uplift_dir.join(file_type.uplift_filename(target));
316 clean_ctx.rm_rf(&uplifted_path)?;
317 let dep_info = uplifted_path.with_extension("d");
319 clean_ctx.rm_rf(&dep_info)?;
320 }
321 }
322 let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
323 clean_ctx.rm_rf(&unhashed_dep_info)?;
324
325 if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
326 dir_glob_str.push(std::path::MAIN_SEPARATOR);
327 }
328 dir_glob_str.push('*');
329 let dir_glob_str: Rc<str> = dir_glob_str.into();
330 if cleaned_packages
331 .entry(dir_glob_str.clone())
332 .or_default()
333 .insert(crate_name.clone())
334 {
335 let paths = [
336 (path_dash, ".d"),
339 (path_dot, ".o"),
341 (path_dot, ".dwo"),
342 (path_dot, ".dwp"),
343 ];
344 clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
345 }
346
347 let dir = escape_glob_path(layout.build_dir().incremental())?;
349 let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
350 clean_ctx.rm_rf_glob(&incremental)?;
351 }
352 }
353 }
354 }
355 }
356
357 Ok(())
358}
359
360fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
361 let pattern = pattern
362 .to_str()
363 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
364 Ok(glob::Pattern::escape(pattern))
365}
366
367impl<'gctx> CleanContext<'gctx> {
368 pub fn new(gctx: &'gctx GlobalContext) -> Self {
369 let progress = CleaningFolderBar::new(gctx, 0);
372 CleanContext {
373 gctx,
374 progress: Box::new(progress),
375 dry_run: false,
376 num_files_removed: 0,
377 num_dirs_removed: 0,
378 total_bytes_removed: 0,
379 }
380 }
381
382 fn rm_rf_package_glob_containing_hash(
388 &mut self,
389 package: &str,
390 pattern: &Path,
391 ) -> CargoResult<()> {
392 let pattern = pattern
394 .to_str()
395 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
396 for path in glob::glob(pattern)? {
397 let path = path?;
398
399 let pkg_name = path
400 .file_name()
401 .and_then(std::ffi::OsStr::to_str)
402 .and_then(|artifact| artifact.rsplit_once('-'))
403 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
404 .0;
405
406 if pkg_name != package {
407 continue;
408 }
409
410 self.rm_rf(&path)?;
411 }
412 Ok(())
413 }
414
415 fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
416 let pattern = pattern
418 .to_str()
419 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
420 for path in glob::glob(pattern)? {
421 self.rm_rf(&path?)?;
422 }
423 Ok(())
424 }
425
426 fn rm_rf_prefix_list(
433 &mut self,
434 pattern: &str,
435 path_matchers: &[(&str, &str)],
436 ) -> CargoResult<()> {
437 for path in glob::glob(pattern)? {
438 let path = path?;
439 let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
440 if path_matchers
441 .iter()
442 .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
443 {
444 self.rm_rf(&path)?;
445 }
446 }
447 Ok(())
448 }
449
450 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
451 let meta = match fs::symlink_metadata(path) {
452 Ok(meta) => meta,
453 Err(e) => {
454 if e.kind() != std::io::ErrorKind::NotFound {
455 self.gctx
456 .shell()
457 .warn(&format!("cannot access {}: {e}", path.display()))?;
458 }
459 return Ok(());
460 }
461 };
462
463 if !self.dry_run {
465 self.gctx
466 .shell()
467 .verbose(|shell| shell.status("Removing", path.display()))?;
468 }
469 self.progress.display_now()?;
470
471 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
472 if let Ok(meta) = meta {
473 self.total_bytes_removed += meta.len();
477 }
478 self.num_files_removed += 1;
479 if !self.dry_run {
480 paths::remove_file(path)?;
481 }
482 Ok(())
483 };
484
485 if !meta.is_dir() {
486 return rm_file(path, Ok(meta));
487 }
488
489 for entry in walkdir::WalkDir::new(path).contents_first(true) {
490 let entry = entry?;
491 self.progress.on_clean()?;
492 if self.dry_run {
493 self.gctx
498 .shell()
499 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
500 }
501 if entry.file_type().is_dir() {
502 self.num_dirs_removed += 1;
503 if !self.dry_run {
508 paths::remove_dir_all(entry.path())?;
509 }
510 } else {
511 rm_file(entry.path(), entry.metadata())?;
512 }
513 }
514
515 Ok(())
516 }
517
518 pub fn display_summary(&self) -> CargoResult<()> {
519 let status = if self.dry_run { "Summary" } else { "Removed" };
520 let byte_count = if self.total_bytes_removed == 0 {
521 String::new()
522 } else {
523 let bytes = HumanBytes(self.total_bytes_removed);
524 format!(", {bytes:.1} total")
525 };
526 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
532 (0, 0) => format!("0 files"),
533 (0, 1) => format!("1 directory"),
534 (0, 2..) => format!("{} directories", self.num_dirs_removed),
535 (1, _) => format!("1 file"),
536 (2.., _) => format!("{} files", self.num_files_removed),
537 };
538 self.gctx
539 .shell()
540 .status(status, format!("{file_count}{byte_count}"))?;
541 if self.dry_run {
542 self.gctx
543 .shell()
544 .warn("no files deleted due to --dry-run")?;
545 }
546 Ok(())
547 }
548
549 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
555 let num_paths = paths
556 .iter()
557 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
558 .sum();
559 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
560 for path in paths {
561 self.rm_rf(path)?;
562 }
563 Ok(())
564 }
565}
566
567trait CleaningProgressBar {
568 fn display_now(&mut self) -> CargoResult<()>;
569 fn on_clean(&mut self) -> CargoResult<()>;
570 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
571 Ok(())
572 }
573}
574
575struct CleaningFolderBar<'gctx> {
576 bar: Progress<'gctx>,
577 max: usize,
578 cur: usize,
579}
580
581impl<'gctx> CleaningFolderBar<'gctx> {
582 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
583 Self {
584 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
585 max,
586 cur: 0,
587 }
588 }
589
590 fn cur_progress(&self) -> usize {
591 std::cmp::min(self.cur, self.max)
592 }
593}
594
595impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
596 fn display_now(&mut self) -> CargoResult<()> {
597 self.bar.tick_now(self.cur_progress(), self.max, "")
598 }
599
600 fn on_clean(&mut self) -> CargoResult<()> {
601 self.cur += 1;
602 self.bar.tick(self.cur_progress(), self.max, "")
603 }
604}
605
606struct CleaningPackagesBar<'gctx> {
607 bar: Progress<'gctx>,
608 max: usize,
609 cur: usize,
610 num_files_folders_cleaned: usize,
611 package_being_cleaned: String,
612}
613
614impl<'gctx> CleaningPackagesBar<'gctx> {
615 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
616 Self {
617 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
618 max,
619 cur: 0,
620 num_files_folders_cleaned: 0,
621 package_being_cleaned: String::new(),
622 }
623 }
624
625 fn cur_progress(&self) -> usize {
626 std::cmp::min(self.cur, self.max)
627 }
628
629 fn format_message(&self) -> String {
630 format!(
631 ": {}, {} files/folders cleaned",
632 self.package_being_cleaned, self.num_files_folders_cleaned
633 )
634 }
635}
636
637impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
638 fn display_now(&mut self) -> CargoResult<()> {
639 self.bar
640 .tick_now(self.cur_progress(), self.max, &self.format_message())
641 }
642
643 fn on_clean(&mut self) -> CargoResult<()> {
644 self.bar
645 .tick(self.cur_progress(), self.max, &self.format_message())?;
646 self.num_files_folders_cleaned += 1;
647 Ok(())
648 }
649
650 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
651 self.cur += 1;
652 self.package_being_cleaned = String::from(package);
653 self.bar
654 .tick(self.cur_progress(), self.max, &self.format_message())
655 }
656}