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.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())?;
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_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_examples(), Some(layout.examples()))
242 }
243 TargetKind::Test | TargetKind::Bench => (layout.legacy_deps(), None),
245 _ => (layout.legacy_deps(), Some(layout.dest())),
246 };
247 let mut dir_glob_str = escape_glob_path(dir)?;
248 let dir_glob = Path::new(&dir_glob_str);
249 for file_type in file_types {
250 let hashed_name = file_type.output_filename(target, Some("*"));
252 let unhashed_name = file_type.output_filename(target, None);
253
254 clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
255 clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
256
257 if let Some(uplift_dir) = uplift_dir {
259 let uplifted_path = uplift_dir.join(file_type.uplift_filename(target));
260 clean_ctx.rm_rf(&uplifted_path)?;
261 let dep_info = uplifted_path.with_extension("d");
263 clean_ctx.rm_rf(&dep_info)?;
264 }
265 }
266 let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
267 clean_ctx.rm_rf(&unhashed_dep_info)?;
268
269 if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
270 dir_glob_str.push(std::path::MAIN_SEPARATOR);
271 }
272 dir_glob_str.push('*');
273 let dir_glob_str: Rc<str> = dir_glob_str.into();
274 if cleaned_packages
275 .entry(dir_glob_str.clone())
276 .or_default()
277 .insert(crate_name.clone())
278 {
279 let paths = [
280 (path_dash, ".d"),
283 (path_dot, ".o"),
285 (path_dot, ".dwo"),
286 (path_dot, ".dwp"),
287 ];
288 clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
289 }
290
291 let dir = escape_glob_path(layout.incremental())?;
293 let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
294 clean_ctx.rm_rf_glob(&incremental)?;
295 }
296 }
297 }
298 }
299
300 Ok(())
301}
302
303fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
304 let pattern = pattern
305 .to_str()
306 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
307 Ok(glob::Pattern::escape(pattern))
308}
309
310impl<'gctx> CleanContext<'gctx> {
311 pub fn new(gctx: &'gctx GlobalContext) -> Self {
312 let progress = CleaningFolderBar::new(gctx, 0);
315 CleanContext {
316 gctx,
317 progress: Box::new(progress),
318 dry_run: false,
319 num_files_removed: 0,
320 num_dirs_removed: 0,
321 total_bytes_removed: 0,
322 }
323 }
324
325 fn rm_rf_package_glob_containing_hash(
331 &mut self,
332 package: &str,
333 pattern: &Path,
334 ) -> CargoResult<()> {
335 let pattern = pattern
337 .to_str()
338 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
339 for path in glob::glob(pattern)? {
340 let path = path?;
341
342 let pkg_name = path
343 .file_name()
344 .and_then(std::ffi::OsStr::to_str)
345 .and_then(|artifact| artifact.rsplit_once('-'))
346 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
347 .0;
348
349 if pkg_name != package {
350 continue;
351 }
352
353 self.rm_rf(&path)?;
354 }
355 Ok(())
356 }
357
358 fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
359 let pattern = pattern
361 .to_str()
362 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
363 for path in glob::glob(pattern)? {
364 self.rm_rf(&path?)?;
365 }
366 Ok(())
367 }
368
369 fn rm_rf_prefix_list(
376 &mut self,
377 pattern: &str,
378 path_matchers: &[(&str, &str)],
379 ) -> CargoResult<()> {
380 for path in glob::glob(pattern)? {
381 let path = path?;
382 let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
383 if path_matchers
384 .iter()
385 .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
386 {
387 self.rm_rf(&path)?;
388 }
389 }
390 Ok(())
391 }
392
393 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
394 let meta = match fs::symlink_metadata(path) {
395 Ok(meta) => meta,
396 Err(e) => {
397 if e.kind() != std::io::ErrorKind::NotFound {
398 self.gctx
399 .shell()
400 .warn(&format!("cannot access {}: {e}", path.display()))?;
401 }
402 return Ok(());
403 }
404 };
405
406 if !self.dry_run {
408 self.gctx
409 .shell()
410 .verbose(|shell| shell.status("Removing", path.display()))?;
411 }
412 self.progress.display_now()?;
413
414 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
415 if let Ok(meta) = meta {
416 self.total_bytes_removed += meta.len();
420 }
421 self.num_files_removed += 1;
422 if !self.dry_run {
423 paths::remove_file(path)?;
424 }
425 Ok(())
426 };
427
428 if !meta.is_dir() {
429 return rm_file(path, Ok(meta));
430 }
431
432 for entry in walkdir::WalkDir::new(path).contents_first(true) {
433 let entry = entry?;
434 self.progress.on_clean()?;
435 if self.dry_run {
436 self.gctx
441 .shell()
442 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
443 }
444 if entry.file_type().is_dir() {
445 self.num_dirs_removed += 1;
446 if !self.dry_run {
451 paths::remove_dir_all(entry.path())?;
452 }
453 } else {
454 rm_file(entry.path(), entry.metadata())?;
455 }
456 }
457
458 Ok(())
459 }
460
461 pub fn display_summary(&self) -> CargoResult<()> {
462 let status = if self.dry_run { "Summary" } else { "Removed" };
463 let byte_count = if self.total_bytes_removed == 0 {
464 String::new()
465 } else {
466 let bytes = HumanBytes(self.total_bytes_removed);
467 format!(", {bytes:.1} total")
468 };
469 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
475 (0, 0) => format!("0 files"),
476 (0, 1) => format!("1 directory"),
477 (0, 2..) => format!("{} directories", self.num_dirs_removed),
478 (1, _) => format!("1 file"),
479 (2.., _) => format!("{} files", self.num_files_removed),
480 };
481 self.gctx
482 .shell()
483 .status(status, format!("{file_count}{byte_count}"))?;
484 if self.dry_run {
485 self.gctx
486 .shell()
487 .warn("no files deleted due to --dry-run")?;
488 }
489 Ok(())
490 }
491
492 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
498 let num_paths = paths
499 .iter()
500 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
501 .sum();
502 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
503 for path in paths {
504 self.rm_rf(path)?;
505 }
506 Ok(())
507 }
508}
509
510trait CleaningProgressBar {
511 fn display_now(&mut self) -> CargoResult<()>;
512 fn on_clean(&mut self) -> CargoResult<()>;
513 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
514 Ok(())
515 }
516}
517
518struct CleaningFolderBar<'gctx> {
519 bar: Progress<'gctx>,
520 max: usize,
521 cur: usize,
522}
523
524impl<'gctx> CleaningFolderBar<'gctx> {
525 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
526 Self {
527 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
528 max,
529 cur: 0,
530 }
531 }
532
533 fn cur_progress(&self) -> usize {
534 std::cmp::min(self.cur, self.max)
535 }
536}
537
538impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
539 fn display_now(&mut self) -> CargoResult<()> {
540 self.bar.tick_now(self.cur_progress(), self.max, "")
541 }
542
543 fn on_clean(&mut self) -> CargoResult<()> {
544 self.cur += 1;
545 self.bar.tick(self.cur_progress(), self.max, "")
546 }
547}
548
549struct CleaningPackagesBar<'gctx> {
550 bar: Progress<'gctx>,
551 max: usize,
552 cur: usize,
553 num_files_folders_cleaned: usize,
554 package_being_cleaned: String,
555}
556
557impl<'gctx> CleaningPackagesBar<'gctx> {
558 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
559 Self {
560 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
561 max,
562 cur: 0,
563 num_files_folders_cleaned: 0,
564 package_being_cleaned: String::new(),
565 }
566 }
567
568 fn cur_progress(&self) -> usize {
569 std::cmp::min(self.cur, self.max)
570 }
571
572 fn format_message(&self) -> String {
573 format!(
574 ": {}, {} files/folders cleaned",
575 self.package_being_cleaned, self.num_files_folders_cleaned
576 )
577 }
578}
579
580impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
581 fn display_now(&mut self) -> CargoResult<()> {
582 self.bar
583 .tick_now(self.cur_progress(), self.max, &self.format_message())
584 }
585
586 fn on_clean(&mut self) -> CargoResult<()> {
587 self.bar
588 .tick(self.cur_progress(), self.max, &self.format_message())?;
589 self.num_files_folders_cleaned += 1;
590 Ok(())
591 }
592
593 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
594 self.cur += 1;
595 self.package_being_cleaned = String::from(package);
596 self.bar
597 .tick(self.cur_progress(), self.max, &self.format_message())
598 }
599}