build: support cross-compiling Windows (x64 and arm64) from Linux - #31300
Conversation
WalkthroughThis PR enables cross-compiling Bun to Windows from Linux hosts by adding winsysroot detection/fetching (xwin), target-driven LLVM/tool selection and host tool resolution, wiring /winsysroot through compile/link flags, adding Docker/bootstrap/CI steps, and providing Windows build profiles and docs. ChangesWindows Cross-Compilation from Linux
Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Updated 4:06 AM PT - May 26th, 2026
✅ @Jarred-Sumner, your commit 639cdd2915b86e7e07db86ccd3f60723318499f2 passed in 🧪 To try this PR locally: bunx bun-pr 31300That installs a local version of the PR into your bun-31300 --bun |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.buildkite/ci.mjs:
- Around line 668-671: The fallback install string inside the x64 branch (the
array element starting with "which nasm || (apt-get update -qq && apt-get
install -y -qq nasm) || true") assumes apt-get; update that string to be
distro-aware by checking available package managers (apt-get, dnf/yum, apk,
pacman) and running the appropriate install command if nasm is missing, keeping
the overall pattern (which nasm || (<detect-and-install-nasm>) || true) and
preserving the arch conditional (the (arch === "x64" ? [...] : []) expression).
In `@scripts/build.ts`:
- Line 400: USAGE/help text omits the newly accepted --winsysroot flag, making
it undiscoverable; update the help output to include the --winsysroot option
name and short description by adding it to the USAGE/help string (the constant
or template referenced when printing --help) so that when the parser accepts
--winsysroot the CLI help shows the flag and its purpose; ensure the help entry
mirrors the style/placement of existing flags in the same USAGE string or
help-generating function.
In `@scripts/build/winsysroot.ts`:
- Around line 106-107: The code calls rmSync(dest, { recursive: true, force:
true }) then mkdirSync(dest...) without validating dest; add a safety guard that
rejects empty, root ("/" or drive letters like "C:\\"), "." or path traversal
values and enforces a configured winsysroot prefix (cfg.winsysroot) before
performing recursive delete. Implement a small validator function (used before
rmSync) that checks dest is non-empty, not equal to path.parse(dest).root, not
"."/"/", and startsWith or isWithin the expected base (cfg.winsysroot) and throw
or exit if validation fails; only call rmSync and mkdirSync after that
validation passes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8bdbf85e-b8d7-4012-9769-4a7f202b6da2
📒 Files selected for processing (18)
.buildkite/Dockerfile.buildkite/ci.mjsdocs/project/building-windows.mdxscripts/bootstrap.shscripts/build.tsscripts/build/CLAUDE.mdscripts/build/bun.tsscripts/build/cargo-config.tsscripts/build/compile.tsscripts/build/config.tsscripts/build/configure.tsscripts/build/deps/webkit.tsscripts/build/flags.tsscripts/build/profiles.tsscripts/build/rust.tsscripts/build/source.tsscripts/build/tools.tsscripts/build/winsysroot.ts
There was a problem hiding this comment.
No issues found on this pass and all earlier feedback is addressed, but this is a substantial build-system/CI feature — and you flagged the xwin --accept-license question for explicit sign-off — so leaving it for a human reviewer.
Extended reasoning...
Overview
This PR adds Windows x64/arm64 cross-compilation from Linux hosts: toolchain selection parameterized by target OS (tools.ts), a new winsysroot config field plumbed through compile/link/rc/cargo flags, a new winsysroot.ts module that downloads the xwin binary and runs xwin splat to fetch the MSVC CRT + Windows SDK in CI, four new build profiles, a redesigned rust_shim ninja rule (per-build-dir stamp instead of shared source-tree output), two new soft_fail CI lanes in .buildkite/ci.mjs, and Dockerfile/bootstrap.sh additions (nasm, *-pc-windows-msvc rustup targets). 19 files changed across the build system, CI config, and docs.
Security risks
No injection or auth concerns. The notable policy item is that ensureWindowsSysroot() runs a freshly-downloaded xwin binary (pinned GitHub release) with --accept-license, which auto-accepts Microsoft's license terms for the CRT/SDK components on every CI cross-build. The PR description explicitly calls this out as wanting reviewer sign-off. The rmSync on the sysroot dest now has content-aware guards (refuses non-absolute/root paths and won't wipe a non-sysroot-looking directory), addressing the earlier safety concern.
Level of scrutiny
High. While the new CI lanes are soft_fail and their artifacts aren't released, the changes are not isolated to the new path: resolveLlvmToolchain() gained a targetOs parameter that every build flows through, crossLangLto is now gated !windows (affects native Windows + --lto=on), the rust_shim rule was redesigned for all Windows builds (native and cross), and cargo-config.ts switched from cfg.cxx to cfg.hostCxx. The PR's validation claims byte-identical native Linux build.ninja, which is reassuring, but the native Windows and macOS paths are only validated by "this PR's CI exercises them."
Other factors
This PR went through several rounds of bug-hunter feedback (two 🔴 findings — the configure-time-vs-fetch ordering and the cross-arch shim contamination — plus a number of nits), all of which the author addressed across five follow-up commits; every inline thread is now resolved and the current pass found nothing new. That's a good sign for correctness, but the scope (new cross-compile target, toolchain resolution refactor, CI changes, third-party SDK fetch with license acceptance) is well beyond what I'd auto-approve. The author's own "Notes for reviewers" section asks for human judgment on the licensing question and on when to flip the lanes from soft_fail.
|
Update on the Root cause: with
Verified locally: a lowercase-layout sysroot now passes configure for both arches, the aliases are created idempotently, rcflags resolve the SDK include dirs, and the native Linux configure is unaffected. The remaining compile/link validation happens in the cross lanes on this build. The |
|
Build 57472: the sysroot-layout fix landed — both cross lanes now fetch + validate the splat, configure, and compile ~575 objects (all the C deps, codegen, esbuild) before hitting the next blocker: the build container exports d8141d6 scrubs The |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.buildkite/Dockerfile:
- Around line 177-186: The Dockerfile hardcodes XWIN_VERSION which must stay in
sync with the winsysroot build script (winsysroot.ts); to fix, move XWIN_VERSION
into a single shared source (e.g., a .env, build-arg, or common JSON) and update
both the Dockerfile and winsysroot.ts to read that shared variable, or if you
cannot centralize now, add a clear cross-reference comment in winsysroot.ts
pointing to XWIN_VERSION in the Dockerfile and vice versa so future edits stay
synchronized; locate usages by searching for the symbol XWIN_VERSION and the
winsysroot.ts module to apply the change.
- Around line 188-196: The pipeline that downloads and extracts xwin can mask
curl failures because tar exits 0 on empty input; update the RUN step that
defines XWIN_ARCH and invokes curl | tar (referencing XWIN_ARCH, XWIN_VERSION,
curl and tar, and the /tmp/xwin-${XWIN_VERSION}-${XWIN_ARCH}-unknown-linux-musl
path) to enable pipefail (e.g., run "set -o pipefail" or "set -euo pipefail"
before the curl | tar pipeline) so the shell returns a non-zero exit code if
curl fails, preventing a silent success when /tmp/xwin-* is missing or
incomplete. Ensure the rest of the chained commands still execute only on
success.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 332cf37f-fb5e-4c02-b4cf-a725b8b38103
📒 Files selected for processing (13)
.buildkite/Dockerfile.buildkite/ci.mjsdocs/project/building-windows.mdxscripts/bootstrap.shscripts/build.tsscripts/build/CLAUDE.mdscripts/build/bun.tsscripts/build/compile.tsscripts/build/config.tsscripts/build/configure.tsscripts/build/deps/boringssl.tsscripts/build/rust.tsscripts/build/winsysroot.ts
|
Build 57481 (the include-path fix): big step — the x64 cross lane compiled 1075/1151 objects and the arm64 lane 918/1234 (all vendor C++, BoringSSL, the unified WebCore/JSC bindings, codegen) before stopping on a single real source issue: 491e4c6 adds the missing include (no behaviour change for native Windows builds, which already compiled this file). 49dbb27 addresses the open review feedback: the windows-cross group now honours the manual |
|
Build 57499: the cross lanes are now one file away from a full compile — x64 reached 1129/1151 objects and arm64 the equivalent (all deps, BoringSSL incl. the NASM assembly, WebCore/JSC unified sources, codegen). The sole failure is 6d62bcf adds The |
|
Build 57510: my mistake in the previous commit — The hard failures on 57510 were two darwin aarch64 test jobs that expired waiting for agents (macOS pool capacity) — unrelated to this diff, and they were retried within the same build. |
There was a problem hiding this comment.
No issues found on my end — all earlier feedback has been addressed — but this is a substantial build-system/CI change and you explicitly flagged the xwin --accept-license (Microsoft SDK terms) question for human sign-off, so leaving this for a maintainer to approve.
Extended reasoning...
Overview
This PR adds Windows x64/arm64 cross-compilation from Linux hosts, touching 21 files across the build system (scripts/build/*.ts — config, tools, flags, rust, bun, configure, a new winsysroot.ts module), CI infrastructure (.buildkite/ci.mjs, .buildkite/Dockerfile, scripts/bootstrap.sh), one source file (image_wic_shim.cpp adding <cstdint>), docs, and a new internal test. The new CI lanes are soft_fail: true and their artifacts are not consumed by tests, signing, or release — the native Windows lanes remain authoritative.
Security risks
The main consideration is supply-chain/licensing rather than a code vulnerability: the build now (in CI and via bootstrap.sh/Dockerfile) downloads and runs the xwin binary from a pinned GitHub release, which in turn downloads MSVC CRT/STL + Windows SDK + ATL from Microsoft's CDN under --accept-license. The PR description explicitly calls this out as needing human sign-off. fetchWindowsSysroot() includes path-safety guards (refuses non-absolute/root dests, refuses to wipe directories that don't look like sysroots) added in response to earlier review. No injection/auth/data-exposure surface in runtime code — the only src/ change is a one-line #include <cstdint>.
Level of scrutiny
High — this is build/CI infrastructure that runs on every PR and on agent-image provisioning. While the cross lanes themselves are soft-fail and isolated, the changes to config.ts, tools.ts, rust.ts, configure.ts, and flags.ts are on the hot path for all builds (gated by cfg.windows && cfg.host.os !== "windows" so native paths should be unchanged, and the PR's validation section claims byte-identical build.ninja for native Linux). The Dockerfile and bootstrap.sh changes affect agent-image baking.
Other factors
Over the course of this PR I left ~14 inline findings (two 🔴, the rest 🟡 nits); every one has been addressed and resolved by the author with follow-up commits, and the bug-hunting system found nothing new on the current revision. The author has been iterating against live CI (builds 57455 → 57510) and the most recent push fixes the last known blocker (--include-atl argument placement). Given the scope, the explicit licensing-terms question, and that the cross lanes haven't yet had a confirmed green run, a maintainer should give this the final look.
|
Build 57551: both Windows cross-compile lanes pass. The build itself is red only on unrelated lanes: the known-flaky From here the open items are for a maintainer: sign-off on the |
|
Status: the Windows cross-compile work is done and its lanes keep passing; the branch is now being iterated on directly by the maintainer.
|
881cc5f to
5df8f04
Compare
Add --os=windows cross-compilation support to the build system so bun.exe for both x64 and arm64 can be compiled and linked from a Linux host: - resolveConfig(): new Windows cross block that resolves a "winsysroot" (xwin splat of the MSVC CRT/STL + Windows SDK in Visual Studio layout, detected at /opt/winsysroot, /opt/xwin, WINDOWS_SYSROOT, or --winsysroot=) and sets crossTarget=<arch>-pc-windows-msvc. Sanitizers are forced off and cross builds get their own default build dir (build/<profile>-windows-<arch>). - tools.ts: toolchain selection is keyed on the *target* OS, so a windows target resolves clang-cl/llvm-lib/lld-link/llvm-rc/llvm-mt/nasm from the host LLVM on any host. A separate host clang/clang++ is resolved for build-time codegen tools and host-side cargo links. - flags.ts: pass /winsysroot to clang-cl and /winsysroot: to lld-link when cross-compiling. - bun.ts/rust.ts: llvm-rc gets explicit SDK include dirs, the windows strip (copy) and bun_shim_impl rules follow the host shell, and the shim's lld-link invocation gets /winsysroot for kernel32/ntdll import libs. - cargo-config.ts: the generated .cargo/config.toml uses the host clang++ for the host triple instead of clang-cl. - profiles: windows-x64, windows-arm64, windows-x64-release, windows-arm64-release. - CI image provisioning (scripts/bootstrap.sh, .buildkite/Dockerfile): install nasm, the windows-msvc rustup targets, and an xwin splat at /opt/winsysroot. - docs: cross-compiling section in building-windows.mdx.
…t in CI
The two new soft-fail Buildkite steps (windows-{x64,aarch64}-cross-build)
do a full compile + link of bun.exe from Linux agents. CI no longer relies
on agent images carrying an xwin splat: when cross-compiling for Windows
with --ci/--buildkite and no sysroot is configured, build.ts fetches one
into the per-build cache via scripts/build/winsysroot.ts (pinned xwin
release, same splat layout as the documented local setup). The image-level
splat provisioning in bootstrap.sh/.buildkite/Dockerfile is dropped
accordingly; nasm stays in the Dockerfile for BoringSSL's win-x64 assembly.
- ci.mjs: make the nasm fallback install distro-aware (apt/dnf/yum) - winsysroot.ts: refuse to wipe directories that don't look like a Windows sysroot before re-splatting - rust.ts: key the bun_shim_impl edge on a per-build-dir stamp and treat the shared source-tree exe as an input, so alternating x64/arm64 builds in one checkout can't embed a stale wrong-arch shim - compile.ts/boringssl.ts: host-aware nasm hint and nasm -I quoting - config.ts: never swap cfg.ld to rust-lld for windows targets - build.ts: document os/arch/abi/winsysroot and the windows cross profiles in --help
emitBun() bakes the winsysroot's MSVC/SDK include dirs into the llvm-rc edge at configure time, so fetching the sysroot after configure (as the previous commit did) left the rc edge without /I flags on a fresh CI agent. Move ensureWindowsSysroot() into configure() (CI only, still a no-op when the sysroot is complete) and fail configure loudly if the sysroot yields no include dirs.
…/MANIFEST:EMBED lld-link only handles /MANIFEST:EMBED itself when it was built with libxml2 and otherwise shells out to mt.exe. The host LLVM's lld-link has libxml2, but rustc's bundled lld-link (used for the cross-language-LTO links so it can read rustc's newer bitcode) has neither, and mt.exe does not exist on non-Windows hosts — the LTO link died with "unable to find mt.exe in PATH". The windows-app-info.rc → llvm-rc → .res step already exists for the icon and VERSIONINFO; add the manifest there as the RT_MANIFEST id-1 resource and drop the linker flags. Same resource in the final PE, produced the same way with any linker (verified: MANIFEST type-24 resource present, longPathAware/SegmentHeap content intact).
autobuild-preview-pr-239-245e88fc adds the cross-compiled Windows artifacts, including the bun-webkit-windows-amd64-lto ThinLTO variant the new Windows LTO config consumes. Every non-Windows artifact in that release is built from the same configuration as main cf8fb22b. Swap to the oven-sh/WebKit main commit once #239 merges, before this PR lands.
5df8f04 to
16da0f2
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/build/CLAUDE.md`:
- Line 145: The inline code span contains a trailing space (`linker = `) which
triggers MD038; update the markdown in the sentence referencing
generateCargoConfig(cfg) and the repo-root `.cargo/config.toml` so the inline
code reads `linker =` (remove the trailing space inside the backticks), and scan
the file for other occurrences of `linker = ` to normalize them as `linker =`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a0ddfa5c-bff4-435f-8636-e3918d401c95
📒 Files selected for processing (19)
.buildkite/Dockerfile.buildkite/ci.mjsdocs/project/building-windows.mdxscripts/bootstrap.shscripts/build.tsscripts/build/CLAUDE.mdscripts/build/bun.tsscripts/build/cargo-config.tsscripts/build/compile.tsscripts/build/config.tsscripts/build/configure.tsscripts/build/deps/boringssl.tsscripts/build/deps/webkit.tsscripts/build/flags.tsscripts/build/profiles.tsscripts/build/rust.tsscripts/build/source.tsscripts/build/tools.tsscripts/build/winsysroot.ts
NOICF was a temporary diagnostic state (unfolded PDB symbolication for the Strong<Impl> corruption investigation); SAFEICF only folds functions whose address is never taken, so the ClassInfo/constructor identity guarantees that ruled out plain /OPT:ICF still hold. With ThinLTO on the x64 cross builds the folding also claws back part of the cross-module inlining growth.
The cross-compiled windows WebKit artifacts (clang-cl 21 on Linux) emit Vpermb (AVX512_VBMI) in convert_utf16be_to_latin1 / convert_valid_ utf16be_to_latin1 and Vmovdqa64 (AVX512F) in the validate_utf16* kernels where the native-built objects didn't. All six symbols are icelake implementations selected only through simdutf's runtime CPUID dispatch (the icelake gate requires the AVX-512BW/CD/VL/VBMI2/VPOPCNTDQ set), the same gate every other icelake entry in this allowlist already relies on.
The cross-compiled Windows WebKit artifacts now ship sicudt.lib as the same per-item zstd repack the Linux artifacts use (filtered, then compressed with a shared trained dictionary), with udata.cpp calling the weak bun_icu_maybe_decompress hook. Widen the hook's platform gate so Windows builds define it; with the unpatched/uncompressed native windows-release.ps1 artifacts the hook simply never fires.
…ata (TEMPORARY) autobuild-preview-pr-239-bca1d74d adds the filtered + per-item-zstd ICU data table to every Windows artifact (sicudt.lib 24.6 MB -> 11.9 MB) on top of the earlier ThinLTO -lto variant. Swap to the oven-sh/WebKit main commit once #239 merges, before this PR lands.
…-cross-compile # Conflicts: # scripts/build/bun.ts # scripts/build/rust.ts
The Windows cross-compile + ThinLTO + filtered/zstd-ICU WebKit work (oven-sh/WebKit#239) is merged to main; point at the main autobuild instead of the PR preview release.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/build/bun.ts (1)
892-911:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPick one SDK/MSVC version instead of adding every include directory.
If a winsysroot contains multiple
VC/Tools/MSVC/*orWindows Kits/10/Include/*versions,llvm-rcwill search them inreaddirSync()order, which is nondeterministic and can compile resources against a different header set than the rest of the toolchain. Prefer choosing a single version deterministically, typically the newest one.♻️ Suggested direction
function windowsSysrootIncludeDirs(winsysroot: string): string[] { const dirs: string[] = []; + const newest = (names: string[]) => + names.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1); + const msvcRoot = resolve(winsysroot, "VC", "Tools", "MSVC"); - if (existsSync(msvcRoot)) { - for (const ver of readdirSync(msvcRoot)) { - const d = resolve(msvcRoot, ver, "include"); - if (existsSync(d)) dirs.push(d); - } + const msvcVer = existsSync(msvcRoot) ? newest(readdirSync(msvcRoot)) : undefined; + if (msvcVer) { + const d = resolve(msvcRoot, msvcVer, "include"); + if (existsSync(d)) dirs.push(d); } + const sdkRoot = resolve(winsysroot, "Windows Kits", "10"); const sdkInclude = ["Include", "include"].map(name => resolve(sdkRoot, name)).find(existsSync); if (sdkInclude !== undefined) { - for (const ver of readdirSync(sdkInclude)) { - for (const sub of ["ucrt", "shared", "um"]) { - const d = resolve(sdkInclude, ver, sub); - if (existsSync(d)) dirs.push(d); - } + const sdkVer = newest(readdirSync(sdkInclude)); + if (sdkVer) { + for (const sub of ["ucrt", "shared", "um"]) { + const d = resolve(sdkInclude, sdkVer, sub); + if (existsSync(d)) dirs.push(d); + } } } return dirs; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/bun.ts` around lines 892 - 911, The function windowsSysrootIncludeDirs currently collects all include dirs from every version under VC/Tools/MSVC and Windows Kits/10/Include, causing nondeterministic ordering; change it to pick a single deterministic version (e.g., the newest) for both msvcRoot and sdkInclude instead of iterating all entries: list directory entries via readdirSync(msvcRoot) and readdirSync(sdkInclude), sort them (numeric/lexicographic appropriate for version names), select the latest entry, then build include paths only for that selected version (use the same selection for subdirs "ucrt","shared","um"); ensure you still check existsSync before adding and return only those chosen dirs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@scripts/build/bun.ts`:
- Around line 892-911: The function windowsSysrootIncludeDirs currently collects
all include dirs from every version under VC/Tools/MSVC and Windows
Kits/10/Include, causing nondeterministic ordering; change it to pick a single
deterministic version (e.g., the newest) for both msvcRoot and sdkInclude
instead of iterating all entries: list directory entries via
readdirSync(msvcRoot) and readdirSync(sdkInclude), sort them
(numeric/lexicographic appropriate for version names), select the latest entry,
then build include paths only for that selected version (use the same selection
for subdirs "ucrt","shared","um"); ensure you still check existsSync before
adding and return only those chosen dirs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 99c09636-7bb8-4e74-99a5-3034eaf29e66
📒 Files selected for processing (6)
scripts/build/bun.tsscripts/build/deps/webkit.tsscripts/build/flags.tsscripts/build/rust.tsscripts/verify-baseline-static/allowlist-x64-windows.txtsrc/jsc/bindings/bun_icu_decompress.cpp
| // Application manifest (longPathAware + SegmentHeap), embedded as a resource | ||
| // rather than via the linker's /MANIFEST:EMBED — that path needs libxml2 (or | ||
| // mt.exe) inside the linker, which rustc's bundled lld-link doesn't have. | ||
| // 1 = CREATEPROCESS_MANIFEST_RESOURCE_ID, 24 = RT_MANIFEST. | ||
| 1 24 "@BUN_MANIFEST_PATH@" |
There was a problem hiding this comment.
🟡 nit: switching from /MANIFEST:EMBED /MANIFESTINPUT: to a raw 1 24 RT_MANIFEST resource changes the shipping native Windows manifest, not just the cross build — lld-link's manifest path used to merge a default <trustInfo><requestedExecutionLevel level='asInvoker' uiAccess='false'/> block into src/bun.exe.manifest (which only has <windowsSettings>), whereas the .rc route embeds the file verbatim, so that block is now absent. Practical impact is essentially nil (Installer Detection and File/Registry Virtualization only apply to 32-bit processes; bun is 64-bit-only), but it contradicts the PR description's "Same resource in the final PE for all Windows builds, native included" claim and bun build --compile outputs inherit it. If you want to preserve the prior bytes exactly, add the <trustInfo> block to src/bun.exe.manifest directly.
Extended reasoning...
What changed
This PR removes manifestLinkFlags() from scripts/build/bun.ts (which previously returned ['/MANIFEST:EMBED', '/MANIFESTINPUT:src/bun.exe.manifest'] and was spliced into ldflags in both emitBun() and emitLinkOnly()) and replaces it with a raw 1 24 "@BUN_MANIFEST_PATH@" RT_MANIFEST entry in src/windows-app-info.rc. The motivation (per the new comment in bun.ts and the PR description) is that rustc's bundled lld-link — used for the cross-language-LTO link — has no libxml2 and can't shell out to mt.exe on a Linux host, so /MANIFEST:EMBED doesn't work there. The .rc → llvm-rc → .res route works with any linker.
The change applies to all Windows builds: manifestLinkFlags() was removed entirely, and emitWindowsResources() runs for both the native Azure lanes and the new cross lanes. So the shipping bun-windows-x64/bun-windows-aarch64 artifacts (the ones the release step uploads, not the soft_fail cross artifacts) now embed their manifest via the .rc route too.
What the two routes embed
With /MANIFEST:EMBED, lld-link calls createDefaultXml() (lld/COFF/DriverUtils.cpp), which — because config->manifestUAC defaults to true and the old manifestLinkFlags() never passed /MANIFESTUAC:NO — emits:
<trustInfo>
<security>
<requestedPrivileges>
<requestedExecutionLevel level='asInvoker' uiAccess='false'/>
</requestedPrivileges>
</security>
</trustInfo>It then merges that default with the /MANIFESTINPUT:src/bun.exe.manifest file (via libxml2 or mt.exe), producing a manifest with both the user's <windowsSettings> (longPathAware + SegmentHeap) and the default <trustInfo> block.
With the .rc route, llvm-rc embeds src/bun.exe.manifest verbatim as resource type 24 / id 1. That file (verified) contains only:
<asmv3:application>
<asmv3:windowsSettings>
<longPathAware …>true</longPathAware>
<heapType …>SegmentHeap</heapType>
</asmv3:windowsSettings>
</asmv3:application>No <trustInfo>. So the embedded manifest in the shipping native bun.exe loses the <requestedExecutionLevel> element after this PR.
Step-by-step proof
- Before this PR, native Windows
build-bunstep:emitBun()→ldflags = [..., ...manifestLinkFlags(cfg), ...]→ link rule passes/link ... /MANIFEST:EMBED /MANIFESTINPUT:.../src/bun.exe.manifestto clang-cl, which forwards to lld-link. - lld-link (built with libxml2 in the apt.llvm.org / Windows LLVM distros):
config->manifest = Embed,config->manifestUAC = true(default, never overridden) →createDefaultXml()writes the<trustInfo>block →createManifestXml()merges it with the/MANIFESTINPUT:file → result is embedded as RT_MANIFEST id 1. mt.exe -inputresource:bun.exe;#1 -out:con(orllvm-readobj --coff-resources) on a pre-PRbun.exewould show both<windowsSettings>and<trustInfo>.- After this PR, same native lane:
ldflagsno longer contains/MANIFEST:*.emitWindowsResources()writeswindows-app-info.rcwith1 24 ".../src/bun.exe.manifest"→ llvm-rc compiles it to a .res with the file's bytes verbatim → lld-link merges the .res into the PE's resource section without touching the manifest content. - The same dump on a post-PR
bun.exewould show only<windowsSettings>— no<trustInfo>.
The PR description says it verified "type-24 resource present, longPathAware/SegmentHeap intact" — i.e. it checked that the user-authored settings survived, but not that the linker-merged default didn't disappear.
Why this is only a nit
Per Microsoft's documentation, the two UAC behaviors that <requestedExecutionLevel> opts an executable out of — Installer Detection (heuristic elevation prompts for setup-like binaries) and File/Registry Virtualization (redirecting writes to %ProgramFiles%/HKLM into per-user stores) — apply only to 32-bit processes. Bun ships only x64 and arm64 binaries (buildPlatforms in ci.mjs has no 32-bit entries), and bun.exe doesn't match installer-name heuristics anyway. For a 64-bit executable without <trustInfo>, the loader's effective behavior is identical to level='asInvoker': no elevation prompt, no virtualization. So there is no observable runtime difference.
It's worth flagging anyway because (a) it's an unannounced byte-level change to the shipping native binary that the PR description explicitly claims is content-identical ("Same resource in the final PE for all Windows builds, native included"), (b) bun build --compile outputs inherit bun.exe's resource section via rescle, so user-produced standalone executables also lose the block, and (c) rescle.cpp:1040 does find("requestedExecutionLevel") on the loaded manifest — with the new manifest that returns npos and originalExecutionLevel_ gets garbage via unsigned-wrap arithmetic in substr(), though since bun never calls SetExecutionLevel() that dead value is never read.
Suggested fix
If preserving the prior manifest bytes is desired, add the <trustInfo> block to src/bun.exe.manifest directly:
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>(as a sibling of the existing <asmv3:application> element). This keeps the .rc route working without any linker manifest tooling, and makes the embedded resource match what lld-link used to produce. ~6 lines; non-blocking.
* oven/main (3 new commits): build: support cross-compiling Windows (x64 and arm64) from Linux (oven-sh#31300) Bun.serve: restore per-request GC memory accounting to fix elevated RSS under HTTP load (oven-sh#31422) Optimize Buffer.toString('hex'/'base64') for large buffers (oven-sh#31421) Auto-merged: scripts/build.ts, scripts/build/bun.ts, scripts/build/deps/webkit.ts, scripts/build/flags.ts, scripts/build/rust.ts, scripts/build/source.ts, scripts/build/tools.ts Resolved conflict in scripts/build/config.ts: kept OHOS closing block + upstream Windows cross-compilation block
* oven/main (3 new commits): build: support cross-compiling Windows (x64 and arm64) from Linux (oven-sh#31300) Bun.serve: restore per-request GC memory accounting to fix elevated RSS under HTTP load (oven-sh#31422) Optimize Buffer.toString('hex'/'base64') for large buffers (oven-sh#31421) Auto-merged: scripts/build.ts, scripts/build/bun.ts, scripts/build/deps/webkit.ts, scripts/build/flags.ts, scripts/build/rust.ts, scripts/build/source.ts, scripts/build/tools.ts Resolved conflict in scripts/build/config.ts: kept OHOS closing block + upstream Windows cross-compilation block
Replaces the native Windows build lanes with cross-compiled ones — the same model macOS uses since #31303. Follow-up to #31300, which added the toolchain support and ran the cross builds as soft-fail validation lanes alongside the native ones. ### What changes - The three `windows` entries in `buildPlatforms` become `crossCompile: true` (amazonlinux docker images), so they go through the regular `build-cpp` / `build-rust` / `build-bun` split with the **same step keys, labels, and artifact names** as before — just on the Linux fleet. These are now the Windows artifacts the tests consume and the release ships. - The soft-fail `windows-cross` validation lanes are deleted (they're the real lanes now). - The windows **rust** step runs on the same amazonlinux image as the cpp/link steps (configure provides the xwin sysroot + clang-cl env that `cc`-crate build scripts need), instead of a native Windows VM. - Stays on native Windows machines, consuming the cross-built artifacts: the test shards, `windows-sign` (smctl is Windows-only), and the x64-baseline `verify-baseline` step (Intel SDE). ### Consequences - **All three windows lanes ship non-LTO for now.** The x64 cross toolchain fully supports ThinLTO + cross-language LTO (`--lto=on`), but LLVM's thin backends miscompile JSC on x86-64 at -O1+ (verified via the experiment PRs: cross-language off changes nothing, backends at -O0 are clean), so LTO is no longer the windows default — same situation that keeps linux on full LTO. Windows LTO returns as a follow-up (full-LTO COFF configuration or an upstream fix). The size wins (SAFEICF, filtered + zstd ICU) are LTO-independent and stay. - No Azure Windows build VMs anymore — Windows machines are only used where Windows is actually required (tests / signing / SDE). ### Risk / what this PR's CI proves The cross-built **WebKit** artifacts (JSC/WTF/ICU incl. the lazily-decompressed zstd data table) already went through the full Windows test matrix in #31300 — the native lanes there linked against them. The cross-built **bun.exe itself** has only had build-only validation until now; this PR's CI run is the first time the Windows test suites execute a fully cross-compiled bun.exe, which is exactly the gate it needs to pass before merging. The `ci-cpp-only`/`ci-rust-only`/`ci-link-only` split for a Windows target on a Linux host is also exercised end-to-end here for the first time (the validation lanes used the single-step full build). ### windows-aarch64 heap corruption: root-caused and fixed (in this PR) The first full runs of the cross-built **windows-aarch64** binary died with `STATUS_HEAP_CORRUPTION` (0xC0000374) on every cold `bun install`. Crash dumps (procdump on the ARM64 fleet, symbolicated against the PDB) showed segment-heap corruption in the small-block region holding BoringSSL's parsed certificate/ASN.1 allocations, with the failing heap calls coming from the TLS/cert path and from libarchive's charset-converter setup on extraction workers. Two changes in this PR address it (also proposed standalone as #31461): - **Bind BoringSSL's mimalloc allocator hooks on Windows** (`/INCLUDE:OPENSSL_memory_alloc/free/get_size`): the hooks are referenced only as COFF weak externals, which never pull the defining archive member, so they have silently never been active on Windows and every BoringSSL allocation landed on the CRT's NT heap. - **Cache libarchive's codepage lookup** (vendored patch): `get_current_codepage()/get_current_oemcp()` queried `setlocale(LC_CTYPE, NULL)` once per created charset converter — one per extracted archive across many concurrent worker threads. With the fix, the corruption is gone across all arm64 shards (zero `0xC0000374` exits in two full runs). x64 and x64-baseline pass 8/8. ### windows-aarch64 DNS failures: root-caused and fixed (in this PR) After the heap fix, the arm64 shards still failed c-ares-backed DNS (`dns.resolve*`, `lookupService`, mongodb SRV) with ECONNREFUSED — the resolver discovered no DNS servers and fell back to localhost, while the natively built binary on the same machines was fine. Instrumented c-ares + binary diffing traced it to the **Universal CRT payload in the VS manifest (what xwin downloads, any xwin version) shipping an ancient arm64 UCRT** whose `__stdio_common_vsprintf` mis-formats on ARM64: every discovered DNS server formatted as garbage, so the list parsed to nothing — and every printf-family call in the cross arm64 binary was suspect. Fix (also proposed standalone as #31492): fetch the *serviced* UCRT static libs from the official `Microsoft.Windows.SDK.CPP.<arch>` NuGet (pinned `10.0.26100.8249`, the same libs a real Visual Studio install has) into a cache overlay and add it as `/libpath:` ahead of `/winsysroot`; plus bump xwin to 0.9.0 with `--sdk-version 10.0.26100` / `--crt-version 14.44.17.14` pinned so manifest refreshes can't drift the toolchain. Same SDK, same headers, same minimum supported Windows — only the lib binaries are newer. Validated on the arm64 fleet: DNS discovery now returns the real resolver and all `dns.*` queries pass, and **all 24 windows test shards (x64, x64-baseline, aarch64) are green** on the validation build. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Adds support for cross-compiling
bun.exefor Windows x64 and arm64 from a Linux host — compile and link, through the normal build system — plus CI lanes that exercise it on every build.How it works
Windows native builds already use clang-cl + lld-link + llvm-lib/llvm-rc (LLVM 21.1.8). All of those tools ship in every LLVM distribution and are inherently cross-capable, so the missing pieces were:
scripts/build/tools.ts): a--os=windowsbuild now resolvesclang-cl,lld-link,llvm-lib,llvm-rc,llvm-mt,nasmfrom the host LLVM on any host. Build-time host tools (dep codegen helpers, host-side cargo artifacts) get a separate plainclang/clang++so they keep targeting the host.winsysrootinscripts/build/config.ts): an xwin splat of the MSVC CRT/STL + Windows SDK in Visual Studio layout, passed to clang-cl as/winsysrootand to lld-link as/winsysroot:. This is the cross equivalent of theINCLUDE/LIBenv a VS dev shell provides natively. Locally it's detected at/opt/winsysroot,/opt/xwin,$WINDOWS_SYSROOT, or--winsysroot=; CI agent images bake the same splat at/opt/winsysroot(.buildkite/Dockerfile,scripts/bootstrap.sh), and when an agent doesn't have one, configure fetches it into the per-build cache (scripts/build/winsysroot.ts, pinned xwin release →xwin splatfrom Microsoft's CDN). Configure also validates the splat and adds the title-caseInclude/Libaliases clang-cl/lld-link expect (xwin's winsysroot-style layout writes them lowercase).crossTarget=<arch>-pc-windows-msvc,--target=for clang-cl, smoke test skipped, sanitizers forced off, separate default build dir (build/<profile>-windows-<arch>), llvm-rc gets explicit SDK include dirs, thebun_shim_impl.execargo link gets/winsysroot, windows-only rules (rust_shim, strip-copy) now follow the host shell, and the WebKit prebuilt extraction cache is keyed per-OS so cross and native extractions don't collide.windows-x64,windows-arm64,windows-x64-release,windows-arm64-release(on a Windows host the regular profiles are unchanged)..buildkite/ci.mjs): two new build lanes,windows-x64-cross-buildandwindows-aarch64-cross-build, run a full--os=windowsbuild (deps + C++ + cargo + link →bun.exe) on the amazonlinux Linux agents. They aresoft_failuntil they have a green history and their artifacts are not consumed by tests/release — the native Azure Windows lanes stay authoritative. The Linux agent images getnasm, the*-pc-windows-msvcrustup targets, and the baked/opt/winsysrootsplat (Dockerfile + bootstrap.sh); agents from older images fall back to the configure-time fetch.docs/project/building-windows.mdx.ThinLTO + cross-language LTO (x64)
The x64 cross lane (and
--lto=onlocally) now builds with the same LTO setup the macOS cross builds use — something the native Windows lanes never had:-flto=thin -fno-split-lto-unit(clang-cl accepts both directly); no-fwhole-program-vtableson COFF (WPD drops vtable symbols that associative COMDAT sections still reference and the LTO codegen aborts).bun-webkit-windows-amd64-ltoThinLTO bitcode variant (Cross-compile the Windows JSC artifacts on Linux, add LTO variants WebKit#239).-Clinker-plugin-lto; no-Zsplit-lto-unit— every COFF module is split=0), so lld-link runs one ThinLTO graph across Rust, bun C++, and JSC.gcc-ld/lld-link(its LLVM is newer than the host clang's, and bitcode is only forward-compatible); the link rule pins that choice with/clang:-B<dir>because clang-cl has no working--ld-path=spelling. Cargo-driven links (bun_shim_impl.exe) keep the host lld-link — rustc mis-drives its own gcc-ld wrapper when it's named as the target linker.windows-app-info.rc→ llvm-rc →.resstep (RT_MANIFEST id 1) instead of/MANIFEST:EMBED— rustc's lld-link has no libxml2 andmt.exedoesn't exist on Linux. Same resource in the final PE for all Windows builds, native included (verified: type-24 resource present, longPathAware/SegmentHeap intact).-ltoWebKit prebuilt — LLVM's CodeView emitter aborts on ARM64 NEON tuple registers), for--baseline(no-baseline-ltovariant), and for native Windows hosts.Verified locally (Linux x64 host, full
--profile=ci-release --os=windows --arch=x64build → 102 MBbun.exe, PE32+ x64, manifest/icon/VERSIONINFO resources, 18 MB stack reserve). Re-linking with/mllvm:-print-importsshows the cross-language importing is real:The
always_inlineboundary accessors from the macOS work get imported as expected (JSC__JSGlobalObject__vm,Bun__RETURN_IF_EXCEPTION,JSC__JSValue__jsNumberFromDouble,Bun__StackCheck__getMaxStack, …), plus 733uws_*/us_*socket functions into Rust CGUs.Size: the first cut measured 102.1 MB (vs 90.25 MB native canary) — the delta was the unfiltered 32 MB ICU data table in the first cross-built WebKit artifacts plus ThinLTO inlining growth under
/OPT:NOICF. Both are addressed in this PR + oven-sh/WebKit#239:bun.exe/OPT:SAFEICFrestoredThe native Windows lanes get the same ICU + SAFEICF wins through the shared WEBKIT_VERSION/flags (without the ThinLTO text growth). CI's
binary-sizestep on this PR (build 58147, all 288 jobs green):(bun-linux-x64 on the same build is 71.23 MB — the Windows/Linux gap is now ~3 MB.)
Supporting changes in this PR:
/OPT:SAFEICFrestored (NOICF was a temporary symbolication aid), the lazy ICU decompression hook (bun_icu_decompress.cpp) enabled for Windows, and sixsimdutf::icelakeallowlist ceilings widened inscripts/verify-baseline-static/allowlist-x64-windows.txt(the cross-built WTF emitsVpermb/Vmovdqa64in kernels the native objects didn't — all behind simdutf's CPUID dispatch; verified against the failing CI artifact with the scanner).WEBKIT_VERSION now points at oven-sh/WebKit main
963f8758c29e(oven-sh/WebKit#239 merged). This PR's CI is the first full-matrix run of bun against the cross-compiled Windows WebKit artifacts.Usage (local)
Validation
On a Linux x64 host (Debian LLVM 21.1.8):
--profile=windows-x64/windows-arm64configure and generate the expected ninja graph (clang-cl/showIncludes /MTd --target=<arch>-pc-windows-msvc /winsysroot ..., lld-link/machine: /winsysroot: /DEF: /MANIFEST:EMBED ..., llvm-lib archiving, llvm-rc with SDK include dirs), and the CI path resolves the winsysroot to the per-build cache when none is provisioned.TARGET_PLATFORM=win32for both arches.bun_rust.libproduced foraarch64-pc-windows-msvcandx86_64-pc-windows-msvc./MANIFEST:EMBED+/MANIFESTINPUT:path works on Linux (verified againstsrc/bun.exe.manifest).build.ninjaand.cargo/config.tomlfor the native Linux debug build are byte-identical with and without this change; the native Windows/macOS paths resolve the same toolchain and emit the same commands as before (this PR's CI exercises them).windows-*-cross-buildCI lanes, which now pass for both architectures: a fullbun.exeis produced from a Linux agent in ~9 minutes (x64) and ~13 minutes (arm64), including the xwin sysroot fetch.Notes for reviewers
xwin --accept-license, which accepts Microsoft's license terms for the SDK/CRT components it downloads — same terms the Windows CI images accept when installing VS Build Tools, but worth an explicit sign-off.soft_fail: trueand their output isn't signed, tested, or released. Flipping them to required (or moving the real Windows build lanes onto Linux agents) can happen once they've proven stable.Rebase note (integration with #31303)
Rebased onto main after #31303 (macOS cross-compilation from Linux) landed — both changes touch the same build-script seams, so conflicts were resolved by combining the two cross paths rather than picking a side:
config.ts: sanitizers stay forced off for both darwin-cross and Windows-from-unix;crossLangLtoinitially kept the Windows gate on top of main's darwin-cross handling (since lifted — see the ThinLTO section above); main'swantRustLld/ld64StripSwaprestructure is kept.tools.ts: main'sclangResourceDirprobe now keys off the MSVC-style toolchain selection (!msvcTarget) instead of the host OS — it still runs for darwin cross and stays skipped whenccis clang-cl; the host clang/clang++ lookups for Windows cross sit alongside main's ld64.lld/llvm-strip/dsymutil lookups.rust.ts:rustCanCrossFromLinuxreturns true for darwin (from main) and still returns false for windows-msvc (the shared rust box isn't provisioned with a winsysroot).deps/webkit.ts: the prebuilt extraction key combines both sides (-windowsfor Windows cross alongside-macos/-freebsd/-android).scripts/bootstrap.sh: Linux agents get both the*-apple-darwinand*-pc-windows-msvcrustup targets.configure.ts: bothensureMacosSdk()andensureWindowsSysroot()run..buildkite/ci.mjs,flags.ts,bun.ts,build.tsapplied cleanly around main's changes.Re-validated after the rebase: prettier /
tsc -p scripts/build/bash -n bootstrap.sh,--configure-onlyfor--os=windowsx64 + aarch64 against a stub sysroot (clang-cl--target=<arch>-pc-windows-msvc /winsysroot, lld-link, llvm-lib, NASM edges for x64 all emitted as before), and the native Linux debug configure.