Skip to content

build: support cross-compiling Windows (x64 and arm64) from Linux - #31300

Merged
Jarred-Sumner merged 27 commits into
mainfrom
farm/a92c73e4/windows-cross-compile
May 26, 2026
Merged

build: support cross-compiling Windows (x64 and arm64) from Linux#31300
Jarred-Sumner merged 27 commits into
mainfrom
farm/a92c73e4/windows-cross-compile

Conversation

@robobun

@robobun robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Adds support for cross-compiling bun.exe for 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:

  • Toolchain selection by target, not host (scripts/build/tools.ts): a --os=windows build now resolves clang-cl, lld-link, llvm-lib, llvm-rc, llvm-mt, nasm from the host LLVM on any host. Build-time host tools (dep codegen helpers, host-side cargo artifacts) get a separate plain clang/clang++ so they keep targeting the host.
  • A Windows "sysroot" (winsysroot in scripts/build/config.ts): an xwin splat of the MSVC CRT/STL + Windows SDK in Visual Studio layout, passed to clang-cl as /winsysroot and to lld-link as /winsysroot:. This is the cross equivalent of the INCLUDE/LIB env 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 splat from Microsoft's CDN). Configure also validates the splat and adds the title-case Include/Lib aliases clang-cl/lld-link expect (xwin's winsysroot-style layout writes them lowercase).
  • Cross plumbing following the existing Android/FreeBSD pattern: 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, the bun_shim_impl.exe cargo 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.
  • Profiles: windows-x64, windows-arm64, windows-x64-release, windows-arm64-release (on a Windows host the regular profiles are unchanged).
  • CI (.buildkite/ci.mjs): two new build lanes, windows-x64-cross-build and windows-aarch64-cross-build, run a full --os=windows build (deps + C++ + cargo + link → bun.exe) on the amazonlinux Linux agents. They are soft_fail until 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 get nasm, the *-pc-windows-msvc rustup targets, and the baked /opt/winsysroot splat (Dockerfile + bootstrap.sh); agents from older images fall back to the configure-time fetch.
  • Docs: "Cross-compiling from Linux" section in docs/project/building-windows.mdx.

ThinLTO + cross-language LTO (x64)

The x64 cross lane (and --lto=on locally) now builds with the same LTO setup the macOS cross builds use — something the native Windows lanes never had:

  • bun's C/C++ compiles with -flto=thin -fno-split-lto-unit (clang-cl accepts both directly); no -fwhole-program-vtables on COFF (WPD drops vtable symbols that associative COMDAT sections still reference and the LTO codegen aborts).
  • The WebKit prebuilt is the new bun-webkit-windows-amd64-lto ThinLTO bitcode variant (Cross-compile the Windows JSC artifacts on Linux, add LTO variants WebKit#239).
  • rustc emits LLVM bitcode (-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.
  • The final link uses rustc's 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.
  • The manifest is now embedded by the existing windows-app-info.rc → llvm-rc → .res step (RT_MANIFEST id 1) instead of /MANIFEST:EMBED — rustc's lld-link has no libxml2 and mt.exe doesn't exist on Linux. Same resource in the final PE for all Windows builds, native included (verified: type-24 resource present, longPathAware/SegmentHeap intact).
  • LTO stays off for windows arm64 (no -lto WebKit prebuilt — LLVM's CodeView emitter aborts on ARM64 NEON tuple registers), for --baseline (no -baseline-lto variant), and for native Windows hosts.

Verified locally (Linux x64 host, full --profile=ci-release --os=windows --arch=x64 build → 102 MB bun.exe, PE32+ x64, manifest/icon/VERSIONINFO resources, 18 MB stack reserve). Re-linking with /mllvm:-print-imports shows the cross-language importing is real:

ThinLTO import edges count
total 171,416
C++ → Rust 12,570 functions into 126 Rust CGUs
Rust → C++ 4,120 functions into 92 C++ modules
Rust → Rust 15,954
C++ → C++ (incl. JSC↔bun) 138,772

The always_inline boundary accessors from the macOS work get imported as expected (JSC__JSGlobalObject__vm, Bun__RETURN_IF_EXCEPTION, JSC__JSValue__jsNumberFromDouble, Bun__StackCheck__getMaxStack, …), plus 733 uws_*/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:

windows x64 bun.exe size
bun v1.3.14 93.9 MB
current canary (native, before this PR) 90.25 MB
cross ThinLTO, first cut 102.1 MB
+ /OPT:SAFEICF restored −5.4 MB
+ ICU data filtered like the native build −7.4 MB
+ ICU data per-item zstd (lazy decompression, oven-sh/WebKit#237 ported to Windows) −12.9 MB
cross ThinLTO now (built locally) 77.8 MB

The native Windows lanes get the same ICU + SAFEICF wins through the shared WEBKIT_VERSION/flags (without the ThinLTO text growth). CI's binary-size step on this PR (build 58147, all 288 jobs green):

target this PR canary bun v1.3.14
bun-windows-x64 74.33 MB 90.25 MB (−15.9) 93.9 MB (−19.6)
bun-windows-x64-baseline 73.35 MB 89.30 MB (−15.9) 93.2 MB
bun-windows-aarch64 69.72 MB 87.14 MB (−17.4)

(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:SAFEICF restored (NOICF was a temporary symbolication aid), the lazy ICU decompression hook (bun_icu_decompress.cpp) enabled for Windows, and six simdutf::icelake allowlist ceilings widened in scripts/verify-baseline-static/allowlist-x64-windows.txt (the cross-built WTF emits Vpermb/Vmovdqa64 in 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)

xwin --accept-license --arch x86_64,aarch64 splat \
  --use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs \
  --output /opt/winsysroot

bun run build --profile=windows-arm64          # → build/debug-windows-aarch64/bun-debug.exe
bun run build --profile=windows-x64-release    # → build/release-windows-x64/bun.exe

Validation

On a Linux x64 host (Debian LLVM 21.1.8):

  • --profile=windows-x64 / windows-arm64 configure 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.
  • Codegen runs with TARGET_PLATFORM=win32 for both arches.
  • The full Rust workspace builds for both Windows targets through the generated cargo invocations: bun_rust.lib produced for aarch64-pc-windows-msvc and x86_64-pc-windows-msvc.
  • lld-link's /MANIFEST:EMBED + /MANIFESTINPUT: path works on Linux (verified against src/bun.exe.manifest).
  • No change to existing platforms: build.ninja and .cargo/config.toml for 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).
  • The end-to-end compile + link is validated by this PR's windows-*-cross-build CI lanes, which now pass for both architectures: a full bun.exe is produced from a Linux agent in ~9 minutes (x64) and ~13 minutes (arm64), including the xwin sysroot fetch.

Notes for reviewers

  • The CI fetch runs 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.
  • The cross lanes are soft_fail: true and 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; crossLangLto initially kept the Windows gate on top of main's darwin-cross handling (since lifted — see the ThinLTO section above); main's wantRustLld/ld64StripSwap restructure is kept.
  • tools.ts: main's clangResourceDir probe now keys off the MSVC-style toolchain selection (!msvcTarget) instead of the host OS — it still runs for darwin cross and stays skipped when cc is clang-cl; the host clang/clang++ lookups for Windows cross sit alongside main's ld64.lld/llvm-strip/dsymutil lookups.
  • rust.ts: rustCanCrossFromLinux returns 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 (-windows for Windows cross alongside -macos/-freebsd/-android).
  • scripts/bootstrap.sh: Linux agents get both the *-apple-darwin and *-pc-windows-msvc rustup targets.
  • configure.ts: both ensureMacosSdk() and ensureWindowsSysroot() run.
  • .buildkite/ci.mjs, flags.ts, bun.ts, build.ts applied cleanly around main's changes.

Re-validated after the rebase: prettier / tsc -p scripts/build / bash -n bootstrap.sh, --configure-only for --os=windows x64 + 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.

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Windows Cross-Compilation from Linux

Layer / File(s) Summary
Dockerfile & bootstrap
.buildkite/Dockerfile, scripts/bootstrap.sh
Installs nasm in images/bootstrap, adds Windows MSVC Rust targets, and adds install_windows_sysroot()/xwin_version plumbing to populate /opt/winsysroot when needed.
Buildkite CI pipeline
.buildkite/ci.mjs
Adds getWindowsCrossBuildStep(arch, options) and a grouped windows-cross step with per-arch soft-fail cross-builds, wired to depend on linux image builds when available.
Documentation
docs/project/building-windows.mdx, scripts/build/CLAUDE.md
Adds a “Cross-compiling from Linux” section, LTO notes, and internal docs updates describing hostCxx usage and winsysroot behavior.
Windows sysroot provisioning
scripts/build/winsysroot.ts
Implements XWIN_VERSION, isCompleteWindowsSysroot(dir, arch), ensureWindowsSysroot(cfg), and fetchWindowsSysroot() to download/extract and xwin splat a winsysroot with safety checks and case-aliasing.
Config & toolchain resolution
scripts/build/config.ts, scripts/build/tools.ts, scripts/build/configure.ts
Adds hostCc/hostCxx/winsysroot to Config/Toolchain, target-aware resolveLlvmToolchain, detectWindowsSysroot(), and resolveConfig changes (ASAN/LTO gating, buildDir suffix, winsysroot resolution, crossTarget selection).
Profiles & WebKit dep
scripts/build/profiles.ts, scripts/build/deps/webkit.ts
Adds windows-x64/windows-arm64 (debug/release) profiles and updates WEBKIT_VERSION + prebuilt cache key to append -windows for cross-compiles.
Cargo config & flags
scripts/build/cargo-config.ts, scripts/build/flags.ts
Generates per-target Cargo linker using cfg.hostCxx, adds /winsysroot compile and /winsysroot:<value> linker flags, and adjusts Windows LTO and linker options.
scripts/build.ts: ninja env & CLI
scripts/build.ts
Adds ninjaEnv(cfg, env) to scrub CPATH-style include vars during Windows cross-compiles, passes cfg to ninja invocations, and accepts winsysroot CLI override (documented in help).
Windows resources and linking
scripts/build/bun.ts
Embeds icon+manifest via resource compilation, templates .rc, adds winsysroot include dirs to rc /I flags, and removes linker-based manifest embedding.
Compile/link tool wiring
scripts/build/compile.ts, scripts/build/source.ts, scripts/build/tools.ts, scripts/build/deps/boringssl.ts
Uses host-aware quoting, pins link tool path via -clang:-B<dir>, compiles host tools with hostCc, reforms NASM flags and include handling for boringssl.
Rust shim & LTO
scripts/build/rust.ts
Reworks rust_shim rule outputs/stamping for cross builds, conditionally adds /winsysroot to shim link args, and updates LTO gating for Windows cross ThinLTO scenarios.
Allowlist & ICU gating
scripts/verify-baseline-static/allowlist-x64-windows.txt, src/jsc/bindings/bun_icu_decompress.cpp
Adjusts CPU feature tags in the Windows allowlist and expands the ICU decompress compile guard to include Windows.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Jarred-Sumner
  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding support for cross-compiling Windows x64 and arm64 from Linux. It directly relates to the substantial changeset focused on Windows cross-compilation infrastructure.
Description check ✅ Passed The description comprehensively covers the PR's scope and implementation. It includes what the PR does, how it works, ThinLTO details, usage examples, validation results, and important notes for reviewers, exceeding the basic template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:06 AM PT - May 26th, 2026

@Jarred-Sumner, your commit 639cdd2915b86e7e07db86ccd3f60723318499f2 passed in Build #58193! 🎉


🧪   To try this PR locally:

bunx bun-pr 31300

That installs a local version of the PR into your bun-31300 executable, so you can run:

bun-31300 --bun

Comment thread scripts/build/cargo-config.ts
Comment thread docs/project/building-windows.mdx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3d5fe5e and 1060fe4.

📒 Files selected for processing (18)
  • .buildkite/Dockerfile
  • .buildkite/ci.mjs
  • docs/project/building-windows.mdx
  • scripts/bootstrap.sh
  • scripts/build.ts
  • scripts/build/CLAUDE.md
  • scripts/build/bun.ts
  • scripts/build/cargo-config.ts
  • scripts/build/compile.ts
  • scripts/build/config.ts
  • scripts/build/configure.ts
  • scripts/build/deps/webkit.ts
  • scripts/build/flags.ts
  • scripts/build/profiles.ts
  • scripts/build/rust.ts
  • scripts/build/source.ts
  • scripts/build/tools.ts
  • scripts/build/winsysroot.ts

Comment thread .buildkite/ci.mjs
Comment thread scripts/build.ts
Comment thread scripts/build/winsysroot.ts
Comment thread scripts/build/rust.ts Outdated
Comment thread scripts/build/tools.ts
Comment thread scripts/build/compile.ts
Comment thread scripts/build/config.ts
Comment thread scripts/build/bun.ts
Comment thread scripts/build/config.ts Outdated
Comment thread scripts/build/CLAUDE.md Outdated
Comment thread scripts/build/config.ts Outdated
Comment thread scripts/build/rust.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Update on the windows-*-cross-build lanes (build 57455): both lanes got all the way through the xwin splat on the agents and then failed the post-splat completeness check — xwin splat finished but .../winsysroot is missing expected SDK files.

Root cause: with --use-winsysroot-style, xwin writes the SDK tree as Windows Kits/10/include and lib (lowercase), while the check — and clang-cl/lld-link themselves (llvm/lib/WindowsDriver/MSVCPaths.cpp, lld/COFF/Driver.cpp) — compose those paths as title-case Include/Lib. xwin only creates the title-case aliases for its non-winsysroot layout, so on a case-sensitive filesystem nothing resolves.

9d3809e:

  • makes the sysroot sentinel case-tolerant and keys it on the target arch
  • creates the Include/Lib aliases at configure time (works for fetched, baked, and developer-provided splats)
  • gives local builds a clear configure-time error for an incomplete sysroot
  • stops xwin's progress bars from flooding the CI log (they were several MB of redraws per lane)
  • restores the agent-image provisioning: .buildkite/Dockerfile + scripts/bootstrap.sh bake the splat at /opt/winsysroot (plus nasm and the aliases), with the configure-time fetch kept as the fallback for agents that don't have it baked

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 debian-13-x64-asan-test-bun failure on 57455 was a test shard unrelated to this diff (no runtime or test code is touched); this push re-runs it.

Comment thread .buildkite/ci.mjs Outdated
@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

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 C_INCLUDE_PATH/CPLUS_INCLUDE_PATH for the host gcc toolchain, clang adds those for every compile regardless of --target, so the first vendor C++ files resolved <vector>/<ostream> from the host libstdc++ instead of the MSVC STL ('bits/c++config.h' file not found).

d8141d6 scrubs CPATH/C_INCLUDE_PATH/CPLUS_INCLUDE_PATH/OBJC_INCLUDE_PATH from the ninja environment for Windows cross builds only. Verified locally: with one of those vars pointing at a host libstdc++, clang-cl targeting *-pc-windows-msvc reproduces the exact CI error, and with the change the vars are absent from ninja's environment for cross builds while native builds keep them untouched.

The debian-13-x64-test-bun failure on 57472 is the flaky streams-leak.test.ts perf-threshold test (expected >20%, got 11–19% across retries) — unrelated to this diff; this push re-runs it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1060fe4 and d8141d6.

📒 Files selected for processing (13)
  • .buildkite/Dockerfile
  • .buildkite/ci.mjs
  • docs/project/building-windows.mdx
  • scripts/bootstrap.sh
  • scripts/build.ts
  • scripts/build/CLAUDE.md
  • scripts/build/bun.ts
  • scripts/build/compile.ts
  • scripts/build/config.ts
  • scripts/build/configure.ts
  • scripts/build/deps/boringssl.ts
  • scripts/build/rust.ts
  • scripts/build/winsysroot.ts

Comment thread .buildkite/Dockerfile Outdated
Comment thread .buildkite/Dockerfile Outdated
Comment thread scripts/build/rust.ts
@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

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: src/jsc/bindings/image_wic_shim.cpp uses int32_t/uint8_t without including <cstdint> — a Visual Studio header set provides them transitively, the xwin-splatted SDK does not.

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-platforms subset filter, the Dockerfile downloads the xwin tarball to a file instead of piping curl into tar, XWIN_VERSION copies cross-reference each other, and the stale rust_shim comment clause is gone.

Comment thread docs/project/building-windows.mdx Outdated
@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

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 src/jsc/bindings/windows/rescle.cpp'atlstr.h' file not found: ATL is an optional VS component that the xwin splat wasn't including.

6d62bcf adds --include-atl to every splat (configure-time fetch, Dockerfile, bootstrap.sh, docs). xwin merges the ATL headers/libs into the VC include/lib dirs, so clang-cl and lld-link find them through /winsysroot with no flag changes. The sysroot sentinel now also requires the ATL headers, so the splats cached on agents from earlier runs get re-fetched instead of silently reused without ATL.

The debian-13-x64-test-bun failure on 57499 is the same streams-leak.test.ts perf-threshold flake as the previous build — unrelated to this diff; the push re-runs it.

Comment thread scripts/bootstrap.sh Outdated
@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Build 57510: my mistake in the previous commit — --include-atl is a top-level xwin option (payload selection), not a splat option, so both cross lanes failed at argument parsing before splatting. d0cfd2f moves it before the subcommand in all four invocations and the two error hints; the exact argument order is verified against the pinned xwin 0.6.7 binary. The bootstrap splat also drops stdout now (same as the Dockerfile/configure-time callers), which closes the last open review thread.

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.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Build 57551: both Windows cross-compile lanes pass. windows-x64-cross-build finished in 9m07s and windows-aarch64-cross-build in 12m47s — each one fetches the xwin sysroot (CRT + SDK + ATL), compiles all 1151 targets (deps, BoringSSL + NASM asm, WebCore/JSC bindings, the Rust workspace), and lld-link produces bun.exe on the Linux agent. The PR description's validation section is updated accordingly.

The build itself is red only on unrelated lanes: the known-flaky streams-leak.test.ts perf threshold on alpine-3.23-x64-test-bun (same test that flaked on the debian lanes in earlier builds) and two darwin-aarch64 test jobs that expired waiting for macOS agents (pool capacity; they're being retried within the build). Nothing in this diff touches those paths.

From here the open items are for a maintainer: sign-off on the xwin --accept-license terms (called out in the description) and, once the lanes have a green history, deciding when to drop soft_fail and/or whether to publish the cross-built artifacts.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Status: the Windows cross-compile work is done and its lanes keep passing; the branch is now being iterated on directly by the maintainer.

  • The branch has since been rebased and extended directly on top of this work (ThinLTO + cross-language LTO for the Windows x64 cross build, embedding bun.exe.manifest via the resource compiler, a temporary WEBKIT_VERSION pin to a preview build). On the current tip (16da0f2, build 58098) both cross lanes still pass — windows-x64-cross-build-bun and windows-aarch64-cross-build-bun — along with 71 other checks; the remaining red (package-binary-size, windows-x64-baseline-verify-baseline, one darwin-14 test shard) corresponds to those newer experimental commits / known-flaky lanes, not the original cross-compile plumbing.
  • For history: with the original branch contents, the cross lanes passed on builds 57551, 57604, 57938 and the only test red ever observed was pre-existing Windows transpiler.test.js stack-overflow tests unrelated to this diff (they passed again on 58098).
  • Open decisions from the description remain: sign-off on the CI sysroot fetch running xwin --accept-license, and when to drop soft_fail from the cross lanes / publish their artifacts.

@robobun
robobun force-pushed the farm/a92c73e4/windows-cross-compile branch from 881cc5f to 5df8f04 Compare May 25, 2026 14:35
robobun added 5 commits May 26, 2026 00:49
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.
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/a92c73e4/windows-cross-compile branch from 5df8f04 to 16da0f2 Compare May 26, 2026 01:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d8141d6 and 16da0f2.

📒 Files selected for processing (19)
  • .buildkite/Dockerfile
  • .buildkite/ci.mjs
  • docs/project/building-windows.mdx
  • scripts/bootstrap.sh
  • scripts/build.ts
  • scripts/build/CLAUDE.md
  • scripts/build/bun.ts
  • scripts/build/cargo-config.ts
  • scripts/build/compile.ts
  • scripts/build/config.ts
  • scripts/build/configure.ts
  • scripts/build/deps/boringssl.ts
  • scripts/build/deps/webkit.ts
  • scripts/build/flags.ts
  • scripts/build/profiles.ts
  • scripts/build/rust.ts
  • scripts/build/source.ts
  • scripts/build/tools.ts
  • scripts/build/winsysroot.ts

Comment thread scripts/build/CLAUDE.md
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.
Comment thread test/internal/windows-cross-config.test.ts
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
Comment thread scripts/build/compile.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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Pick one SDK/MSVC version instead of adding every include directory.

If a winsysroot contains multiple VC/Tools/MSVC/* or Windows Kits/10/Include/* versions, llvm-rc will search them in readdirSync() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 16da0f2 and 639cdd2.

📒 Files selected for processing (6)
  • scripts/build/bun.ts
  • scripts/build/deps/webkit.ts
  • scripts/build/flags.ts
  • scripts/build/rust.ts
  • scripts/verify-baseline-static/allowlist-x64-windows.txt
  • src/jsc/bindings/bun_icu_decompress.cpp

Comment thread src/windows-app-info.rc
Comment on lines +5 to +9
// 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@"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

  1. Before this PR, native Windows build-bun step: emitBun()ldflags = [..., ...manifestLinkFlags(cfg), ...] → link rule passes /link ... /MANIFEST:EMBED /MANIFESTINPUT:.../src/bun.exe.manifest to clang-cl, which forwards to lld-link.
  2. 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.
  3. mt.exe -inputresource:bun.exe;#1 -out:con (or llvm-readobj --coff-resources) on a pre-PR bun.exe would show both <windowsSettings> and <trustInfo>.
  4. After this PR, same native lane: ldflags no longer contains /MANIFEST:*. emitWindowsResources() writes windows-app-info.rc with 1 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.
  5. The same dump on a post-PR bun.exe would 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.

@Jarred-Sumner
Jarred-Sumner merged commit 4c954ab into main May 26, 2026
80 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/a92c73e4/windows-cross-compile branch May 26, 2026 12:18
springmin pushed a commit to springmin/bun that referenced this pull request May 26, 2026
* 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
springmin pushed a commit to springmin/bun that referenced this pull request May 26, 2026
* 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
Jarred-Sumner added a commit that referenced this pull request May 28, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants