Skip to content

Commit 53edee8

Browse files
authored
ci: cross-compile Windows tests and CLI binaries on Linux with cargo-xwin (#1824)
## Summary Build the Windows test binaries and the CLI/binding once on a Linux runner with cargo-xwin, then just run them on Windows (download-only). Same approach as voidzero-dev/vite-task#443. ### Cache-miss comparison | Run | Wall time | | --- | ---: | | [`main` (cache miss)](https://github.com/voidzero-dev/vite-plus/actions/runs/27357067695) | 34m45s | | [this PR (Windows CLI binary-cache miss)](https://github.com/voidzero-dev/vite-plus/actions/runs/28646362896/attempts/1) | 17m09s | Net: **17m36s faster**, about **51% less wall time**. Both runs include the Socket Firewall Free job. The PR run missed the `windows-cli-xwin` binary cache after the reviewed cache-key/path refactors; the Windows SDK and package-manager caches were warm. Latest binary-cache-miss breakdown: - `build-windows-tests`: **2m45s**, followed by the download-only Windows test job in **3m05s**. - `build-windows-cli`: **7m46s**, followed by the slowest download-only Windows consumer tail in **6m52s**. - All Windows test, CLI E2E, snapshot, SFW, and ecosystem jobs passed using the Linux-built artifacts. ### Warm-cache comparison | Run | Wall time | | --- | ---: | | [Before: `main` (NAPI binding cache hit)](https://github.com/voidzero-dev/vite-plus/actions/runs/28568784291) | 11m57s | | [After: this PR (`windows-cli-xwin` cache hit)](https://github.com/voidzero-dev/vite-plus/actions/runs/28646362896/attempts/3) | 7m13s | Net: **4m44s faster**, about **40% less wall time**. These are full successful CI executions with the native caches already populated. In the PR run, `build-windows-cli` restored the exact binary cache, skipped native compilation, re-uploaded the artifact, and completed in **9s**. Why this is faster: - Linux is a much faster place to cross-compile the Windows binaries (cargo-xwin + clang-cl/lld-link against the cached MSVC CRT / Windows SDK), instead of every Windows job rebuilding the same ~24-minute release binding + CLI binaries. - Before, the run was gated by the single slowest Windows job (~30m, each doing its own full build); after, the native work is performed once on Linux and the Windows jobs only download and execute the resulting artifacts. - The Windows jobs (`Test`, `CLI E2E`, `CLI snap`, `sfw`) now download a prebuilt artifact and skip native compilation, so they need no Rust toolchain or dev drive. - `test`'s Windows leg runs from a nextest archive (`build-windows-tests`) with no toolchain at all. - Releases are unaffected and keep building natively on Windows (their cache keys differ via `RELEASE_BUILD`/`VERSION`). CI Windows binaries are clang-cl/lld-link-built; the e2e/snap suites still run them on real Windows. ```mermaid flowchart LR subgraph Before["Before: every Windows job rebuilds the workspace"] B1["Windows jobs x5"] --> B2["Compile binding + vp on Windows<br/>~24m each"] B2 --> B3["Install vp"] B3 --> B4["Run e2e / snap / sfw"] end subgraph After["After: build once on Linux, run on Windows"] A1["Linux: build-windows-cli"] --> A2["vp.exe + binding artifact"] A2 --> A3["CLI E2E / snap / sfw (Windows)<br/>download + run, no toolchain"] A4["Linux: build-windows-tests"] --> A5["nextest archive"] A5 --> A6["Test (Windows)<br/>run-only"] end ```
1 parent ff5ad75 commit 53edee8

7 files changed

Lines changed: 450 additions & 58 deletions

File tree

.github/actions/build-upstream/action.yml

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ inputs:
88
description: 'Print the output after the build'
99
required: false
1010
default: 'false'
11+
skip-native:
12+
description: >-
13+
Skip the NAPI binding cache and all native cargo/napi build steps; the
14+
native binaries must already be in place (e.g. downloaded from the
15+
windows-cli-binaries artifact produced by the build-windows-cli job).
16+
required: false
17+
default: 'false'
1118

1219
runs:
1320
using: 'composite'
@@ -18,14 +25,17 @@ runs:
1825
target: ${{ inputs.target }}
1926
upload: 'false'
2027

21-
# Compute cache key once before any builds modify files
22-
# (packages/cli/package.json is modified by syncTestPackageExports during build-ts)
23-
# Include env vars (RELEASE_BUILD, DEBUG, VERSION) to ensure cache miss on release builds
28+
# Compute the shared input hash before builds modify files
29+
# (packages/cli/package.json is modified by syncTestPackageExports during build-ts).
30+
- uses: ./.github/actions/compute-native-cache-input-hash
31+
id: native-cache-inputs
32+
33+
# Include env vars (RELEASE_BUILD, DEBUG, VERSION) to ensure cache miss on release builds.
2434
- name: Compute NAPI binding cache key
2535
id: cache-key
2636
shell: bash
2737
run: |
28-
echo "key=napi-binding-v3-${INPUTS_TARGET}-${RELEASE_BUILD}-${DEBUG}-${VERSION}-${NPM_TAG}-${{ hashFiles('packages/tools/.upstream-versions.json', 'rust-toolchain.toml', 'Cargo.lock', 'crates/**/*.rs', 'crates/*/Cargo.toml', 'packages/cli/binding/**/*.rs', 'packages/cli/binding/Cargo.toml', 'Cargo.toml', '.cargo/config.toml', 'packages/cli/package.json', 'packages/cli/build.ts', 'packages/cli/tsdown.config.ts') }}" >> $GITHUB_OUTPUT
38+
echo "key=napi-binding-v3-${INPUTS_TARGET}-${RELEASE_BUILD}-${DEBUG}-${VERSION}-${NPM_TAG}-${{ steps.native-cache-inputs.outputs.hash }}" >> "$GITHUB_OUTPUT"
2939
env:
3040
INPUTS_TARGET: ${{ inputs.target }}
3141

@@ -38,6 +48,7 @@ runs:
3848
# Cache NAPI bindings and Rust CLI binary (the slow parts, especially on Windows)
3949
- name: Restore NAPI binding cache
4050
id: cache-restore
51+
if: inputs.skip-native != 'true'
4152
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
4253
with:
4354
path: |
@@ -52,6 +63,15 @@ runs:
5263
${{ steps.rust-target.outputs.dir }}/${{ inputs.target }}/release/vp-setup.exe
5364
key: ${{ steps.cache-key.outputs.key }}
5465

66+
# Single source of truth for "build the native binaries in this job":
67+
# false when the caller supplied prebuilt ones (skip-native) or the cache
68+
# already has them. Every native build step (and the cache save) gates on
69+
# this one flag instead of repeating the two-part condition.
70+
- name: Decide native build
71+
id: native
72+
shell: bash
73+
run: echo "build=${{ inputs.skip-native != 'true' && steps.cache-restore.outputs.cache-hit != 'true' }}" >> "$GITHUB_OUTPUT"
74+
5575
# Apply Vite+ branding patches to vite source (CI checks out
5676
# upstream vite which doesn't have branding patches)
5777
- name: Brand vite
@@ -69,14 +89,14 @@ runs:
6989
7090
# Install zig + cargo-zigbuild for musl cross-compilation (napi-cross only supports gnu)
7191
- name: Add musl Rust target
72-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'musl')
92+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'musl')
7393
shell: bash
7494
run: rustup target add ${INPUTS_TARGET}
7595
env:
7696
INPUTS_TARGET: ${{ inputs.target }}
7797

7898
- name: Setup zig (musl)
79-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'musl')
99+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'musl')
80100
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
81101
with:
82102
version: 0.15.2
@@ -87,15 +107,15 @@ runs:
87107
# the pinned git commit. TODO: revert to taiki-e/install-action once
88108
# cargo-zigbuild > 0.22.3 is released.
89109
- name: Install cargo-zigbuild (musl)
90-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'musl')
110+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'musl')
91111
shell: bash
92112
run: cargo install --locked --git https://github.com/rust-cross/cargo-zigbuild --rev 7e791b4be71b9870e0abccedf7885486803cd923 cargo-zigbuild
93113

94114
# NAPI builds - only run on cache miss (slow, especially on Windows)
95115
# Must run before vite-plus TypeScript builds which depend on the bindings
96116
- name: Build NAPI bindings (Linux gnu)
97117
shell: bash
98-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'linux') && !contains(inputs.target, 'musl')
118+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'linux') && !contains(inputs.target, 'musl')
99119
run: |
100120
pnpm --filter=vite-plus build-native --target ${INPUTS_TARGET} --use-napi-cross
101121
env:
@@ -106,7 +126,7 @@ runs:
106126

107127
- name: Build NAPI bindings (Linux musl)
108128
shell: bash
109-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'musl')
129+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'musl')
110130
run: |
111131
pnpm --filter=vite-plus build-native --target ${INPUTS_TARGET} -x
112132
env:
@@ -117,15 +137,15 @@ runs:
117137

118138
- name: Build NAPI bindings (non-Linux targets)
119139
shell: bash
120-
if: steps.cache-restore.outputs.cache-hit != 'true' && !contains(inputs.target, 'linux')
140+
if: steps.native.outputs.build == 'true' && !contains(inputs.target, 'linux')
121141
run: |
122142
pnpm --filter=vite-plus build-native --target ${INPUTS_TARGET}
123143
env:
124144
DEBUG: napi:*
125145
INPUTS_TARGET: ${{ inputs.target }}
126146

127147
- name: Build Rust CLI binary (Linux gnu)
128-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'linux') && !contains(inputs.target, 'musl')
148+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'linux') && !contains(inputs.target, 'musl')
129149
shell: bash
130150
run: |
131151
pnpm exec napi build --use-napi-cross --target ${INPUTS_TARGET} --release -p vite_global_cli
@@ -136,7 +156,7 @@ runs:
136156
INPUTS_TARGET: ${{ inputs.target }}
137157

138158
- name: Build Rust CLI binary (Linux musl)
139-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'musl')
159+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'musl')
140160
shell: bash
141161
run: |
142162
pnpm exec napi build -x --target ${INPUTS_TARGET} --release -p vite_global_cli
@@ -147,28 +167,28 @@ runs:
147167
INPUTS_TARGET: ${{ inputs.target }}
148168

149169
- name: Build Rust CLI binary (non-Linux targets)
150-
if: steps.cache-restore.outputs.cache-hit != 'true' && !contains(inputs.target, 'linux')
170+
if: steps.native.outputs.build == 'true' && !contains(inputs.target, 'linux')
151171
shell: bash
152172
run: cargo build --release --target ${INPUTS_TARGET} -p vite_global_cli
153173
env:
154174
INPUTS_TARGET: ${{ inputs.target }}
155175

156176
- name: Build trampoline shim binary (Windows only)
157-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'windows')
177+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'windows')
158178
shell: bash
159179
run: cargo build --release --target ${INPUTS_TARGET} -p vite_trampoline
160180
env:
161181
INPUTS_TARGET: ${{ inputs.target }}
162182

163183
- name: Build installer binary (Windows only)
164-
if: steps.cache-restore.outputs.cache-hit != 'true' && contains(inputs.target, 'windows')
184+
if: steps.native.outputs.build == 'true' && contains(inputs.target, 'windows')
165185
shell: bash
166186
run: cargo build --release --target ${INPUTS_TARGET} -p vite_installer
167187
env:
168188
INPUTS_TARGET: ${{ inputs.target }}
169189

170190
- name: Save NAPI binding cache
171-
if: steps.cache-restore.outputs.cache-hit != 'true'
191+
if: steps.native.outputs.build == 'true'
172192
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
173193
with:
174194
path: |
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: 'Build Windows CLI binaries'
2+
description: >-
3+
Cross-compile the Windows release NAPI binding and vp/vp-shim/vp-setup
4+
binaries on Linux with cargo-xwin, cache them by source hash, and upload
5+
them as an artifact for Windows jobs to consume (with build-upstream's
6+
skip-native input). Run on a Linux runner after checkout + clone.
7+
8+
inputs:
9+
save-cache:
10+
description: "Whether setup-xwin's setup-rust should save its cache (usually only on main)."
11+
required: false
12+
default: 'false'
13+
artifact-name:
14+
description: 'Name for the uploaded binaries artifact.'
15+
required: false
16+
default: 'windows-cli-binaries'
17+
18+
runs:
19+
using: 'composite'
20+
steps:
21+
# The shared action is the single source of truth for inputs that affect
22+
# the native binaries and generated bindings.
23+
- uses: ./.github/actions/compute-native-cache-input-hash
24+
id: native-cache-inputs
25+
26+
# Use a distinct prefix: this cache stores Linux-relative paths and must
27+
# never mix with the caches build-upstream saves on other runners. Sharing
28+
# this one key across ci.yml and e2e-test.yml lets whichever workflow builds
29+
# first serve the other.
30+
- name: Compute binaries cache key
31+
id: cache-key
32+
shell: bash
33+
run: |
34+
echo "key=windows-cli-xwin-v1-${{ steps.native-cache-inputs.outputs.hash }}" >> "$GITHUB_OUTPUT"
35+
36+
- name: Define binary paths
37+
id: binary-paths
38+
shell: bash
39+
run: |
40+
{
41+
echo 'paths<<EOF'
42+
echo 'packages/cli/binding/*.node'
43+
echo 'target/x86_64-pc-windows-msvc/release/vp.exe'
44+
echo 'target/x86_64-pc-windows-msvc/release/vp-shim.exe'
45+
echo 'target/x86_64-pc-windows-msvc/release/vp-setup.exe'
46+
echo 'EOF'
47+
} >> "$GITHUB_OUTPUT"
48+
49+
- name: Restore binaries cache
50+
id: binaries-cache
51+
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
52+
with:
53+
path: ${{ steps.binary-paths.outputs.paths }}
54+
key: ${{ steps.cache-key.outputs.key }}
55+
56+
- uses: ./.github/actions/setup-xwin
57+
if: steps.binaries-cache.outputs.cache-hit != 'true'
58+
with:
59+
save-cache: ${{ inputs.save-cache }}
60+
cache-key: windows-cli-cross
61+
62+
- uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1
63+
if: steps.binaries-cache.outputs.cache-hit != 'true'
64+
65+
# --skip-format: formatting the generated index.cjs/index.d.cts needs the
66+
# repo vite config, which imports the not-yet-built vite-plus dist; this
67+
# action skips the TS builds on purpose.
68+
- name: Build NAPI binding
69+
if: steps.binaries-cache.outputs.cache-hit != 'true'
70+
shell: bash
71+
run: pnpm --filter=vite-plus build-native --target x86_64-pc-windows-msvc -x --skip-format
72+
env:
73+
DEBUG: 'napi:*'
74+
XWIN_ACCEPT_LICENSE: '1'
75+
# See setup-xwin / build-windows-tests for why this Detours escape
76+
# hatch is needed.
77+
CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
78+
79+
- name: Build Rust CLI binaries
80+
if: steps.binaries-cache.outputs.cache-hit != 'true'
81+
shell: bash
82+
run: cargo xwin build --release --target x86_64-pc-windows-msvc -p vite_global_cli -p vite_trampoline -p vite_installer
83+
env:
84+
XWIN_ACCEPT_LICENSE: '1'
85+
CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
86+
87+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
88+
with:
89+
name: ${{ inputs.artifact-name }}
90+
# Both path roots are workspace-relative, so the artifact preserves the
91+
# repo layout and a plain download into the workspace places every file
92+
# where tool install-global-cli expects it. The generated binding JS
93+
# (index.cjs etc.) is NOT included: those files are committed, and the
94+
# unformatted copies this action produces (--skip-format) must not
95+
# overwrite them in consumer checkouts (vp check would flag them).
96+
path: ${{ steps.binary-paths.outputs.paths }}
97+
if-no-files-found: error
98+
retention-days: 7
99+
100+
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
101+
if: steps.binaries-cache.outputs.cache-hit != 'true'
102+
with:
103+
path: ${{ steps.binary-paths.outputs.paths }}
104+
key: ${{ steps.cache-key.outputs.key }}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: 'Compute native cache input hash'
2+
description: 'Hash every input that can affect the native CLI binaries and generated bindings.'
3+
4+
outputs:
5+
hash:
6+
description: 'Combined hash of the native build inputs.'
7+
value: ${{ steps.compute.outputs.hash }}
8+
9+
runs:
10+
using: 'composite'
11+
steps:
12+
- name: Compute native cache input hash
13+
id: compute
14+
shell: bash
15+
run: |
16+
echo "hash=$NATIVE_CACHE_INPUT_HASH" >> "$GITHUB_OUTPUT"
17+
env:
18+
NATIVE_CACHE_INPUT_HASH: >-
19+
${{
20+
hashFiles(
21+
'packages/tools/.upstream-versions.json',
22+
'rust-toolchain.toml',
23+
'Cargo.lock',
24+
'Cargo.toml',
25+
'.cargo/config.toml',
26+
'crates/**',
27+
'packages/cli/binding/src/**',
28+
'packages/cli/binding/build.rs',
29+
'packages/cli/binding/Cargo.toml',
30+
'packages/cli/binding/index.*',
31+
'packages/cli/src/vite-config-entry-basenames.json',
32+
'package.json',
33+
'pnpm-lock.yaml',
34+
'pnpm-workspace.yaml',
35+
'packages/cli/package.json',
36+
'packages/cli/build.ts',
37+
'packages/cli/tsdown.config.ts',
38+
'.github/actions/build-upstream/action.yml',
39+
'.github/actions/build-windows-cli/action.yml',
40+
'.github/actions/compute-native-cache-input-hash/action.yml',
41+
'.github/actions/setup-xwin/action.yml'
42+
)
43+
}}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: 'Setup cargo-xwin'
2+
description: >-
3+
Install the Rust toolchain and cargo-xwin for cross-compiling to
4+
x86_64-pc-windows-msvc on Linux, with the MSVC CRT / Windows SDK cached.
5+
6+
inputs:
7+
cache-key:
8+
description: 'setup-rust cache-key prefix; pass a distinct value per caller.'
9+
required: true
10+
save-cache:
11+
description: 'Whether setup-rust should save its cache (usually only on main).'
12+
required: false
13+
default: 'false'
14+
tools:
15+
description: >-
16+
Extra comma-separated taiki-e/install-action tools to install alongside
17+
cargo-xwin (e.g. "cargo-nextest").
18+
required: false
19+
default: ''
20+
21+
runs:
22+
using: 'composite'
23+
steps:
24+
# llvm-tools provides llvm-ar in the toolchain's rustlib bin dir; cargo-xwin
25+
# symlinks the MSVC-style llvm-lib/llvm-dlltool (used by cc-rs to archive
26+
# C/C++ deps) and lld-link from there when the runner image has no matching
27+
# system LLVM.
28+
- uses: oxc-project/setup-rust@68c3199c5339f965e6e163924c3c450773eba42b # main (pending v1.0.17 — Swatinem/rust-cache v2.9.1 for node24)
29+
with:
30+
save-cache: ${{ inputs.save-cache }}
31+
cache-key: ${{ inputs.cache-key }}
32+
components: llvm-tools
33+
34+
- run: rustup target add x86_64-pc-windows-msvc
35+
shell: bash
36+
37+
- uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
38+
with:
39+
tool: ${{ inputs.tools != '' && format('{0},cargo-xwin', inputs.tools) || 'cargo-xwin' }}
40+
41+
# Downloading and unpacking the MSVC CRT/Windows SDK dominates cold wall
42+
# time (~10 minutes); the unpacked result is immutable for a given manifest
43+
# version, so cache it (combined restore + post-job save). Bump the key when
44+
# bumping cargo-xwin.
45+
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
46+
with:
47+
path: ~/.cache/cargo-xwin
48+
key: cargo-xwin-msvc-17

0 commit comments

Comments
 (0)