From 0beb5fe93de712c44c2e08ac6287c1c62d107b38 Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Mon, 21 Nov 2022 18:39:02 +0100 Subject: [PATCH 1/6] Simple explanations for why cargo rebuilds crates --- crates/cargo-test-support/src/compare.rs | 144 ++++++++- src/cargo/core/compiler/custom_build.rs | 6 +- src/cargo/core/compiler/fingerprint.rs | 202 ++++++++----- .../core/compiler/fingerprint/dirty_reason.rs | 281 ++++++++++++++++++ src/cargo/core/compiler/job.rs | 23 +- src/cargo/core/compiler/job_queue.rs | 23 +- src/cargo/core/compiler/mod.rs | 4 +- src/cargo/ops/cargo_install.rs | 4 +- .../ops/common_for_install_and_uninstall.rs | 6 +- tests/testsuite/artifact_dep.rs | 2 + tests/testsuite/build_script.rs | 72 ++++- tests/testsuite/config_include.rs | 1 + tests/testsuite/freshness.rs | 145 +++++++-- tests/testsuite/git.rs | 1 + tests/testsuite/lto.rs | 2 + tests/testsuite/rustc.rs | 1 + tests/testsuite/test.rs | 1 + 17 files changed, 768 insertions(+), 150 deletions(-) create mode 100644 src/cargo/core/compiler/fingerprint/dirty_reason.rs diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index d7a8362104c..da1d0992c7a 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -16,6 +16,11 @@ //! `[WARNING]`) to match cargo's "status" output and allows you to ignore //! the alignment. See the source of `substitute_macros` for a complete list //! of substitutions. +//! - `[DIRTY-MSVC]` (only when the line starts with it) would be replaced by +//! `[DIRTY]` when `cfg(target_env = "msvc")` or the line will be ignored otherwise. +//! Tests that work around [issue 7358](https://github.com/rust-lang/cargo/issues/7358) +//! can use this to avoid duplicating the `with_stderr` call like: +//! `if cfg!(target_env = "msvc") {e.with_stderr("...[DIRTY]...");} else {e.with_stderr("...");}`. //! //! # Normalization //! @@ -108,7 +113,9 @@ fn normalize_actual(actual: &str, cwd: Option<&Path>) -> String { /// Normalizes the expected string so that it can be compared against the actual output. fn normalize_expected(expected: &str, cwd: Option<&Path>) -> String { - let expected = substitute_macros(expected); + let expected = replace_dirty_msvc(expected); + let expected = substitute_macros(&expected); + if cfg!(windows) { normalize_windows(&expected, cwd) } else { @@ -121,6 +128,29 @@ fn normalize_expected(expected: &str, cwd: Option<&Path>) -> String { } } +fn replace_dirty_msvc_impl(s: &str, is_msvc: bool) -> String { + if is_msvc { + s.replace("[DIRTY-MSVC]", "[DIRTY]") + } else { + use itertools::Itertools; + + let mut new = s + .lines() + .filter(|it| !it.starts_with("[DIRTY-MSVC]")) + .join("\n"); + + if s.ends_with("\n") { + new.push_str("\n"); + } + + new + } +} + +fn replace_dirty_msvc(s: &str) -> String { + replace_dirty_msvc_impl(s, cfg!(target_env = "msvc")) +} + /// Normalizes text for both actual and expected strings on Windows. fn normalize_windows(text: &str, cwd: Option<&Path>) -> String { // Let's not deal with / vs \ (windows...) @@ -170,6 +200,7 @@ fn substitute_macros(input: &str) -> String { ("[DOCUMENTING]", " Documenting"), ("[SCRAPING]", " Scraping"), ("[FRESH]", " Fresh"), + ("[DIRTY]", " Dirty"), ("[UPDATING]", " Updating"), ("[ADDING]", " Adding"), ("[REMOVING]", " Removing"), @@ -637,3 +668,114 @@ fn wild_str_cmp() { assert_ne!(WildStr::new(a), WildStr::new(b)); } } + +#[test] +fn dirty_msvc() { + let case = |expected: &str, wild: &str, msvc: bool| { + assert_eq!(expected, &replace_dirty_msvc_impl(wild, msvc)); + }; + + // no replacements + case("aa", "aa", false); + case("aa", "aa", true); + + // with replacements + case( + "\ +[DIRTY] a", + "\ +[DIRTY-MSVC] a", + true, + ); + case( + "", + "\ +[DIRTY-MSVC] a", + false, + ); + case( + "\ +[DIRTY] a +[COMPILING] a", + "\ +[DIRTY-MSVC] a +[COMPILING] a", + true, + ); + case( + "\ +[COMPILING] a", + "\ +[DIRTY-MSVC] a +[COMPILING] a", + false, + ); + + // test trailing newline behavior + case( + "\ +A +B +", "\ +A +B +", true, + ); + + case( + "\ +A +B +", "\ +A +B +", false, + ); + + case( + "\ +A +B", "\ +A +B", true, + ); + + case( + "\ +A +B", "\ +A +B", false, + ); + + case( + "\ +[DIRTY] a +", + "\ +[DIRTY-MSVC] a +", + true, + ); + case( + "\n", + "\ +[DIRTY-MSVC] a +", + false, + ); + + case( + "\ +[DIRTY] a", + "\ +[DIRTY-MSVC] a", + true, + ); + case( + "", + "\ +[DIRTY-MSVC] a", + false, + ); +} diff --git a/src/cargo/core/compiler/custom_build.rs b/src/cargo/core/compiler/custom_build.rs index b3929b8df70..630c2119ecc 100644 --- a/src/cargo/core/compiler/custom_build.rs +++ b/src/cargo/core/compiler/custom_build.rs @@ -1,4 +1,4 @@ -use super::job::{Freshness, Job, Work}; +use super::job::{Job, Work}; use super::{fingerprint, Context, LinkType, Unit}; use crate::core::compiler::artifact; use crate::core::compiler::context::Metadata; @@ -484,11 +484,11 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { }); let mut job = if cx.bcx.build_config.build_plan { - Job::new_dirty(Work::noop()) + Job::new_dirty(Work::noop(), None) } else { fingerprint::prepare_target(cx, unit, false)? }; - if job.freshness() == Freshness::Dirty { + if job.freshness().is_dirty() { job.before(dirty); } else { job.before(fresh); diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index eb78fd8cae7..e2eb0f8e0fe 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -312,7 +312,10 @@ //! See the `A-rebuild-detection` flag on the issue tracker for more: //! +mod dirty_reason; + use std::collections::hash_map::{Entry, HashMap}; + use std::env; use std::hash::{self, Hash, Hasher}; use std::io; @@ -341,6 +344,8 @@ use super::custom_build::BuildDeps; use super::job::{Job, Work}; use super::{BuildContext, Context, FileFlavor, Unit}; +pub use dirty_reason::DirtyReason; + /// Determines if a `unit` is up-to-date, and if not prepares necessary work to /// update the persisted fingerprint. /// @@ -393,9 +398,16 @@ pub fn prepare_target(cx: &mut Context<'_, '_>, unit: &Unit, force: bool) -> Car source.verify(unit.pkg.package_id())?; } - if compare.is_ok() && !force { - return Ok(Job::new_fresh()); - } + let dirty_reason = match compare { + Ok(_) => { + if force { + Some(DirtyReason::Forced) + } else { + return Ok(Job::new_fresh()); + } + } + Err(e) => e.downcast::().ok(), + }; // Clear out the old fingerprint file if it exists. This protects when // compilation is interrupted leaving a corrupt file. For example, a @@ -466,7 +478,7 @@ pub fn prepare_target(cx: &mut Context<'_, '_>, unit: &Unit, force: bool) -> Car Work::new(move |_| write_fingerprint(&loc, &fingerprint)) }; - Ok(Job::new_dirty(write_fingerprint)) + Ok(Job::new_dirty(write_fingerprint, dirty_reason)) } /// Dependency edge information for fingerprints. This is generated for each @@ -559,14 +571,28 @@ pub struct Fingerprint { } /// Indication of the status on the filesystem for a particular unit. -#[derive(Default)] -enum FsStatus { +#[derive(Clone, Default, Debug)] +pub enum FsStatus { /// This unit is to be considered stale, even if hash information all - /// matches. The filesystem inputs have changed (or are missing) and the - /// unit needs to subsequently be recompiled. + /// matches. #[default] Stale, + /// File system inputs have changed (or are missing), or there were + /// changes to the environment variables that affect this unit. See + /// the variants of [`StaleItem`] for more information. + StaleItem(StaleItem), + + /// A dependency was stale. + StaleDependency { + name: InternedString, + dep_mtime: FileTime, + max_mtime: FileTime, + }, + + /// A dependency was stale. + StaleDepFingerprint { name: InternedString }, + /// This unit is up-to-date. All outputs and their corresponding mtime are /// listed in the payload here for other dependencies to compare against. UpToDate { mtimes: HashMap }, @@ -576,7 +602,10 @@ impl FsStatus { fn up_to_date(&self) -> bool { match self { FsStatus::UpToDate { .. } => true, - FsStatus::Stale => false, + FsStatus::Stale + | FsStatus::StaleItem(_) + | FsStatus::StaleDependency { .. } + | FsStatus::StaleDepFingerprint { .. } => false, } } } @@ -677,7 +706,8 @@ enum LocalFingerprint { RerunIfEnvChanged { var: String, val: Option }, } -enum StaleItem { +#[derive(Clone, Debug)] +pub enum StaleItem { MissingFile(PathBuf), ChangedFile { reference: PathBuf, @@ -823,56 +853,53 @@ impl Fingerprint { /// The purpose of this is exclusively to produce a diagnostic message /// indicating why we're recompiling something. This function always returns /// an error, it will never return success. - fn compare(&self, old: &Fingerprint) -> CargoResult<()> { + fn compare(&self, old: &Fingerprint) -> Result<(), DirtyReason> { if self.rustc != old.rustc { - bail!("rust compiler has changed") + Err(DirtyReason::RustcChanged)? } if self.features != old.features { - bail!( - "features have changed: previously {}, now {}", - old.features, - self.features - ) + Err(DirtyReason::FeaturesChanged { + old: old.features.clone(), + new: self.features.clone(), + })? } if self.target != old.target { - bail!("target configuration has changed") + Err(DirtyReason::TargetConfigurationChanged)? } if self.path != old.path { - bail!("path to the source has changed") + Err(DirtyReason::PathToSourceChanged)? } if self.profile != old.profile { - bail!("profile configuration has changed") + Err(DirtyReason::ProfileConfigurationChanged)? } if self.rustflags != old.rustflags { - bail!( - "RUSTFLAGS has changed: previously {:?}, now {:?}", - old.rustflags, - self.rustflags - ) + Err(DirtyReason::RustflagsChanged { + old: old.rustflags.clone(), + new: self.rustflags.clone(), + })? } if self.metadata != old.metadata { - bail!("metadata changed") + Err(DirtyReason::MetadataChanged)? } if self.config != old.config { - bail!("configuration settings have changed") + Err(DirtyReason::ConfigSettingsChanged)? } if self.compile_kind != old.compile_kind { - bail!("compile kind (rustc target) changed") + Err(DirtyReason::CompileKindChanged)? } let my_local = self.local.lock().unwrap(); let old_local = old.local.lock().unwrap(); if my_local.len() != old_local.len() { - bail!("local lens changed"); + Err(DirtyReason::LocalLengthsChanged)? } for (new, old) in my_local.iter().zip(old_local.iter()) { match (new, old) { (LocalFingerprint::Precalculated(a), LocalFingerprint::Precalculated(b)) => { if a != b { - bail!( - "precalculated components have changed: previously {}, now {}", - b, - a - ) + Err(DirtyReason::PrecalculatedComponentsChanged { + old: b.to_string(), + new: a.to_string(), + })? } } ( @@ -880,11 +907,10 @@ impl Fingerprint { LocalFingerprint::CheckDepInfo { dep_info: bdep }, ) => { if adep != bdep { - bail!( - "dep info output changed: previously {:?}, now {:?}", - bdep, - adep - ) + Err(DirtyReason::DepInfoOutputChanged { + old: bdep.clone(), + new: adep.clone(), + })? } } ( @@ -898,18 +924,16 @@ impl Fingerprint { }, ) => { if aout != bout { - bail!( - "rerun-if-changed output changed: previously {:?}, now {:?}", - bout, - aout - ) + Err(DirtyReason::RerunIfChangedOutputFileChanged { + old: bout.clone(), + new: aout.clone(), + })? } if apaths != bpaths { - bail!( - "rerun-if-changed output changed: previously {:?}, now {:?}", - bpaths, - apaths, - ) + Err(DirtyReason::RerunIfChangedOutputPathsChanged { + old: bpaths.clone(), + new: apaths.clone(), + })? } } ( @@ -923,57 +947,59 @@ impl Fingerprint { }, ) => { if *akey != *bkey { - bail!("env vars changed: previously {}, now {}", bkey, akey); + Err(DirtyReason::EnvVarsChanged { + old: bkey.clone(), + new: akey.clone(), + })? } if *avalue != *bvalue { - bail!( - "env var `{}` changed: previously {:?}, now {:?}", - akey, - bvalue, - avalue - ) + Err(DirtyReason::EnvVarChanged { + name: akey.clone(), + old_value: bvalue.clone(), + new_value: avalue.clone(), + })? } } - (a, b) => bail!( - "local fingerprint type has changed ({} => {})", - b.kind(), - a.kind() - ), + (a, b) => Err(DirtyReason::LocalFingerprintTypeChanged { + old: b.kind(), + new: a.kind(), + })?, } } if self.deps.len() != old.deps.len() { - bail!("number of dependencies has changed") + Err(DirtyReason::NumberOfDependenciesChanged { + old: old.deps.len(), + new: self.deps.len(), + })? } for (a, b) in self.deps.iter().zip(old.deps.iter()) { if a.name != b.name { - let e = format_err!("`{}` != `{}`", a.name, b.name) - .context("unit dependency name changed"); - return Err(e); + Err(DirtyReason::UnitDependencyNameChanged { + old: b.name.clone(), + new: a.name.clone(), + })? } if a.fingerprint.hash_u64() != b.fingerprint.hash_u64() { - let e = format_err!( - "new ({}/{:x}) != old ({}/{:x})", - a.name, - a.fingerprint.hash_u64(), - b.name, - b.fingerprint.hash_u64() - ) - .context("unit dependency information changed"); - return Err(e); + Err(DirtyReason::UnitDependencyInfoChanged { + new_name: a.name.clone(), + new_fingerprint: a.fingerprint.hash_u64(), + old_name: b.name.clone(), + old_fingerprint: b.fingerprint.hash_u64(), + })? } } if !self.fs_status.up_to_date() { - bail!("current filesystem status shows we're outdated"); + Err(DirtyReason::FsStatusOutdated(self.fs_status.clone()))? } // This typically means some filesystem modifications happened or // something transitive was odd. In general we should strive to provide // a better error message than this, so if you see this message a lot it // likely means this method needs to be updated! - bail!("two fingerprint comparison turned up nothing obvious"); + Err(DirtyReason::NothingObvious) } /// Dynamically inspect the local filesystem to update the `fs_status` field @@ -1034,7 +1060,15 @@ impl Fingerprint { let dep_mtimes = match &dep.fingerprint.fs_status { FsStatus::UpToDate { mtimes } => mtimes, // If our dependency is stale, so are we, so bail out. - FsStatus::Stale => return Ok(()), + FsStatus::Stale + | FsStatus::StaleItem(_) + | FsStatus::StaleDependency { .. } + | FsStatus::StaleDepFingerprint { .. } => { + self.fs_status = FsStatus::StaleDepFingerprint { + name: dep.name.clone(), + }; + return Ok(()); + } }; // If our dependency edge only requires the rmeta file to be present @@ -1072,6 +1106,13 @@ impl Fingerprint { "dependency on `{}` is newer than we are {} > {} {:?}", dep.name, dep_mtime, max_mtime, pkg_root ); + + self.fs_status = FsStatus::StaleDependency { + name: dep.name.clone(), + dep_mtime: *dep_mtime, + max_mtime: *max_mtime, + }; + return Ok(()); } } @@ -1085,6 +1126,7 @@ impl Fingerprint { local.find_stale_item(mtime_cache, pkg_root, target_root, cargo_exe)? { item.log(); + self.fs_status = FsStatus::StaleItem(item); return Ok(()); } } @@ -1652,8 +1694,10 @@ fn compare_old_fingerprint( ); } let result = new_fingerprint.compare(&old_fingerprint); - assert!(result.is_err()); - result + match result { + Ok(_) => panic!("compare should not return Ok"), + Err(e) => Err(e.into()), + } } fn log_compare(unit: &Unit, compare: &CargoResult<()>) { diff --git a/src/cargo/core/compiler/fingerprint/dirty_reason.rs b/src/cargo/core/compiler/fingerprint/dirty_reason.rs new file mode 100644 index 00000000000..396f4f7f4d9 --- /dev/null +++ b/src/cargo/core/compiler/fingerprint/dirty_reason.rs @@ -0,0 +1,281 @@ +use super::*; +use crate::core::Shell; + +use std::fmt; +use std::fmt::Debug; + +#[derive(Clone, Debug)] +pub enum DirtyReason { + RustcChanged, + FeaturesChanged { + old: String, + new: String, + }, + TargetConfigurationChanged, + PathToSourceChanged, + ProfileConfigurationChanged, + RustflagsChanged { + old: Vec, + new: Vec, + }, + MetadataChanged, + ConfigSettingsChanged, + CompileKindChanged, + LocalLengthsChanged, + PrecalculatedComponentsChanged { + old: String, + new: String, + }, + DepInfoOutputChanged { + old: PathBuf, + new: PathBuf, + }, + RerunIfChangedOutputFileChanged { + old: PathBuf, + new: PathBuf, + }, + RerunIfChangedOutputPathsChanged { + old: Vec, + new: Vec, + }, + EnvVarsChanged { + old: String, + new: String, + }, + EnvVarChanged { + name: String, + old_value: Option, + new_value: Option, + }, + LocalFingerprintTypeChanged { + old: &'static str, + new: &'static str, + }, + NumberOfDependenciesChanged { + old: usize, + new: usize, + }, + UnitDependencyNameChanged { + old: InternedString, + new: InternedString, + }, + UnitDependencyInfoChanged { + old_name: InternedString, + old_fingerprint: u64, + + new_name: InternedString, + new_fingerprint: u64, + }, + FsStatusOutdated(FsStatus), + NothingObvious, + Forced, +} + +// still need to implement Display for Error +impl fmt::Display for DirtyReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "dirty") + } +} + +impl std::error::Error for DirtyReason {} + +trait ShellExt { + fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()>; +} + +impl ShellExt for Shell { + fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()> { + self.status("Dirty", format_args!("{}: {s}", &unit.pkg)) + } +} + +struct FileTimeDiff { + old_time: FileTime, + new_time: FileTime, +} + +impl fmt::Display for FileTimeDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s_diff = self.new_time.seconds() - self.old_time.seconds(); + if s_diff >= 1 { + fmt::Display::fmt( + &humantime::Duration::from(std::time::Duration::from_secs(s_diff as u64)), + f, + ) + } else { + // format nanoseconds as it is, humantime would display ms, us and ns + let ns_diff = self.new_time.nanoseconds() - self.old_time.nanoseconds(); + write!(f, "{ns_diff}ns") + } + } +} + +#[derive(Copy, Clone)] +struct After { + old_time: FileTime, + new_time: FileTime, + what: &'static str, +} + +impl fmt::Display for After { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + old_time, + new_time, + what, + } = *self; + let diff = FileTimeDiff { old_time, new_time }; + + write!(f, "{new_time}, {diff} after {what} at {old_time}") + } +} + +impl DirtyReason { + fn after(old_time: FileTime, new_time: FileTime, what: &'static str) -> After { + After { + old_time, + new_time, + what, + } + } + + pub fn present_to(&self, s: &mut Shell, unit: &Unit, root: &Path) -> CargoResult<()> { + match self { + DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"), + DirtyReason::FeaturesChanged { .. } => { + s.dirty_because(unit, "the list of features changed")?; + + Ok(()) + } + DirtyReason::TargetConfigurationChanged => { + s.dirty_because(unit, "the target configuration changed") + } + DirtyReason::PathToSourceChanged => { + s.dirty_because(unit, "the path to the source changed") + } + DirtyReason::ProfileConfigurationChanged => { + s.dirty_because(unit, "the profile configuration changed") + } + DirtyReason::RustflagsChanged { .. } => { + s.dirty_because(unit, "the rustflags changed")?; + + Ok(()) + } + DirtyReason::MetadataChanged => s.dirty_because(unit, "the metadata changed"), + DirtyReason::ConfigSettingsChanged => { + s.dirty_because(unit, "the config settings changed") + } + DirtyReason::CompileKindChanged => { + s.dirty_because(unit, "the rustc compile kind changed") + } + DirtyReason::LocalLengthsChanged => { + s.dirty_because(unit, "the local lengths changed")?; + s.note( + "This could happen because of added/removed `cargo:rerun-if` instructions in the build script", + )?; + + Ok(()) + } + DirtyReason::PrecalculatedComponentsChanged { .. } => { + s.dirty_because(unit, "the precalculated components changed")?; + + Ok(()) + } + DirtyReason::DepInfoOutputChanged { .. } => { + s.dirty_because(unit, "the dependency info output changed") + } + DirtyReason::RerunIfChangedOutputFileChanged { .. } => { + s.dirty_because(unit, "rerun-if-changed output file path changed")?; + + Ok(()) + } + DirtyReason::RerunIfChangedOutputPathsChanged { .. } => { + s.dirty_because(unit, "the rerun-if-changed instructions changed")?; + + Ok(()) + } + DirtyReason::EnvVarsChanged { .. } => { + s.dirty_because(unit, "the environment variables changed")?; + + Ok(()) + } + DirtyReason::EnvVarChanged { name, .. } => { + s.dirty_because(unit, format_args!("the env variable {name} changed"))?; + + Ok(()) + } + DirtyReason::LocalFingerprintTypeChanged { .. } => { + s.dirty_because(unit, "the local fingerprint type changed")?; + + Ok(()) + } + DirtyReason::NumberOfDependenciesChanged { old, new } => s.dirty_because( + unit, + format_args!("number of dependencies changed ({old} => {new})",), + ), + DirtyReason::UnitDependencyNameChanged { old, new } => s.dirty_because( + unit, + format_args!("name of dependency changed ({old} => {new})"), + ), + DirtyReason::UnitDependencyInfoChanged { .. } => { + s.dirty_because(unit, "dependency info changed") + } + DirtyReason::FsStatusOutdated(status) => match status { + FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"), + FsStatus::StaleItem(item) => match item { + StaleItem::MissingFile(missing_file) => { + let file = missing_file.strip_prefix(root).unwrap_or(&missing_file); + s.dirty_because( + unit, + format_args!("the file `{}` is missing", file.display()), + ) + } + StaleItem::ChangedFile { + stale, + stale_mtime, + reference_mtime, + .. + } => { + let file = stale.strip_prefix(root).unwrap_or(&stale); + let after = Self::after(*reference_mtime, *stale_mtime, "last build"); + s.dirty_because( + unit, + format_args!("the file `{}` has changed ({after})", file.display()), + ) + } + StaleItem::ChangedEnv { var, .. } => { + s.dirty_because( + unit, + format_args!("the environment variable {var} changed"), + )?; + Ok(()) + } + }, + FsStatus::StaleDependency { + name, + dep_mtime, + max_mtime, + .. + } => { + let after = Self::after(*max_mtime, *dep_mtime, "last build"); + s.dirty_because( + unit, + format_args!("the dependency {name} was rebuilt ({after})"), + ) + } + FsStatus::StaleDepFingerprint { name } => { + s.dirty_because(unit, format_args!("the dependency {name} was rebuilt")) + } + FsStatus::UpToDate { .. } => { + unreachable!() + } + }, + DirtyReason::NothingObvious => { + // See comment in fingerprint compare method. + s.dirty_because(unit, "the fingerprint comparison turned up nothing obvious") + } + DirtyReason::Forced => s.dirty_because(unit, "forced"), + } + } +} diff --git a/src/cargo/core/compiler/job.rs b/src/cargo/core/compiler/job.rs index b80b8506695..a385d80ef00 100644 --- a/src/cargo/core/compiler/job.rs +++ b/src/cargo/core/compiler/job.rs @@ -1,3 +1,4 @@ +use crate::core::compiler::fingerprint::DirtyReason; use std::fmt; use std::mem; @@ -49,10 +50,10 @@ impl Job { } /// Creates a new job representing a unit of work. - pub fn new_dirty(work: Work) -> Job { + pub fn new_dirty(work: Work, dirty_reason: Option) -> Job { Job { work, - fresh: Freshness::Dirty, + fresh: Freshness::Dirty(dirty_reason), } } @@ -65,8 +66,8 @@ impl Job { /// Returns whether this job was fresh/dirty, where "fresh" means we're /// likely to perform just some small bookkeeping where "dirty" means we'll /// probably do something slow like invoke rustc. - pub fn freshness(&self) -> Freshness { - self.fresh + pub fn freshness(&self) -> &Freshness { + &self.fresh } pub fn before(&mut self, next: Work) { @@ -85,8 +86,18 @@ impl fmt::Debug for Job { /// /// A fresh package does not necessarily need to be rebuilt (unless a dependency /// was also rebuilt), and a dirty package must always be rebuilt. -#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum Freshness { Fresh, - Dirty, + Dirty(Option), +} + +impl Freshness { + pub fn is_dirty(&self) -> bool { + matches!(self, Freshness::Dirty(_)) + } + + pub fn is_fresh(&self) -> bool { + matches!(self, Freshness::Fresh) + } } diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index 66b790439c4..608d37d7790 100644 --- a/src/cargo/core/compiler/job_queue.rs +++ b/src/cargo/core/compiler/job_queue.rs @@ -54,7 +54,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Write as _; use std::io; use std::marker; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread::{self, Scope}; use std::time::Duration; @@ -676,7 +676,7 @@ impl<'cfg> DrainState<'cfg> { // NOTE: An error here will drop the job without starting it. // That should be OK, since we want to exit as soon as // possible during an error. - self.note_working_on(cx.bcx.config, &unit, job.freshness())?; + self.note_working_on(cx.bcx.config, cx.bcx.ws.root(), &unit, job.freshness())?; } self.run(&unit, job, cx, scope); } @@ -1116,7 +1116,7 @@ impl<'cfg> DrainState<'cfg> { assert!(self.active.insert(id, unit.clone()).is_none()); let messages = self.messages.clone(); - let fresh = job.freshness(); + let is_fresh = job.freshness().is_fresh(); let rmeta_required = cx.rmeta_required(unit); let doit = move |state: JobState<'_, '_>| { @@ -1167,8 +1167,8 @@ impl<'cfg> DrainState<'cfg> { } }; - match fresh { - Freshness::Fresh => { + match is_fresh { + true => { self.timings.add_fresh(); // Running a fresh job on the same thread is often much faster than spawning a new // thread to run the job. @@ -1180,7 +1180,7 @@ impl<'cfg> DrainState<'cfg> { _marker: marker::PhantomData, }); } - Freshness::Dirty => { + false => { self.timings.add_dirty(); scope.spawn(move || { doit(JobState { @@ -1355,8 +1355,9 @@ impl<'cfg> DrainState<'cfg> { fn note_working_on( &mut self, config: &Config, + ws_root: &Path, unit: &Unit, - fresh: Freshness, + fresh: &Freshness, ) -> CargoResult<()> { if (self.compiled.contains(&unit.pkg.package_id()) && !unit.mode.is_doc() @@ -1370,7 +1371,13 @@ impl<'cfg> DrainState<'cfg> { match fresh { // Any dirty stage which runs at least one command gets printed as // being a compiled package. - Dirty => { + Dirty(dirty_reason) => { + if let Some(reason) = dirty_reason { + config + .shell() + .verbose(|shell| reason.present_to(shell, unit, ws_root))?; + } + if unit.mode.is_doc() { self.documented.insert(unit.pkg.package_id()); config.shell().status("Documenting", &unit.pkg)?; diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 71767bb704a..ff6101e5b09 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -164,11 +164,11 @@ fn compile<'cfg>( // We run these targets later, so this is just a no-op for now. Job::new_fresh() } else if build_plan { - Job::new_dirty(rustc(cx, unit, &exec.clone())?) + Job::new_dirty(rustc(cx, unit, &exec.clone())?, None) } else { let force = exec.force_rebuild(unit) || force_rebuild; let mut job = fingerprint::prepare_target(cx, unit, force)?; - job.before(if job.freshness() == Freshness::Dirty { + job.before(if job.freshness().is_dirty() { let work = if unit.mode.is_doc() || unit.mode.is_doc_scrape() { rustdoc(cx, unit)? } else { diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index ea933ce0814..72bcb41c8d5 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{env, fs}; -use crate::core::compiler::{CompileKind, DefaultExecutor, Executor, Freshness, UnitOutput}; +use crate::core::compiler::{CompileKind, DefaultExecutor, Executor, UnitOutput}; use crate::core::{Dependency, Edition, Package, PackageId, Source, SourceId, Workspace}; use crate::ops::CompileFilter; use crate::ops::{common_for_install_and_uninstall::*, FilterRule}; @@ -683,7 +683,7 @@ fn is_installed( let tracker = InstallTracker::load(config, root)?; let (freshness, _duplicates) = tracker.check_upgrade(dst, pkg, force, opts, target, &rustc.verbose_version)?; - Ok(freshness == Freshness::Fresh) + Ok(freshness.is_fresh()) } /// Checks if vers can only be satisfied by exactly one version of a package in a registry, and it's diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs index 6b8e8d792aa..1762f9a241d 100644 --- a/src/cargo/ops/common_for_install_and_uninstall.rs +++ b/src/cargo/ops/common_for_install_and_uninstall.rs @@ -170,7 +170,7 @@ impl InstallTracker { // Check if any tracked exe's are already installed. let duplicates = self.find_duplicates(dst, &exes); if force || duplicates.is_empty() { - return Ok((Freshness::Dirty, duplicates)); + return Ok((Freshness::Dirty(None), duplicates)); } // Check if all duplicates come from packages of the same name. If // there are duplicates from other packages, then --force will be @@ -200,7 +200,7 @@ impl InstallTracker { let source_id = pkg.package_id().source_id(); if source_id.is_path() { // `cargo install --path ...` is always rebuilt. - return Ok((Freshness::Dirty, duplicates)); + return Ok((Freshness::Dirty(None), duplicates)); } let is_up_to_date = |dupe_pkg_id| { let info = self @@ -224,7 +224,7 @@ impl InstallTracker { if matching_duplicates.iter().all(is_up_to_date) { Ok((Freshness::Fresh, duplicates)) } else { - Ok((Freshness::Dirty, duplicates)) + Ok((Freshness::Dirty(None), duplicates)) } } else { // Format the error message. diff --git a/tests/testsuite/artifact_dep.rs b/tests/testsuite/artifact_dep.rs index e8943beaa40..01f5a7c56ed 100644 --- a/tests/testsuite/artifact_dep.rs +++ b/tests/testsuite/artifact_dep.rs @@ -2321,8 +2321,10 @@ fn calc_bin_artifact_fingerprint() { .masquerade_as_nightly_cargo(&["bindeps"]) .with_stderr( "\ +[DIRTY] bar v0.5.0 ([CWD]/bar): the file `bar/src/main.rs` has changed ([..]) [COMPILING] bar v0.5.0 ([CWD]/bar) [RUNNING] `rustc --crate-name bar [..]` +[DIRTY] foo v0.1.0 ([CWD]): the dependency bar was rebuilt [CHECKING] foo v0.1.0 ([CWD]) [RUNNING] `rustc --crate-name foo [..]` [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] diff --git a/tests/testsuite/build_script.rs b/tests/testsuite/build_script.rs index e81f6ef1ece..117db0468bf 100644 --- a/tests/testsuite/build_script.rs +++ b/tests/testsuite/build_script.rs @@ -1214,6 +1214,7 @@ fn only_rerun_build_script() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([CWD]): the precalculated components changed [COMPILING] foo v0.5.0 ([CWD]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc --crate-name foo [..]` @@ -1320,6 +1321,7 @@ fn testing_and_such() { p.cargo("test -vj1") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([CWD]): the precalculated components changed [COMPILING] foo v0.5.0 ([CWD]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc --crate-name foo [..]` @@ -1717,6 +1719,7 @@ fn out_dir_is_preserved() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo [..]: the file `build.rs` has changed ([..]) [COMPILING] foo [..] [RUNNING] `rustc --crate-name build_script_build [..] [RUNNING] `[..]/build-script-build` @@ -1741,6 +1744,7 @@ fn out_dir_is_preserved() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo [..]: the precalculated components changed [COMPILING] foo [..] [RUNNING] `[..]build-script-build` [RUNNING] `rustc --crate-name foo [..] @@ -3005,6 +3009,7 @@ fn changing_an_override_invalidates() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the precalculated components changed [COMPILING] foo v0.5.0 ([..] [RUNNING] `rustc [..] -L native=bar` [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] @@ -3295,6 +3300,7 @@ fn rebuild_only_on_explicit_paths() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the file `foo` is missing [COMPILING] foo v0.5.0 ([..]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc [..] src/lib.rs [..]` @@ -3313,6 +3319,7 @@ fn rebuild_only_on_explicit_paths() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the file `foo` has changed ([..]) [COMPILING] foo v0.5.0 ([..]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc [..] src/lib.rs [..]` @@ -3351,6 +3358,7 @@ fn rebuild_only_on_explicit_paths() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the file `foo` has changed ([..]) [COMPILING] foo v0.5.0 ([..]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc [..] src/lib.rs [..]` @@ -3360,11 +3368,12 @@ fn rebuild_only_on_explicit_paths() { .run(); // .. as does deleting a file - println!("run foo delete"); + println!("run bar delete"); fs::remove_file(p.root().join("bar")).unwrap(); p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the file `bar` is missing [COMPILING] foo v0.5.0 ([..]) [RUNNING] `[..]/build-script-build` [RUNNING] `rustc [..] src/lib.rs [..]` @@ -4692,12 +4701,29 @@ fn rerun_if_directory() { ) .build(); - let dirty = || { - p.cargo("check") - .with_stderr( - "[COMPILING] foo [..]\n\ - [FINISHED] [..]", - ) + let dirty = |dirty_line: &str, compile_build_script: bool| { + let mut dirty_line = dirty_line.to_string(); + + if !dirty_line.is_empty() { + dirty_line.push('\n'); + } + + let compile_build_script_line = if compile_build_script { + "[RUNNING] `rustc --crate-name build_script_build [..]\n" + } else { + "" + }; + + p.cargo("check -v") + .with_stderr(format!( + "\ +{dirty_line}\ +[COMPILING] foo [..] +{compile_build_script_line}\ +[RUNNING] `[..]build-script-build[..]` +[RUNNING] `rustc --crate-name foo [..] +[FINISHED] [..]", + )) .run(); }; @@ -4706,10 +4732,13 @@ fn rerun_if_directory() { }; // Start with a missing directory. - dirty(); + dirty("", true); // Because the directory doesn't exist, it will trigger a rebuild every time. // https://github.com/rust-lang/cargo/issues/6003 - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` is missing", + false, + ); if is_coarse_mtime() { sleep_ms(1000); @@ -4717,7 +4746,10 @@ fn rerun_if_directory() { // Empty directory. fs::create_dir(p.root().join("somedir")).unwrap(); - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` has changed ([..])", + false, + ); fresh(); if is_coarse_mtime() { @@ -4727,7 +4759,10 @@ fn rerun_if_directory() { // Add a file. p.change_file("somedir/foo", ""); p.change_file("somedir/bar", ""); - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` has changed ([..])", + false, + ); fresh(); if is_coarse_mtime() { @@ -4736,7 +4771,10 @@ fn rerun_if_directory() { // Add a symlink. p.symlink("foo", "somedir/link"); - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` has changed ([..])", + false, + ); fresh(); if is_coarse_mtime() { @@ -4746,7 +4784,10 @@ fn rerun_if_directory() { // Move the symlink. fs::remove_file(p.root().join("somedir/link")).unwrap(); p.symlink("bar", "somedir/link"); - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` has changed ([..])", + false, + ); fresh(); if is_coarse_mtime() { @@ -4755,7 +4796,10 @@ fn rerun_if_directory() { // Remove a file. fs::remove_file(p.root().join("somedir/foo")).unwrap(); - dirty(); + dirty( + "[DIRTY] foo v0.1.0 ([..]): the file `somedir` has changed ([..])", + false, + ); fresh(); } diff --git a/tests/testsuite/config_include.rs b/tests/testsuite/config_include.rs index b2d0ddf66c6..26167ad2620 100644 --- a/tests/testsuite/config_include.rs +++ b/tests/testsuite/config_include.rs @@ -74,6 +74,7 @@ fn works_with_cli() { .masquerade_as_nightly_cargo(&["config-include"]) .with_stderr( "\ +[DIRTY] foo v0.0.1 ([..]): the rustflags changed [COMPILING] foo v0.0.1 [..] [RUNNING] `rustc [..]-W unsafe-code -W unused` [FINISHED] [..] diff --git a/tests/testsuite/freshness.rs b/tests/testsuite/freshness.rs index c5a5bb222be..793ebb94c60 100644 --- a/tests/testsuite/freshness.rs +++ b/tests/testsuite/freshness.rs @@ -38,10 +38,12 @@ fn modifying_and_moving() { p.root().join("target").move_into_the_past(); p.change_file("src/a.rs", "#[allow(unused)]fn main() {}"); - p.cargo("build") + p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.0.1 ([CWD]): the file `src/a.rs` has changed ([..]) [COMPILING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc --crate-name foo [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] ", ) @@ -83,10 +85,12 @@ fn modify_only_some_files() { lib.move_into_the_past(); // Make sure the binary is rebuilt, not the lib - p.cargo("build") + p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.0.1 ([CWD]): the file `src/b.rs` has changed ([..]) [COMPILING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc --crate-name foo [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] ", ) @@ -147,6 +151,7 @@ fn rebuild_sub_package_then_while_package() { p.cargo("build -pb -v") .with_stderr( "\ +[DIRTY] b v0.0.1 ([..]): the file `b/src/lib.rs` has changed ([..]) [COMPILING] b [..] [RUNNING] `rustc --crate-name b [..] [FINISHED] dev [..] @@ -163,8 +168,10 @@ fn rebuild_sub_package_then_while_package() { .with_stderr( "\ [FRESH] b [..] +[DIRTY] a [..]: the dependency b was rebuilt ([..]) [COMPILING] a [..] [RUNNING] `rustc --crate-name a [..] +[DIRTY] foo [..]: the dependency b was rebuilt ([..]) [COMPILING] foo [..] [RUNNING] `rustc --crate-name foo [..] [FINISHED] dev [..] @@ -492,21 +499,38 @@ fn changing_bin_features_caches_targets() { /* Targets should be cached from the first build */ - let mut e = p.cargo("build"); + let mut e = p.cargo("build -v"); + // MSVC does not include hash in binary filename, so it gets recompiled. if cfg!(target_env = "msvc") { - e.with_stderr("[COMPILING] foo[..]\n[FINISHED] dev[..]"); + e.with_stderr( + "\ +[DIRTY] foo v0.0.1 ([..]): the list of features changed +[COMPILING] foo[..] +[RUNNING] `rustc [..] +[FINISHED] dev[..]", + ); } else { - e.with_stderr("[FINISHED] dev[..]"); + e.with_stderr("[FRESH] foo v0.0.1 ([..])\n[FINISHED] dev[..]"); } e.run(); p.rename_run("foo", "off2").with_stdout("feature off").run(); - let mut e = p.cargo("build --features foo"); + let mut e = p.cargo("build --features foo -v"); if cfg!(target_env = "msvc") { - e.with_stderr("[COMPILING] foo[..]\n[FINISHED] dev[..]"); + e.with_stderr( + "\ +[DIRTY] foo v0.0.1 ([..]): the list of features changed +[COMPILING] foo[..] +[RUNNING] `rustc [..] +[FINISHED] dev[..]", + ); } else { - e.with_stderr("[FINISHED] dev[..]"); + e.with_stderr( + "\ +[FRESH] foo v0.0.1 ([..]) +[FINISHED] dev[..]", + ); } e.run(); p.rename_run("foo", "on2").with_stdout("feature on").run(); @@ -843,11 +867,13 @@ fn rebuild_if_environment_changes() { "#, ); - p.cargo("run") + p.cargo("run -v") .with_stdout("new desc") .with_stderr( "\ +[DIRTY] foo v0.0.1 ([CWD]): the metadata changed [COMPILING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] [RUNNING] `target/debug/foo[EXE]` ", @@ -1186,23 +1212,43 @@ fn changing_rustflags_is_cached() { let p = project().file("src/lib.rs", "").build(); // This isn't ever cached, we always have to recompile - for _ in 0..2 { - p.cargo("build") - .with_stderr( - "\ + p.cargo("build") + .with_stderr( + "\ [COMPILING] foo v0.0.1 ([..]) [FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", - ) - .run(); - p.cargo("build") - .env("RUSTFLAGS", "-C linker=cc") - .with_stderr( - "\ + ) + .run(); + p.cargo("build -v") + .env("RUSTFLAGS", "-C linker=cc") + .with_stderr( + "\ +[DIRTY] foo v0.0.1 ([..]): the rustflags changed [COMPILING] foo v0.0.1 ([..]) +[RUNNING] `rustc [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", - ) - .run(); - } + ) + .run(); + + p.cargo("build -v") + .with_stderr( + "\ +[DIRTY] foo v0.0.1 ([..]): the rustflags changed +[COMPILING] foo v0.0.1 ([..]) +[RUNNING] `rustc [..] +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); + p.cargo("build -v") + .env("RUSTFLAGS", "-C linker=cc") + .with_stderr( + "\ +[DIRTY] foo v0.0.1 ([..]): the rustflags changed +[COMPILING] foo v0.0.1 ([..]) +[RUNNING] `rustc [..] +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); } #[cargo_test] @@ -1480,12 +1526,18 @@ fn bust_patched_dep() { sleep_ms(1000); } - p.cargo("build") + p.cargo("build -v") .with_stderr( "\ +[DIRTY] registry1 v0.1.0 ([..]): the file `reg1new/src/lib.rs` has changed ([..]) [COMPILING] registry1 v0.1.0 ([..]) +[RUNNING] `rustc [..] +[DIRTY] registry2 v0.1.0: the dependency registry1 was rebuilt [COMPILING] registry2 v0.1.0 +[RUNNING] `rustc [..] +[DIRTY] foo v0.0.1 ([..]): the dependency registry2 was rebuilt [COMPILING] foo v0.0.1 ([..]) +[RUNNING] `rustc [..] [FINISHED] [..] ", ) @@ -1598,10 +1650,13 @@ fn rebuild_on_mid_build_file_modification() { ) .run(); - p.cargo("build") + p.cargo("build -v") .with_stderr( "\ +[FRESH] proc_macro_dep v0.1.0 ([..]/proc_macro_dep) +[DIRTY] root v0.1.0 ([..]/root): the file `root/src/lib.rs` has changed ([..]) [COMPILING] root v0.1.0 ([..]/root) +[RUNNING] `rustc [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] ", ) @@ -1877,6 +1932,7 @@ fn simulated_docker_deps_stay_cached() { [FRESH] regdep_env [..] [FRESH] regdep_old_style [..] [FRESH] regdep_rerun [..] +[DIRTY] foo [..]: the precalculated components changed [COMPILING] foo [..] [RUNNING] [..]/foo-[..]/build-script-build[..] [RUNNING] `rustc --crate-name foo[..] @@ -2108,6 +2164,7 @@ fn rerun_if_changes() { .env("FOO", "1") .with_stderr( "\ +[DIRTY] foo [..]: the env variable FOO changed [COMPILING] foo [..] [RUNNING] `[..]build-script-build` [RUNNING] `rustc [..] @@ -2125,6 +2182,7 @@ fn rerun_if_changes() { .env("BAR", "1") .with_stderr( "\ +[DIRTY] foo [..]: the env variable BAR changed [COMPILING] foo [..] [RUNNING] `[..]build-script-build` [RUNNING] `rustc [..] @@ -2142,6 +2200,7 @@ fn rerun_if_changes() { .env("BAR", "2") .with_stderr( "\ +[DIRTY] foo [..]: the env variable FOO changed [COMPILING] foo [..] [RUNNING] `[..]build-script-build` [RUNNING] `rustc [..] @@ -2439,12 +2498,15 @@ fn linking_interrupted() { drop(rustc_conn.read_exact(&mut buf)); // Build again, shouldn't be fresh. - p.cargo("test --test t1") + p.cargo("test --test t1 -v") .with_stderr( "\ +[DIRTY] foo v0.0.1 ([..]): the config settings changed [COMPILING] foo [..] +[RUNNING] `rustc --crate-name foo [..] +[RUNNING] `rustc --crate-name t1 [..] [FINISHED] [..] -[RUNNING] tests/t1.rs (target/debug/deps/t1[..]) +[RUNNING] `[..]target/debug/deps/t1-[..][EXE]` ", ) .run(); @@ -2513,25 +2575,43 @@ fn env_in_code_causes_rebuild() { .env_remove("FOO") .with_stderr("[FINISHED] [..]") .run(); - p.cargo("build") + p.cargo("build -v") .env("FOO", "bar") - .with_stderr("[COMPILING][..]\n[FINISHED][..]") + .with_stderr( + "\ +[DIRTY] foo [..]: the environment variable FOO changed +[COMPILING][..] +[RUNNING] `rustc [..] +[FINISHED][..]", + ) .run(); p.cargo("build") .env("FOO", "bar") .with_stderr("[FINISHED][..]") .run(); - p.cargo("build") + p.cargo("build -v") .env("FOO", "baz") - .with_stderr("[COMPILING][..]\n[FINISHED][..]") + .with_stderr( + "\ +[DIRTY] foo [..]: the environment variable FOO changed +[COMPILING][..] +[RUNNING] `rustc [..] +[FINISHED][..]", + ) .run(); p.cargo("build") .env("FOO", "baz") .with_stderr("[FINISHED][..]") .run(); - p.cargo("build") + p.cargo("build -v") .env_remove("FOO") - .with_stderr("[COMPILING][..]\n[FINISHED][..]") + .with_stderr( + "\ +[DIRTY] foo [..]: the environment variable FOO changed +[COMPILING][..] +[RUNNING] `rustc [..] +[FINISHED][..]", + ) .run(); p.cargo("build") .env_remove("FOO") @@ -2616,6 +2696,7 @@ fn cargo_env_changes() { .arg("-v") .with_stderr( "\ +[DIRTY] foo v1.0.0 ([..]): the environment variable CARGO changed [CHECKING] foo [..] [RUNNING] `rustc [..] [FINISHED] [..] diff --git a/tests/testsuite/git.rs b/tests/testsuite/git.rs index cdcbe95848d..35152e34822 100644 --- a/tests/testsuite/git.rs +++ b/tests/testsuite/git.rs @@ -2521,6 +2521,7 @@ fn include_overrides_gitignore() { p.cargo("build -v") .with_stderr( "\ +[DIRTY] foo v0.5.0 ([..]): the precalculated components changed [COMPILING] foo v0.5.0 ([..]) [RUNNING] `[..]build-script-build[..]` [RUNNING] `rustc --crate-name foo src/lib.rs [..]` diff --git a/tests/testsuite/lto.rs b/tests/testsuite/lto.rs index ba1041c10ab..6e7e2e7cb4b 100644 --- a/tests/testsuite/lto.rs +++ b/tests/testsuite/lto.rs @@ -595,6 +595,7 @@ fn dylib() { [COMPILING] registry-shared v0.0.1 [FRESH] registry v0.0.1 [RUNNING] `rustc --crate-name registry_shared [..]-C embed-bitcode=no[..] +[DIRTY] bar v0.0.0 ([..]): dependency info changed [COMPILING] bar [..] [RUNNING] `rustc --crate-name bar [..]--crate-type dylib [..]-C embed-bitcode=no[..] [FINISHED] [..] @@ -612,6 +613,7 @@ fn dylib() { [FRESH] registry-shared v0.0.1 [COMPILING] registry v0.0.1 [RUNNING] `rustc --crate-name registry [..] +[DIRTY] bar v0.0.0 ([..]): dependency info changed [COMPILING] bar [..] [RUNNING] `rustc --crate-name bar [..]--crate-type dylib [..]-C embed-bitcode=no[..] [RUNNING] `rustc --crate-name bar [..]-C lto [..]--test[..] diff --git a/tests/testsuite/rustc.rs b/tests/testsuite/rustc.rs index d15cf68a139..1edc848129e 100644 --- a/tests/testsuite/rustc.rs +++ b/tests/testsuite/rustc.rs @@ -639,6 +639,7 @@ fn rustc_fingerprint() { .with_stderr_does_not_contain("-C debug-assertions") .with_stderr( "\ +[DIRTY] foo [..]: the profile configuration changed [COMPILING] foo [..] [RUNNING] `rustc [..] [FINISHED] [..] diff --git a/tests/testsuite/test.rs b/tests/testsuite/test.rs index 88f5f34c2ff..add0a991f24 100644 --- a/tests/testsuite/test.rs +++ b/tests/testsuite/test.rs @@ -2661,6 +2661,7 @@ fn bin_does_not_rebuild_tests() { p.cargo("test -v --no-run") .with_stderr( "\ +[DIRTY] foo v0.0.1 ([..]): the file `src/main.rs` has changed ([..]) [COMPILING] foo v0.0.1 ([..]) [RUNNING] `rustc [..] src/main.rs [..]` [RUNNING] `rustc [..] src/main.rs [..]` From 2071acd5ec3e998c129eb03207bcfb83172efd4c Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Wed, 28 Dec 2022 18:44:05 +0100 Subject: [PATCH 2/6] Add -v to some feature tests --- tests/testsuite/features.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/testsuite/features.rs b/tests/testsuite/features.rs index cd5fcfe619c..8553b53c709 100644 --- a/tests/testsuite/features.rs +++ b/tests/testsuite/features.rs @@ -480,11 +480,14 @@ fn no_feature_doesnt_build() { .run(); p.process(&p.bin("foo")).with_stdout("").run(); - p.cargo("build --features bar") + p.cargo("build --features bar -v") .with_stderr( "\ [COMPILING] bar v0.0.1 ([CWD]/bar) +[RUNNING] `rustc --crate-name bar [..] +[DIRTY-MSVC] foo v0.0.1 ([CWD]): the list of features changed [COMPILING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc --crate-name foo [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] ", ) @@ -537,10 +540,12 @@ fn default_feature_pulled_in() { .run(); p.process(&p.bin("foo")).with_stdout("bar\n").run(); - p.cargo("build --no-default-features") + p.cargo("build --no-default-features -v") .with_stderr( "\ +[DIRTY-MSVC] foo v0.0.1 ([CWD]): the list of features changed [COMPILING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc --crate-name foo [..] [FINISHED] dev [unoptimized + debuginfo] target(s) in [..] ", ) From ef0951eb8660ac5c768969a634ab78af913402fd Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Thu, 29 Dec 2022 22:22:28 +0100 Subject: [PATCH 3/6] Apply patch Co-authored-by: Weihang Lo --- src/cargo/core/compiler/fingerprint.rs | 114 +++++++++--------- .../core/compiler/fingerprint/dirty_reason.rs | 9 -- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index e2eb0f8e0fe..f65bc80842e 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -399,14 +399,15 @@ pub fn prepare_target(cx: &mut Context<'_, '_>, unit: &Unit, force: bool) -> Car } let dirty_reason = match compare { - Ok(_) => { + Ok(None) => { if force { Some(DirtyReason::Forced) } else { return Ok(Job::new_fresh()); } } - Err(e) => e.downcast::().ok(), + Ok(reason) => reason, + Err(_) => None, }; // Clear out the old fingerprint file if it exists. This protects when @@ -853,53 +854,53 @@ impl Fingerprint { /// The purpose of this is exclusively to produce a diagnostic message /// indicating why we're recompiling something. This function always returns /// an error, it will never return success. - fn compare(&self, old: &Fingerprint) -> Result<(), DirtyReason> { + fn compare(&self, old: &Fingerprint) -> DirtyReason { if self.rustc != old.rustc { - Err(DirtyReason::RustcChanged)? + return DirtyReason::RustcChanged } if self.features != old.features { - Err(DirtyReason::FeaturesChanged { + return DirtyReason::FeaturesChanged { old: old.features.clone(), new: self.features.clone(), - })? + } } if self.target != old.target { - Err(DirtyReason::TargetConfigurationChanged)? + return DirtyReason::TargetConfigurationChanged; } if self.path != old.path { - Err(DirtyReason::PathToSourceChanged)? + return DirtyReason::PathToSourceChanged; } if self.profile != old.profile { - Err(DirtyReason::ProfileConfigurationChanged)? + return DirtyReason::ProfileConfigurationChanged; } if self.rustflags != old.rustflags { - Err(DirtyReason::RustflagsChanged { + return DirtyReason::RustflagsChanged { old: old.rustflags.clone(), new: self.rustflags.clone(), - })? + }; } if self.metadata != old.metadata { - Err(DirtyReason::MetadataChanged)? + return DirtyReason::MetadataChanged; } if self.config != old.config { - Err(DirtyReason::ConfigSettingsChanged)? + return DirtyReason::ConfigSettingsChanged; } if self.compile_kind != old.compile_kind { - Err(DirtyReason::CompileKindChanged)? + return DirtyReason::CompileKindChanged; } let my_local = self.local.lock().unwrap(); let old_local = old.local.lock().unwrap(); if my_local.len() != old_local.len() { - Err(DirtyReason::LocalLengthsChanged)? + return DirtyReason::LocalLengthsChanged; } for (new, old) in my_local.iter().zip(old_local.iter()) { match (new, old) { (LocalFingerprint::Precalculated(a), LocalFingerprint::Precalculated(b)) => { if a != b { - Err(DirtyReason::PrecalculatedComponentsChanged { + return DirtyReason::PrecalculatedComponentsChanged { old: b.to_string(), new: a.to_string(), - })? + }; } } ( @@ -907,10 +908,10 @@ impl Fingerprint { LocalFingerprint::CheckDepInfo { dep_info: bdep }, ) => { if adep != bdep { - Err(DirtyReason::DepInfoOutputChanged { + return DirtyReason::DepInfoOutputChanged { old: bdep.clone(), new: adep.clone(), - })? + }; } } ( @@ -924,16 +925,16 @@ impl Fingerprint { }, ) => { if aout != bout { - Err(DirtyReason::RerunIfChangedOutputFileChanged { + return DirtyReason::RerunIfChangedOutputFileChanged { old: bout.clone(), new: aout.clone(), - })? + }; } if apaths != bpaths { - Err(DirtyReason::RerunIfChangedOutputPathsChanged { + return DirtyReason::RerunIfChangedOutputPathsChanged { old: bpaths.clone(), new: apaths.clone(), - })? + }; } } ( @@ -947,59 +948,59 @@ impl Fingerprint { }, ) => { if *akey != *bkey { - Err(DirtyReason::EnvVarsChanged { + return DirtyReason::EnvVarsChanged { old: bkey.clone(), new: akey.clone(), - })? + }; } if *avalue != *bvalue { - Err(DirtyReason::EnvVarChanged { + return DirtyReason::EnvVarChanged { name: akey.clone(), old_value: bvalue.clone(), new_value: avalue.clone(), - })? + }; } } - (a, b) => Err(DirtyReason::LocalFingerprintTypeChanged { + (a, b) => return DirtyReason::LocalFingerprintTypeChanged { old: b.kind(), new: a.kind(), - })?, + }, } } if self.deps.len() != old.deps.len() { - Err(DirtyReason::NumberOfDependenciesChanged { + return DirtyReason::NumberOfDependenciesChanged { old: old.deps.len(), new: self.deps.len(), - })? + }; } for (a, b) in self.deps.iter().zip(old.deps.iter()) { if a.name != b.name { - Err(DirtyReason::UnitDependencyNameChanged { + return DirtyReason::UnitDependencyNameChanged { old: b.name.clone(), new: a.name.clone(), - })? + }; } if a.fingerprint.hash_u64() != b.fingerprint.hash_u64() { - Err(DirtyReason::UnitDependencyInfoChanged { + return DirtyReason::UnitDependencyInfoChanged { new_name: a.name.clone(), new_fingerprint: a.fingerprint.hash_u64(), old_name: b.name.clone(), old_fingerprint: b.fingerprint.hash_u64(), - })? + }; } } if !self.fs_status.up_to_date() { - Err(DirtyReason::FsStatusOutdated(self.fs_status.clone()))? + return DirtyReason::FsStatusOutdated(self.fs_status.clone()); } // This typically means some filesystem modifications happened or // something transitive was odd. In general we should strive to provide // a better error message than this, so if you see this message a lot it // likely means this method needs to be updated! - Err(DirtyReason::NothingObvious) + DirtyReason::NothingObvious } /// Dynamically inspect the local filesystem to update the `fs_status` field @@ -1667,7 +1668,7 @@ fn compare_old_fingerprint( loc: &Path, new_fingerprint: &Fingerprint, mtime_on_use: bool, -) -> CargoResult<()> { +) -> CargoResult> { let old_fingerprint_short = paths::read(loc)?; if mtime_on_use { @@ -1680,7 +1681,7 @@ fn compare_old_fingerprint( let new_hash = new_fingerprint.hash_u64(); if util::to_hex(new_hash) == old_fingerprint_short && new_fingerprint.fs_status.up_to_date() { - return Ok(()); + return Ok(None); } let old_fingerprint_json = paths::read(&loc.with_extension("json"))?; @@ -1693,23 +1694,28 @@ fn compare_old_fingerprint( old_fingerprint_short ); } - let result = new_fingerprint.compare(&old_fingerprint); - match result { - Ok(_) => panic!("compare should not return Ok"), - Err(e) => Err(e.into()), - } + + Ok(Some(new_fingerprint.compare(&old_fingerprint))) } -fn log_compare(unit: &Unit, compare: &CargoResult<()>) { - let ce = match compare { - Ok(..) => return, - Err(e) => e, - }; - info!( - "fingerprint error for {}/{:?}/{:?}", - unit.pkg, unit.mode, unit.target, - ); - info!(" err: {:?}", ce); +fn log_compare(unit: &Unit, compare: &CargoResult>) { + match compare { + Ok(None) => {}, + Ok(Some(reason)) => { + info!( + "fingerprint dirty for {}/{:?}/{:?}", + unit.pkg, unit.mode, unit.target, + ); + info!(" dirty: {reason:?}"); + }, + Err(e) => { + info!( + "fingerprint error for {}/{:?}/{:?}", + unit.pkg, unit.mode, unit.target, + ); + info!(" err: {e:?}"); + }, + } } /// Parses Cargo's internal `EncodedDepInfo` structure that was previously diff --git a/src/cargo/core/compiler/fingerprint/dirty_reason.rs b/src/cargo/core/compiler/fingerprint/dirty_reason.rs index 396f4f7f4d9..9346b6d6759 100644 --- a/src/cargo/core/compiler/fingerprint/dirty_reason.rs +++ b/src/cargo/core/compiler/fingerprint/dirty_reason.rs @@ -71,15 +71,6 @@ pub enum DirtyReason { Forced, } -// still need to implement Display for Error -impl fmt::Display for DirtyReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dirty") - } -} - -impl std::error::Error for DirtyReason {} - trait ShellExt { fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()>; } From 7c8ee49bffdeeb138db5e22f14abbf88cf99c547 Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Thu, 29 Dec 2022 22:26:40 +0100 Subject: [PATCH 4/6] cargo fmt --- src/cargo/core/compiler/fingerprint.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index f65bc80842e..091192cd766 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -856,13 +856,13 @@ impl Fingerprint { /// an error, it will never return success. fn compare(&self, old: &Fingerprint) -> DirtyReason { if self.rustc != old.rustc { - return DirtyReason::RustcChanged + return DirtyReason::RustcChanged; } if self.features != old.features { return DirtyReason::FeaturesChanged { old: old.features.clone(), new: self.features.clone(), - } + }; } if self.target != old.target { return DirtyReason::TargetConfigurationChanged; @@ -961,10 +961,12 @@ impl Fingerprint { }; } } - (a, b) => return DirtyReason::LocalFingerprintTypeChanged { - old: b.kind(), - new: a.kind(), - }, + (a, b) => { + return DirtyReason::LocalFingerprintTypeChanged { + old: b.kind(), + new: a.kind(), + } + } } } @@ -1700,21 +1702,21 @@ fn compare_old_fingerprint( fn log_compare(unit: &Unit, compare: &CargoResult>) { match compare { - Ok(None) => {}, + Ok(None) => {} Ok(Some(reason)) => { info!( "fingerprint dirty for {}/{:?}/{:?}", unit.pkg, unit.mode, unit.target, ); info!(" dirty: {reason:?}"); - }, + } Err(e) => { info!( "fingerprint error for {}/{:?}/{:?}", unit.pkg, unit.mode, unit.target, ); info!(" err: {e:?}"); - }, + } } } From c63b8c0a3a67b941d285b6176873b6a419291dd1 Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Thu, 29 Dec 2022 23:43:29 +0100 Subject: [PATCH 5/6] Minor style fixes --- .../core/compiler/fingerprint/dirty_reason.rs | 51 ++++++------------- src/cargo/core/compiler/job.rs | 2 +- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint/dirty_reason.rs b/src/cargo/core/compiler/fingerprint/dirty_reason.rs index 9346b6d6759..309b57ac037 100644 --- a/src/cargo/core/compiler/fingerprint/dirty_reason.rs +++ b/src/cargo/core/compiler/fingerprint/dirty_reason.rs @@ -1,9 +1,9 @@ -use super::*; -use crate::core::Shell; - use std::fmt; use std::fmt::Debug; +use super::*; +use crate::core::Shell; + #[derive(Clone, Debug)] pub enum DirtyReason { RustcChanged, @@ -135,9 +135,7 @@ impl DirtyReason { match self { DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"), DirtyReason::FeaturesChanged { .. } => { - s.dirty_because(unit, "the list of features changed")?; - - Ok(()) + s.dirty_because(unit, "the list of features changed") } DirtyReason::TargetConfigurationChanged => { s.dirty_because(unit, "the target configuration changed") @@ -148,11 +146,7 @@ impl DirtyReason { DirtyReason::ProfileConfigurationChanged => { s.dirty_because(unit, "the profile configuration changed") } - DirtyReason::RustflagsChanged { .. } => { - s.dirty_because(unit, "the rustflags changed")?; - - Ok(()) - } + DirtyReason::RustflagsChanged { .. } => s.dirty_because(unit, "the rustflags changed"), DirtyReason::MetadataChanged => s.dirty_because(unit, "the metadata changed"), DirtyReason::ConfigSettingsChanged => { s.dirty_because(unit, "the config settings changed") @@ -169,37 +163,25 @@ impl DirtyReason { Ok(()) } DirtyReason::PrecalculatedComponentsChanged { .. } => { - s.dirty_because(unit, "the precalculated components changed")?; - - Ok(()) + s.dirty_because(unit, "the precalculated components changed") } DirtyReason::DepInfoOutputChanged { .. } => { s.dirty_because(unit, "the dependency info output changed") } DirtyReason::RerunIfChangedOutputFileChanged { .. } => { - s.dirty_because(unit, "rerun-if-changed output file path changed")?; - - Ok(()) + s.dirty_because(unit, "rerun-if-changed output file path changed") } DirtyReason::RerunIfChangedOutputPathsChanged { .. } => { - s.dirty_because(unit, "the rerun-if-changed instructions changed")?; - - Ok(()) + s.dirty_because(unit, "the rerun-if-changed instructions changed") } DirtyReason::EnvVarsChanged { .. } => { - s.dirty_because(unit, "the environment variables changed")?; - - Ok(()) + s.dirty_because(unit, "the environment variables changed") } DirtyReason::EnvVarChanged { name, .. } => { - s.dirty_because(unit, format_args!("the env variable {name} changed"))?; - - Ok(()) + s.dirty_because(unit, format_args!("the env variable {name} changed")) } DirtyReason::LocalFingerprintTypeChanged { .. } => { - s.dirty_because(unit, "the local fingerprint type changed")?; - - Ok(()) + s.dirty_because(unit, "the local fingerprint type changed") } DirtyReason::NumberOfDependenciesChanged { old, new } => s.dirty_because( unit, @@ -235,13 +217,10 @@ impl DirtyReason { format_args!("the file `{}` has changed ({after})", file.display()), ) } - StaleItem::ChangedEnv { var, .. } => { - s.dirty_because( - unit, - format_args!("the environment variable {var} changed"), - )?; - Ok(()) - } + StaleItem::ChangedEnv { var, .. } => s.dirty_because( + unit, + format_args!("the environment variable {var} changed"), + ), }, FsStatus::StaleDependency { name, diff --git a/src/cargo/core/compiler/job.rs b/src/cargo/core/compiler/job.rs index a385d80ef00..b3e53af65d2 100644 --- a/src/cargo/core/compiler/job.rs +++ b/src/cargo/core/compiler/job.rs @@ -1,8 +1,8 @@ -use crate::core::compiler::fingerprint::DirtyReason; use std::fmt; use std::mem; use super::job_queue::JobState; +use crate::core::compiler::fingerprint::DirtyReason; use crate::util::CargoResult; pub struct Job { From 6913486a98f0e92208ed81464ed2df519f4ed029 Mon Sep 17 00:00:00 2001 From: Dinu Blanovschi Date: Thu, 29 Dec 2022 23:52:12 +0100 Subject: [PATCH 6/6] Use `DirtyReason::Forced` for cargo install `Freshness` --- src/cargo/core/compiler/mod.rs | 1 + src/cargo/ops/common_for_install_and_uninstall.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index ff6101e5b09..8e477607171 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -45,6 +45,7 @@ pub use self::compile_kind::{CompileKind, CompileTarget}; pub use self::context::{Context, Metadata}; pub use self::crate_type::CrateType; pub use self::custom_build::{BuildOutput, BuildScriptOutputs, BuildScripts}; +pub(crate) use self::fingerprint::DirtyReason; pub use self::job::Freshness; use self::job::{Job, Work}; use self::job_queue::{JobQueue, JobState}; diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs index 1762f9a241d..078b8b20d9d 100644 --- a/src/cargo/ops/common_for_install_and_uninstall.rs +++ b/src/cargo/ops/common_for_install_and_uninstall.rs @@ -11,7 +11,7 @@ use ops::FilterRule; use serde::{Deserialize, Serialize}; use toml_edit::easy as toml; -use crate::core::compiler::Freshness; +use crate::core::compiler::{DirtyReason, Freshness}; use crate::core::Target; use crate::core::{Dependency, FeatureValue, Package, PackageId, QueryKind, Source, SourceId}; use crate::ops::{self, CompileFilter, CompileOptions}; @@ -170,7 +170,7 @@ impl InstallTracker { // Check if any tracked exe's are already installed. let duplicates = self.find_duplicates(dst, &exes); if force || duplicates.is_empty() { - return Ok((Freshness::Dirty(None), duplicates)); + return Ok((Freshness::Dirty(Some(DirtyReason::Forced)), duplicates)); } // Check if all duplicates come from packages of the same name. If // there are duplicates from other packages, then --force will be @@ -200,7 +200,7 @@ impl InstallTracker { let source_id = pkg.package_id().source_id(); if source_id.is_path() { // `cargo install --path ...` is always rebuilt. - return Ok((Freshness::Dirty(None), duplicates)); + return Ok((Freshness::Dirty(Some(DirtyReason::Forced)), duplicates)); } let is_up_to_date = |dupe_pkg_id| { let info = self @@ -224,7 +224,7 @@ impl InstallTracker { if matching_duplicates.iter().all(is_up_to_date) { Ok((Freshness::Fresh, duplicates)) } else { - Ok((Freshness::Dirty(None), duplicates)) + Ok((Freshness::Dirty(Some(DirtyReason::Forced)), duplicates)) } } else { // Format the error message.