mod dep_info;
mod dirty_reason;
mod rustdoc;
use crate::util::data_structures::HashMap;
use std::collections::hash_map::Entry;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::hash::{self, Hash, Hasher};
use std::io::{self};
use std::ops::Not;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use anyhow::Context as _;
use anyhow::format_err;
use cargo_util::paths;
use filetime::FileTime;
use serde::de;
use serde::ser;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::compiler::unit_graph::UnitDep;
use crate::util;
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::log_message::LogMessage;
use crate::util::{StableHasher, internal, path_args};
use crate::workspace::Package;
use crate::{CARGO_ENV, GlobalContext};
use super::BuildContext;
use super::BuildRunner;
use super::FileFlavor;
use super::Job;
use super::Unit;
use super::UnitIndex;
use super::Work;
use super::custom_build::BuildDeps;
pub use self::dep_info::Checksum;
pub use self::dep_info::parse_dep_info;
pub use self::dep_info::parse_rustc_dep_info;
pub use self::dep_info::translate_dep_info;
pub use self::dirty_reason::DirtyReason;
pub use self::rustdoc::RustdocFingerprint;
enum FingerprintComparison {
Fresh,
Dirty {
reason: DirtyReason,
},
}
#[tracing::instrument(
skip(build_runner, unit),
fields(package_id = %unit.pkg.package_id(), target = unit.target.name())
)]
pub fn prepare_target(
build_runner: &mut BuildRunner<'_, '_>,
unit: &Unit,
force: bool,
) -> CargoResult<Job> {
let bcx = build_runner.bcx;
let loc = build_runner.files().fingerprint_file_path(unit, "");
debug!("fingerprint at: {}", loc.display());
let fingerprint = calculate(build_runner, unit)?;
let mtime_on_use = build_runner.bcx.gctx.cli_unstable().mtime_on_use;
let dirty_reason = match compare_old_fingerprint(unit, &loc, &*fingerprint, mtime_on_use, force)
{
FingerprintComparison::Fresh => None,
FingerprintComparison::Dirty { reason } => Some(reason),
};
if let Some(logger) = bcx.logger {
let index = bcx.unit_to_index[unit];
let mut cause = None;
let status = match dirty_reason.as_ref() {
Some(reason) if reason.is_fresh_build() => util::log_message::FingerprintStatus::New,
Some(reason) => {
cause = Some(reason.clone());
util::log_message::FingerprintStatus::Dirty
}
None => util::log_message::FingerprintStatus::Fresh,
};
logger.log(LogMessage::UnitFingerprint {
index,
status,
cause,
});
}
let Some(dirty_reason) = dirty_reason else {
return Ok(Job::new_fresh());
};
let source_id = unit.pkg.package_id().source_id();
let sources = bcx.packages.sources();
let source = sources
.get(source_id)
.ok_or_else(|| internal("missing package source"))?;
source.verify(unit.pkg.package_id())?;
if loc.exists() {
paths::write(&loc, b"")?;
}
let write_fingerprint = if unit.mode.is_run_custom_build() {
let build_script_outputs = Arc::clone(&build_runner.build_script_outputs);
let metadata = build_runner.get_run_build_script_metadata(unit);
let (gen_local, _overridden) = build_script_local_fingerprints(build_runner, unit)?;
let output_path = build_runner.build_explicit_deps[unit]
.build_script_output
.clone();
Work::new(move |_| {
let outputs = build_script_outputs.lock().unwrap();
let output = outputs
.get(metadata)
.expect("output must exist after running");
let deps = BuildDeps::new(&output_path, Some(output));
if let Some(new_local) = (gen_local)(&deps, None)? {
*fingerprint.local.lock().unwrap() = new_local;
}
write_fingerprint(&loc, &fingerprint)
})
} else {
Work::new(move |_| write_fingerprint(&loc, &fingerprint))
};
Ok(Job::new_dirty(write_fingerprint, dirty_reason))
}
#[derive(Clone)]
struct DepFingerprint {
pkg_id: u64,
name: InternedString,
public: bool,
only_requires_rmeta: bool,
fingerprint: Arc<Fingerprint>,
}
#[derive(Serialize, Deserialize)]
pub struct Fingerprint {
rustc: u64,
features: String,
declared_features: String,
target: u64,
profile: u64,
path: u64,
deps: Vec<DepFingerprint>,
local: Mutex<Vec<LocalFingerprint>>,
#[serde(skip)]
memoized_hash: Mutex<Option<u64>>,
rustflags: Vec<String>,
config: u64,
compile_kind: u64,
#[serde(skip)]
index: UnitIndex,
#[serde(skip)]
fs_status: FsStatus,
#[serde(skip)]
outputs: Vec<PathBuf>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(tag = "fs_status", rename_all = "kebab-case")]
pub enum FsStatus {
#[default]
Stale,
StaleItem(StaleItem),
StaleDependency {
unit: UnitIndex,
#[serde(with = "serde_file_time")]
dep_mtime: FileTime,
#[serde(with = "serde_file_time")]
max_mtime: FileTime,
},
StaleDepFingerprint { unit: UnitIndex },
#[serde(skip)]
UpToDate { mtimes: HashMap<PathBuf, FileTime> },
}
impl FsStatus {
fn up_to_date(&self) -> bool {
match self {
FsStatus::UpToDate { .. } => true,
FsStatus::Stale
| FsStatus::StaleItem(_)
| FsStatus::StaleDependency { .. }
| FsStatus::StaleDepFingerprint { .. } => false,
}
}
}
mod serde_file_time {
use filetime::FileTime;
use serde::Deserialize;
use serde::Serialize;
pub(super) fn serialize<S>(ft: &FileTime, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let secs_as_millis = ft.unix_seconds() as f64 * 1000.0;
let nanos_as_millis = ft.nanoseconds() as f64 / 1_000_000.0;
(secs_as_millis + nanos_as_millis).serialize(s)
}
pub(super) fn deserialize<'de, D>(d: D) -> Result<FileTime, D::Error>
where
D: serde::Deserializer<'de>,
{
let millis = f64::deserialize(d)?;
let secs = (millis / 1000.0) as i64;
let nanos = ((millis % 1000.0) * 1_000_000.0) as u32;
Ok(FileTime::from_unix_time(secs, nanos))
}
}
impl Serialize for DepFingerprint {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
(
&self.pkg_id,
&self.name,
&self.public,
&self.fingerprint.hash_u64(),
)
.serialize(ser)
}
}
impl<'de> Deserialize<'de> for DepFingerprint {
fn deserialize<D>(d: D) -> Result<DepFingerprint, D::Error>
where
D: de::Deserializer<'de>,
{
let (pkg_id, name, public, hash) = <(u64, String, bool, u64)>::deserialize(d)?;
Ok(DepFingerprint {
pkg_id,
name: name.into(),
public,
fingerprint: Arc::new(Fingerprint {
memoized_hash: Mutex::new(Some(hash)),
..Fingerprint::new()
}),
only_requires_rmeta: false,
})
}
}
#[derive(Debug, Serialize, Deserialize, Hash)]
enum LocalFingerprint {
Precalculated(String),
CheckDepInfo { dep_info: PathBuf, checksum: bool },
RerunIfChanged {
output: PathBuf,
paths: Vec<PathBuf>,
},
RerunIfEnvChanged { var: String, val: Option<String> },
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "stale_item", rename_all = "kebab-case")]
pub enum StaleItem {
MissingFile {
path: PathBuf,
},
UnableToReadFile {
path: PathBuf,
},
FailedToReadMetadata {
path: PathBuf,
},
FileSizeChanged {
path: PathBuf,
old_size: u64,
new_size: u64,
},
ChangedFile {
reference: PathBuf,
#[serde(with = "serde_file_time")]
reference_mtime: FileTime,
stale: PathBuf,
#[serde(with = "serde_file_time")]
stale_mtime: FileTime,
},
ChangedChecksum {
source: PathBuf,
stored_checksum: Checksum,
new_checksum: Checksum,
},
MissingChecksum {
path: PathBuf,
},
ChangedEnv {
var: String,
previous: Option<String>,
current: Option<String>,
},
}
impl LocalFingerprint {
#[allow(clippy::disallowed_methods)]
fn from_env<K: AsRef<str>>(
key: K,
env_config: &Arc<HashMap<String, OsString>>,
) -> LocalFingerprint {
let key = key.as_ref();
let var = key.to_owned();
let val = if let Some(val) = env_config.get(key) {
val.to_str().map(ToOwned::to_owned)
} else {
env::var(key).ok()
};
LocalFingerprint::RerunIfEnvChanged { var, val }
}
fn find_stale_item(
&self,
mtime_cache: &mut HashMap<PathBuf, FileTime>,
checksum_cache: &mut HashMap<PathBuf, Checksum>,
pkg: &Package,
build_root: &Path,
cargo_exe: &Path,
gctx: &GlobalContext,
) -> CargoResult<Option<StaleItem>> {
let pkg_root = pkg.root();
match self {
LocalFingerprint::CheckDepInfo { dep_info, checksum } => {
let dep_info = build_root.join(dep_info);
let Some(info) = parse_dep_info(pkg_root, build_root, &dep_info)? else {
return Ok(Some(StaleItem::MissingFile { path: dep_info }));
};
for (key, previous) in info.env.iter() {
if let Some(value) = pkg.manifest().metadata().env_var(key.as_str()) {
if Some(value.as_ref()) == previous.as_deref() {
continue;
}
}
let current = if key == CARGO_ENV {
Some(cargo_exe.to_str().ok_or_else(|| {
format_err!(
"cargo exe path {} must be valid UTF-8",
cargo_exe.display()
)
})?)
} else {
if let Some(value) = gctx.env_config()?.get(key) {
value.to_str()
} else {
gctx.get_env(key).ok()
}
};
if current == previous.as_deref() {
continue;
}
return Ok(Some(StaleItem::ChangedEnv {
var: key.clone(),
previous: previous.clone(),
current: current.map(Into::into),
}));
}
if *checksum {
Ok(find_stale_file(
mtime_cache,
checksum_cache,
&dep_info,
info.files.iter().map(|(file, checksum)| (file, *checksum)),
*checksum,
))
} else {
Ok(find_stale_file(
mtime_cache,
checksum_cache,
&dep_info,
info.files.into_keys().map(|p| (p, None)),
*checksum,
))
}
}
LocalFingerprint::RerunIfChanged { output, paths } => Ok(find_stale_file(
mtime_cache,
checksum_cache,
&build_root.join(output),
paths.iter().map(|p| (pkg_root.join(p), None)),
false,
)),
LocalFingerprint::RerunIfEnvChanged { .. } => Ok(None),
LocalFingerprint::Precalculated(..) => Ok(None),
}
}
fn kind(&self) -> &'static str {
match self {
LocalFingerprint::Precalculated(..) => "precalculated",
LocalFingerprint::CheckDepInfo { .. } => "dep-info",
LocalFingerprint::RerunIfChanged { .. } => "rerun-if-changed",
LocalFingerprint::RerunIfEnvChanged { .. } => "rerun-if-env-changed",
}
}
}
impl Fingerprint {
fn new() -> Fingerprint {
Fingerprint {
rustc: 0,
target: 0,
profile: 0,
path: 0,
features: String::new(),
declared_features: String::new(),
deps: Vec::new(),
local: Mutex::new(Vec::new()),
memoized_hash: Mutex::new(None),
rustflags: Vec::new(),
config: 0,
compile_kind: 0,
index: UnitIndex::default(),
fs_status: FsStatus::Stale,
outputs: Vec::new(),
}
}
pub fn clear_memoized(&self) {
*self.memoized_hash.lock().unwrap() = None;
}
fn hash_u64(&self) -> u64 {
if let Some(s) = *self.memoized_hash.lock().unwrap() {
return s;
}
let ret = util::hash_u64(self);
*self.memoized_hash.lock().unwrap() = Some(ret);
ret
}
fn compare(&self, old: &Fingerprint) -> DirtyReason {
if self.rustc != old.rustc {
return DirtyReason::RustcChanged;
}
if self.features != old.features {
return DirtyReason::FeaturesChanged {
old: old.features.clone(),
new: self.features.clone(),
};
}
if self.declared_features != old.declared_features {
return DirtyReason::DeclaredFeaturesChanged {
old: old.declared_features.clone(),
new: self.declared_features.clone(),
};
}
if self.target != old.target {
return DirtyReason::TargetConfigurationChanged;
}
if self.path != old.path {
return DirtyReason::PathToSourceChanged;
}
if self.profile != old.profile {
return DirtyReason::ProfileConfigurationChanged;
}
if self.rustflags != old.rustflags {
return DirtyReason::RustflagsChanged {
old: old.rustflags.clone(),
new: self.rustflags.clone(),
};
}
if self.config != old.config {
return DirtyReason::ConfigSettingsChanged;
}
if self.compile_kind != old.compile_kind {
return DirtyReason::CompileKindChanged;
}
let my_local = self.local.lock().unwrap();
let old_local = old.local.lock().unwrap();
if my_local.len() != old_local.len() {
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 {
return DirtyReason::PrecalculatedComponentsChanged {
old: b.to_string(),
new: a.to_string(),
};
}
}
(
LocalFingerprint::CheckDepInfo {
dep_info: a_dep,
checksum: checksum_a,
},
LocalFingerprint::CheckDepInfo {
dep_info: b_dep,
checksum: checksum_b,
},
) => {
if a_dep != b_dep {
return DirtyReason::DepInfoOutputChanged {
old: b_dep.clone(),
new: a_dep.clone(),
};
}
if checksum_a != checksum_b {
return DirtyReason::ChecksumUseChanged { old: *checksum_b };
}
}
(
LocalFingerprint::RerunIfChanged {
output: a_out,
paths: a_paths,
},
LocalFingerprint::RerunIfChanged {
output: b_out,
paths: b_paths,
},
) => {
if a_out != b_out {
return DirtyReason::RerunIfChangedOutputFileChanged {
old: b_out.clone(),
new: a_out.clone(),
};
}
if a_paths != b_paths {
return DirtyReason::RerunIfChangedOutputPathsChanged {
old: b_paths.clone(),
new: a_paths.clone(),
};
}
}
(
LocalFingerprint::RerunIfEnvChanged {
var: a_key,
val: a_value,
},
LocalFingerprint::RerunIfEnvChanged {
var: b_key,
val: b_value,
},
) => {
if *a_key != *b_key {
return DirtyReason::EnvVarsChanged {
old: b_key.clone(),
new: a_key.clone(),
};
}
if *a_value != *b_value {
return DirtyReason::EnvVarChanged {
name: a_key.clone(),
old_value: b_value.clone(),
new_value: a_value.clone(),
};
}
}
(a, b) => {
return DirtyReason::LocalFingerprintTypeChanged {
old: b.kind().to_owned(),
new: a.kind().to_owned(),
};
}
}
}
if self.deps.len() != old.deps.len() {
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 {
return DirtyReason::UnitDependencyNameChanged {
old: b.name,
new: a.name,
};
}
if a.fingerprint.hash_u64() != b.fingerprint.hash_u64() {
return DirtyReason::UnitDependencyInfoChanged {
unit: a.fingerprint.index,
};
}
}
if !self.fs_status.up_to_date() {
return DirtyReason::FsStatusOutdated(self.fs_status.clone());
}
DirtyReason::NothingObvious
}
fn check_filesystem(
&mut self,
mtime_cache: &mut HashMap<PathBuf, FileTime>,
checksum_cache: &mut HashMap<PathBuf, Checksum>,
pkg: &Package,
build_root: &Path,
cargo_exe: &Path,
gctx: &GlobalContext,
) -> CargoResult<()> {
assert!(!self.fs_status.up_to_date());
let pkg_root = pkg.root();
let mut mtimes = HashMap::default();
for output in self.outputs.iter() {
let Ok(mtime) = paths::mtime(output) else {
let item = StaleItem::FailedToReadMetadata {
path: output.clone(),
};
self.fs_status = FsStatus::StaleItem(item);
return Ok(());
};
assert!(mtimes.insert(output.clone(), mtime).is_none());
}
let opt_max = mtimes.iter().max_by_key(|kv| kv.1);
let Some((max_path, max_mtime)) = opt_max else {
self.fs_status = FsStatus::UpToDate { mtimes };
return Ok(());
};
debug!(
"max output mtime for {:?} is {:?} {}",
pkg_root, max_path, max_mtime
);
for dep in self.deps.iter() {
let dep_mtimes = match &dep.fingerprint.fs_status {
FsStatus::UpToDate { mtimes } => mtimes,
FsStatus::Stale
| FsStatus::StaleItem(_)
| FsStatus::StaleDependency { .. }
| FsStatus::StaleDepFingerprint { .. } => {
self.fs_status = FsStatus::StaleDepFingerprint {
unit: dep.fingerprint.index,
};
return Ok(());
}
};
let (dep_path, dep_mtime) = if dep.only_requires_rmeta {
dep_mtimes
.iter()
.find(|(path, _mtime)| {
path.extension().and_then(|s| s.to_str()) == Some("rmeta")
})
.expect("failed to find rmeta")
} else {
match dep_mtimes.iter().max_by_key(|kv| kv.1) {
Some(dep_mtime) => dep_mtime,
None => continue,
}
};
debug!(
"max dep mtime for {:?} is {:?} {}",
pkg_root, dep_path, dep_mtime
);
if dep_mtime > max_mtime {
info!(
"dependency on `{}` is newer than we are {} > {} {:?}",
dep.name, dep_mtime, max_mtime, pkg_root
);
self.fs_status = FsStatus::StaleDependency {
unit: dep.fingerprint.index,
dep_mtime: *dep_mtime,
max_mtime: *max_mtime,
};
return Ok(());
}
}
for local in self.local.get_mut().unwrap().iter() {
if let Some(item) = local.find_stale_item(
mtime_cache,
checksum_cache,
pkg,
build_root,
cargo_exe,
gctx,
)? {
item.log();
self.fs_status = FsStatus::StaleItem(item);
return Ok(());
}
}
self.fs_status = FsStatus::UpToDate { mtimes };
debug!("filesystem up-to-date {:?}", pkg_root);
Ok(())
}
}
impl hash::Hash for Fingerprint {
fn hash<H: Hasher>(&self, h: &mut H) {
let Fingerprint {
rustc,
ref features,
ref declared_features,
target,
path,
profile,
ref deps,
ref local,
config,
compile_kind,
ref rustflags,
..
} = *self;
let local = local.lock().unwrap();
(
rustc,
features,
declared_features,
target,
path,
profile,
&*local,
config,
compile_kind,
rustflags,
)
.hash(h);
h.write_usize(deps.len());
for DepFingerprint {
pkg_id,
name,
public,
fingerprint,
only_requires_rmeta: _, } in deps
{
pkg_id.hash(h);
name.hash(h);
public.hash(h);
h.write_u64(fingerprint.hash_u64());
}
}
}
impl DepFingerprint {
fn new(
build_runner: &mut BuildRunner<'_, '_>,
parent: &Unit,
dep: &UnitDep,
) -> CargoResult<DepFingerprint> {
let fingerprint = calculate(build_runner, &dep.unit)?;
let pkg_id = if dep.unit.pkg.package_id().source_id().is_path() {
util::hash_u64(dep.unit.pkg.package_id().name())
} else {
util::hash_u64(dep.unit.pkg.package_id())
};
Ok(DepFingerprint {
pkg_id,
name: dep.extern_crate_name,
public: dep.public,
fingerprint,
only_requires_rmeta: build_runner.only_requires_rmeta(parent, &dep.unit),
})
}
}
impl StaleItem {
fn log(&self) {
match self {
StaleItem::MissingFile { path } => {
info!("stale: missing {:?}", path);
}
StaleItem::UnableToReadFile { path } => {
info!("stale: unable to read {:?}", path);
}
StaleItem::FailedToReadMetadata { path } => {
info!("stale: couldn't read metadata {:?}", path);
}
StaleItem::ChangedFile {
reference,
reference_mtime,
stale,
stale_mtime,
} => {
info!("stale: changed {:?}", stale);
info!(" (vs) {:?}", reference);
info!(" {:?} < {:?}", reference_mtime, stale_mtime);
}
StaleItem::FileSizeChanged {
path,
new_size,
old_size,
} => {
info!("stale: changed {:?}", path);
info!("prior file size {old_size}");
info!(" new file size {new_size}");
}
StaleItem::ChangedChecksum {
source,
stored_checksum,
new_checksum,
} => {
info!("stale: changed {:?}", source);
info!("prior checksum {stored_checksum}");
info!(" new checksum {new_checksum}");
}
StaleItem::MissingChecksum { path } => {
info!("stale: no prior checksum {:?}", path);
}
StaleItem::ChangedEnv {
var,
previous,
current,
} => {
info!("stale: changed env {:?}", var);
info!(" {:?} != {:?}", previous, current);
}
}
}
}
fn calculate(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<Arc<Fingerprint>> {
if let Some(s) = build_runner.fingerprints.get(unit) {
return Ok(Arc::clone(s));
}
let mut fingerprint = if unit.mode.is_run_custom_build() {
calculate_run_custom_build(build_runner, unit)?
} else if unit.mode.is_doc_test() {
panic!("doc tests do not fingerprint");
} else {
calculate_normal(build_runner, unit)?
};
let build_root = build_root(build_runner);
let cargo_exe = build_runner.bcx.gctx.cargo_exe()?;
fingerprint.check_filesystem(
&mut build_runner.mtime_cache,
&mut build_runner.checksum_cache,
&unit.pkg,
&build_root,
cargo_exe,
build_runner.bcx.gctx,
)?;
let fingerprint = Arc::new(fingerprint);
build_runner
.fingerprints
.insert(unit.clone(), Arc::clone(&fingerprint));
Ok(fingerprint)
}
fn calculate_normal(
build_runner: &mut BuildRunner<'_, '_>,
unit: &Unit,
) -> CargoResult<Fingerprint> {
let deps = {
let deps = Vec::from(build_runner.unit_deps(unit));
let mut deps = deps
.into_iter()
.filter(|dep| !dep.unit.target.is_bin() || dep.unit.artifact.is_true())
.map(|dep| DepFingerprint::new(build_runner, unit, &dep))
.collect::<CargoResult<Vec<_>>>()?;
deps.sort_by(|a, b| a.pkg_id.cmp(&b.pkg_id));
deps
};
let build_root = build_root(build_runner);
let is_any_doc_gen = unit.mode.is_doc() || unit.mode.is_doc_scrape();
let rustdoc_depinfo_enabled = build_runner.bcx.gctx.cli_unstable().rustdoc_depinfo;
let local = if is_any_doc_gen && !rustdoc_depinfo_enabled {
let fingerprint = pkg_fingerprint(build_runner.bcx, &unit.pkg).with_context(|| {
format!(
"failed to determine package fingerprint for documenting {}",
unit.pkg
)
})?;
vec![LocalFingerprint::Precalculated(fingerprint)]
} else {
let dep_info = dep_info_loc(build_runner, unit);
let dep_info = dep_info.strip_prefix(&build_root).unwrap().to_path_buf();
vec![LocalFingerprint::CheckDepInfo {
dep_info,
checksum: build_runner.bcx.gctx.cli_unstable().checksum_freshness,
}]
};
let outputs = build_runner
.outputs(unit)?
.iter()
.filter(|output| !matches!(output.flavor, FileFlavor::DebugInfo | FileFlavor::Auxiliary))
.map(|output| output.path.clone())
.collect();
let extra_flags = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
&unit.rustdocflags
} else {
&unit.rustflags
}
.to_vec();
let profile_hash = util::hash_u64((
&unit.profile,
unit.mode,
build_runner.bcx.extra_args_for(unit),
build_runner.lto[unit],
unit.pkg.manifest().lint_rustflags(),
unit.profile
.trim_paths
.as_ref()
.filter(|trim_paths| !trim_paths.is_none())
.map(|_| {
build_runner
.bcx
.gctx
.get_env(super::trim_paths::WS_REMAP_ENV)
.ok()
.filter(|prefix| !prefix.is_empty())
}),
));
let mut config = StableHasher::new();
let linker = if unit.target.for_host() && !build_runner.bcx.gctx.target_applies_to_host()? {
build_runner.compilation.host_linker()
} else {
build_runner.compilation.target_linker(unit.kind)
};
if let Some(linker) = linker {
linker.hash(&mut config);
}
if unit.mode.is_doc() && build_runner.bcx.gctx.cli_unstable().rustdoc_map {
if let Ok(map) = build_runner.bcx.gctx.doc_extern_map() {
map.hash(&mut config);
}
}
if let Some(allow_features) = &build_runner.bcx.gctx.cli_unstable().allow_features {
allow_features.hash(&mut config);
}
(unit.target.is_lib()
&& build_runner.unit_deps(unit).iter().any(|dep| !dep.public)
&& super::is_public_dependency_enabled(build_runner, unit))
.hash(&mut config);
build_runner
.bcx
.gctx
.should_embed_metadata()
.not()
.hash(&mut config);
let compile_kind = unit.kind.fingerprint_hash();
let mut declared_features = unit.pkg.summary().features().keys().collect::<Vec<_>>();
declared_features.sort(); Ok(Fingerprint {
rustc: util::hash_u64(&build_runner.bcx.rustc().verbose_version),
target: util::hash_u64(&unit.target),
profile: profile_hash,
path: util::hash_u64(path_args(build_runner.bcx.ws, unit).0),
features: format!("{:?}", unit.features),
declared_features: format!("{declared_features:?}"),
deps,
local: Mutex::new(local),
memoized_hash: Mutex::new(None),
config: Hasher::finish(&config),
compile_kind,
index: build_runner.bcx.unit_to_index[unit],
rustflags: extra_flags,
fs_status: FsStatus::Stale,
outputs,
})
}
fn calculate_run_custom_build(
build_runner: &mut BuildRunner<'_, '_>,
unit: &Unit,
) -> CargoResult<Fingerprint> {
assert!(unit.mode.is_run_custom_build());
let (gen_local, overridden) = build_script_local_fingerprints(build_runner, unit)?;
let deps = &build_runner.build_explicit_deps[unit];
let local = (gen_local)(
deps,
Some(&|| {
const IO_ERR_MESSAGE: &str = "\
An I/O error happened. Please make sure you can access the file.
By default, if your project contains a build script, cargo scans all files in
it to determine whether a rebuild is needed. If you don't expect to access the
file, specify `rerun-if-changed` in your build script.
See https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed for more information.";
pkg_fingerprint(build_runner.bcx, &unit.pkg).map_err(|err| {
let mut message = format!("failed to determine package fingerprint for build script for {}", unit.pkg);
if err.root_cause().is::<io::Error>() {
message = format!("{}\n{}", message, IO_ERR_MESSAGE)
}
err.context(message)
})
}),
)?
.unwrap();
let output = deps.build_script_output.clone();
let deps = if overridden {
vec![]
} else {
let deps = Vec::from(build_runner.unit_deps(unit));
deps.into_iter()
.map(|dep| DepFingerprint::new(build_runner, unit, &dep))
.collect::<CargoResult<Vec<_>>>()?
};
let rustflags = unit.rustflags.to_vec();
Ok(Fingerprint {
local: Mutex::new(local),
rustc: util::hash_u64(&build_runner.bcx.rustc().verbose_version),
deps,
outputs: if overridden { Vec::new() } else { vec![output] },
rustflags,
index: build_runner.bcx.unit_to_index[unit],
..Fingerprint::new()
})
}
fn build_script_local_fingerprints(
build_runner: &mut BuildRunner<'_, '_>,
unit: &Unit,
) -> CargoResult<(
Box<
dyn FnOnce(
&BuildDeps,
Option<&dyn Fn() -> CargoResult<String>>,
) -> CargoResult<Option<Vec<LocalFingerprint>>>
+ Send,
>,
bool,
)> {
assert!(unit.mode.is_run_custom_build());
if let Some(fingerprint) = build_script_override_fingerprint(build_runner, unit) {
debug!("override local fingerprints deps {}", unit.pkg);
return Ok((
Box::new(
move |_: &BuildDeps, _: Option<&dyn Fn() -> CargoResult<String>>| {
Ok(Some(vec![fingerprint]))
},
),
true, ));
}
let pkg_root = unit.pkg.root().to_path_buf();
let build_dir = build_root(build_runner);
let env_config = Arc::clone(build_runner.bcx.gctx.env_config()?);
let calculate =
move |deps: &BuildDeps, pkg_fingerprint: Option<&dyn Fn() -> CargoResult<String>>| {
if deps.rerun_if_changed.is_empty() && deps.rerun_if_env_changed.is_empty() {
match pkg_fingerprint {
Some(f) => {
let s = f()?;
debug!(
"old local fingerprints deps {:?} precalculated={:?}",
pkg_root, s
);
return Ok(Some(vec![LocalFingerprint::Precalculated(s)]));
}
None => return Ok(None),
}
}
Ok(Some(local_fingerprints_deps(
deps,
&build_dir,
&pkg_root,
&env_config,
)))
};
Ok((Box::new(calculate), false))
}
fn build_script_override_fingerprint(
build_runner: &mut BuildRunner<'_, '_>,
unit: &Unit,
) -> Option<LocalFingerprint> {
let build_script_outputs = build_runner.build_script_outputs.lock().unwrap();
let metadata = build_runner.get_run_build_script_metadata(unit);
let output = build_script_outputs.get(metadata)?;
let s = format!(
"overridden build state with hash: {}",
util::hash_u64(output)
);
Some(LocalFingerprint::Precalculated(s))
}
fn local_fingerprints_deps(
deps: &BuildDeps,
build_root: &Path,
pkg_root: &Path,
env_config: &Arc<HashMap<String, OsString>>,
) -> Vec<LocalFingerprint> {
debug!("new local fingerprints deps {:?}", pkg_root);
let mut local = Vec::new();
if !deps.rerun_if_changed.is_empty() {
let output = deps
.build_script_output
.strip_prefix(build_root)
.unwrap()
.to_path_buf();
let paths = deps
.rerun_if_changed
.iter()
.map(|p| p.strip_prefix(pkg_root).unwrap_or(p).to_path_buf())
.collect();
local.push(LocalFingerprint::RerunIfChanged { output, paths });
}
local.extend(
deps.rerun_if_env_changed
.iter()
.map(|s| LocalFingerprint::from_env(s, env_config)),
);
local
}
fn write_fingerprint(loc: &Path, fingerprint: &Fingerprint) -> CargoResult<()> {
debug_assert_ne!(fingerprint.rustc, 0);
let hash = fingerprint.hash_u64();
debug!("write fingerprint ({:x}) : {}", hash, loc.display());
paths::write(loc, util::to_hex(hash).as_bytes())?;
let json = serde_json::to_string(fingerprint).unwrap();
if cfg!(debug_assertions) {
let f: Fingerprint = serde_json::from_str(&json).unwrap();
assert_eq!(f.hash_u64(), hash);
}
paths::write(&loc.with_extension("json"), json.as_bytes())?;
Ok(())
}
pub fn prepare_init(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> {
let new1 = build_runner.files().fingerprint_dir(unit);
if !new1.exists() && !unit.mode.is_doc_test() {
paths::create_dir_all(&new1)?;
}
Ok(())
}
pub fn dep_info_loc(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> PathBuf {
build_runner.files().fingerprint_file_path(unit, "dep-")
}
fn build_root(build_runner: &BuildRunner<'_, '_>) -> PathBuf {
build_runner.bcx.ws.build_dir().into_path_unlocked()
}
fn compare_old_fingerprint(
unit: &Unit,
old_hash_path: &Path,
new_fingerprint: &Fingerprint,
mtime_on_use: bool,
forced: bool,
) -> FingerprintComparison {
if mtime_on_use {
let t = FileTime::from_system_time(SystemTime::now());
debug!("mtime-on-use forcing {:?} to {}", old_hash_path, t);
paths::set_file_time_no_err(old_hash_path, t);
}
let compare = _compare_old_fingerprint(old_hash_path, new_fingerprint);
match compare.as_ref() {
Ok(FingerprintComparison::Fresh) => {}
Ok(FingerprintComparison::Dirty { 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:?}");
}
}
match compare {
Ok(FingerprintComparison::Fresh) if forced => FingerprintComparison::Dirty {
reason: DirtyReason::Forced,
},
Ok(cmp) => cmp,
Err(_) => FingerprintComparison::Dirty {
reason: DirtyReason::FreshBuild,
},
}
}
fn _compare_old_fingerprint(
old_hash_path: &Path,
new_fingerprint: &Fingerprint,
) -> CargoResult<FingerprintComparison> {
let old_fingerprint_short = paths::read(old_hash_path)?;
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(FingerprintComparison::Fresh);
}
let old_fingerprint_json = paths::read(&old_hash_path.with_extension("json"))?;
let old_fingerprint: Fingerprint = serde_json::from_str(&old_fingerprint_json)
.with_context(|| internal("failed to deserialize json"))?;
if !old_fingerprint_short.is_empty() {
debug_assert_eq!(
util::to_hex(old_fingerprint.hash_u64()),
old_fingerprint_short
);
}
let reason = new_fingerprint.compare(&old_fingerprint);
Ok(FingerprintComparison::Dirty { reason })
}
fn pkg_fingerprint(bcx: &BuildContext<'_, '_>, pkg: &Package) -> CargoResult<String> {
let source_id = pkg.package_id().source_id();
let sources = bcx.packages.sources();
let source = sources
.get(source_id)
.ok_or_else(|| internal("missing package source"))?;
source.fingerprint(pkg)
}
fn find_stale_file<I, P>(
mtime_cache: &mut HashMap<PathBuf, FileTime>,
checksum_cache: &mut HashMap<PathBuf, Checksum>,
reference: &Path,
paths: I,
use_checksums: bool,
) -> Option<StaleItem>
where
I: IntoIterator<Item = (P, Option<(u64, Checksum)>)>,
P: AsRef<Path>,
{
let reference_mtime = match paths::mtime(reference) {
Ok(mtime) => mtime,
Err(..) => {
return Some(StaleItem::MissingFile {
path: reference.to_path_buf(),
});
}
};
let skippable_dirs = if let Ok(cargo_home) = home::cargo_home() {
let skippable_dirs: Vec<_> = ["git", "registry"]
.into_iter()
.map(|subfolder| cargo_home.join(subfolder))
.collect();
Some(skippable_dirs)
} else {
None
};
for (path, prior_checksum) in paths {
let path = path.as_ref();
if let Some(ref skippable_dirs) = skippable_dirs {
if skippable_dirs.iter().any(|dir| path.starts_with(dir)) {
continue;
}
}
if use_checksums {
let Some((file_len, prior_checksum)) = prior_checksum else {
return Some(StaleItem::MissingChecksum {
path: path.to_path_buf(),
});
};
let path_buf = path.to_path_buf();
let path_checksum = match checksum_cache.entry(path_buf) {
Entry::Occupied(o) => *o.get(),
Entry::Vacant(v) => {
let Ok(current_file_len) = fs::metadata(&path).map(|m| m.len()) else {
return Some(StaleItem::FailedToReadMetadata {
path: path.to_path_buf(),
});
};
if current_file_len != file_len {
return Some(StaleItem::FileSizeChanged {
path: path.to_path_buf(),
new_size: current_file_len,
old_size: file_len,
});
}
let Ok(file) = File::open(path) else {
return Some(StaleItem::MissingFile {
path: path.to_path_buf(),
});
};
let Ok(checksum) = Checksum::compute(prior_checksum.algo(), file) else {
return Some(StaleItem::UnableToReadFile {
path: path.to_path_buf(),
});
};
*v.insert(checksum)
}
};
if path_checksum == prior_checksum {
continue;
}
return Some(StaleItem::ChangedChecksum {
source: path.to_path_buf(),
stored_checksum: prior_checksum,
new_checksum: path_checksum,
});
} else {
let path_mtime = match mtime_cache.entry(path.to_path_buf()) {
Entry::Occupied(o) => *o.get(),
Entry::Vacant(v) => {
let Ok(mtime) = paths::mtime_recursive(path) else {
return Some(StaleItem::MissingFile {
path: path.to_path_buf(),
});
};
*v.insert(mtime)
}
};
if path_mtime <= reference_mtime {
continue;
}
return Some(StaleItem::ChangedFile {
reference: reference.to_path_buf(),
reference_mtime,
stale: path.to_path_buf(),
stale_mtime: path_mtime,
});
}
}
debug!(
"all paths up-to-date relative to {:?} mtime={}",
reference, reference_mtime
);
None
}