Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
98f80ec
build: support cross-compiling Windows targets from Linux
robobun May 24, 2026
eee64c4
docs: fix cfg.hostCxx references and cross-compile build dir name
robobun May 24, 2026
b9c61d9
ci: add Windows cross-compile build lanes; always fetch the winsysroo…
robobun May 24, 2026
d299067
Address review feedback on Windows cross-compile support
robobun May 24, 2026
4f53a8a
Fetch the Windows sysroot during configure, before the graph is emitted
robobun May 24, 2026
75f44c5
Gate cross-language LTO off for Windows targets
robobun May 24, 2026
f7c75a1
docs: list winsysroot.ts in the build-script inventory and fix a stal…
robobun May 24, 2026
626b84a
Drop the vestigial restat flag on the rust_shim rule
robobun May 24, 2026
00a0df4
Fix Windows sysroot layout handling and bake the splat into CI images
robobun May 24, 2026
0b89904
Keep host include-path env vars out of Windows cross-compiles
robobun May 24, 2026
c5cce66
Include <cstdint> in image_wic_shim.cpp
robobun May 24, 2026
30e0067
Address review feedback on the cross-compile CI plumbing
robobun May 24, 2026
425220d
Add a source check for explicit <cstdint> in the SDK shims
robobun May 24, 2026
8e45ba3
docs: create the Include/Lib aliases in the manual splat example
robobun May 24, 2026
9801dc5
Include ATL in the Windows cross-compile sysroot
robobun May 24, 2026
3a9650a
Pass --include-atl as a top-level xwin option
robobun May 24, 2026
130eb2d
ci: retrigger
robobun May 24, 2026
0b8742c
Enable ThinLTO with cross-language LTO for Windows x64 cross builds
Jarred-Sumner May 26, 2026
a93ffb3
docs/ci: note the Windows x64 cross LTO configuration
Jarred-Sumner May 26, 2026
0e44653
windows: embed bun.exe.manifest via the resource compiler instead of …
Jarred-Sumner May 26, 2026
16da0f2
Point WEBKIT_VERSION at the oven-sh/WebKit#239 preview build (TEMPORARY)
Jarred-Sumner May 26, 2026
1ebba50
windows: restore /OPT:SAFEICF
Jarred-Sumner May 26, 2026
3deb890
verify-baseline: widen icelake simdutf ceilings for the cross-built WTF
Jarred-Sumner May 26, 2026
1a83cd3
Enable the lazy zstd ICU data decompression hook on Windows
Jarred-Sumner May 26, 2026
551b532
Bump WEBKIT_VERSION to the oven-sh/WebKit#239 preview with zstd ICU d…
Jarred-Sumner May 26, 2026
8cda76f
Merge remote-tracking branch 'origin/main' into farm/a92c73e4/windows…
Jarred-Sumner May 26, 2026
639cdd2
Bump WEBKIT_VERSION to oven-sh/WebKit main 963f8758c29e
Jarred-Sumner May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .buildkite/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common apt-transport-https \
ca-certificates gnupg lsb-release unzip xz-utils \
libxml2-dev ruby ruby-dev bison gawk perl make golang ccache qemu-user-static \
nasm \
&& add-apt-repository ppa:ubuntu-toolchain-r/test \
&& apt-get update \
&& apt-get install -y gcc-13 g++-13 libgcc-13-dev libstdc++-13-dev \
Expand Down Expand Up @@ -127,6 +128,7 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
&& rustup default nightly \
&& rustup target add aarch64-linux-android x86_64-linux-android \
&& rustup target add x86_64-unknown-freebsd \
&& rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc \
&& rustup component add rust-src

# Android NDK — sysroot/libc++/compiler-rt for --abi=android cross-compile.
Expand Down Expand Up @@ -172,6 +174,30 @@ RUN FBSD_ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "arm64"; else echo "am
&& rm /tmp/base.txz
ENV FREEBSD_SYSROOT=/opt/freebsd-sysroot

# Windows sysroot — xwin splat of the MSVC CRT/STL + Windows SDK + ATL (VS
# layout) for --os=windows cross-compile; clang-cl/lld-link consume it via
# /winsysroot (see scripts/build/config.ts `winsysroot`). Both target arches
# in one splat; --include-debug-libs so /MTd debug links work; --include-atl
# for <atlstr.h> (rescle.cpp).
# --accept-license accepts the Microsoft license terms for the SDK/CRT
# components, same as the Windows CI images do when installing VS Build Tools.
# If the image predates this layer, configure falls back to fetching the same
# splat at build time (scripts/build/winsysroot.ts — keep XWIN_VERSION in sync).
# The Include/Lib aliases exist because clang-cl/lld-link compose SDK paths
# in title case while the winsysroot-style splat writes lowercase.
ARG XWIN_VERSION="0.6.7"
RUN XWIN_ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64"; else echo "x86_64"; fi) \
&& curl -fsSL "https://github.com/Jake-Shadle/xwin/releases/download/${XWIN_VERSION}/xwin-${XWIN_VERSION}-${XWIN_ARCH}-unknown-linux-musl.tar.gz" \
-o /tmp/xwin.tar.gz \
&& tar -xzf /tmp/xwin.tar.gz -C /tmp \
&& /tmp/xwin-${XWIN_VERSION}-${XWIN_ARCH}-unknown-linux-musl/xwin --accept-license --arch x86_64,aarch64 --include-atl --cache-dir /tmp/xwin-cache \
splat --use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs --output /opt/winsysroot \
> /dev/null \
&& ln -s include "/opt/winsysroot/Windows Kits/10/Include" \
&& ln -s lib "/opt/winsysroot/Windows Kits/10/Lib" \
&& rm -rf /tmp/xwin.tar.gz /tmp/xwin-${XWIN_VERSION}-${XWIN_ARCH}-unknown-linux-musl /tmp/xwin-cache
ENV WINDOWS_SYSROOT=/opt/winsysroot

RUN ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "arm64"; else echo "amd64"; fi) && \
echo "Downloading buildkite" && \
curl -fsSL "https://github.com/buildkite/agent/releases/download/v3.87.0/buildkite-agent-linux-${ARCH}-3.87.0.tar.gz" -o /tmp/buildkite-agent.tar.gz && \
Expand Down
73 changes: 73 additions & 0 deletions .buildkite/ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,57 @@ function getLinkBunStep(platform, options) {
};
}

/**
* Cross-compiled Windows build (full compile + link on a Linux agent).
* Validates that bun.exe for the given arch can be built from Linux with
* clang-cl + lld-link + an xwin Windows sysroot — baked into newer agent
* images (.buildkite/Dockerfile), fetched at configure time on agents that
* don't have one (scripts/build/winsysroot.ts). The x64 lane additionally
* exercises the ThinLTO + cross-language LTO configuration (the ci-release
* default for windows x64 cross — see config.ts), which the native Windows
* lanes never had: clang-cl/rustc bitcode + the -lto WebKit prebuilt linked
* by rustc's lld-link. The produced binary is not consumed by tests or
* release — the native Windows lanes above stay authoritative — so the step
* is soft_fail until it has a green history.
*
* Runs on the same amazonlinux docker image the other Linux/cross builds
* use; `--buildkite=off` keeps the per-step artifact upload/download
* machinery (which assumes the cpp/rust/link split and native artifact
* names) out of the picture.
*
* @param {Arch} arch
* @param {PipelineOptions} options
* @returns {Step}
*/
function getWindowsCrossBuildStep(arch, options) {
const hostPlatform = { os: "linux", arch, distro: "amazonlinux", release: "2023", features: ["docker"] };
return {
key: `windows-${arch}-cross-build`,
label: `${getBuildkiteEmoji("windows")} ${arch}-cross - build-bun`,
agents: getEc2Agent(hostPlatform, options, {
// Full build (deps + C++ + cargo + link) in one step — size for cores.
instanceType: arch === "aarch64" ? "r8g.4xlarge" : "r7i.4xlarge",
}),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
soft_fail: true,
timeout_in_minutes: 120,
command: [
// BoringSSL's win-x64 assembly is NASM syntax; newer images carry nasm
// (Dockerfile), best-effort install on older ones. Distro-aware: the
// agent may be the Ubuntu-based build container (apt) or an Amazon
// Linux host (dnf/yum). `|| true` keeps a missing package manager from
// failing the step — the build's own "nasm not found" error is clearer.
...(arch === "x64"
? [
"which nasm || (apt-get update -qq && apt-get install -y -qq nasm) || dnf install -y -q nasm || yum install -y -q nasm || sudo dnf install -y -q nasm || true",
]
: []),
`node --experimental-strip-types scripts/build.ts --profile=ci-release --os=windows --arch=${arch} --buildkite=off`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
],
};
}

/**
* Returns the artifact triplet for a platform, e.g. "bun-linux-aarch64" or "bun-linux-x64-musl-baseline".
* Matches the naming convention in cmake/targets/BuildBun.cmake.
Expand Down Expand Up @@ -1428,6 +1479,28 @@ async function getPipeline(options = {}) {
);
}),
);

// Windows cross-compile validation: full builds of bun.exe (x64 + arm64)
// from Linux agents. See getWindowsCrossBuildStep(). Honours the same
// platform filtering as the per-target groups above, so a manual build
// that narrows `build-platforms` to a non-windows subset doesn't spawn
// the cross lanes.
if (relevantBuildPlatforms.some(({ os }) => os === "windows")) {
const crossImageDependsOn = ["x64", "aarch64"]
.map(arch => getImageKey({ os: "linux", arch, distro: "amazonlinux", release: "2023", features: ["docker"] }))
.filter(imageKey => imagePlatforms.has(imageKey))
.map(imageKey => `${imageKey}-build-image`);
steps.push(
getStepWithDependsOn(
{
key: "windows-cross",
group: `${getBuildkiteEmoji("windows")} cross (linux)`,
steps: [getWindowsCrossBuildStep("x64", options), getWindowsCrossBuildStep("aarch64", options)],
},
...crossImageDependsOn,
),
);
}
}

if (!isMainBranch()) {
Expand Down
51 changes: 51 additions & 0 deletions docs/project/building-windows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,54 @@ bun-debug test "C:\bun\test\js\bun\resolve\import-meta.test.js"
### failed to write output 'bun-debug.exe': permission denied

you cannot overwrite `bun-debug.exe` if it is already open. you likely have a running instance, maybe in the vscode debugger?

## Cross-compiling from Linux

Windows binaries (both x64 and arm64) can also be built on a Linux host. The build uses the host LLVM's `clang-cl`, `lld-link`, `llvm-lib` and `llvm-rc` — which are part of every LLVM distribution — plus an "xwin splat" of the MSVC CRT/STL and Windows SDK for headers and import libraries.

### Prerequisites

1. The same LLVM version a native build uses (see `scripts/bootstrap.sh` `llvm_version_exact`), installed so that `clang-cl`, `lld-link`, `llvm-lib` and `llvm-rc` are available. On Debian/Ubuntu, `apt.llvm.org` packages provide all of them.
2. `nasm` (only needed for Windows x64 — BoringSSL's x64 assembly is NASM syntax).
3. Rust std for the Windows targets (`rust-toolchain.toml` lists them; `rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc` if missing).
4. A Windows sysroot: an [xwin](https://github.com/Jake-Shadle/xwin) splat of the MSVC CRT, Windows SDK, and ATL laid out like a Visual Studio install. Note that downloading these components means accepting Microsoft's license terms for them.

```bash
cargo install xwin # or download a release binary
xwin --accept-license --arch x86_64,aarch64 --include-atl splat \
--use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs \
--output /opt/winsysroot
# clang-cl/lld-link look up SDK paths as "Include"/"Lib"; the splat writes
# them lowercase, so alias both spellings (needs the same privileges as the
# splat — configure creates these itself when the directory is writable).
ln -s include "/opt/winsysroot/Windows Kits/10/Include"
ln -s lib "/opt/winsysroot/Windows Kits/10/Lib"
```

The build looks for the sysroot at `/opt/winsysroot` (or `/opt/xwin`) automatically; elsewhere, set `WINDOWS_SYSROOT=<path>` or pass `--winsysroot=<path>` (a user-writable path also lets configure manage the aliases for you). Configure validates the splat at the start of every cross build. CI agents bake the same splat into their images (`.buildkite/Dockerfile`, `scripts/bootstrap.sh`); when an agent doesn't have one, the build fetches it into its cache dir at configure time.

### Building

```bash
# Debug builds
bun run build --profile=windows-x64
bun run build --profile=windows-arm64

# Release builds
bun run build --profile=windows-x64-release
bun run build --profile=windows-arm64-release
```

Output lands in `build/debug-windows-x64/bun-debug.exe`, `build/release-windows-aarch64/bun-profile.exe` + `bun.exe`, etc. Equivalent raw flags: `bun run build --os=windows --arch=aarch64`.

Cross-compiled executables are not run on the host (the `--revision` smoke test is skipped), so test them on a Windows machine or under Wine.

### LTO

x64 release cross builds support ThinLTO with cross-language (Rust↔C++) LTO, and CI's windows x64 cross lane builds with it by default. Locally it's opt-in:

```bash
bun run build --profile=windows-x64-release --lto=on
```

This compiles bun's C/C++ with `-flto=thin`, makes rustc emit LLVM bitcode (`-Clinker-plugin-lto`), pulls the `bun-webkit-windows-amd64-lto` ThinLTO prebuilt, and links everything with rustc's bundled `lld-link` (its LLVM is new enough to read both compilers' bitcode). There is no LTO for arm64 (no `-lto` WebKit prebuilt — LLVM's CodeView emitter can't handle ARM64 NEON tuple registers during LTO codegen) or for `--baseline`.
68 changes: 68 additions & 0 deletions scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,7 @@ install_build_essentials() {
linux)
install_packages \
make \
nasm \
python3 \
libtool \
ruby \
Expand All @@ -1142,6 +1143,7 @@ install_build_essentials() {
install_rust
install_android_ndk
install_freebsd_sysroot
install_windows_sysroot
install_ccache
install_docker
}
Expand Down Expand Up @@ -1318,6 +1320,9 @@ install_rust() {
# preinstalling keeps that step off the network.
execute_as_user "$rustup" target add aarch64-apple-darwin
execute_as_user "$rustup" target add x86_64-apple-darwin
# Windows cross-compile targets (--os=windows from a linux host).
execute_as_user "$rustup" target add x86_64-pc-windows-msvc
execute_as_user "$rustup" target add aarch64-pc-windows-msvc
# rust-src for -Zbuild-std (Tier 3 targets without prebuilt std).
execute_as_user "$rustup" component add rust-src
;;
Expand Down Expand Up @@ -1401,6 +1406,69 @@ install_freebsd_sysroot() {
# arch-appropriate /opt/freebsd-sysroot{,-arm64} by well-known path.
}

xwin_version() {
# Keep in sync with XWIN_VERSION in scripts/build/winsysroot.ts and
# .buildkite/Dockerfile.
print "0.6.7"
}

install_windows_sysroot() {
case "$os" in
linux) ;;
*) return ;;
esac

# MSVC CRT/STL + Windows SDK splat for --os=windows cross-compiles,
# laid out like a Visual Studio install so clang-cl/lld-link's
# /winsysroot flag works (see scripts/build/config.ts `winsysroot`).
# Fetched with xwin, which downloads the components from Microsoft's CDN;
# --accept-license accepts the Microsoft Software License Terms for the
# Build Tools/SDK on behalf of this machine (same terms the Windows CI
# images accept when installing VS Build Tools). Machines that skip this
# step still work: configure fetches the same splat at build time when
# none is present (scripts/build/winsysroot.ts).
sysroot="/opt/winsysroot"
# Same sentinel scripts/build/winsysroot.ts isCompleteWindowsSysroot()
# uses: the SDK lib tree plus a kernel32 import lib plus the ATL headers
# (--include-atl), so a half-splatted or pre-ATL sysroot isn't treated as
# complete. xwin writes the SDK dirs/files lowercase; a copied VS install
# is title-case — accept both.
if ls "$sysroot/Windows Kits/10/"[Ll]ib/*/um/x64/kernel32.[Ll]ib >/dev/null 2>&1 &&
ls "$sysroot"/VC/Tools/MSVC/*/include/atlstr.h >/dev/null 2>&1; then
return
fi

xwin_ver="$(xwin_version)"
case "$arch" in
aarch64) xwin_triple="aarch64-unknown-linux-musl" ;;
*) xwin_triple="x86_64-unknown-linux-musl" ;;
esac
xwin_tar=$(download_file "https://github.com/Jake-Shadle/xwin/releases/download/${xwin_ver}/xwin-${xwin_ver}-${xwin_triple}.tar.gz")
xwin_dir="$(dirname "$xwin_tar")/xwin-extract"
execute mkdir -p "$xwin_dir"
execute tar -xzf "$xwin_tar" -C "$xwin_dir" --strip-components=1

execute_sudo rm -rf "$sysroot"
execute_sudo mkdir -p "$sysroot"
# Both target arches in one splat; --include-debug-libs so /MTd (debug
# CRT) links work; --include-atl for <atlstr.h> (rescle.cpp);
# winsysroot-style + MS arch notation so clang-cl and lld-link resolve it
# with a single /winsysroot flag; symlinks stay ON (default) to fix
# include/lib casing on a case-sensitive filesystem.
# stdout is dropped: xwin draws progress bars there even without a TTY,
# which floods the image-build log. Errors stay on stderr.
execute_sudo "$xwin_dir/xwin" --accept-license --arch x86_64,aarch64 --include-atl --cache-dir "$xwin_dir/cache" \
splat --use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs \
--output "$sysroot" >/dev/null
# clang-cl/lld-link compose SDK paths as "Include"/"Lib" (title case);
# the winsysroot-style splat writes lowercase — alias both spellings.
execute_sudo ln -s include "$sysroot/Windows Kits/10/Include"
execute_sudo ln -s lib "$sysroot/Windows Kits/10/Lib"
execute_sudo rm -rf "$xwin_dir"
# No WINDOWS_SYSROOT export — detectWindowsSysroot() picks up
# /opt/winsysroot by well-known path.
}

install_docker() {
case "$pm" in
brew)
Expand Down
30 changes: 25 additions & 5 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,23 @@ async function main(): Promise<void> {
: { profile: args.profile, overrides: args.overrides };

const ninjaArgv = (cfg: { buildDir: string }) => ["-C", cfg.buildDir, ...args.ninjaArgs, ...args.ninjaTargets];
const ninjaEnv = (env: Record<string, string>) => ({ ...process.env, ...env });
// GNU-style include-path vars (CPATH, C_INCLUDE_PATH, CPLUS_INCLUDE_PATH,
// OBJC_INCLUDE_PATH) apply to every clang invocation regardless of
// --target. The CI build containers set them for the *host* gcc toolchain
// (.buildkite/Dockerfile), which hijacks <vector> & co. away from the MSVC
// STL when cross-compiling for Windows ("'bits/c++config.h' file not
// found"). Scrub them for Windows cross builds — they are host-targeted by
// definition. Native Windows builds (INCLUDE/LIB from the VS dev shell) and
// every other target keep the environment as provisioned.
const ninjaEnv = (cfg: { windows: boolean; host: { os: string } }, env: Record<string, string>) => {
const merged: NodeJS.ProcessEnv = { ...process.env, ...env };
if (cfg.windows && cfg.host.os !== "windows") {
for (const name of ["CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"]) {
delete merged[name];
}
}
return merged;
};

if (isCI) {
// CI: machine/env dump + collapsible groups + annotation-on-failure.
Expand All @@ -109,7 +125,7 @@ async function main(): Promise<void> {
}

await startGroup("Build", () =>
spawnWithAnnotations("ninja", ninjaArgv(result.cfg), { label: "ninja", env: ninjaEnv(result.env) }),
spawnWithAnnotations("ninja", ninjaArgv(result.cfg), { label: "ninja", env: ninjaEnv(result.cfg, result.env) }),
);

// cpp-only/rust-only: upload build outputs for downstream link-only.
Expand Down Expand Up @@ -182,7 +198,7 @@ async function main(): Promise<void> {
}
const ninja = spawnSync("ninja", ninjaArgv(result.cfg), {
stdio,
env: ninjaEnv(result.env),
env: ninjaEnv(result.cfg, result.env),
// cargo's compile output (now part of the ninja graph via emitRust) can
// be tens of MB on a cold build; the default 1 MB maxBuffer ENOBUFSes.
maxBuffer: 1024 * 1024 * 1024,
Expand Down Expand Up @@ -392,6 +408,7 @@ function parseArgs(argv: string[]): CliArgs {
"androidNdk",
"macosSdk",
"osxDeploymentTarget",
"winsysroot",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]);

for (let i = 0; i < argv.length; i++) {
Expand Down Expand Up @@ -501,13 +518,16 @@ Options:
--profile=<name> Build profile (default: debug)
Profiles: debug, debug-local, debug-no-asan,
release, release-local, release-asan,
release-assertions, ci-*
release-assertions, ci-*,
windows-{x64,arm64}[-release] (cross-compile
from a non-Windows host)
--<field>=<value> Override a config field. Boolean fields take
on/off/true/false/yes/no/1/0.
Fields: asan, lto, assertions, logs, baseline,
canary, valgrind, webkit (prebuilt|local),
buildDir, mode (full|cpp-only|link-only),
unifiedSources, timeTrace
unifiedSources, timeTrace, os, arch, abi,
winsysroot (Windows cross-compile SDK root)
--target=<name> Build a specific ninja target (repeatable)
--configure-only Emit build.ninja, don't run it
-j<N>, -v, -k<N> Passed through to ninja
Expand Down
Loading
Loading