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)?;
121 let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
123 .into_iter()
124 .filter_map(|kind| match kind {
125 CompileKind::Target(target) => match Layout::new(ws, Some(target), &prof_dir_name) {
126 Ok(layout) => Some(Ok((kind, layout))),
127 Err(e) => Some(Err(e)),
128 },
129 CompileKind::Host => None,
130 })
131 .collect::<CargoResult<_>>()?;
132 let layouts = if targets.is_empty() {
135 vec![(CompileKind::Host, &host_layout)]
136 } else {
137 target_layouts
138 .iter()
139 .map(|(kind, layout)| (*kind, layout))
140 .collect()
141 };
142 let layouts_with_host: Vec<(CompileKind, &Layout)> =
144 std::iter::once((CompileKind::Host, &host_layout))
145 .chain(layouts.iter().map(|(k, l)| (*k, *l)))
146 .collect();
147
148 let mut pkg_ids = Vec::new();
155 for spec_str in spec.iter() {
156 let spec = PackageIdSpec::parse(spec_str)?;
158 if spec.partial_version().is_some() {
159 clean_ctx.gctx.shell().warn(&format!(
160 "version qualifier in `-p {}` is ignored, \
161 cleaning all versions of `{}` found",
162 spec_str,
163 spec.name()
164 ))?;
165 }
166 if spec.url().is_some() {
167 clean_ctx.gctx.shell().warn(&format!(
168 "url qualifier in `-p {}` ignored, \
169 cleaning all versions of `{}` found",
170 spec_str,
171 spec.name()
172 ))?;
173 }
174 let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
175 if matches.is_empty() {
176 let mut suggestion = String::new();
177 suggestion.push_str(&edit_distance::closest_msg(
178 &spec.name(),
179 resolve.iter(),
180 |id| id.name().as_str(),
181 "package",
182 ));
183 anyhow::bail!(
184 "package ID specification `{}` did not match any packages{}",
185 spec,
186 suggestion
187 );
188 }
189 pkg_ids.extend(matches);
190 }
191 let packages = pkg_set.get_many(pkg_ids)?;
192
193 clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
194
195 let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
198 for pkg in packages {
199 let pkg_dir = format!("{}-*", pkg.name());
200 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
201
202 for (_, layout) in &layouts_with_host {
204 let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
205 clean_ctx
206 .rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?;
207 }
208
209 for target in pkg.targets() {
210 if target.is_custom_build() {
211 for (_, layout) in &layouts_with_host {
213 let dir = escape_glob_path(layout.build_dir().build())?;
214 clean_ctx.rm_rf_package_glob_containing_hash(
215 &pkg.name(),
216 &Path::new(&dir).join(&pkg_dir),
217 )?;
218 }
219 continue;
220 }
221 let crate_name: Rc<str> = target.crate_name().into();
222 let path_dot: &str = &format!("{crate_name}.");
223 let path_dash: &str = &format!("{crate_name}-");
224 for &mode in &[
225 CompileMode::Build,
226 CompileMode::Test,
227 CompileMode::Check { test: false },
228 ] {
229 for (compile_kind, layout) in &layouts {
230 if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
231 let dir = layout.build_dir().build_unit(&pkg.name());
232 clean_ctx.rm_rf_glob(&dir)?;
233 continue;
234 }
235
236 let triple = target_data.short_name(compile_kind);
237 let (file_types, _unsupported) = target_data
238 .info(*compile_kind)
239 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
240 let artifact_dir = layout
241 .artifact_dir()
242 .expect("artifact-dir was not locked during clean");
243 let (dir, uplift_dir) = match target.kind() {
244 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
245 (layout.build_dir().examples(), Some(artifact_dir.examples()))
246 }
247 TargetKind::Test | TargetKind::Bench => {
249 (layout.build_dir().legacy_deps(), None)
250 }
251 _ => (layout.build_dir().legacy_deps(), Some(artifact_dir.dest())),
252 };
253 let mut dir_glob_str = escape_glob_path(dir)?;
254 let dir_glob = Path::new(&dir_glob_str);
255 for file_type in file_types {
256 let hashed_name = file_type.output_filename(target, Some("*"));
258 let unhashed_name = file_type.output_filename(target, None);
259
260 clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
261 clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
262
263 if let Some(uplift_dir) = uplift_dir {
265 let uplifted_path = uplift_dir.join(file_type.uplift_filename(target));
266 clean_ctx.rm_rf(&uplifted_path)?;
267 let dep_info = uplifted_path.with_extension("d");
269 clean_ctx.rm_rf(&dep_info)?;
270 }
271 }
272 let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
273 clean_ctx.rm_rf(&unhashed_dep_info)?;
274
275 if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
276 dir_glob_str.push(std::path::MAIN_SEPARATOR);
277 }
278 dir_glob_str.push('*');
279 let dir_glob_str: Rc<str> = dir_glob_str.into();
280 if cleaned_packages
281 .entry(dir_glob_str.clone())
282 .or_default()
283 .insert(crate_name.clone())
284 {
285 let paths = [
286 (path_dash, ".d"),
289 (path_dot, ".o"),
291 (path_dot, ".dwo"),
292 (path_dot, ".dwp"),
293 ];
294 clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
295 }
296
297 let dir = escape_glob_path(layout.build_dir().incremental())?;
299 let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
300 clean_ctx.rm_rf_glob(&incremental)?;
301 }
302 }
303 }
304 }
305
306 Ok(())
307}
308
309fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
310 let pattern = pattern
311 .to_str()
312 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
313 Ok(glob::Pattern::escape(pattern))
314}
315
316impl<'gctx> CleanContext<'gctx> {
317 pub fn new(gctx: &'gctx GlobalContext) -> Self {
318 let progress = CleaningFolderBar::new(gctx, 0);
321 CleanContext {
322 gctx,
323 progress: Box::new(progress),
324 dry_run: false,
325 num_files_removed: 0,
326 num_dirs_removed: 0,
327 total_bytes_removed: 0,
328 }
329 }
330
331 fn rm_rf_package_glob_containing_hash(
337 &mut self,
338 package: &str,
339 pattern: &Path,
340 ) -> CargoResult<()> {
341 let pattern = pattern
343 .to_str()
344 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
345 for path in glob::glob(pattern)? {
346 let path = path?;
347
348 let pkg_name = path
349 .file_name()
350 .and_then(std::ffi::OsStr::to_str)
351 .and_then(|artifact| artifact.rsplit_once('-'))
352 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
353 .0;
354
355 if pkg_name != package {
356 continue;
357 }
358
359 self.rm_rf(&path)?;
360 }
361 Ok(())
362 }
363
364 fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
365 let pattern = pattern
367 .to_str()
368 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
369 for path in glob::glob(pattern)? {
370 self.rm_rf(&path?)?;
371 }
372 Ok(())
373 }
374
375 fn rm_rf_prefix_list(
382 &mut self,
383 pattern: &str,
384 path_matchers: &[(&str, &str)],
385 ) -> CargoResult<()> {
386 for path in glob::glob(pattern)? {
387 let path = path?;
388 let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
389 if path_matchers
390 .iter()
391 .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
392 {
393 self.rm_rf(&path)?;
394 }
395 }
396 Ok(())
397 }
398
399 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
400 let meta = match fs::symlink_metadata(path) {
401 Ok(meta) => meta,
402 Err(e) => {
403 if e.kind() != std::io::ErrorKind::NotFound {
404 self.gctx
405 .shell()
406 .warn(&format!("cannot access {}: {e}", path.display()))?;
407 }
408 return Ok(());
409 }
410 };
411
412 if !self.dry_run {
414 self.gctx
415 .shell()
416 .verbose(|shell| shell.status("Removing", path.display()))?;
417 }
418 self.progress.display_now()?;
419
420 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
421 if let Ok(meta) = meta {
422 self.total_bytes_removed += meta.len();
426 }
427 self.num_files_removed += 1;
428 if !self.dry_run {
429 paths::remove_file(path)?;
430 }
431 Ok(())
432 };
433
434 if !meta.is_dir() {
435 return rm_file(path, Ok(meta));
436 }
437
438 for entry in walkdir::WalkDir::new(path).contents_first(true) {
439 let entry = entry?;
440 self.progress.on_clean()?;
441 if self.dry_run {
442 self.gctx
447 .shell()
448 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
449 }
450 if entry.file_type().is_dir() {
451 self.num_dirs_removed += 1;
452 if !self.dry_run {
457 paths::remove_dir_all(entry.path())?;
458 }
459 } else {
460 rm_file(entry.path(), entry.metadata())?;
461 }
462 }
463
464 Ok(())
465 }
466
467 pub fn display_summary(&self) -> CargoResult<()> {
468 let status = if self.dry_run { "Summary" } else { "Removed" };
469 let byte_count = if self.total_bytes_removed == 0 {
470 String::new()
471 } else {
472 let bytes = HumanBytes(self.total_bytes_removed);
473 format!(", {bytes:.1} total")
474 };
475 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
481 (0, 0) => format!("0 files"),
482 (0, 1) => format!("1 directory"),
483 (0, 2..) => format!("{} directories", self.num_dirs_removed),
484 (1, _) => format!("1 file"),
485 (2.., _) => format!("{} files", self.num_files_removed),
486 };
487 self.gctx
488 .shell()
489 .status(status, format!("{file_count}{byte_count}"))?;
490 if self.dry_run {
491 self.gctx
492 .shell()
493 .warn("no files deleted due to --dry-run")?;
494 }
495 Ok(())
496 }
497
498 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
504 let num_paths = paths
505 .iter()
506 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
507 .sum();
508 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
509 for path in paths {
510 self.rm_rf(path)?;
511 }
512 Ok(())
513 }
514}
515
516trait CleaningProgressBar {
517 fn display_now(&mut self) -> CargoResult<()>;
518 fn on_clean(&mut self) -> CargoResult<()>;
519 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
520 Ok(())
521 }
522}
523
524struct CleaningFolderBar<'gctx> {
525 bar: Progress<'gctx>,
526 max: usize,
527 cur: usize,
528}
529
530impl<'gctx> CleaningFolderBar<'gctx> {
531 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
532 Self {
533 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
534 max,
535 cur: 0,
536 }
537 }
538
539 fn cur_progress(&self) -> usize {
540 std::cmp::min(self.cur, self.max)
541 }
542}
543
544impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
545 fn display_now(&mut self) -> CargoResult<()> {
546 self.bar.tick_now(self.cur_progress(), self.max, "")
547 }
548
549 fn on_clean(&mut self) -> CargoResult<()> {
550 self.cur += 1;
551 self.bar.tick(self.cur_progress(), self.max, "")
552 }
553}
554
555struct CleaningPackagesBar<'gctx> {
556 bar: Progress<'gctx>,
557 max: usize,
558 cur: usize,
559 num_files_folders_cleaned: usize,
560 package_being_cleaned: String,
561}
562
563impl<'gctx> CleaningPackagesBar<'gctx> {
564 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
565 Self {
566 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
567 max,
568 cur: 0,
569 num_files_folders_cleaned: 0,
570 package_being_cleaned: String::new(),
571 }
572 }
573
574 fn cur_progress(&self) -> usize {
575 std::cmp::min(self.cur, self.max)
576 }
577
578 fn format_message(&self) -> String {
579 format!(
580 ": {}, {} files/folders cleaned",
581 self.package_being_cleaned, self.num_files_folders_cleaned
582 )
583 }
584}
585
586impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
587 fn display_now(&mut self) -> CargoResult<()> {
588 self.bar
589 .tick_now(self.cur_progress(), self.max, &self.format_message())
590 }
591
592 fn on_clean(&mut self) -> CargoResult<()> {
593 self.bar
594 .tick(self.cur_progress(), self.max, &self.format_message())?;
595 self.num_files_folders_cleaned += 1;
596 Ok(())
597 }
598
599 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
600 self.cur += 1;
601 self.package_being_cleaned = String::from(package);
602 self.bar
603 .tick(self.cur_progress(), self.max, &self.format_message())
604 }
605}