/** * Build-config regression tests for cross-compiling Windows binaries from a * non-Windows host (scripts/build/config.ts + flags.ts), with a focus on the * LTO configuration: Windows x64 cross builds use ThinLTO with cross-language * (Rust↔C++) LTO through rustc's bundled lld-link. * * These exercise the configure-time logic only — no compiler, sysroot, or * WebKit download is involved — so they run on every platform. Scenarios that * specifically cover the "windows target on a non-windows host" path are * skipped on Windows, where the same inputs intentionally resolve to the * native toolchain instead. */ import { describe, expect, test } from "bun:test"; import { isWindows, tempDir } from "harness"; import { join } from "node:path"; import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../../scripts/build/config.ts"; import { webkit } from "../../../scripts/build/deps/webkit.ts"; import { computeFlags } from "../../../scripts/build/flags.ts"; import { rustTarget } from "../../../scripts/build/rust.ts"; /** A fully-populated fake toolchain — resolveConfig never spawns any of these. */ function mockToolchain(overrides: Partial = {}): Toolchain { return { cc: "/fake/llvm/bin/clang-cl", cxx: "/fake/llvm/bin/clang-cl", clangVersion: "21.1.8", clangResourceDir: "/fake/llvm/lib/clang/21", ar: "/fake/llvm/bin/llvm-lib", ranlib: undefined, ld: "/fake/llvm/bin/lld-link", ld64Lld: undefined, rustLld: undefined, rustLlvmVersion: "22.1.4", rustSysroot: undefined, rustHostTriple: undefined, strip: "/fake/llvm/bin/llvm-strip", llvmStrip: "/fake/llvm/bin/llvm-strip", dsymutil: undefined, bun: "/fake/bin/bun", jsRuntime: "/fake/bin/bun", esbuild: "/fake/bin/esbuild", ccache: undefined, cmake: "/fake/bin/cmake", cargo: undefined, cargoHome: undefined, rustupHome: undefined, msvcLinker: undefined, rc: "/fake/llvm/bin/llvm-rc", mt: undefined, nasm: "/fake/bin/nasm", ...overrides, }; } /** * Shorthand: resolve a config for a Windows target the way the CI cross lane * does (`--profile=ci-release --os=windows --arch=`): Release + ci so * the LTO default applies, with an explicit fake sysroot so the local-build * "create one with xwin" error never triggers. */ function resolveWindowsCross(partial: PartialConfig = {}, toolchain = mockToolchain()): Config { return resolveConfig( { os: "windows", arch: "x64", buildType: "Release", ci: true, buildkite: false, winsysroot: "/fake/winsysroot", ...partial, }, toolchain, ); } describe.skipIf(isWindows)("Windows cross-compile LTO config (non-windows host)", () => { test("ci release x64 cross builds default to ThinLTO with cross-language LTO", () => { const cfg = resolveWindowsCross(); expect(cfg.windows).toBe(true); expect(cfg.crossTarget).toBe("x86_64-pc-windows-msvc"); expect(cfg.lto).toBe(true); // Rust↔C++ inlining: rustc emits bitcode (-Clinker-plugin-lto) and the // final lld-link runs one ThinLTO graph across both halves. expect(cfg.crossLangLto).toBe(true); }); test("no -lto WebKit prebuilt exists for arm64 — LTO is forced off there", () => { // arm64: LLVM's CodeView emitter aborts on ARM64 NEON tuple registers // during LTO codegen, so oven-sh/WebKit ships no windows-arm64-lto. const arm64 = resolveWindowsCross({ arch: "aarch64" }); expect(arm64.lto).toBe(false); expect(arm64.crossLangLto).toBe(false); // Forced off even when explicitly requested, so the WebKit fetch never // 404s on a tarball that doesn't exist. expect(resolveWindowsCross({ arch: "aarch64", lto: true }).lto).toBe(false); }); test("local (non-ci) release builds stay non-LTO unless asked", () => { const local = resolveWindowsCross({ ci: false }); expect(local.lto).toBe(false); const explicit = resolveWindowsCross({ ci: false, lto: true, baseline: false }); expect(explicit.lto).toBe(true); expect(explicit.crossLangLto).toBe(true); }); test("compile flags use clang-cl ThinLTO without whole-program vtables", () => { const flags = computeFlags(resolveWindowsCross({ lto: true, baseline: false })); expect(flags.cxxflags).toContain("-flto=thin"); expect(flags.cflags).toContain("-flto=thin"); // Every summaried module must agree on EnableSplitLTOUnit; rustc's // bitcode says 0, so the C/C++ side must too. expect(flags.cxxflags).toContain("-fno-split-lto-unit"); expect(flags.cflags).toContain("-fno-split-lto-unit"); // WPD drops vtable symbols that COFF associative COMDAT sections still // reference and the LTO codegen aborts — never passed on Windows. expect(flags.cxxflags).not.toContain("-fwhole-program-vtables"); expect(flags.cxxflags).not.toContain("-fforce-emit-vtables"); // The unix link-side LTO spellings must not leak into lld-link's flags // (everything after /link is parsed as MSVC-style options). expect(flags.ldflags.some(f => f.includes("-flto"))).toBe(false); expect(flags.ldflags.some(f => f.includes("--lto-O"))).toBe(false); // Non-LTO windows configs get none of the LTO flags. const plain = computeFlags(resolveWindowsCross({ lto: false })); expect(plain.cxxflags.some(f => f.includes("-flto"))).toBe(false); expect(plain.cxxflags).not.toContain("-fno-split-lto-unit"); }); test("the link uses rustc's lld-link sibling when rustc's LLVM is newer than clang's", () => { // resolveConfig swaps cfg.ld so lld-link can read the LLVM-22 bitcode // rustc emits under -Clinker-plugin-lto (bitcode is forward-compatible // only). rustc's gcc-ld/ ships every lld flavor; windows needs the // lld-link sibling of the host-flavored rust-lld that findRustLld() // resolves. using dir = tempDir("win-cross-rust-lld", { "gcc-ld/ld.lld": "", "gcc-ld/lld-link": "", }); const rustLld = join(String(dir), "gcc-ld", "ld.lld"); const cfg = resolveWindowsCross( { lto: true, baseline: false }, mockToolchain({ rustLld, rustLlvmVersion: "22.1.4" }), ); expect(cfg.ld).toBe(join(String(dir), "gcc-ld", "lld-link")); // Cargo-driven links (bun_shim_impl.exe) must NOT follow the swap: rustc // treats a linker inside its own gcc-ld/ as rust-lld and prepends // `-flavor link`, which breaks the wrapper. They keep the host lld-link. expect(cfg.msvcLinker).toBe("/fake/llvm/bin/lld-link"); // Without LTO there's no bitcode skew to work around — keep the host // LLVM's lld-link. const plain = resolveWindowsCross({ lto: false }, mockToolchain({ rustLld, rustLlvmVersion: "22.1.4" })); expect(plain.ld).toBe("/fake/llvm/bin/lld-link"); // If rustc's gcc-ld/ ever stops shipping lld-link, fall back to the host // lld-link — validateBunConfig() then reports the version skew at // configure time instead of an opaque "Invalid record" at link time. using bare = tempDir("win-cross-rust-lld-bare", { "gcc-ld/ld.lld": "" }); const bareCfg = resolveWindowsCross( { lto: true, baseline: false }, mockToolchain({ rustLld: join(String(bare), "gcc-ld", "ld.lld"), rustLlvmVersion: "22.1.4" }), ); expect(bareCfg.ld).toBe("/fake/llvm/bin/lld-link"); }); test("LTO selects the -lto WebKit prebuilt with a windows-keyed cache dir", () => { // Default windows x64 cross config (baseline=true, lto=true): every x64 // WebKit is built at the nehalem floor, so the plain -lto tarball is the // one baseline fetches too. const def = webkit.source(resolveWindowsCross()); if (def.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${def.kind}`); expect(def.url).toContain("bun-webkit-windows-amd64-lto.tar.gz"); expect(def.destDir).toContain("-windows"); expect(def.destDir).toEndWith("-lto"); const plain = webkit.source(resolveWindowsCross({ lto: false })); if (plain.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${plain.kind}`); expect(plain.url).toContain("bun-webkit-windows-amd64.tar.gz"); expect(plain.destDir).not.toEndWith("-lto"); const arm64 = webkit.source(resolveWindowsCross({ arch: "aarch64" })); if (arm64.kind !== "prebuilt") throw new Error(`expected prebuilt WebKit source, got ${arm64.kind}`); expect(arm64.url).toContain("bun-webkit-windows-arm64.tar.gz"); }); test("rust side targets pc-windows-msvc triples", () => { const cfg = resolveWindowsCross(); expect(rustTarget(cfg)).toBe("x86_64-pc-windows-msvc"); expect(rustTarget(resolveWindowsCross({ arch: "aarch64" }))).toBe("aarch64-pc-windows-msvc"); }); test("linux LTO config uses ThinLTO with WPD and no-split-lto-unit", () => { const linux = resolveConfig( { os: "linux", arch: "x64", abi: "gnu", buildType: "Release", ci: true, buildkite: false, linuxSysroot: "/fake" }, mockToolchain({ cc: "/fake/llvm/bin/clang", cxx: "/fake/llvm/bin/clang++", ld: "/fake/llvm/bin/ld.lld" }), ); expect(linux.lto).toBe(true); const linuxFlags = computeFlags(linux); expect(linuxFlags.cxxflags).toContain("-flto=thin"); expect(linuxFlags.cxxflags).toContain("-fwhole-program-vtables"); // rustc bitcode says EnableSplitLTOUnit=0; clang must match so lld doesn't // reject with "inconsistent LTO Unit splitting". WPD falls back to // index-only mode (still devirtualizes, just without the hybrid split). expect(linuxFlags.cxxflags).toContain("-fno-split-lto-unit"); }); });