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