Skip to content

Commit cd396f3

Browse files
committed
Fix freshness when linking is interrupted.
1 parent cc53eca commit cd396f3

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

src/cargo/core/compiler/fingerprint.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,24 @@ pub fn prepare_target<'a, 'cfg>(
376376
return Ok(Job::new(Work::noop(), Fresh));
377377
}
378378

379+
// Clear out the old fingerprint file if it exists. This protects when
380+
// compilation is interrupted leaving a corrupt file. For example, a
381+
// project with a lib.rs and integration test:
382+
//
383+
// 1. Build the integration test.
384+
// 2. Make a change to lib.rs.
385+
// 3. Build the integration test, hit Ctrl-C while linking (with gcc).
386+
// 4. Build the integration test again.
387+
//
388+
// Without this line, then step 4 will think the integration test is
389+
// "fresh" because the mtime of the output file is newer than all of its
390+
// dependencies. But the executable is corrupt and needs to be rebuilt.
391+
// Clearing the fingerprint ensures that Cargo never mistakes it as
392+
// up-to-date until after a successful build.
393+
if loc.exists() {
394+
paths::write(&loc, b"")?;
395+
}
396+
379397
let write_fingerprint = if unit.mode.is_run_custom_build() {
380398
// For build scripts the `local` field of the fingerprint may change
381399
// while we're executing it. For example it could be in the legacy
@@ -1501,7 +1519,10 @@ fn compare_old_fingerprint(
15011519
let old_fingerprint_json = paths::read(&loc.with_extension("json"))?;
15021520
let old_fingerprint: Fingerprint = serde_json::from_str(&old_fingerprint_json)
15031521
.chain_err(|| internal("failed to deserialize json"))?;
1504-
debug_assert_eq!(util::to_hex(old_fingerprint.hash()), old_fingerprint_short);
1522+
// Fingerprint can be empty after a failed rebuild (see comment in prepare_target).
1523+
if !old_fingerprint_short.is_empty() {
1524+
debug_assert_eq!(util::to_hex(old_fingerprint.hash()), old_fingerprint_short);
1525+
}
15051526
let result = new_fingerprint.compare(&old_fingerprint);
15061527
assert!(result.is_err());
15071528
result

tests/testsuite/freshness.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::io;
66
use std::io::prelude::*;
77
use std::net::TcpListener;
88
use std::path::{Path, PathBuf};
9+
use std::process::Stdio;
910
use std::thread;
1011
use std::time::SystemTime;
1112

@@ -2323,3 +2324,87 @@ LLVM version: 9.0
23232324
assert_eq!(check("beta1", true), beta1_name);
23242325
assert_eq!(check("nightly1", true), nightly1_name);
23252326
}
2327+
2328+
#[cargo_test]
2329+
fn linking_interrupted() {
2330+
// Interrupt during the linking phase shouldn't leave test executable as "fresh".
2331+
2332+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2333+
let addr = listener.local_addr().unwrap();
2334+
2335+
// Create a linker that we can interrupt.
2336+
let linker = project()
2337+
.at("linker")
2338+
.file("Cargo.toml", &basic_manifest("linker", "1.0.0"))
2339+
.file(
2340+
"src/main.rs",
2341+
&r#"
2342+
use std::io::Read;
2343+
2344+
fn main() {
2345+
// Figure out the output filename.
2346+
let output = match std::env::args().find(|a| a.starts_with("/OUT:")) {
2347+
Some(s) => s[5..].to_string(),
2348+
None => {
2349+
let mut args = std::env::args();
2350+
loop {
2351+
if args.next().unwrap() == "-o" {
2352+
break;
2353+
}
2354+
}
2355+
args.next().unwrap()
2356+
}
2357+
};
2358+
std::fs::remove_file(&output).unwrap();
2359+
std::fs::write(&output, "").unwrap();
2360+
// Tell the test that we are ready to be interrupted.
2361+
let mut socket = std::net::TcpStream::connect("__ADDR__").unwrap();
2362+
// Wait for the test to tell us to exit.
2363+
let _ = socket.read(&mut [0; 1]);
2364+
}
2365+
"#
2366+
.replace("__ADDR__", &addr.to_string()),
2367+
)
2368+
.build();
2369+
linker.cargo("build").run();
2370+
2371+
// Build it once so that the fingerprint gets saved to disk.
2372+
let p = project()
2373+
.file("src/lib.rs", "")
2374+
.file("tests/t1.rs", "")
2375+
.build();
2376+
p.cargo("test --test t1 --no-run").run();
2377+
// Make a change, start a build, then interrupt it.
2378+
p.change_file("src/lib.rs", "// modified");
2379+
let linker_env = format!(
2380+
"CARGO_TARGET_{}_LINKER",
2381+
rustc_host().to_uppercase().replace('-', "_")
2382+
);
2383+
let mut cmd = p
2384+
.cargo("test --test t1 --no-run")
2385+
.env(&linker_env, linker.bin("linker"))
2386+
.build_command();
2387+
let mut child = cmd
2388+
.stdout(Stdio::null())
2389+
.stderr(Stdio::null())
2390+
.spawn()
2391+
.unwrap();
2392+
// Wait for linking to start.
2393+
let mut conn = listener.accept().unwrap().0;
2394+
2395+
// Interrupt the child.
2396+
child.kill().unwrap();
2397+
// Note: rustc and the linker are still running, let them exit here.
2398+
conn.write(b"X").unwrap();
2399+
2400+
// Build again, shouldn't be fresh.
2401+
p.cargo("test --test t1")
2402+
.with_stderr(
2403+
"\
2404+
[COMPILING] foo [..]
2405+
[FINISHED] [..]
2406+
[RUNNING] target/debug/deps/t1[..]
2407+
",
2408+
)
2409+
.run();
2410+
}

0 commit comments

Comments
 (0)