Skip to content

fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation#1776

Merged
stephentoub merged 2 commits into
github:mainfrom
redsun82:redsun82-fix-buildrs-self-invalidation
Jun 23, 2026
Merged

fix(rust): backdate extracted CLI mtime to stop build.rs self-invalidation#1776
stephentoub merged 2 commits into
github:mainfrom
redsun82:redsun82-fix-buildrs-self-invalidation

Conversation

@redsun82

Copy link
Copy Markdown
Contributor

Problem

When the bundled-cli cargo feature is off, rust/build.rs extracts the Copilot CLI binary into a cache dir and emits cargo: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 stale has_extracted_cli cfg and leave runtime resolution failing with BinaryNotFound (see src/resolve.rs::extracted_cli_path).

The trap: on a cold cache the same build.rs creates that binary mid-build, seconds after a network download + extract. Cargo stamps the build script's output reference file when the script is spawned — before the binary is written. So the freshly-extracted binary's mtime ends up newer than its own rerun-if-changed reference. The next identical cargo invocation 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:

stale: changed '<cache>/copilot'
  (vs)  '<target>/build/<crate>-<hash>/output'
FileTime{...942} < FileTime{...950}   # output(reference) < binary

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 via std::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 a cargo:warning and continue (worst case = today's redundant-rebuild behaviour, never a broken build).

Why this is correct for all SDK consumers

  • Embed mode (bundled-cli on): unaffected — that path emits no rerun-if-changed on any build-created file, so it has no self-invalidation. No change needed there.
  • COPILOT_CLI_EXTRACT_DIR set vs default cache dir: both flow through the same extract_to_cache, so one fix covers both.
  • sccache / cross-machine target/ reuse: unaffected/helped — the cache binary isn't a compiler input, and an epoch mtime is deterministic across machines.
  • Concurrent/parallel builds: safe — each build backdates its own PID+nanos staging file before the atomic rename; last rename wins and the final file is epoch-dated regardless.
  • Windows/NTFS (epoch 1601) & FAT (floor 1980): 1970 is fine on NTFS/APFS/ext4; the platform cache dir is never FAT. A FAT COPILOT_CLI_EXTRACT_DIR would clamp to 1980 — still older than any real reference, still benign.
  • Deleted-file recovery + version-bump contract: preserved — a missing file still can't be stat'd, so cargo still treats it as stale and reruns; the cli-version.txt watch and version-keyed cache path are untouched.

Verification

Reproduced the consumer scenario (cli-version.txt present, --no-default-features) with a fresh extract dir:

  • Build 1 (cold): extracts → binary mtime is 1970-01-01 (epoch 0).
  • Build 2 (identical): Finished in 0.05s, no Compiling github-copilot-sdk, no stale/dirty in the fingerprint log — a true no-op.

cargo +nightly fmt --check, cargo clippy -D warnings (the repo's just lint-rust), and all non-e2e test targets (173 tests) pass. The e2e suite is unrunnable in this environment (requires npm install in nodejs/ for the CLI) — orthogonal to this change.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings June 23, 2026 14:51
@redsun82 redsun82 requested a review from a team as a code owner June 23, 2026 14:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@SteveSandersonMS SteveSandersonMS left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@redsun82

Copy link
Copy Markdown
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>
@redsun82 redsun82 force-pushed the redsun82-fix-buildrs-self-invalidation branch from 4c00731 to 33e1890 Compare June 23, 2026 15:10
@redsun82

Copy link
Copy Markdown
Contributor Author

(the last push is a force rebase Copilot decided to do 😅)

Comment thread rust/build.rs Outdated
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>
@redsun82 redsun82 requested a review from stephentoub June 23, 2026 15:56
@stephentoub stephentoub added this pull request to the merge queue Jun 23, 2026
Merged via the queue into github:main with commit cafa530 Jun 23, 2026
15 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants