Rust SDK: bundle Copilot CLI by default#1385
Merged
SteveSandersonMS merged 5 commits intoMay 22, 2026
Merged
Conversation
Flips the embedded-CLI experience to on-by-default with an opt-out
cargo feature, matching the .NET SDK's NuGet bundling behavior.
Build-time
----------
* `bundled-cli` cargo feature replaces `embedded-cli`. It is now in the
default feature set, so `cargo add github-copilot-sdk` plus default
Cargo.toml settings ships a self-contained CLI binary inside the
consumer's compiled artifact. Consumers opt out via
`default-features = false` (the closest Cargo-native analog of .NET's
`CopilotSkipCliDownload=true`).
* build.rs no longer requires `COPILOT_CLI_VERSION` at consumer build
time. It resolves the version + per-platform SHA-256 hashes from one
of three sources, in order:
1. `COPILOT_CLI_VERSION` env-var override (advanced; fetches live
SHA256SUMS.txt from GitHub Releases).
2. A `bundled_cli_version.txt` snapshot file at the crate root. The
file is gitignored locally and ships only in published crate
tarballs; the rust-crate publish workflow regenerates it on every
publish from `nodejs/package-lock.json` + the matching release's
SHA256SUMS.txt.
3. Sibling `../nodejs/package-lock.json` plus live SHA256SUMS.txt
(the mono-repo contributor path; identical to today's behavior).
* `rust/scripts/snapshot-bundled-cli-version.sh` performs the snapshot.
The `publish-rust` job in `.github/workflows/publish.yml` invokes it
between "Set version" and "Package (dry run)", with a follow-up
verification step that fails the publish if the file was somehow not
generated.
* `Cargo.toml` migrates from `exclude = [...]` to `include = [...]` so
the gitignored snapshot file is included by `cargo publish`. Cargo's
include/exclude are mutually exclusive; today's exclude entries
(RELEASING.md, rustfmt configs, etc.) are not matched by the new
include patterns and therefore continue to be filtered out of the
published tarball.
Runtime
-------
* `embeddedcli::path()` continues to lazy-extract to
`~/.cache/github-copilot-sdk-{version}/copilot[.exe]` and SHA-verify
on first use. No change for the common case.
* New `ClientOptions::with_bundled_cli_extract_dir(PathBuf)` builder +
matching public field let app developers redirect the bundled-CLI
extraction (e.g. to a CI-scratch directory) without changing the
global cache layout. Implemented via a `pub(crate)` install_at helper
on `embeddedcli`; `Client::start` threads the override through the
resolve step.
* When the `bundled-cli` feature is disabled, `embeddedcli::path()` and
`bundled_version()` return `None`, the runtime sha2/zstd deps are not
in the crate graph at all, and resolve falls back to PATH-based
binary lookup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bundled-cli feature is now the default-on cargo feature. The Rust SDK Tests rust-bundled-cli matrix job simplifies to just cargo build (default features). COPILOT_CLI_VERSION env var is no longer required because build.rs auto-reads the sibling nodejs/package-lock.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the Rust SDK to bundle the Copilot CLI by default, aligning the Rust crate UX with the .NET SDK’s “it just works” bundling experience. It introduces a publish-time snapshot mechanism to pin the CLI version/hashes without requiring consumers to set build-time environment variables, and adds an option to override the runtime extraction directory.
Changes:
- Switch Cargo features to
bundled-cli(enabled by default) and gate runtime deps (sha2,zstd) behind it. - Add publish-time CLI version/hash snapshotting (
bundled_cli_version.txt) and updatebuild.rsto resolve version/hash from env override, snapshot file, or monorepo lockfile. - Add
ClientOptions::bundled_cli_extract_dirto control where the bundled CLI is extracted at runtime.
Show a summary per file
| File | Description |
|---|---|
| rust/src/resolve.rs | Threads an optional extraction directory into bundled-CLI resolution. |
| rust/src/lib.rs | Exposes bundled_cli_extract_dir on ClientOptions and plumbs it into Client::start. |
| rust/src/embeddedcli.rs | Splits installation to support installing into a caller-specified directory. |
| rust/scripts/snapshot-bundled-cli-version.sh | New publish-time script to snapshot CLI version + SHA-256 hashes into bundled_cli_version.txt. |
| rust/README.md | Documents default-on bundling, opt-out via default-features = false, and extraction override. |
| rust/Cargo.toml | Switches to include = [...] packaging and introduces default bundled-cli feature. |
| rust/build.rs | Resolves bundled CLI version/hash from env/snapshot/lockfile; bundles by default when feature enabled. |
| rust/.gitignore | Ignores the generated bundled_cli_version.txt locally. |
| .github/workflows/rust-sdk-tests.yml | Updates CI commentary and bundling smoke-test steps; adjusts archive caching. |
| .github/workflows/publish.yml | Adds snapshot + verification steps before Rust packaging/publishing. |
Copilot's findings
- Files reviewed: 10/10 changed files
- Comments generated: 5
Build-time * �uild.rs no longer extracts the binary or re-compresses it with zstd. Instead it embeds the raw .tar.gz / .zip from GitHub Releases. The Downloading message is the only build-time output (no Caching log on cache hit). Build.rs still SHA-256 verifies the download and probes the archive layout to confirm the binary entry exists - if upstream changes layout, the build fails loudly rather than shipping a broken bundle. * Removed sha2 + zstd runtime deps. The bytes are part of the consumer's signed binary by the time we extract; further hashing adds no value. ar + late2 (Linux/macOS) and zip (Windows) are now target-conditional optional runtime deps gated on �undled-cli. Build-deps stay unconditional so cross-compilation works. Runtime * embeddedcli::install() now archive-extracts at first call rather than zstd-decompressing. Per-version install dir means we can skip extraction when the target file already exists. About 1.9s on Windows for a fresh extract. Resolution surface * Dropped PATH scanning + all the homebrew/nvm/fnm/volta/nodenv walks (~280 lines removed from resolve.rs). * Kept the .NET/TypeScript-equivalent resolution order: CliProgram::Path > COPILOT_CLI_PATH > bundled CLI > error. No silent fallback. * Demoted esolve + embeddedcli modules and their contents to pub(crate). They were public introspection helpers with no C#/.NET equivalent. Client::start is now the only consumer. * dirs runtime dep is now gated on �undled-cli (only used by the default install-dir resolution). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Reword doc references from \~/.cache/...\ (Linux-specific) to the platform cache dir returned by dirs::cache_dir() (Windows: %LOCALAPPDATA%, macOS: ~/Library/Caches/, Linux: \ or ~/.cache). Hits embeddedcli.rs, lib.rs ClientOptions doc, and README. * Restore CLI version in the bundle-cache key in rust-sdk-tests.yml so old archives drop out when the pinned version bumps (was OS-only, would have accumulated indefinitely). * Main \cargo test\ job now runs with \--no-default-features --features test-support\ so it doesn't pull the 130MB CLI archive on every test run (the dedicated bundle job already exercises that pipeline; tests use COPILOT_CLI_PATH from setup-copilot). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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.
Flips the Rust SDK's embedded-CLI experience to on by default, matching the .NET SDK's NuGet bundling behaviour. Consumers who just want a working binary now get one with no configuration; consumers who want a smaller binary opt out via
default-features = false.The companion goal was to remove the developer ceremony around the existing design (which required setting
COPILOT_CLI_VERSIONat consumer build time). After this PR, the CLI version and per-platform SHA-256 hashes are snapshotted fromnodejs/package-lock.jsonautomatically at SDK publish time, mirroring how the .NET SDK's_GenerateVersionProps BeforeTargets="Pack"writesGitHub.Copilot.SDK.propsbefore NuGet packing.What changed
Cargo features:
embedded-cli(opt-in) tobundled-cli(opt-out). It is now in the default feature set. Consumers opt out viadefault-features = false(the closest Cargo-native analog of .NET'sCopilotSkipCliDownload=true).tar+flate2on Linux/macOS,zipon Windows.sha2+zstdfrom runtime deps. The build-time SHA-256 verification still happens (against the snapshotted hash). Once embedded, the bytes are part of the consumer's signed binary and trusted.Build-time pipeline:
build.rsnow embeds the raw.tar.gz/.zipfrom GitHub Releases viainclude_bytes!()and no longer extracts or re-compresses with zstd. The recompress was the dominant build cost (~30s per build); skipping it makes warm rebuilds significantly faster.build.rsresolves the version and hash from one of three sources, in order:COPILOT_CLI_VERSIONenv var (advanced override, fetches liveSHA256SUMS.txt).bundled_cli_version.txtsnapshot file (published-crate consumer).../nodejs/package-lock.json+ liveSHA256SUMS.txt(mono-repo contributor build).build.rsprobes the downloaded archive to confirmcopilot[.exe]is present at the expected path. If the upstream layout ever changes, the build fails loudly rather than shipping a broken bundle.cargo:warning=Downloading <url>on cache miss; silent on cache hit. Cargo's only mechanism for warning-level build script output iscargo:warning=; there is noinfochannel.Version pinning (no developer ceremony):
rust/scripts/snapshot-bundled-cli-version.shreadsnodejs/package-lock.jsonand the matching release'sSHA256SUMS.txt, then writesrust/bundled_cli_version.txt.publish-rustjob in.github/workflows/publish.ymlinvokes the script between "Set version" and "Package (dry run)", with a verification step that fails the publish if the file was not generated.include/excludeare mutually exclusive, soCargo.tomlmigrates fromexcludetoincludeso the gitignored file ships in published tarballs. The previously-excluded files (RELEASING.md, the rustfmt configs, etc.) are not matched by the newincludepatterns and continue to be filtered out.Resolution surface (reduced):
resolve.rs).CliProgram::Path>COPILOT_CLI_PATH> bundled CLI >Error::BinaryNotFound. No silent PATH fallback.resolveandembeddedclimodules topub(crate). They were public introspection helpers with no C# / TypeScript equivalent.Client::startis now the only consumer.embeddedcli::bundled_version()public helper.App-developer override for the runtime extraction location:
ClientOptions::bundled_cli_extract_dir: Option<PathBuf>field plus a matchingwith_bundled_cli_extract_dir(PathBuf)builder.embeddedcli::install()was split so an internalpub(crate) install_at(dir)helper can target an arbitrary directory;Client::startthreads the override through.dirs::cache_dir()(%LOCALAPPDATA%on Windows,~/Library/Caches/on macOS,$XDG_CACHE_HOMEor~/.cache/on Linux), undergithub-copilot-sdk-{version}/.README rewrite: new "Embedded CLI" section explains the default-on bundling, the
default-features = falseopt-out, the publish-time snapshot mechanism, andwith_bundled_cli_extract_dir. Features table updated.What did NOT change
dirs::cache_dir()).copilot-{platform}.{tar.gz,zip}).Validation
End-to-end test against a real Copilot CLI: built a fresh consumer crate in a temp directory that pulled the SDK via a path dependency, ran
cargo build(visibleDownloading https://.../copilot-win32-x64.zipwarning), then ran the resulting binary which extracted the CLI in ~1.9s, created a session, sent a message, and got the expected reply. Confirms the full pipeline works default-on with no consumer configuration.Other validation:
cargo check --features test-support(bundled path) - greencargo check --no-default-features(opt-out path) - greencargo check --tests --features test-support- greencargo clippy --tests --features test-support -- -D warnings- greencargo fmt --check- greencargo fmt --checkagainst the nightly.rustfmt.nightly.toml- greencargo package --list --allow-dirtyconfirmsbundled_cli_version.txtis included when presentReviewer attention
Cargo.toml'sincludelist: please confirm no source patterns are missing relative to today's default plus the previously-excluded files we wanted to keep out.publish.yml: the snapshot + verify steps are bash-only and assumenode+curlare available on theubuntu-latestrunner (they are).rust-sdk-tests.yml: the maincargo testjob now runs with--no-default-features --features test-supportso it does not pull the 130 MB CLI archive every run. The dedicated cross-platformbundlejob still exercises the bundling pipeline.