Skip to content

Commit cafa530

Browse files
redsun82Copilot
andauthored
fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation (#1776)
* fix(rust): backdate extracted CLI mtime to stop self-invalidation When `bundled-cli` is off, `build.rs` watches the extracted CLI binary via `cargo:rerun-if-changed` so a deleted cache binary forces a re-extract. On a cold cache the same build script *creates* that binary mid-build, after the download — so its mtime ends up newer than cargo's build-script `output` reference (stamped when the script was spawned). The next identical `cargo` invocation then sees the watched file as "changed", reruns build.rs, recompiles the SDK crate, and relinks every downstream crate (observed ~9 min of wasted CI on a second cargo invocation in the same job). Backdate the staged binary's mtime to the Unix epoch before the atomic rename (rename preserves mtime), so it lands unambiguously older than any real build reference and a no-change rebuild stays a true no-op. Best-effort: on error we emit a `cargo:warning` and continue, reverting to the prior redundant-rebuild behaviour rather than breaking the build. The deleted-file recovery contract is untouched — a missing file still can't be stat'd, so cargo still reruns. Embed mode (`bundled-cli` on) is unaffected: it emits no `rerun-if-changed` on any build-created file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(rust): write staging binary through a single file handle Address review: open the staging file once and perform the write, permission-set, and mtime-backdate on one handle instead of reopening it for each step. Write and chmod failures stay fatal; the epoch backdate stays best-effort (warn and continue). Behavior is otherwise unchanged: the extracted binary still lands epoch-dated so a no-change rebuild is a true no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0667a46 commit cafa530

1 file changed

Lines changed: 48 additions & 15 deletions

File tree

rust/build.rs

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::io::Read;
1+
use std::io::{Read, Write};
22
use std::path::{Path, PathBuf};
33
use std::time::Duration;
44

@@ -359,22 +359,55 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P
359359
std::process::id(),
360360
));
361361

362-
if let Err(e) = std::fs::write(&staging_path, &bytes) {
363-
let _ = std::fs::remove_file(&staging_path);
364-
panic!(
365-
"failed to write staging file {}: {e}",
366-
staging_path.display()
367-
);
368-
}
369-
370-
#[cfg(unix)]
371362
{
372-
use std::os::unix::fs::PermissionsExt;
373-
if let Err(e) =
374-
std::fs::set_permissions(&staging_path, std::fs::Permissions::from_mode(0o755))
375-
{
363+
let mut f = std::fs::File::create(&staging_path).unwrap_or_else(|e| {
376364
let _ = std::fs::remove_file(&staging_path);
377-
panic!("failed to chmod {}: {e}", staging_path.display());
365+
panic!(
366+
"failed to create staging file {}: {e}",
367+
staging_path.display()
368+
);
369+
});
370+
371+
if let Err(e) = f.write_all(&bytes) {
372+
let _ = std::fs::remove_file(&staging_path);
373+
panic!(
374+
"failed to write staging file {}: {e}",
375+
staging_path.display()
376+
);
377+
}
378+
379+
#[cfg(unix)]
380+
{
381+
use std::os::unix::fs::PermissionsExt;
382+
if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) {
383+
let _ = std::fs::remove_file(&staging_path);
384+
panic!("failed to chmod {}: {e}", staging_path.display());
385+
}
386+
}
387+
388+
// Backdate the staged binary to the Unix epoch before it lands. We emit
389+
// `cargo:rerun-if-changed` on `final_path` (see caller) so a *deleted*
390+
// cache binary forces a re-extract — but cargo stamps the build-script
391+
// `output` reference when the script is spawned, seconds before this
392+
// freshly-downloaded binary is written. A current mtime would therefore
393+
// be *newer* than that reference, so the next identical `cargo`
394+
// invocation would see the watched file as "changed" and pointlessly
395+
// rerun build.rs + recompile the crate + relink every downstream crate.
396+
// Pinning to the epoch keeps the file unambiguously older than any real
397+
// build reference; `rename` preserves mtime (same inode), so it lands
398+
// already-backdated and a no-change rebuild stays a true no-op. The
399+
// deleted-file recovery contract is untouched: a missing file can't be
400+
// stat'd, so cargo still treats it as stale and reruns regardless.
401+
//
402+
// Best-effort: a filesystem that refuses the epoch (e.g. FAT's 1980 floor
403+
// clamps it — still older than any real reference) or rejects the call
404+
// just reverts to the pre-fix redundant-rebuild behaviour, never a broken
405+
// build.
406+
if let Err(e) = f.set_modified(std::time::SystemTime::UNIX_EPOCH) {
407+
println!(
408+
"cargo:warning=Could not backdate {} (a redundant rebuild may occur): {e}",
409+
staging_path.display()
410+
);
378411
}
379412
}
380413

0 commit comments

Comments
 (0)