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::edit_distance;
6use crate::util::errors::CargoResult;
7use crate::util::interning::InternedString;
8use crate::util::{human_readable_bytes, GlobalContext, Progress, ProgressStyle};
9use anyhow::bail;
10use cargo_util::paths;
11use std::collections::{HashMap, HashSet};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::rc::Rc;
15
16pub struct CleanOptions<'gctx> {
17 pub gctx: &'gctx GlobalContext,
18 pub spec: Vec<String>,
20 pub targets: Vec<String>,
22 pub profile_specified: bool,
24 pub requested_profile: InternedString,
26 pub doc: bool,
28 pub dry_run: bool,
30}
31
32pub struct CleanContext<'gctx> {
33 pub gctx: &'gctx GlobalContext,
34 progress: Box<dyn CleaningProgressBar + 'gctx>,
35 pub dry_run: bool,
36 num_files_removed: u64,
37 num_dirs_removed: u64,
38 total_bytes_removed: u64,
39}
40
41pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
43 let mut target_dir = ws.target_dir();
44 let gctx = opts.gctx;
45 let mut clean_ctx = CleanContext::new(gctx);
46 clean_ctx.dry_run = opts.dry_run;
47
48 if opts.doc {
49 if !opts.spec.is_empty() {
50 bail!("--doc cannot be used with -p");
57 }
58 target_dir = target_dir.join("doc");
60 clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?;
61 } else {
62 let profiles = Profiles::new(&ws, opts.requested_profile)?;
63
64 if opts.profile_specified {
65 let dir_name = profiles.get_dir_name();
69 target_dir = target_dir.join(dir_name);
70 }
71
72 if opts.spec.is_empty() {
78 clean_ctx.remove_paths(&[target_dir.into_path_unlocked()])?;
79 } else {
80 clean_specs(
81 &mut clean_ctx,
82 &ws,
83 &profiles,
84 &opts.targets,
85 &opts.spec,
86 opts.dry_run,
87 )?;
88 }
89 }
90
91 clean_ctx.display_summary()?;
92 Ok(())
93}
94
95fn clean_specs(
96 clean_ctx: &mut CleanContext<'_>,
97 ws: &Workspace<'_>,
98 profiles: &Profiles,
99 targets: &[String],
100 spec: &[String],
101 dry_run: bool,
102) -> CargoResult<()> {
103 let requested_kinds = CompileKind::from_requested_targets(clean_ctx.gctx, targets)?;
105 let target_data = RustcTargetData::new(ws, &requested_kinds)?;
106 let (pkg_set, resolve) = ops::resolve_ws(ws, dry_run)?;
107 let prof_dir_name = profiles.get_dir_name();
108 let host_layout = Layout::new(ws, None, &prof_dir_name)?;
109 let target_layouts: Vec<(CompileKind, Layout)> = requested_kinds
111 .into_iter()
112 .filter_map(|kind| match kind {
113 CompileKind::Target(target) => match Layout::new(ws, Some(target), &prof_dir_name) {
114 Ok(layout) => Some(Ok((kind, layout))),
115 Err(e) => Some(Err(e)),
116 },
117 CompileKind::Host => None,
118 })
119 .collect::<CargoResult<_>>()?;
120 let layouts = if targets.is_empty() {
123 vec![(CompileKind::Host, &host_layout)]
124 } else {
125 target_layouts
126 .iter()
127 .map(|(kind, layout)| (*kind, layout))
128 .collect()
129 };
130 let layouts_with_host: Vec<(CompileKind, &Layout)> =
132 std::iter::once((CompileKind::Host, &host_layout))
133 .chain(layouts.iter().map(|(k, l)| (*k, *l)))
134 .collect();
135
136 let mut pkg_ids = Vec::new();
143 for spec_str in spec.iter() {
144 let spec = PackageIdSpec::parse(spec_str)?;
146 if spec.partial_version().is_some() {
147 clean_ctx.gctx.shell().warn(&format!(
148 "version qualifier in `-p {}` is ignored, \
149 cleaning all versions of `{}` found",
150 spec_str,
151 spec.name()
152 ))?;
153 }
154 if spec.url().is_some() {
155 clean_ctx.gctx.shell().warn(&format!(
156 "url qualifier in `-p {}` ignored, \
157 cleaning all versions of `{}` found",
158 spec_str,
159 spec.name()
160 ))?;
161 }
162 let matches: Vec<_> = resolve.iter().filter(|id| spec.matches(*id)).collect();
163 if matches.is_empty() {
164 let mut suggestion = String::new();
165 suggestion.push_str(&edit_distance::closest_msg(
166 &spec.name(),
167 resolve.iter(),
168 |id| id.name().as_str(),
169 "package",
170 ));
171 anyhow::bail!(
172 "package ID specification `{}` did not match any packages{}",
173 spec,
174 suggestion
175 );
176 }
177 pkg_ids.extend(matches);
178 }
179 let packages = pkg_set.get_many(pkg_ids)?;
180
181 clean_ctx.progress = Box::new(CleaningPackagesBar::new(clean_ctx.gctx, packages.len()));
182
183 let mut cleaned_packages: HashMap<_, HashSet<_>> = HashMap::default();
186 for pkg in packages {
187 let pkg_dir = format!("{}-*", pkg.name());
188 clean_ctx.progress.on_cleaning_package(&pkg.name())?;
189
190 for (_, layout) in &layouts_with_host {
192 let dir = escape_glob_path(layout.fingerprint())?;
193 clean_ctx
194 .rm_rf_package_glob_containing_hash(&pkg.name(), &Path::new(&dir).join(&pkg_dir))?;
195 }
196
197 for target in pkg.targets() {
198 if target.is_custom_build() {
199 for (_, layout) in &layouts_with_host {
201 let dir = escape_glob_path(layout.build())?;
202 clean_ctx.rm_rf_package_glob_containing_hash(
203 &pkg.name(),
204 &Path::new(&dir).join(&pkg_dir),
205 )?;
206 }
207 continue;
208 }
209 let crate_name: Rc<str> = target.crate_name().into();
210 let path_dot: &str = &format!("{crate_name}.");
211 let path_dash: &str = &format!("{crate_name}-");
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
220 let (file_types, _unsupported) = target_data
221 .info(*compile_kind)
222 .rustc_outputs(mode, target.kind(), triple)?;
223 let (dir, uplift_dir) = match target.kind() {
224 TargetKind::ExampleBin | TargetKind::ExampleLib(..) => {
225 (layout.examples(), Some(layout.examples()))
226 }
227 TargetKind::Test | TargetKind::Bench => (layout.deps(), None),
229 _ => (layout.deps(), Some(layout.dest())),
230 };
231 let mut dir_glob_str = escape_glob_path(dir)?;
232 let dir_glob = Path::new(&dir_glob_str);
233 for file_type in file_types {
234 let hashed_name = file_type.output_filename(target, Some("*"));
236 let unhashed_name = file_type.output_filename(target, None);
237
238 clean_ctx.rm_rf_glob(&dir_glob.join(&hashed_name))?;
239 clean_ctx.rm_rf(&dir.join(&unhashed_name))?;
240
241 if let Some(uplift_dir) = uplift_dir {
243 let uplifted_path = uplift_dir.join(file_type.uplift_filename(target));
244 clean_ctx.rm_rf(&uplifted_path)?;
245 let dep_info = uplifted_path.with_extension("d");
247 clean_ctx.rm_rf(&dep_info)?;
248 }
249 }
250 let unhashed_dep_info = dir.join(format!("{}.d", crate_name));
251 clean_ctx.rm_rf(&unhashed_dep_info)?;
252
253 if !dir_glob_str.ends_with(std::path::MAIN_SEPARATOR) {
254 dir_glob_str.push(std::path::MAIN_SEPARATOR);
255 }
256 dir_glob_str.push('*');
257 let dir_glob_str: Rc<str> = dir_glob_str.into();
258 if cleaned_packages
259 .entry(dir_glob_str.clone())
260 .or_default()
261 .insert(crate_name.clone())
262 {
263 let paths = [
264 (path_dash, ".d"),
267 (path_dot, ".o"),
269 (path_dot, ".dwo"),
270 (path_dot, ".dwp"),
271 ];
272 clean_ctx.rm_rf_prefix_list(&dir_glob_str, &paths)?;
273 }
274
275 let dir = escape_glob_path(layout.incremental())?;
277 let incremental = Path::new(&dir).join(format!("{}-*", crate_name));
278 clean_ctx.rm_rf_glob(&incremental)?;
279 }
280 }
281 }
282 }
283
284 Ok(())
285}
286
287fn escape_glob_path(pattern: &Path) -> CargoResult<String> {
288 let pattern = pattern
289 .to_str()
290 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
291 Ok(glob::Pattern::escape(pattern))
292}
293
294impl<'gctx> CleanContext<'gctx> {
295 pub fn new(gctx: &'gctx GlobalContext) -> Self {
296 let progress = CleaningFolderBar::new(gctx, 0);
299 CleanContext {
300 gctx,
301 progress: Box::new(progress),
302 dry_run: false,
303 num_files_removed: 0,
304 num_dirs_removed: 0,
305 total_bytes_removed: 0,
306 }
307 }
308
309 fn rm_rf_package_glob_containing_hash(
315 &mut self,
316 package: &str,
317 pattern: &Path,
318 ) -> CargoResult<()> {
319 let pattern = pattern
321 .to_str()
322 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
323 for path in glob::glob(pattern)? {
324 let path = path?;
325
326 let pkg_name = path
327 .file_name()
328 .and_then(std::ffi::OsStr::to_str)
329 .and_then(|artifact| artifact.rsplit_once('-'))
330 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?
331 .0;
332
333 if pkg_name != package {
334 continue;
335 }
336
337 self.rm_rf(&path)?;
338 }
339 Ok(())
340 }
341
342 fn rm_rf_glob(&mut self, pattern: &Path) -> CargoResult<()> {
343 let pattern = pattern
345 .to_str()
346 .ok_or_else(|| anyhow::anyhow!("expected utf-8 path"))?;
347 for path in glob::glob(pattern)? {
348 self.rm_rf(&path?)?;
349 }
350 Ok(())
351 }
352
353 fn rm_rf_prefix_list(
360 &mut self,
361 pattern: &str,
362 path_matchers: &[(&str, &str)],
363 ) -> CargoResult<()> {
364 for path in glob::glob(pattern)? {
365 let path = path?;
366 let filename = path.file_name().and_then(|name| name.to_str()).unwrap();
367 if path_matchers
368 .iter()
369 .any(|(prefix, suffix)| filename.starts_with(prefix) && filename.ends_with(suffix))
370 {
371 self.rm_rf(&path)?;
372 }
373 }
374 Ok(())
375 }
376
377 pub fn rm_rf(&mut self, path: &Path) -> CargoResult<()> {
378 let meta = match fs::symlink_metadata(path) {
379 Ok(meta) => meta,
380 Err(e) => {
381 if e.kind() != std::io::ErrorKind::NotFound {
382 self.gctx
383 .shell()
384 .warn(&format!("cannot access {}: {e}", path.display()))?;
385 }
386 return Ok(());
387 }
388 };
389
390 if !self.dry_run {
392 self.gctx
393 .shell()
394 .verbose(|shell| shell.status("Removing", path.display()))?;
395 }
396 self.progress.display_now()?;
397
398 let mut rm_file = |path: &Path, meta: Result<std::fs::Metadata, _>| {
399 if let Ok(meta) = meta {
400 self.total_bytes_removed += meta.len();
404 }
405 self.num_files_removed += 1;
406 if !self.dry_run {
407 paths::remove_file(path)?;
408 }
409 Ok(())
410 };
411
412 if !meta.is_dir() {
413 return rm_file(path, Ok(meta));
414 }
415
416 for entry in walkdir::WalkDir::new(path).contents_first(true) {
417 let entry = entry?;
418 self.progress.on_clean()?;
419 if self.dry_run {
420 self.gctx
425 .shell()
426 .verbose(|shell| Ok(writeln!(shell.out(), "{}", entry.path().display())?))?;
427 }
428 if entry.file_type().is_dir() {
429 self.num_dirs_removed += 1;
430 if !self.dry_run {
435 paths::remove_dir_all(entry.path())?;
436 }
437 } else {
438 rm_file(entry.path(), entry.metadata())?;
439 }
440 }
441
442 Ok(())
443 }
444
445 pub fn display_summary(&self) -> CargoResult<()> {
446 let status = if self.dry_run { "Summary" } else { "Removed" };
447 let byte_count = if self.total_bytes_removed == 0 {
448 String::new()
449 } else {
450 if self.total_bytes_removed < 1024 {
452 format!(", {}B total", self.total_bytes_removed)
453 } else {
454 let (bytes, unit) = human_readable_bytes(self.total_bytes_removed);
455 format!(", {bytes:.1}{unit} total")
456 }
457 };
458 let file_count = match (self.num_files_removed, self.num_dirs_removed) {
464 (0, 0) => format!("0 files"),
465 (0, 1) => format!("1 directory"),
466 (0, 2..) => format!("{} directories", self.num_dirs_removed),
467 (1, _) => format!("1 file"),
468 (2.., _) => format!("{} files", self.num_files_removed),
469 };
470 self.gctx
471 .shell()
472 .status(status, format!("{file_count}{byte_count}"))?;
473 if self.dry_run {
474 self.gctx
475 .shell()
476 .warn("no files deleted due to --dry-run")?;
477 }
478 Ok(())
479 }
480
481 pub fn remove_paths(&mut self, paths: &[PathBuf]) -> CargoResult<()> {
487 let num_paths = paths
488 .iter()
489 .map(|path| walkdir::WalkDir::new(path).into_iter().count())
490 .sum();
491 self.progress = Box::new(CleaningFolderBar::new(self.gctx, num_paths));
492 for path in paths {
493 self.rm_rf(path)?;
494 }
495 Ok(())
496 }
497}
498
499trait CleaningProgressBar {
500 fn display_now(&mut self) -> CargoResult<()>;
501 fn on_clean(&mut self) -> CargoResult<()>;
502 fn on_cleaning_package(&mut self, _package: &str) -> CargoResult<()> {
503 Ok(())
504 }
505}
506
507struct CleaningFolderBar<'gctx> {
508 bar: Progress<'gctx>,
509 max: usize,
510 cur: usize,
511}
512
513impl<'gctx> CleaningFolderBar<'gctx> {
514 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
515 Self {
516 bar: Progress::with_style("Cleaning", ProgressStyle::Percentage, gctx),
517 max,
518 cur: 0,
519 }
520 }
521
522 fn cur_progress(&self) -> usize {
523 std::cmp::min(self.cur, self.max)
524 }
525}
526
527impl<'gctx> CleaningProgressBar for CleaningFolderBar<'gctx> {
528 fn display_now(&mut self) -> CargoResult<()> {
529 self.bar.tick_now(self.cur_progress(), self.max, "")
530 }
531
532 fn on_clean(&mut self) -> CargoResult<()> {
533 self.cur += 1;
534 self.bar.tick(self.cur_progress(), self.max, "")
535 }
536}
537
538struct CleaningPackagesBar<'gctx> {
539 bar: Progress<'gctx>,
540 max: usize,
541 cur: usize,
542 num_files_folders_cleaned: usize,
543 package_being_cleaned: String,
544}
545
546impl<'gctx> CleaningPackagesBar<'gctx> {
547 fn new(gctx: &'gctx GlobalContext, max: usize) -> Self {
548 Self {
549 bar: Progress::with_style("Cleaning", ProgressStyle::Ratio, gctx),
550 max,
551 cur: 0,
552 num_files_folders_cleaned: 0,
553 package_being_cleaned: String::new(),
554 }
555 }
556
557 fn cur_progress(&self) -> usize {
558 std::cmp::min(self.cur, self.max)
559 }
560
561 fn format_message(&self) -> String {
562 format!(
563 ": {}, {} files/folders cleaned",
564 self.package_being_cleaned, self.num_files_folders_cleaned
565 )
566 }
567}
568
569impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> {
570 fn display_now(&mut self) -> CargoResult<()> {
571 self.bar
572 .tick_now(self.cur_progress(), self.max, &self.format_message())
573 }
574
575 fn on_clean(&mut self) -> CargoResult<()> {
576 self.bar
577 .tick(self.cur_progress(), self.max, &self.format_message())?;
578 self.num_files_folders_cleaned += 1;
579 Ok(())
580 }
581
582 fn on_cleaning_package(&mut self, package: &str) -> CargoResult<()> {
583 self.cur += 1;
584 self.package_being_cleaned = String::from(package);
585 self.bar
586 .tick(self.cur_progress(), self.max, &self.format_message())
587 }
588}