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)?;
120 let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
122 .into_iter()
123 .filter_map(|kind| match kind {
124 CompileKind::Target(target) => match Layout::new(ws, Some(target), &prof_dir_name) {
125 Ok(layout) => Some(Ok((kind, layout))),
126 Err(e) => Some(Err(e)),
127 },
128 CompileKind::Host => None,
129 })
130 .collect::<CargoResult<_>>()?;
131 let layouts = if targets.is_empty() {
134 vec![(CompileKind::Host, &host_layout)]
135 } else {
136 target_layouts
137 .iter()
138 .map(|(kind, layout)| (*kind, layout))
139 .collect()
140 };
141 let layouts_with_host: Vec<(CompileKind, &Layout)> =
143 std::iter::once((CompileKind::Host, &host_layout))
144 .chain(layouts.iter().map(|(k, l)| (*k, *l)))
145 .collect();
146
147 let mut pkg_ids = Vec::new();
154 for spec_str in spec.iter() {
155 let spec = PackageIdSpec::parse(spec_str)?;
157 if spec.partial_version().is_some() {
158 clean_ctx.gctx.shell().warn(&format!(
159 "version qualifier in `-p {}` is ignored, \
160 cleaning all versions of `{}` found",
161 spec_str,
162 spec.name()
163 ))?;
164 }
165 if spec.url().is_some() {
166 clean_ctx.gctx.shell().warn(&format!(
167 "url qualifier in `-p {}` ignored, \
168 cleaning all versions of `{}` found",
169 spec_str,
170 spec.name()
171 ))?;
172 }
173 let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
174 if matches.is_empty() {
175 let mut suggestion = String::new();
176 suggestion.push_str(&edit_distance::closest_msg(
177 &spec.name(),
178 resolve.iter(),
179 |id| id.name().as_str(),
180 "package",
181 ));
182 anyhow::bail!(
183 "package ID specification `{}` did not match any packages{}",
184 spec,
185 suggestion
186 );
187 }
188 pkg_ids.extend(matches);
189 }
190 let packages = pkg_set.get_many(pkg_ids)?;
191
192 clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
193
194 let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
197 for pkg in packages {
198 let pkg_dir = format!("{}-*", pkg.name());
199 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
200
201 for (_, layout) in &layouts_with_host {
203 let dir = escape_glob_path(layout.build_dir().legacy_fingerprint())?;
204 clean_ctx
205 .rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?;
206 }
207
208 for target in pkg.targets() {
209 if target.is_custom_build() {
210 for (_, layout) in &layouts_with_host {
212 let dir = escape_glob_path(layout.build_dir().build())?;
213 clean_ctx.rm_rf_package_glob_containing_hash(
214 &pkg.name(),
215 &Path::new(&dir).join(&pkg_dir),
216 )?;
217 }
218 continue;
219 }
220 let crate_name: Rc<str> = target.crate_name().into();
221 let path_dot: &str = &format!("{crate_name}.");
222 let path_dash: &str = &format!("{crate_name}-");
223 for &mode in &[
224 CompileMode::Build,
225 CompileMode::Test,
226 CompileMode::Check { test: false },
227 ] {
228 for (compile_kind, layout) in &layouts {
229 if clean_ctx.gctx.cli_unstable().build_dir_new_layout {
230 let dir = layout.build_dir().build_unit(&pkg.name());
231 clean_ctx.rm_rf_glob(&dir)?;
232 continue;
233 }
234
235 let triple = target_data.short_name(compile_kind);
236 let (file_types, _unsupported) = target_data
237 .info(*compile_kind)
238 .rustc_outputs(mode, target.kind(), triple, clean_ctx.gctx)?;
239 let (dir, uplift_dir) = match target.kind() {
240 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => (
241 layout.build_dir().examples(),
242 Some(layout.artifact_dir().examples()),
243 ),
244 TargetKind::Test | TargetKind::Bench => {
246 (layout.build_dir().legacy_deps(), None)
247 }
248 _ => (
249 layout.build_dir().legacy_deps(),
250 Some(layout.artifact_dir().dest()),
251 ),
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}