fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation#1776
Merged
stephentoub merged 2 commits intoJun 23, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a cold-cache CI issue in the Rust SDK where build.rs self-invalidates: the Copilot CLI binary is created during the build, ends up newer than Cargo’s build-script reference timestamp, and forces an unnecessary rerun/recompile on the next identical cargo invocation.
Changes:
- Backdate the freshly extracted staged Copilot CLI binary’s mtime to
SystemTime::UNIX_EPOCH(best-effort), so it cannot appear “newer” than Cargo’s build-script reference file on subsequent builds. - Emit a
cargo:warning(and continue) if the filesystem refuses setting the timestamp, preserving the pre-fix behavior rather than breaking builds.
Show a summary per file
| File | Description |
|---|---|
rust/build.rs |
After extracting the CLI to a staging file, sets its modified time to the Unix epoch before the atomic rename, preventing Cargo rerun-if-changed self-invalidation on cold caches. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 0
Contributor
Author
|
@SteveSandersonMS I don't have the permission to merge here, would you mind doing that for me? |
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>
4c00731 to
33e1890
Compare
Contributor
Author
|
(the last push is a force rebase Copilot decided to do 😅) |
stephentoub
reviewed
Jun 23, 2026
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>
stephentoub
approved these changes
Jun 23, 2026
edburns
pushed a commit
that referenced
this pull request
Jun 23, 2026
…ation (#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>
anishi1222
added a commit
to anishi1222/copilot-sdk
that referenced
this pull request
Jun 24, 2026
* HTTP request callback support (github#1689) * fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation (github#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> * Address review feedback on HTTP request callback support (+ cross-SDK parity) (github#1775) * Strong-name sign .NET SDK (github#1778) Use the canonical .NET Open.snk key to strong-name sign the GitHub.Copilot.SDK assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Co-authored-by: Paolo Tranquilli <redsun82@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
anishi1222
added a commit
to anishi1222/copilot-sdk
that referenced
this pull request
Jun 24, 2026
* HTTP request callback support (github#1689) * fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation (github#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> * Address review feedback on HTTP request callback support (+ cross-SDK parity) (github#1775) * Strong-name sign .NET SDK (github#1778) Use the canonical .NET Open.snk key to strong-name sign the GitHub.Copilot.SDK assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Co-authored-by: Paolo Tranquilli <redsun82@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When the
bundled-clicargo feature is off,rust/build.rsextracts the Copilot CLI binary into a cache dir and emitscargo:rerun-if-changed=<extracted binary path>on it. That watch is deliberate: it's how a deleted cached binary forces a re-extract, so cargo doesn't replay a stalehas_extracted_clicfg and leave runtime resolution failing withBinaryNotFound(seesrc/resolve.rs::extracted_cli_path).The trap: on a cold cache the same
build.rscreates that binary mid-build, seconds after a network download + extract. Cargo stamps the build script'soutputreference file when the script is spawned — before the binary is written. So the freshly-extracted binary's mtime ends up newer than its ownrerun-if-changedreference. The next identicalcargoinvocation sees the watched file as "changed", reruns build.rs, recompiles the SDK crate, and relinks every downstream crate. A warm cache never reproduces (the file already exists, not created that build) — so it's a cold-cache / CI-only redundant rebuild.Cargo's own fingerprint log proves it:
In a downstream consumer's CI this wasted ~9 minutes (a second cargo invocation in the same job recompiled the SDK + a Tauri backend with unchanged sources).
Fix
In
extract_to_cache, after writing + chmod'ing the staged binary but before the atomic rename into place, backdate the staged file's mtime to the Unix epoch viastd::fs::File::set_modified(SystemTime::UNIX_EPOCH). The same-fs sibling rename preserves the mtime, so the file lands already-backdated and unambiguously older than any real build reference — a second identical invocation becomes a true no-op. Best-effort: on error we emit acargo:warningand continue (worst case = today's redundant-rebuild behaviour, never a broken build).Why this is correct for all SDK consumers
bundled-clion): unaffected — that path emits norerun-if-changedon any build-created file, so it has no self-invalidation. No change needed there.COPILOT_CLI_EXTRACT_DIRset vs default cache dir: both flow through the sameextract_to_cache, so one fix covers both.target/reuse: unaffected/helped — the cache binary isn't a compiler input, and an epoch mtime is deterministic across machines.COPILOT_CLI_EXTRACT_DIRwould clamp to 1980 — still older than any real reference, still benign.cli-version.txtwatch and version-keyed cache path are untouched.Verification
Reproduced the consumer scenario (
cli-version.txtpresent,--no-default-features) with a fresh extract dir:1970-01-01 (epoch 0).Finished in 0.05s, noCompiling github-copilot-sdk, nostale/dirtyin the fingerprint log — a true no-op.cargo +nightly fmt --check,cargo clippy -D warnings(the repo'sjust lint-rust), and all non-e2e test targets (173 tests) pass. The e2e suite is unrunnable in this environment (requiresnpm installinnodejs/for the CLI) — orthogonal to this change.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com