From 802c607079eb85d8e968e2f7fd3b270cb1ba6f74 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 17:43:17 +0000 Subject: [PATCH 1/6] ci: cross-compile Windows tests on Linux and shard runs via nextest archives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows CI jobs previously spent most of their wall-clock compiling the workspace on slow windows-latest runners, six times over. Instead: - A new build-windows-tests job cross-compiles all test binaries for x86_64-pc-windows-msvc on a Linux runner with cargo-xwin (clang-cl + lld-link against the xwin-fetched MSVC CRT/Windows SDK) and packs them into a portable `cargo nextest archive`, uploaded as an artifact. - Six test-windows shards download the archive and only run tests via `cargo nextest run --archive-file --workspace-remap`, partitioned with nextest's native `--partition count:N/6` — no Rust toolchain, dev drive, or compilation on the Windows runners. - The e2e harness's VT_SHARD_INDEX/VT_SHARD_TOTAL self-sharding is removed; nextest partitioning replaces its only user (Windows CI). - fspy's oxlint test now resolves CARGO_MANIFEST_DIR at run time instead of `env!`, since compile-time paths bake in the build machine's checkout and break when the binary runs on another machine. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf --- .github/workflows/ci.yml | 162 +++++++++++------- crates/fspy/tests/oxlint.rs | 7 +- .../vite_task_bin/tests/e2e_snapshots/main.rs | 37 ---- 3 files changed, 102 insertions(+), 104 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd6cc8624..ad0654c15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,70 +78,16 @@ jobs: cargo_cmd: cargo-zigbuild build_target: x86_64-unknown-linux-gnu.2.17 shard: linux-gnu - scope: '' - run_env: '' - os: namespace-profile-mac-default target: aarch64-apple-darwin cargo_cmd: cargo build_target: aarch64-apple-darwin shard: macos-arm64 - scope: '' - run_env: '' - os: namespace-profile-mac-default target: x86_64-apple-darwin cargo_cmd: cargo build_target: x86_64-apple-darwin shard: macos-x64 - scope: '' - run_env: '' - # Windows e2e fixtures dominate wall-clock (60s per PTY step vs 20s on - # Unix). Coverage is partitioned by crate: the e2e shards run - # `-p vite_task_bin` and the non-e2e shard runs - # `--workspace --exclude vite_task_bin`; the union is the workspace - # by construction. The e2e_snapshots harness self-shards via - # VT_SHARD_INDEX/VT_SHARD_TOTAL across the 5 e2e jobs. - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-e2e-1 - scope: '-p vite_task_bin' - run_env: 'VT_SHARD_INDEX=1 VT_SHARD_TOTAL=5' - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-e2e-2 - scope: '-p vite_task_bin' - run_env: 'VT_SHARD_INDEX=2 VT_SHARD_TOTAL=5' - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-e2e-3 - scope: '-p vite_task_bin' - run_env: 'VT_SHARD_INDEX=3 VT_SHARD_TOTAL=5' - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-e2e-4 - scope: '-p vite_task_bin' - run_env: 'VT_SHARD_INDEX=4 VT_SHARD_TOTAL=5' - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-e2e-5 - scope: '-p vite_task_bin' - run_env: 'VT_SHARD_INDEX=5 VT_SHARD_TOTAL=5' - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc - shard: windows-non-e2e - scope: '--workspace --exclude vite_task_bin' - run_env: '' runs-on: ${{ matrix.os }} steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 @@ -149,15 +95,6 @@ jobs: - name: Update submodules run: git submodule update --init --recursive - - name: Setup Dev Drive - uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0 - if: runner.os == 'Windows' - with: - drive-size: 10GB - env-mapping: | - CARGO_HOME,{{ DEV_DRIVE }}/.cargo - RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup - - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 with: save-cache: ${{ github.ref_name == 'main' }} @@ -177,13 +114,13 @@ jobs: if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - name: Build tests - run: ${{ matrix.cargo_cmd }} test --no-run ${{ matrix.scope }} --target ${{ matrix.build_target }} + run: ${{ matrix.cargo_cmd }} test --no-run --target ${{ matrix.build_target }} # Default `cargo test` runs only tests that need nothing beyond the # Rust toolchain; this step verifies that contract before Node.js # and pnpm enter the picture. - name: Run tests - run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} + run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} # x86_64-apple-darwin runs on arm64 runner under Rosetta; install x64 Node # so fspy's x86_64 preload dylib can be injected into spawned node procs. @@ -192,7 +129,98 @@ jobs: architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }} - name: Run ignored tests - run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} -- --ignored + run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored + + # Windows tests are cross-compiled on a fast Linux runner with cargo-xwin + # (clang-cl + lld-link against the xwin-downloaded MSVC CRT/Windows SDK) + # and packed into a portable nextest archive. The test-windows shards then + # only download the archive and run it — no Rust toolchain or compilation + # on the slow Windows runners. Windows e2e fixtures dominate wall-clock + # (60s per PTY step vs 20s on Unix), so the run is split across shards via + # nextest's count partitioning, which spreads tests round-robin. + build-windows-tests: + needs: detect-changes + if: needs.detect-changes.outputs.code-changed == 'true' + name: Build Windows tests + runs-on: namespace-profile-linux-x64-default + env: + XWIN_ACCEPT_LICENSE: '1' + # The MSVC STL from xwin's VS17 manifest static-asserts a minimum Clang + # version newer than what runner images ship. Microsoft's documented + # escape hatch lets Detours (the only C++ in the workspace, and far older + # than any STL/clang concern here) compile with the system clang-cl. + # cargo-xwin folds a pre-set CXXFLAGS into the env it generates. + CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - name: Update submodules + run: git submodule update --init --recursive + + - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 + with: + save-cache: ${{ github.ref_name == 'main' }} + cache-key: windows-cross + + - run: rustup target add x86_64-pc-windows-msvc + + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10 + with: + tool: cargo-nextest,cargo-xwin + + - name: Build test archive + run: | + # cargo-xwin resolves the rustflags configured in .cargo/config.toml, + # appends its -Lnative Windows SDK paths, and re-exports the result as + # CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS, so the plain cargo + # that nextest invokes cross-compiles exactly like `cargo xwin` would. + eval "$(cargo xwin env --target x86_64-pc-windows-msvc | grep '^export ')" + # `cargo xwin env` renders its intended *removal* of RUSTFLAGS as + # `export RUSTFLAGS="";` — actually remove it so it cannot shadow the + # target-specific rustflags set above. + unset RUSTFLAGS + cargo nextest archive --workspace --target x86_64-pc-windows-msvc --archive-file windows-tests.tar.zst + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-test-archive + path: windows-tests.tar.zst + retention-days: 7 + + test-windows: + needs: build-windows-tests + name: Test (windows-${{ matrix.partition }}) + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3, 4, 5, 6] + runs-on: windows-latest + env: + PARTITION: count:${{ matrix.partition }}/6 + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-test-archive + + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10 + with: + tool: cargo-nextest + + # The default (non-ignored) test set needs nothing beyond the test + # binaries themselves; this step verifies that contract before Node.js + # and pnpm enter the picture. `cargo-nextest` is invoked directly so the + # job never depends on the runner's Rust toolchain. + - name: Run tests + run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --partition "$PARTITION" + + - uses: oxc-project/setup-node@ab97f03642370d79a7e96dd286bd02a1be40e0ba # v1.3.0 + + # --no-tests=pass: a shard's partition of the (much smaller) ignored + # test set may legitimately come up empty. + - name: Run ignored tests + run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --partition "$PARTITION" --run-ignored ignored-only --no-tests pass test-musl: needs: detect-changes @@ -287,6 +315,8 @@ jobs: - clippy - test - test-musl + - build-windows-tests + - test-windows - fmt steps: - run: exit 1 diff --git a/crates/fspy/tests/oxlint.rs b/crates/fspy/tests/oxlint.rs index f2f9c7025..cda78b2dd 100644 --- a/crates/fspy/tests/oxlint.rs +++ b/crates/fspy/tests/oxlint.rs @@ -7,7 +7,12 @@ use test_log::test; /// Get the packages/tools/.bin directory path fn tools_bin_dir() -> std::path::PathBuf { - std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + // Resolve CARGO_MANIFEST_DIR at run time, not via `env!`: a compile-time + // path bakes in the build machine's checkout, which breaks when the test + // binary is cross-compiled and run on another machine (e.g. built on + // Linux with cargo-xwin, run on Windows from a nextest archive). + let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + manifest_dir .parent() .unwrap() .parent() diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index 2de549308..45adbada2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -547,28 +547,6 @@ fn run_case( Ok(()) } -/// Parses `VT_SHARD_INDEX` (1..=total) and `VT_SHARD_TOTAL` env vars for CI -/// sharding. Both unset means "run all trials". Both set selects one shard via -/// round-robin. Exactly one set is a CI misconfiguration and panics. -fn parse_shard_env() -> Option<(usize, usize)> { - let index = std::env::var("VT_SHARD_INDEX").ok(); - let total = std::env::var("VT_SHARD_TOTAL").ok(); - match (index, total) { - (None, None) => None, - (Some(i), Some(t)) => { - let index: usize = i.parse().expect("VT_SHARD_INDEX must be a positive integer"); - let total: usize = t.parse().expect("VT_SHARD_TOTAL must be a positive integer"); - assert!(total > 0, "VT_SHARD_TOTAL must be > 0"); - assert!( - (1..=total).contains(&index), - "VT_SHARD_INDEX must be in 1..={total}, got {index}" - ); - Some((index, total)) - } - _ => panic!("VT_SHARD_INDEX and VT_SHARD_TOTAL must both be set or both unset"), - } -} - #[expect(clippy::disallowed_types, reason = "Path required for CARGO_MANIFEST_DIR path traversal")] fn main() { let tmp_dir = tempfile::tempdir().unwrap(); @@ -598,8 +576,6 @@ fn main() { args.test_threads = Some(1); } - let shard = parse_shard_env(); - let tests: Vec = fixture_paths .into_iter() .flat_map(|fixture_path| { @@ -650,18 +626,5 @@ fn main() { }) .collect(); - let tests = match shard { - Some((index, total)) => { - let count = tests.len(); - tests - .into_iter() - .enumerate() - .filter(|(i, _)| i * total / count + 1 == index) - .map(|(_, t)| t) - .collect() - } - None => tests, - }; - libtest_mimic::run(&args, tests).exit(); } From 2d2b5af2de437f03ec33a9b65cd59647d4be6754 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 17:57:08 +0000 Subject: [PATCH 2/6] ci: install llvm-tools so cargo-xwin can symlink llvm-lib on bare runners The namespace runner image has no system LLVM; cc-rs failed to find llvm-lib when archiving Detours. With the rustup llvm-tools component installed, cargo-xwin symlinks llvm-lib/llvm-dlltool from the toolchain's llvm-ar (and lld-link from rust-lld), verified locally with system LLVM tools hidden. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad0654c15..0e8de92aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,10 +157,15 @@ jobs: - name: Update submodules run: git submodule update --init --recursive + # llvm-tools provides llvm-ar in the toolchain's rustlib bin dir; + # cargo-xwin symlinks the MSVC-style llvm-lib/llvm-dlltool (used by cc-rs + # to archive Detours) and lld-link from there when the runner image has + # no system LLVM. - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 with: save-cache: ${{ github.ref_name == 'main' }} cache-key: windows-cross + components: llvm-tools - run: rustup target add x86_64-pc-windows-msvc From 9fde793ddf72e281186a4e2adef33601d5028539 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:11:09 +0000 Subject: [PATCH 3/6] ci: check out submodules on Windows test shards fspy_detours_sys's bindings test runs bindgen at test time against the Detours submodule headers, so run-only shards still need the submodule. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e8de92aa..ceb8a0683 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,6 +205,12 @@ jobs: steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + # The Detours submodule is needed at run time: fspy_detours_sys's + # bindings test re-generates bindgen bindings against the checked-out + # headers. + - name: Update submodules + run: git submodule update --init --recursive + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: windows-test-archive From 04fb148645399b90810a0d1365d9be27a2a4cd1e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:30:01 +0000 Subject: [PATCH 4/6] ci: cache the xwin-fetched MSVC CRT/Windows SDK Downloading and unpacking the SDK took 9m40s of the build job's 12m41s; the cross-compile itself took 45s. The unpacked SDK is immutable for a given manifest version, so cache it keyed on the xwin major version. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb8a0683..877048451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,16 @@ jobs: with: tool: cargo-nextest,cargo-xwin + # Downloading and unpacking the MSVC CRT/Windows SDK dominates this + # job's wall time (~10 minutes); the unpacked result is immutable for a + # given manifest version, so cache it. Bump the key when bumping the + # xwin version. + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: xwin-cache + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-msvc-17 + - name: Build test archive run: | # cargo-xwin resolves the rustflags configured in .cargo/config.toml, @@ -192,6 +202,12 @@ jobs: path: windows-tests.tar.zst retention-days: 7 + - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + if: steps.xwin-cache.outputs.cache-hit != 'true' + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-msvc-17 + test-windows: needs: build-windows-tests name: Test (windows-${{ matrix.partition }}) From ff81aae82cd51a4a038bdf983afdd10d2903755c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 18:45:16 +0000 Subject: [PATCH 5/6] ci: measure warm xwin-SDK-cache build time Empty commit to re-run CI now that the previous run saved the cargo-xwin SDK cache. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf From c2456f03fa91b57c24e816ab546b3afab8dac4c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 02:00:34 +0000 Subject: [PATCH 6/6] ci: drop Windows test shards from 6 to 4 With compilation moved off the Windows runners, total test execution is only ~2.5 minutes while per-shard fixed overhead (checkout, Node setup) runs 60-110s. Measured across the 6 warm shards, the slowest job was an overhead-heavy one, not the test-heavy one, so 4 shards keeps the same wall-clock within runner variance at two-thirds of the Windows runner minutes. https://claude.ai/code/session_012vwVCFFfjepzPQGKWLPycf --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877048451..af4a597c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,9 +135,10 @@ jobs: # (clang-cl + lld-link against the xwin-downloaded MSVC CRT/Windows SDK) # and packed into a portable nextest archive. The test-windows shards then # only download the archive and run it — no Rust toolchain or compilation - # on the slow Windows runners. Windows e2e fixtures dominate wall-clock - # (60s per PTY step vs 20s on Unix), so the run is split across shards via - # nextest's count partitioning, which spreads tests round-robin. + # on the slow Windows runners. The run is split across shards via nextest's + # count partitioning; with compilation gone, per-shard fixed overhead + # (checkout, Node setup) rivals test execution time, so four shards is the + # measured sweet spot of wall-clock vs runner minutes. build-windows-tests: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' @@ -214,10 +215,10 @@ jobs: strategy: fail-fast: false matrix: - partition: [1, 2, 3, 4, 5, 6] + partition: [1, 2, 3, 4] runs-on: windows-latest env: - PARTITION: count:${{ matrix.partition }}/6 + PARTITION: count:${{ matrix.partition }}/4 steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2