diff --git a/src/ops/cargo_clean.rs b/src/ops/cargo_clean.rs index 0619f45c243..fa33bb274d5 100644 --- a/src/ops/cargo_clean.rs +++ b/src/ops/cargo_clean.rs @@ -2,7 +2,7 @@ use crate::compiler::trim_paths; use crate::compiler::{CompileKind, CompileMode, Layout, RustcTargetData}; use crate::ops; use crate::util::HumanBytes; -use crate::util::data_structures::{IndexMap, IndexSet}; +use crate::util::data_structures::{HashSet, IndexMap, IndexSet}; use crate::util::edit_distance; use crate::util::errors::CargoResult; use crate::util::interning::InternedString; @@ -44,6 +44,7 @@ pub struct CleanContext<'gctx> { num_files_removed: u64, num_dirs_removed: u64, total_bytes_removed: u64, + seen_file_ids: HashSet, } /// Cleans various caches. @@ -512,6 +513,7 @@ impl<'gctx> CleanContext<'gctx> { num_files_removed: 0, num_dirs_removed: 0, total_bytes_removed: 0, + seen_file_ids: HashSet::default(), } } @@ -545,10 +547,7 @@ impl<'gctx> CleanContext<'gctx> { let mut rm_file = |path: &Path, meta: Result| { if let Ok(meta) = meta { - // Note: This can over-count bytes removed for hard-linked - // files. It also under-counts since it only counts the exact - // byte sizes and not the block sizes. - self.total_bytes_removed += meta.len(); + self.total_bytes_removed += count_bytes(&mut self.seen_file_ids, path, &meta); } self.num_files_removed += 1; if !self.dry_run { @@ -729,3 +728,87 @@ impl<'gctx> CleaningProgressBar for CleaningPackagesBar<'gctx> { .tick(self.cur_progress(), self.max, &self.format_message()) } } + +fn count_bytes(seen: &mut HashSet, path: &Path, meta: &fs::Metadata) -> u64 { + if !meta.is_file() { + return meta.len(); + } + // We don't follow symlinks, so the meta here for the symlink itself. + // So we just report the size of the symlink since that is what we + // are deleting it. + if meta.is_symlink() { + return meta.len(); + } + match FileId::new_with_link_count(meta, path) { + Some((id, nlink)) => { + if nlink <= 1 || seen.insert(id) { + meta.len() + } else { + 0 + } + } + None => meta.len(), + } +} + +/// Newtype wrapper to represent a unique file on disk +/// +/// [`FileId`] is similar to [`same_file::Handle`] but does not require holding on +/// to a file descriptor. This is important when dealing with tracking large +/// amounts of files like when cleaning target-dir/build-dir. +/// +/// NOTE: Currently works for hardlinks, but will not work for reflinks. +/// Cargo does not current use reflinks, but may in the future. +#[derive(PartialEq, Eq, Hash)] +struct FileId((u64, u64)); + +impl FileId { + #[cfg(unix)] + fn new_with_link_count(meta: &fs::Metadata, _path: &Path) -> Option<(Self, u64)> { + use std::os::unix::fs::MetadataExt; + Some((FileId((meta.dev(), meta.ino())), meta.nlink() as u64)) + } + + #[cfg(windows)] + fn new_with_link_count(_meta: &fs::Metadata, path: &Path) -> Option<(Self, u64)> { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE}; + use windows_sys::Win32::Storage::FileSystem::{ + BY_HANDLE_FILE_INFORMATION, CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_READ_ATTRIBUTES, + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileInformationByHandle, + OPEN_EXISTING, + }; + + let mut wide: Vec = path.as_os_str().encode_wide().collect(); + wide.push(0); + + let handle = unsafe { + CreateFileW( + wide.as_ptr(), + FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + std::ptr::null(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + std::ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE { + return None; + } + + let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; + let ok = unsafe { GetFileInformationByHandle(handle, &mut info) }; + unsafe { CloseHandle(handle) }; + + if ok == 0 { + return None; + } + + let file_index = ((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64); + Some(( + FileId((info.dwVolumeSerialNumber as u64, file_index)), + info.nNumberOfLinks as u64, + )) + } +} diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 10f79b97b93..0b298e38ca7 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -925,6 +925,32 @@ fn clean_dry_run() { .run(); } +#[cfg(any(unix, windows))] +#[cargo_test] +fn clean_accounts_for_hardlinks() { + let p = project() + .file("src/lib.rs", "") + .file("target/debug/original.bin", &"x".repeat(1024)) + .build(); + let original = p.target_debug_dir().join("original.bin"); + let linked = p.target_debug_dir().join("linked.bin"); + std::fs::hard_link(&original, &linked).unwrap(); + + p.cargo("clean --dry-run") + // Keep .rustc_info.json out of the file count and size. + .env("CARGO_CACHE_RUSTC_INFO", "0") + // Count both the original and hardlink, but only count the size once. + .with_stderr_data( + str![[r#" + Summary 2 files, 1.0KiB total +warning: no files deleted due to --dry-run + +"#]] + .raw(), + ) + .run(); +} + #[cargo_test] fn doc_with_package_selection() { // --doc with -p