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            fmt::Display::fmt(
106                &humantime::Duration::from(std::time::Duration::from_secs(s_diff as u64)),
107                f,
108            )
109        } else {
110            // format nanoseconds as it is, humantime would display ms, us and ns
111            let ns_diff = self.new_time.nanoseconds() - self.old_time.nanoseconds();
112            write!(f, "{ns_diff}ns")
113        }
114    }
115}
116
117#[derive(Copy, Clone)]
118struct After {
119    old_time: FileTime,
120    new_time: FileTime,
121    what: &'static str,
122}
123
124impl fmt::Display for After {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        let Self {
127            old_time,
128            new_time,
129            what,
130        } = *self;
131        let diff = FileTimeDiff { old_time, new_time };
132
133        write!(f, "{new_time}, {diff} after {what} at {old_time}")
134    }
135}
136
137impl DirtyReason {
138    /// Whether a build is dirty because it is a fresh build being kicked off.
139    pub fn is_fresh_build(&self) -> bool {
140        matches!(self, DirtyReason::FreshBuild)
141    }
142
143    fn after(old_time: FileTime, new_time: FileTime, what: &'static str) -> After {
144        After {
145            old_time,
146            new_time,
147            what,
148        }
149    }
150
151    pub fn present_to(&self, s: &mut Shell, unit: &Unit, root: &Path) -> CargoResult<()> {
152        match self {
153            DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"),
154            DirtyReason::FeaturesChanged { .. } => {
155                s.dirty_because(unit, "the list of features changed")
156            }
157            DirtyReason::DeclaredFeaturesChanged { .. } => {
158                s.dirty_because(unit, "the list of declared features changed")
159            }
160            DirtyReason::TargetConfigurationChanged => {
161                s.dirty_because(unit, "the target configuration changed")
162            }
163            DirtyReason::PathToSourceChanged => {
164                s.dirty_because(unit, "the path to the source changed")
165            }
166            DirtyReason::ProfileConfigurationChanged => {
167                s.dirty_because(unit, "the profile configuration changed")
168            }
169            DirtyReason::RustflagsChanged { .. } => s.dirty_because(unit, "the rustflags changed"),
170            DirtyReason::ConfigSettingsChanged => {
171                s.dirty_because(unit, "the config settings changed")
172            }
173            DirtyReason::CompileKindChanged => {
174                s.dirty_because(unit, "the rustc compile kind changed")
175            }
176            DirtyReason::LocalLengthsChanged => {
177                s.dirty_because(unit, "the local lengths changed")?;
178                s.note(
179                    "this could happen because of added/removed `cargo::rerun-if` instructions in the build script",
180                )?;
181
182                Ok(())
183            }
184            DirtyReason::PrecalculatedComponentsChanged { .. } => {
185                s.dirty_because(unit, "the precalculated components changed")
186            }
187            DirtyReason::ChecksumUseChanged { old } => {
188                if *old {
189                    s.dirty_because(
190                        unit,
191                        "the prior compilation used checksum freshness and this one does not",
192                    )
193                } else {
194                    s.dirty_because(unit, "checksum freshness requested, prior compilation did not use checksum freshness")
195                }
196            }
197            DirtyReason::DepInfoOutputChanged { .. } => {
198                s.dirty_because(unit, "the dependency info output changed")
199            }
200            DirtyReason::RerunIfChangedOutputFileChanged { .. } => {
201                s.dirty_because(unit, "rerun-if-changed output file path changed")
202            }
203            DirtyReason::RerunIfChangedOutputPathsChanged { .. } => {
204                s.dirty_because(unit, "the rerun-if-changed instructions changed")
205            }
206            DirtyReason::EnvVarsChanged { .. } => {
207                s.dirty_because(unit, "the environment variables changed")
208            }
209            DirtyReason::EnvVarChanged { name, .. } => {
210                s.dirty_because(unit, format_args!("the env variable {name} changed"))
211            }
212            DirtyReason::LocalFingerprintTypeChanged { .. } => {
213                s.dirty_because(unit, "the local fingerprint type changed")
214            }
215            DirtyReason::NumberOfDependenciesChanged { old, new } => s.dirty_because(
216                unit,
217                format_args!("number of dependencies changed ({old} => {new})",),
218            ),
219            DirtyReason::UnitDependencyNameChanged { old, new } => s.dirty_because(
220                unit,
221                format_args!("name of dependency changed ({old} => {new})"),
222            ),
223            DirtyReason::UnitDependencyInfoChanged { .. } => {
224                s.dirty_because(unit, "dependency info changed")
225            }
226            DirtyReason::FsStatusOutdated(status) => match status {
227                FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"),
228                FsStatus::StaleItem(item) => match item {
229                    StaleItem::MissingFile(missing_file) => {
230                        let file = missing_file.strip_prefix(root).unwrap_or(&missing_file);
231                        s.dirty_because(
232                            unit,
233                            format_args!("the file `{}` is missing", file.display()),
234                        )
235                    }
236                    StaleItem::UnableToReadFile(file) => {
237                        let file = file.strip_prefix(root).unwrap_or(&file);
238                        s.dirty_because(
239                            unit,
240                            format_args!("the file `{}` could not be read", file.display()),
241                        )
242                    }
243                    StaleItem::FailedToReadMetadata(file) => {
244                        let file = file.strip_prefix(root).unwrap_or(&file);
245                        s.dirty_because(
246                            unit,
247                            format_args!("couldn't read metadata for file `{}`", file.display()),
248                        )
249                    }
250                    StaleItem::ChangedFile {
251                        stale,
252                        stale_mtime,
253                        reference_mtime,
254                        ..
255                    } => {
256                        let file = stale.strip_prefix(root).unwrap_or(&stale);
257                        let after = Self::after(*reference_mtime, *stale_mtime, "last build");
258                        s.dirty_because(
259                            unit,
260                            format_args!("the file `{}` has changed ({after})", file.display()),
261                        )
262                    }
263                    StaleItem::ChangedChecksum {
264                        source,
265                        stored_checksum,
266                        new_checksum,
267                    } => {
268                        let file = source.strip_prefix(root).unwrap_or(&source);
269                        s.dirty_because(
270                            unit,
271                            format_args!(
272                                "the file `{}` has changed (checksum didn't match, {stored_checksum} != {new_checksum})",
273                                file.display(),
274                            ),
275                        )
276                    }
277                    StaleItem::FileSizeChanged {
278                        path,
279                        old_size,
280                        new_size,
281                    } => {
282                        let file = path.strip_prefix(root).unwrap_or(&path);
283                        s.dirty_because(
284                            unit,
285                            format_args!(
286                                "file size changed ({old_size} != {new_size}) for `{}`",
287                                file.display()
288                            ),
289                        )
290                    }
291                    StaleItem::MissingChecksum(path) => {
292                        let file = path.strip_prefix(root).unwrap_or(&path);
293                        s.dirty_because(
294                            unit,
295                            format_args!("the checksum for file `{}` is missing", file.display()),
296                        )
297                    }
298                    StaleItem::ChangedEnv { var, .. } => s.dirty_because(
299                        unit,
300                        format_args!("the environment variable {var} changed"),
301                    ),
302                },
303                FsStatus::StaleDependency {
304                    name,
305                    dep_mtime,
306                    max_mtime,
307                    ..
308                } => {
309                    let after = Self::after(*max_mtime, *dep_mtime, "last build");
310                    s.dirty_because(
311                        unit,
312                        format_args!("the dependency {name} was rebuilt ({after})"),
313                    )
314                }
315                FsStatus::StaleDepFingerprint { name } => {
316                    s.dirty_because(unit, format_args!("the dependency {name} was rebuilt"))
317                }
318                FsStatus::UpToDate { .. } => {
319                    unreachable!()
320                }
321            },
322            DirtyReason::NothingObvious => {
323                // See comment in fingerprint compare method.
324                s.dirty_because(unit, "the fingerprint comparison turned up nothing obvious")
325            }
326            DirtyReason::Forced => s.dirty_because(unit, "forced"),
327            DirtyReason::FreshBuild => s.dirty_because(unit, "fresh build"),
328        }
329    }
330}