Skip to content

Commit 5a6b692

Browse files
committed
Auto merge of #6720 - ehuss:build-script-fingerprint, r=alexcrichton
Include build script execution in the fingerprint. This adds information about the execution of a build script to the fingerprint. Previously, no information was included, and cargo relied on dirty propagation in `JobQueue` to trigger recompiles. However, if two separate targets are built via separate commands (such as `cargo build` then `cargo test`), the second command did not know that the build script was updated, and thus was incorrectly treated as "fresh". This works by including the timestamp of the last time the build script was ran in the fingerprint. For overridden build scripts, it includes the replaced output. Fixes #4979
2 parents cb0f763 + 035913f commit 5a6b692

File tree

6 files changed

+170
-38
lines changed

6 files changed

+170
-38
lines changed

src/cargo/core/compiler/context/compilation_files.rs

+13-4
Original file line numberDiff line numberDiff line change
@@ -186,20 +186,29 @@ impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> {
186186
self.layout(unit.kind).fingerprint().join(dir)
187187
}
188188

189-
/// Returns the appropriate directory layout for either a plugin or not.
189+
/// Returns the directory where a compiled build script is stored.
190+
/// `/path/to/target/{debug,release}/build/PKG-HASH`
190191
pub fn build_script_dir(&self, unit: &Unit<'a>) -> PathBuf {
191192
assert!(unit.target.is_custom_build());
192193
assert!(!unit.mode.is_run_custom_build());
193194
let dir = self.pkg_dir(unit);
194195
self.layout(Kind::Host).build().join(dir)
195196
}
196197

197-
/// Returns the appropriate directory layout for either a plugin or not.
198-
pub fn build_script_out_dir(&self, unit: &Unit<'a>) -> PathBuf {
198+
/// Returns the directory where information about running a build script
199+
/// is stored.
200+
/// `/path/to/target/{debug,release}/build/PKG-HASH`
201+
pub fn build_script_run_dir(&self, unit: &Unit<'a>) -> PathBuf {
199202
assert!(unit.target.is_custom_build());
200203
assert!(unit.mode.is_run_custom_build());
201204
let dir = self.pkg_dir(unit);
202-
self.layout(unit.kind).build().join(dir).join("out")
205+
self.layout(unit.kind).build().join(dir)
206+
}
207+
208+
/// Returns the "OUT_DIR" directory for running a build script.
209+
/// `/path/to/target/{debug,release}/build/PKG-HASH/out`
210+
pub fn build_script_out_dir(&self, unit: &Unit<'a>) -> PathBuf {
211+
self.build_script_run_dir(unit).join("out")
203212
}
204213

205214
/// Returns the file stem for a given target/profile combo (with metadata).

src/cargo/core/compiler/custom_build.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>) -> CargoRes
132132
.expect("running a script not depending on an actual script");
133133
let script_dir = cx.files().build_script_dir(build_script_unit);
134134
let script_out_dir = cx.files().build_script_out_dir(unit);
135+
let script_run_dir = cx.files().build_script_run_dir(unit);
135136
let build_plan = bcx.build_config.build_plan;
136137
let invocation_name = unit.buildkey();
137138

@@ -241,13 +242,9 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>) -> CargoRes
241242
let pkg_name = unit.pkg.to_string();
242243
let build_state = Arc::clone(&cx.build_state);
243244
let id = unit.pkg.package_id();
244-
let (output_file, err_file, root_output_file) = {
245-
let build_output_parent = script_out_dir.parent().unwrap();
246-
let output_file = build_output_parent.join("output");
247-
let err_file = build_output_parent.join("stderr");
248-
let root_output_file = build_output_parent.join("root-output");
249-
(output_file, err_file, root_output_file)
250-
};
245+
let output_file = script_run_dir.join("output");
246+
let err_file = script_run_dir.join("stderr");
247+
let root_output_file = script_run_dir.join("root-output");
251248
let host_target_root = cx.files().target_root().to_path_buf();
252249
let all = (
253250
id,
@@ -332,7 +329,7 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>) -> CargoRes
332329
state.build_plan(invocation_name, cmd.clone(), Arc::new(Vec::new()));
333330
} else {
334331
state.running(&cmd);
335-
let timestamp = paths::get_current_filesystem_time(&output_file)?;
332+
let timestamp = paths::set_invocation_time(&script_run_dir)?;
336333
let output = if extra_verbose {
337334
let prefix = format!("[{} {}] ", id.name(), id.version());
338335
state.capture_output(&cmd, Some(prefix), true)

src/cargo/core/compiler/fingerprint.rs

+57-21
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,12 @@ fn calculate<'a, 'cfg>(
455455

456456
// Next, recursively calculate the fingerprint for all of our dependencies.
457457
//
458-
// Skip the fingerprints of build scripts as they may not always be
459-
// available and the dirtiness propagation for modification is tracked
460-
// elsewhere. Also skip fingerprints of binaries because they don't actually
461-
// induce a recompile, they're just dependencies in the sense that they need
462-
// to be built.
463-
let deps = cx.dep_targets(unit);
464-
let deps = deps
458+
// Skip the fingerprints of build scripts, they are included below in the
459+
// `local` vec. Also skip fingerprints of binaries because they don't
460+
// actually induce a recompile, they're just dependencies in the sense
461+
// that they need to be built.
462+
let mut deps = cx
463+
.dep_targets(unit)
465464
.iter()
466465
.filter(|u| !u.target.is_custom_build() && !u.target.is_bin())
467466
.map(|dep| {
@@ -475,8 +474,9 @@ fn calculate<'a, 'cfg>(
475474
})
476475
})
477476
.collect::<CargoResult<Vec<_>>>()?;
477+
deps.sort_by(|a, b| a.pkg_id.cmp(&b.pkg_id));
478478

479-
// And finally, calculate what our own local fingerprint is
479+
// And finally, calculate what our own local fingerprint is.
480480
let local = if use_dep_info(unit) {
481481
let dep_info = dep_info_loc(cx, unit);
482482
let mtime = dep_info_mtime_if_fresh(unit.pkg, &dep_info)?;
@@ -485,8 +485,32 @@ fn calculate<'a, 'cfg>(
485485
let fingerprint = pkg_fingerprint(&cx.bcx, unit.pkg)?;
486486
LocalFingerprint::Precalculated(fingerprint)
487487
};
488-
let mut deps = deps;
489-
deps.sort_by(|a, b| a.pkg_id.cmp(&b.pkg_id));
488+
let mut local = vec![local];
489+
// Include fingerprint for any build scripts this unit requires.
490+
local.extend(
491+
cx.dep_targets(unit)
492+
.iter()
493+
.filter(|u| u.mode.is_run_custom_build())
494+
.map(|dep| {
495+
// If the build script is overridden, use the override info as
496+
// the override. Otherwise, use the last invocation time of
497+
// the build script. If the build script re-runs during this
498+
// run, dirty propagation within the JobQueue will ensure that
499+
// this gets invalidated. This is only here to catch the
500+
// situation when cargo is run a second time for another
501+
// target that wasn't built previously (such as `cargo build`
502+
// then `cargo test`).
503+
build_script_override_fingerprint(cx, unit).unwrap_or_else(|| {
504+
let ts_path = cx
505+
.files()
506+
.build_script_run_dir(dep)
507+
.join("invoked.timestamp");
508+
let ts_path_mtime = paths::mtime(&ts_path).ok();
509+
LocalFingerprint::mtime(cx.files().target_root(), ts_path_mtime, &ts_path)
510+
})
511+
}),
512+
);
513+
490514
let extra_flags = if unit.mode.is_doc() {
491515
bcx.rustdocflags_args(unit)?
492516
} else {
@@ -502,7 +526,7 @@ fn calculate<'a, 'cfg>(
502526
path: util::hash_u64(&super::path_args(&cx.bcx, unit).0),
503527
features: format!("{:?}", bcx.resolve.features_sorted(unit.pkg.package_id())),
504528
deps,
505-
local: vec![local],
529+
local,
506530
memoized_hash: Mutex::new(None),
507531
edition: unit.target.edition(),
508532
rustflags: extra_flags,
@@ -595,19 +619,13 @@ fn build_script_local_fingerprints<'a, 'cfg>(
595619
cx: &mut Context<'a, 'cfg>,
596620
unit: &Unit<'a>,
597621
) -> CargoResult<(Vec<LocalFingerprint>, Option<PathBuf>)> {
598-
let state = cx.build_state.outputs.lock().unwrap();
599622
// First up, if this build script is entirely overridden, then we just
600623
// return the hash of what we overrode it with.
601-
//
602-
// Note that the `None` here means that we don't want to update the local
603-
// fingerprint afterwards because this is all just overridden.
604-
if let Some(output) = state.get(&(unit.pkg.package_id(), unit.kind)) {
624+
if let Some(fingerprint) = build_script_override_fingerprint(cx, unit) {
605625
debug!("override local fingerprints deps");
606-
let s = format!(
607-
"overridden build state with hash: {}",
608-
util::hash_u64(output)
609-
);
610-
return Ok((vec![LocalFingerprint::Precalculated(s)], None));
626+
// Note that the `None` here means that we don't want to update the local
627+
// fingerprint afterwards because this is all just overridden.
628+
return Ok((vec![fingerprint], None));
611629
}
612630

613631
// Next up we look at the previously listed dependencies for the build
@@ -633,6 +651,24 @@ fn build_script_local_fingerprints<'a, 'cfg>(
633651
))
634652
}
635653

654+
/// Create a local fingerprint for an overridden build script.
655+
/// Returns None if it is not overridden.
656+
fn build_script_override_fingerprint<'a, 'cfg>(
657+
cx: &mut Context<'a, 'cfg>,
658+
unit: &Unit<'a>,
659+
) -> Option<LocalFingerprint> {
660+
let state = cx.build_state.outputs.lock().unwrap();
661+
state
662+
.get(&(unit.pkg.package_id(), unit.kind))
663+
.map(|output| {
664+
let s = format!(
665+
"overridden build state with hash: {}",
666+
util::hash_u64(output)
667+
);
668+
LocalFingerprint::Precalculated(s)
669+
})
670+
}
671+
636672
fn local_fingerprints_deps(
637673
deps: &BuildDeps,
638674
target_root: &Path,

src/cargo/core/compiler/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ fn rustc<'a, 'cfg>(
239239
.get_cwd()
240240
.unwrap_or_else(|| cx.bcx.config.cwd())
241241
.to_path_buf();
242+
let fingerprint_dir = cx.files().fingerprint_dir(unit);
242243

243244
return Ok(Work::new(move |state| {
244245
// Only at runtime have we discovered what the extra -L and -l
@@ -289,7 +290,7 @@ fn rustc<'a, 'cfg>(
289290
}
290291

291292
state.running(&rustc);
292-
let timestamp = paths::get_current_filesystem_time(&dep_info_loc)?;
293+
let timestamp = paths::set_invocation_time(&fingerprint_dir)?;
293294
if json_messages {
294295
exec.exec_json(
295296
rustc,

src/cargo/util/paths.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,17 @@ pub fn mtime(path: &Path) -> CargoResult<FileTime> {
187187
Ok(FileTime::from_last_modification_time(&meta))
188188
}
189189

190-
/// get `FileTime::from_system_time(SystemTime::now());` using the exact clock that this file system is using.
191-
pub fn get_current_filesystem_time(path: &Path) -> CargoResult<FileTime> {
190+
/// Record the current time on the filesystem (using the filesystem's clock)
191+
/// using a file at the given directory. Returns the current time.
192+
pub fn set_invocation_time(path: &Path) -> CargoResult<FileTime> {
192193
// note that if `FileTime::from_system_time(SystemTime::now());` is determined to be sufficient,
193194
// then this can be removed.
194-
let timestamp = path.with_file_name("invoked.timestamp");
195+
let timestamp = path.join("invoked.timestamp");
195196
write(
196197
&timestamp,
197198
b"This file has an mtime of when this was started.",
198199
)?;
199-
Ok(mtime(&timestamp)?)
200+
mtime(&timestamp)
200201
}
201202

202203
#[cfg(unix)]

tests/testsuite/freshness.rs

+88
Original file line numberDiff line numberDiff line change
@@ -1616,3 +1616,91 @@ fn rebuild_on_mid_build_file_modification() {
16161616

16171617
t.join().ok().unwrap();
16181618
}
1619+
1620+
#[test]
1621+
fn dirty_both_lib_and_test() {
1622+
// This tests that all artifacts that depend on the results of a build
1623+
// script will get rebuilt when the build script reruns, even for separate
1624+
// commands. It does the following:
1625+
//
1626+
// 1. Project "foo" has a build script which will compile a small
1627+
// staticlib to link against. Normally this would use the `cc` crate,
1628+
// but here we just use rustc to avoid the `cc` dependency.
1629+
// 2. Build the library.
1630+
// 3. Build the unit test. The staticlib intentionally has a bad value.
1631+
// 4. Rewrite the staticlib with the correct value.
1632+
// 5. Build the library again.
1633+
// 6. Build the unit test. This should recompile.
1634+
1635+
let slib = |n| {
1636+
format!(
1637+
r#"
1638+
#[no_mangle]
1639+
pub extern "C" fn doit() -> i32 {{
1640+
return {};
1641+
}}
1642+
"#,
1643+
n
1644+
)
1645+
};
1646+
1647+
let p = project()
1648+
.file(
1649+
"src/lib.rs",
1650+
r#"
1651+
extern "C" {
1652+
fn doit() -> i32;
1653+
}
1654+
1655+
#[test]
1656+
fn t1() {
1657+
assert_eq!(unsafe { doit() }, 1, "doit assert failure");
1658+
}
1659+
"#,
1660+
)
1661+
.file(
1662+
"build.rs",
1663+
r#"
1664+
use std::env;
1665+
use std::path::PathBuf;
1666+
use std::process::Command;
1667+
1668+
fn main() {
1669+
let rustc = env::var_os("RUSTC").unwrap();
1670+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
1671+
assert!(
1672+
Command::new(rustc)
1673+
.args(&[
1674+
"--crate-type=staticlib",
1675+
"--out-dir",
1676+
out_dir.to_str().unwrap(),
1677+
"slib.rs"
1678+
])
1679+
.status()
1680+
.unwrap()
1681+
.success(),
1682+
"slib build failed"
1683+
);
1684+
println!("cargo:rustc-link-lib=slib");
1685+
println!("cargo:rustc-link-search={}", out_dir.display());
1686+
}
1687+
"#,
1688+
)
1689+
.file("slib.rs", &slib(2))
1690+
.build();
1691+
1692+
p.cargo("build").run();
1693+
1694+
// 2 != 1
1695+
p.cargo("test --lib")
1696+
.with_status(101)
1697+
.with_stdout_contains("[..]doit assert failure[..]")
1698+
.run();
1699+
1700+
// Fix the mistake.
1701+
p.change_file("slib.rs", &slib(1));
1702+
1703+
p.cargo("build").run();
1704+
// This should recompile with the new static lib, and the test should pass.
1705+
p.cargo("test --lib").run();
1706+
}

0 commit comments

Comments
 (0)