cargo/core/compiler/fingerprint/
dirty_reason.rs

1use std::fmt;
2use std::fmt::Debug;
3
4use super::*;
5use crate::core::Shell;
6
7/// Tells a better story of why a build is considered "dirty" that leads
8/// to a recompile. Usually constructed via [`Fingerprint::compare`].
9///
10/// [`Fingerprint::compare`]: super::Fingerprint::compare
11#[derive(Clone, Debug)]
12pub enum DirtyReason {
13    RustcChanged,
14    FeaturesChanged {
15        old: String,
16        new: String,
17    },
18    DeclaredFeaturesChanged {
19        old: String,
20        new: String,
21    },
22    TargetConfigurationChanged,
23    PathToSourceChanged,
24    ProfileConfigurationChanged,
25    RustflagsChanged {
26        old: Vec<String>,
27        new: Vec<String>,
28    },
29    ConfigSettingsChanged,
30    CompileKindChanged,
31    LocalLengthsChanged,
32    PrecalculatedComponentsChanged {
33        old: String,
34        new: String,
35    },
36    ChecksumUseChanged {
37        old: bool,
38    },
39    DepInfoOutputChanged {
40        old: PathBuf,
41        new: PathBuf,
42    },
43    RerunIfChangedOutputFileChanged {
44        old: PathBuf,
45        new: PathBuf,
46    },
47    RerunIfChangedOutputPathsChanged {
48        old: Vec<PathBuf>,
49        new: Vec<PathBuf>,
50    },
51    EnvVarsChanged {
52        old: String,
53        new: String,
54    },
55    EnvVarChanged {
56        name: String,
57        old_value: Option<String>,
58        new_value: Option<String>,
59    },
60    LocalFingerprintTypeChanged {
61        old: &'static str,
62        new: &'static str,
63    },
64    NumberOfDependenciesChanged {
65        old: usize,
66        new: usize,
67    },
68    UnitDependencyNameChanged {
69        old: InternedString,
70        new: InternedString,
71    },
72    UnitDependencyInfoChanged {
73        old_name: InternedString,
74        old_fingerprint: u64,
75
76        new_name: InternedString,
77        new_fingerprint: u64,
78    },
79    FsStatusOutdated(FsStatus),
80    NothingObvious,
81    Forced,
82    /// First time to build something.
83    FreshBuild,
84}
85
86trait ShellExt {
87    fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()>;
88}
89
90impl ShellExt for Shell {
91    fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()> {
92        self.status("Dirty", format_args!("{}: {s}", &unit.pkg))
93    }
94}
95
96struct FileTimeDiff {
97    old_time: FileTime,
98    new_time: FileTime,
99}
100
101impl fmt::Display for FileTimeDiff {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        let s_diff = self.new_time.seconds() - self.old_time.seconds();
104        if s_diff >= 1 {
105            write!(f, "{:#}", jiff::SignedDuration::from_secs(s_diff))
106        } else {
107            // format nanoseconds as it is, jiff would display ms, us and ns
108            let ns_diff = self.new_time.nanoseconds() - self.old_time.nanoseconds();
109            write!(f, "{ns_diff}ns")
110        }
111    }
112}
113
114#[derive(Copy, Clone)]
115struct After {
116    old_time: FileTime,
117    new_time: FileTime,
118    what: &'static str,
119}
120
121impl fmt::Display for After {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        let Self {
124            old_time,
125            new_time,
126            what,
127        } = *self;
128        let diff = FileTimeDiff { old_time, new_time };
129
130        write!(f, "{new_time}, {diff} after {what} at {old_time}")
131    }
132}
133
134impl DirtyReason {
135    /// Whether a build is dirty because it is a fresh build being kicked off.
136    pub fn is_fresh_build(&self) -> bool {
137        matches!(self, DirtyReason::FreshBuild)
138    }
139
140    fn after(old_time: FileTime, new_time: FileTime, what: &'static str) -> After {
141        After {
142            old_time,
143            new_time,
144            what,
145        }
146    }
147
148    pub fn present_to(&self, s: &mut Shell, unit: &Unit, root: &Path) -> CargoResult<()> {
149        match self {
150            DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"),
151            DirtyReason::FeaturesChanged { .. } => {
152                s.dirty_because(unit, "the list of features changed")
153            }
154            DirtyReason::DeclaredFeaturesChanged { .. } => {
155                s.dirty_because(unit, "the list of declared features changed")
156            }
157            DirtyReason::TargetConfigurationChanged => {
158                s.dirty_because(unit, "the target configuration changed")
159            }
160            DirtyReason::PathToSourceChanged => {
161                s.dirty_because(unit, "the path to the source changed")
162            }
163            DirtyReason::ProfileConfigurationChanged => {
164                s.dirty_because(unit, "the profile configuration changed")
165            }
166            DirtyReason::RustflagsChanged { .. } => s.dirty_because(unit, "the rustflags changed"),
167            DirtyReason::ConfigSettingsChanged => {
168                s.dirty_because(unit, "the config settings changed")
169            }
170            DirtyReason::CompileKindChanged => {
171                s.dirty_because(unit, "the rustc compile kind changed")
172            }
173            DirtyReason::LocalLengthsChanged => {
174                s.dirty_because(unit, "the local lengths changed")?;
175                s.note(
176                    "this could happen because of added/removed `cargo::rerun-if` instructions in the build script",
177                )?;
178
179                Ok(())
180            }
181            DirtyReason::PrecalculatedComponentsChanged { .. } => {
182                s.dirty_because(unit, "the precalculated components changed")
183            }
184            DirtyReason::ChecksumUseChanged { old } => {
185                if *old {
186                    s.dirty_because(
187                        unit,
188                        "the prior compilation used checksum freshness and this one does not",
189                    )
190                } else {
191                    s.dirty_because(unit, "checksum freshness requested, prior compilation did not use checksum freshness")
192                }
193            }
194            DirtyReason::DepInfoOutputChanged { .. } => {
195                s.dirty_because(unit, "the dependency info output changed")
196            }
197            DirtyReason::RerunIfChangedOutputFileChanged { .. } => {
198                s.dirty_because(unit, "rerun-if-changed output file path changed")
199            }
200            DirtyReason::RerunIfChangedOutputPathsChanged { .. } => {
201                s.dirty_because(unit, "the rerun-if-changed instructions changed")
202            }
203            DirtyReason::EnvVarsChanged { .. } => {
204                s.dirty_because(unit, "the environment variables changed")
205            }
206            DirtyReason::EnvVarChanged { name, .. } => {
207                s.dirty_because(unit, format_args!("the env variable {name} changed"))
208            }
209            DirtyReason::LocalFingerprintTypeChanged { .. } => {
210                s.dirty_because(unit, "the local fingerprint type changed")
211            }
212            DirtyReason::NumberOfDependenciesChanged { old, new } => s.dirty_because(
213                unit,
214                format_args!("number of dependencies changed ({old} => {new})",),
215            ),
216            DirtyReason::UnitDependencyNameChanged { old, new } => s.dirty_because(
217                unit,
218                format_args!("name of dependency changed ({old} => {new})"),
219            ),
220            DirtyReason::UnitDependencyInfoChanged { .. } => {
221                s.dirty_because(unit, "dependency info changed")
222            }
223            DirtyReason::FsStatusOutdated(status) => match status {
224                FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"),
225                FsStatus::StaleItem(item) => match item {
226                    StaleItem::MissingFile(missing_file) => {
227                        let file = missing_file.strip_prefix(root).unwrap_or(&missing_file);
228                        s.dirty_because(
229                            unit,
230                            format_args!("the file `{}` is missing", file.display()),
231                        )
232                    }
233                    StaleItem::UnableToReadFile(file) => {
234                        let file = file.strip_prefix(root).unwrap_or(&file);
235                        s.dirty_because(
236                            unit,
237                            format_args!("the file `{}` could not be read", file.display()),
238                        )
239                    }
240                    StaleItem::FailedToReadMetadata(file) => {
241                        let file = file.strip_prefix(root).unwrap_or(&file);
242                        s.dirty_because(
243                            unit,
244                            format_args!("couldn't read metadata for file `{}`", file.display()),
245                        )
246                    }
247                    StaleItem::ChangedFile {
248                        stale,
249                        stale_mtime,
250                        reference_mtime,
251                        ..
252                    } => {
253                        let file = stale.strip_prefix(root).unwrap_or(&stale);
254                        let after = Self::after(*reference_mtime, *stale_mtime, "last build");
255                        s.dirty_because(
256                            unit,
257                            format_args!("the file `{}` has changed ({after})", file.display()),
258                        )
259                    }
260                    StaleItem::ChangedChecksum {
261                        source,
262                        stored_checksum,
263                        new_checksum,
264                    } => {
265                        let file = source.strip_prefix(root).unwrap_or(&source);
266                        s.dirty_because(
267                            unit,
268                            format_args!(
269                                "the file `{}` has changed (checksum didn't match, {stored_checksum} != {new_checksum})",
270                                file.display(),
271                            ),
272                        )
273                    }
274                    StaleItem::FileSizeChanged {
275                        path,
276                        old_size,
277                        new_size,
278                    } => {
279                        let file = path.strip_prefix(root).unwrap_or(&path);
280                        s.dirty_because(
281                            unit,
282                            format_args!(
283                                "file size changed ({old_size} != {new_size}) for `{}`",
284                                file.display()
285                            ),
286                        )
287                    }
288                    StaleItem::MissingChecksum(path) => {
289                        let file = path.strip_prefix(root).unwrap_or(&path);
290                        s.dirty_because(
291                            unit,
292                            format_args!("the checksum for file `{}` is missing", file.display()),
293                        )
294                    }
295                    StaleItem::ChangedEnv { var, .. } => s.dirty_because(
296                        unit,
297                        format_args!("the environment variable {var} changed"),
298                    ),
299                },
300                FsStatus::StaleDependency {
301                    name,
302                    dep_mtime,
303                    max_mtime,
304                    ..
305                } => {
306                    let after = Self::after(*max_mtime, *dep_mtime, "last build");
307                    s.dirty_because(
308                        unit,
309                        format_args!("the dependency {name} was rebuilt ({after})"),
310                    )
311                }
312                FsStatus::StaleDepFingerprint { name } => {
313                    s.dirty_because(unit, format_args!("the dependency {name} was rebuilt"))
314                }
315                FsStatus::UpToDate { .. } => {
316                    unreachable!()
317                }
318            },
319            DirtyReason::NothingObvious => {
320                // See comment in fingerprint compare method.
321                s.dirty_because(unit, "the fingerprint comparison turned up nothing obvious")
322            }
323            DirtyReason::Forced => s.dirty_because(unit, "forced"),
324            DirtyReason::FreshBuild => s.dirty_because(unit, "fresh build"),
325        }
326    }
327}