Files

1615 lines
68 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
/**
* Build configuration.
*
* One flat struct. All derived booleans computed once in `resolveConfig()`,
* passed everywhere. No `if(ENABLE_X)` depending on `if(CI)` depending on
* `if(RELEASE)` — the chain is resolved here and the result is a plain value.
*/
import { execSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
import { homedir, arch as hostArch, platform as hostPlatform } from "node:os";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { NODEJS_ABI_VERSION, NODEJS_V8_VERSION, NODEJS_VERSION } from "./deps/nodejs-headers.ts";
import { WEBKIT_VERSION } from "./deps/webkit.ts";
import { assert, BuildError } from "./error.ts";
import { resolveMacosSdkPath } from "./macos-sdk.ts";
import { clangTargetArch } from "./tools.ts";
import { cyan, dim, green } from "./tty.ts";
export type OS = "linux" | "darwin" | "windows" | "freebsd" | "haiku";
export type Arch = "x64" | "aarch64";
export type Abi = "gnu" | "musl" | "android";
export type BuildType = "Debug" | "Release" | "RelWithDebInfo" | "MinSizeRel";
export type BuildMode = "full" | "cpp-only" | "rust-only" | "link-only" | "rust-and-link" | "archive-link";
export type WebKitMode = "prebuilt" | "local";
/**
* Host platform — what's running the build. Distinguish from target
* (Config.os/arch/windows) which is what we're building FOR.
*
* Host vs target matters for rust-only cross-compile: a linux CI box
* can cross-compile libbun_rust.a for any linux abi/arch and (with the
* right SDK) darwin. Target determines cargo's `--target` triple and
* rustflags; host determines shell syntax (cmd vs sh), quoting, and
* tool executable suffixes.
*
* For all other modes (full, cpp-only, link-only), host == target
* unless cfg.crossTarget is set (currently: Android), in which case
* the C++ side is cross-compiled via clang's --target/--sysroot.
*/
export interface Host {
os: OS;
arch: Arch;
/** ".exe" on a Windows host, "" elsewhere. Mirrors Config.exeSuffix (target). */
exeSuffix: string;
/**
* Host's Rust target triple — `host:` line from `rustc -vV`. Also the
* `${sysroot}/lib/rustlib/<triple>/` directory name. Stamped at
* `resolveConfig()` from `Toolchain.rustHostTriple` (so the toolchain
* probe is the single source of truth); `undefined` only when no rustc
* is installed.
*/
rustTriple: string | undefined;
}
/**
* Pinned version defaults. Each lives at the top of its own file
* (deps/webkit.ts, deps/nodejs-headers.ts) — look there to bump.
* Overridable via PartialConfig for testing (e.g. trying a WebKit branch).
*/
const versionDefaults = {
nodejsVersion: NODEJS_VERSION,
nodejsAbiVersion: NODEJS_ABI_VERSION,
nodejsV8Version: NODEJS_V8_VERSION,
webkitVersion: WEBKIT_VERSION,
};
/**
* The full resolved build configuration. Every field is concrete — no
* undefined-because-it-depends-on-something-else. This is the single source
* of truth passed to every build function.
*/
export interface Config {
// ─── Target platform ───
os: OS;
arch: Arch;
/** Linux-only. undefined on darwin/windows. */
abi: Abi | undefined;
// ─── Derived platform booleans (computed from os/arch) ───
linux: boolean;
darwin: boolean;
windows: boolean;
freebsd: boolean;
haiku: boolean;
/** linux || darwin || freebsd || haiku */
unix: boolean;
/** darwin || freebsd — kqueue-based event loop */
kqueue: boolean;
x64: boolean;
arm64: boolean;
/**
* What's running the build. Differs from os/arch/windows (target) in
* rust-only cross-compile. Use for: shell syntax in rule commands,
* quoteArgs(), tool executable suffixes. See Host type docs.
*/
host: Host;
/**
* True when the linked binary can execute on this host (same os+arch, and on
* linux same abi). Distinct from `crossTarget === undefined`: a native-arch
* linux-gnu build still passes --target/--sysroot for glibc pinning but the
* output runs fine here.
*/
canRunOnHost: boolean;
// ─── Platform file conventions ───
// Centralized so a new target (or a forgotten .exe) is one edit away.
/** ".exe" on Windows, "" elsewhere. */
exeSuffix: string;
/** ".obj" on Windows, ".o" elsewhere. */
objSuffix: string;
/** "" on Windows, "lib" elsewhere. */
libPrefix: string;
/** ".lib" on Windows, ".a" elsewhere. */
libSuffix: string;
// ─── Build configuration ───
buildType: BuildType;
debug: boolean;
release: boolean;
mode: BuildMode;
// ─── Features (all explicit booleans) ───
lto: boolean;
/**
* Cross-language LTO: rustc emits LLVM bitcode (`-Clinker-plugin-lto`) into
* `libbun_rust.a` so the final lld `-flto=thin` link sees through Rust↔C++
* call edges. When false but `lto` is true, both halves still LTO
* independently (C++ via `-flto=thin`, Rust via `[profile.release] lto =
* "fat"`); only the cross-language inlining is lost.
*
* Normally tracks `lto`. Exists as a separate field so per-target toolchain
* bugs can disable just the cross-language part without giving up LTO
* entirely — see workarounds.ts "globalopt-crash-aarch64-musl".
*/
crossLangLto: boolean;
/** IR PGO: directory for .profraw output (instrumented build). Mutually exclusive with pgoUse. */
pgoGenerate: string | undefined;
/** IR PGO: .profdata file path (optimized build). Mutually exclusive with pgoGenerate. */
pgoUse: string | undefined;
asan: boolean;
assertions: boolean;
logs: boolean;
/** x64-only: target nehalem (no AVX). Default true on x64 — the only x64 build we ship. */
baseline: boolean;
canary: boolean;
/** MinSizeRel → optimize for size. */
smol: boolean;
staticSqlite: boolean;
staticLibatomic: boolean;
tinycc: boolean;
valgrind: boolean;
fuzzilli: boolean;
/**
* Compile usockets bsd_* syscall fault-injection hooks. Runtime-armed via
* `bun:internal-for-testing` socketFaultInjection; disarmed cost is one
* acquire atomic load per syscall, zero when compiled out.
*/
socketFaultInjection: boolean;
/** Bundle small .cpp files into unified TUs (WebKit-style). See unified.ts. */
unifiedSources: boolean;
/**
* Archive each `direct` dep's objects into a per-dep .a (the old
* behaviour). Default off — dep .o files go straight into bun's link/
* cpp-only archive instead. Turn on to bisect duplicate-symbol issues:
* a .a only contributes members the linker actually pulls.
*/
archiveDeps: boolean;
/** Emit clang -ftime-trace .json next to each .o for build profiling. */
timeTrace: boolean;
// ─── Environment ───
ci: boolean;
buildkite: boolean;
// ─── Dependency modes ───
webkit: WebKitMode;
/**
* Deps built from a local checkout instead of the pinned tarball, keyed by
* dep name → absolute source dir. Set via `--local-deps=name=path[,...]`.
* The checkout is used as-is: no fetch, no `.ref` stamp, and the dep's
* `patches` are NOT applied (they target the pinned tarball; a fork
* checkout is expected to carry whatever you're iterating on).
*/
localDeps: Record<string, string>;
// ─── Paths (all absolute) ───
/** Repository root. */
cwd: string;
/** Build output directory, e.g. /path/to/bun/build/debug/. */
buildDir: string;
/** Generated code output, e.g. buildDir/codegen/. */
codegenDir: string;
/** Persistent cache for dep tarballs and builds. */
cacheDir: string;
/** Vendored dependencies (gitignored). */
vendorDir: string;
// ─── Toolchain (resolved absolute paths) ───
cc: string;
cxx: string;
/**
* Compiler for build-time host tools (dep_host_cc codegen helpers).
* Same as `cc` except when cross-compiling for windows from a unix host,
* where `cc` is clang-cl (emits COFF) and host tools need plain clang.
*/
hostCc: string;
/**
* C++ driver for host-side links (cargo's host-triple linker in
* `.cargo/config.toml` — build scripts, proc-macros). Same as `cxx`
* except when cross-compiling for windows from a unix host.
*/
hostCxx: string;
/** Parsed X.Y.Z from clang --version. Captured once at resolve time. */
clangVersion: string | undefined;
/**
* `clang -print-resource-dir` — builtin headers live at `<dir>/include`.
* Used by darwin cross-compiles, which rebuild the C++ include search path
* explicitly. undefined on Windows (nothing consumes it there).
*/
clangResourceDir: string | undefined;
ar: string;
/** llvm-ranlib. undefined on windows (llvm-lib indexes itself). */
ranlib: string | undefined;
/**
* ld.lld on linux, lld-link on windows, ld64.lld when cross-compiling for
* darwin from a non-darwin host. May be empty on native darwin (clang
* invokes the system linker).
*/
ld: string;
/**
* rustc's bundled lld (see `Toolchain.rustLld`). When set and rustc's LLVM
* is newer than clang's under LTO, `resolveConfig()` selects it as `cfg.ld`.
* Forwarded so `validateBunConfig()` can fail loudly when LTO requires it
* but it wasn't found (mismatched LLVM versions → "Invalid record" at link).
*/
rustLld: string | undefined;
/** Parsed `LLVM version:` from `rustc -vV`. Captured once; feeds workarounds.ts. */
rustLlvmVersion: string | undefined;
/**
* `rustc --print sysroot`. Used to locate rustc's bundled `llvm-nm` for
* reading LTO bitcode in `libbun_rust.a` — clang's `llvm-nm` may lag
* rustc's LLVM major and reject the bitcode (#53609, #53656). Unlike
* `rustLld`, this is needed regardless of whether cross-language LTO is
* actually using rust-lld as the linker.
*/
rustSysroot: string | undefined;
strip: string;
/** Set when the target is darwin. Undefined on non-darwin targets. */
dsymutil: string | undefined;
/** Self-host bun for codegen (bun install, bun build). */
bun: string;
/**
* Shell-ready command prefix for running .ts subprocesses (stream.ts,
* fetch-cli.ts, regen). Either the bun path or `node --experimental-strip-types`
* depending on what's running configure. Already quoted — splice directly
* into rule commands.
*/
jsRuntime: string;
esbuild: string;
/** Optional — compiler launcher prefix. */
ccache: string | undefined;
/** cmake executable. Required for nested dep builds. */
cmake: string;
/** cargo executable. undefined when no rust toolchain is available. */
cargo: string | undefined;
/** CARGO_HOME — passed to cargo invocations for reproducibility. */
cargoHome: string | undefined;
/** RUSTUP_HOME — passed to cargo invocations for reproducibility. */
rustupHome: string | undefined;
/**
* RUSTUP_TOOLCHAIN — the `channel` from this repo's `rust-toolchain.toml`.
* Passed explicitly to every cargo invocation so the dep build and the
* workspace build agree on libstd even when `vendor/` is a symlink into a
* sibling worktree (rustup's directory walk follows the resolved path and
* would otherwise pick up that worktree's pin).
*/
rustToolchain: string | undefined;
/** Windows: MSVC link.exe path (to avoid Git's /usr/bin/link shadowing). */
msvcLinker: string | undefined;
/** Windows: llvm-rc for nested cmake (CMAKE_RC_COMPILER). */
rc: string | undefined;
/** Windows: llvm-mt for nested cmake (CMAKE_MT). May be absent in some LLVM distros. */
mt: string | undefined;
/** Windows-x64: nasm for BoringSSL's NASM-syntax assembly. */
nasm: string | undefined;
// ─── macOS SDK (darwin only, undefined elsewhere) ───
/** e.g. "13.0". Passed to deps as -DCMAKE_OSX_DEPLOYMENT_TARGET. */
osxDeploymentTarget: string | undefined;
/**
* SDK path. Native darwin: from `xcrun --show-sdk-path`. Darwin
* cross-compile from a non-darwin host: an extracted MacOSX*.sdk (see
* macos-sdk.ts). Passed to deps as -DCMAKE_OSX_SYSROOT / `-isysroot`.
*/
osxSysroot: string | undefined;
// ─── Cross-compilation (set when host != target for C++) ───
// Generic plumbing shared by every cross target (Android, FreeBSD,
// macOS-from-Linux, and Windows-from-unix).
/** clang `--target=` triple, e.g. "aarch64-unknown-linux-android28". undefined = native. */
crossTarget: string | undefined;
/** clang `--sysroot=` path. For Android: `<ndk>/toolchains/llvm/prebuilt/<host>/sysroot`. */
sysroot: string | undefined;
/**
* Windows cross-compile only: root of an xwin-style splat of the MSVC
* CRT/STL + Windows SDK laid out like a Visual Studio install
* (`VC/Tools/MSVC/<ver>`, `Windows Kits/10`). Passed to clang-cl as
* `/winsysroot` and to lld-link as `/winsysroot:` — the cross equivalent
* of the INCLUDE/LIB env a VS dev shell provides on a Windows host.
* undefined on native Windows builds (VS dev shell supplies the SDK).
*/
winsysroot: string | undefined;
/** NDK compiler-rt/libunwind dir: `<ndk>/toolchains/llvm/prebuilt/<host>/lib/clang/<ver>/lib/linux`. */
androidNdkRuntimeDir: string | undefined;
// ─── Versioning ───
/** Bun's own version (from package.json). */
version: string;
/** Git commit of the bun checkout — feeds into the build's -Dsha equivalent. */
revision: string;
canaryRevision: string;
/** Node.js compat version. Default in versions.ts; override to test a bump. */
nodejsVersion: string;
nodejsAbiVersion: string;
nodejsV8Version: string;
/** WebKit commit. Default in versions.ts; override to test a WebKit branch. */
webkitVersion: string;
}
/**
* Partial config — what profiles and CLI flags provide.
* Resolution fills in the rest.
*/
export interface PartialConfig {
os?: OS;
arch?: Arch;
abi?: Abi;
buildType?: BuildType;
mode?: BuildMode;
lto?: boolean;
pgoGenerate?: string;
pgoUse?: string;
asan?: boolean;
assertions?: boolean;
logs?: boolean;
baseline?: boolean;
canary?: boolean;
staticSqlite?: boolean;
staticLibatomic?: boolean;
tinycc?: boolean;
valgrind?: boolean;
fuzzilli?: boolean;
socketFaultInjection?: boolean;
unifiedSources?: boolean;
archiveDeps?: boolean;
timeTrace?: boolean;
ci?: boolean;
buildkite?: boolean;
webkit?: WebKitMode;
/**
* `name=path[,name=path...]` — build these deps from a local checkout
* (e.g. `mimalloc=~/code/mimalloc`). `~` expands to $HOME; relative paths
* resolve against the repo root. See `Config.localDeps`.
*/
localDeps?: string;
buildDir?: string;
cacheDir?: string;
/** Override NDK location (default: $ANDROID_NDK_ROOT etc). Only used when abi=android. */
androidNdk?: string;
/** Override Android API level (default: ANDROID_API_LEVEL_DEFAULT). Only used when abi=android. */
androidApiLevel?: number;
/** FreeBSD sysroot (extracted base.txz). Only used when os=freebsd. */
freebsdSysroot?: string;
/** FreeBSD release version (default: FREEBSD_VERSION_DEFAULT). Only used when os=freebsd. */
freebsdVersion?: string;
/** Linux glibc sysroot (pinned old glibc/libstdc++). Only used when linux && abi=gnu. */
linuxSysroot?: string;
/**
* macOS SDK path (a MacOSX*.sdk directory). Only used when cross-compiling
* for darwin from a non-darwin host; native darwin builds use xcrun.
* Default: $MACOS_SDK_PATH, a well-known /opt install, or an auto-download
* into the cache dir — see macos-sdk.ts.
*/
macosSdk?: string;
/**
* macOS deployment target (`-mmacosx-version-min`). Only used when
* cross-compiling for darwin from a non-darwin host (native darwin derives
* it from the installed SDK / CI floor). Default: MIN_OSX_DEPLOYMENT_TARGET.
*/
osxDeploymentTarget?: string;
/** Windows sysroot (xwin splat, VS layout). Only used when cross-compiling for os=windows. */
winsysroot?: string;
// Version pins (defaults in versions.ts).
nodejsVersion?: string;
nodejsAbiVersion?: string;
nodejsV8Version?: string;
webkitVersion?: string;
}
/**
* Resolved toolchain — found by tool discovery, passed in separately so
* tests can mock it out.
*/
export interface Toolchain {
cc: string;
cxx: string;
/**
* Host compiler / C++ driver for build-time host tools and host-side
* cargo links. Only set when they differ from `cc`/`cxx` (windows
* cross-compile from a unix host, where cc/cxx are clang-cl);
* resolveConfig() falls back to `cc`/`cxx` otherwise.
*/
hostCc: string | undefined;
hostCxx: string | undefined;
/**
* Parsed clang --version (X.Y.Z). Captured during toolchain resolution
* so downstream checks (workarounds.ts) don't re-spawn. undefined if
* version parsing failed — shouldn't happen since we version-gate cc.
*/
clangVersion: string | undefined;
/** `clang -print-resource-dir`. undefined on Windows. */
clangResourceDir: string | undefined;
ar: string;
ranlib: string | undefined;
ld: string;
/**
* lld's Mach-O port (`ld64.lld`), resolved on non-darwin unix hosts.
* Swapped in as `cfg.ld` when the target is darwin and the host isn't —
* there's no Apple `ld` to drive, and ld.lld only emits ELF.
*/
ld64Lld: string | undefined;
/**
* rustc's bundled lld (`<sysroot>/lib/rustlib/<host>/bin/gcc-ld/ld.lld` on
* unix, `.../bin/rust-lld.exe` on Windows). Used as `ld` for cross-language
* LTO when rustc's LLVM is newer than clang's — LLVM bitcode is only
* forward-compatible, so clang's lld can't read newer rust bitcode but
* rust-lld can read clang's older bitcode. undefined when rustc isn't
* installed or doesn't ship the rust-lld component.
*/
rustLld: string | undefined;
/** Parsed `LLVM version:` from `rustc -vV` (X.Y.Z). */
rustLlvmVersion: string | undefined;
/** `rustc --print sysroot` — see `Config.rustSysroot`. */
rustSysroot: string | undefined;
/** `host:` line from `rustc -vV` — stamped onto `Host.rustTriple` at resolveConfig. */
rustHostTriple: string | undefined;
strip: string;
/**
* llvm-strip. On Linux hosts GNU strip is the default (`strip` above) but
* can't read Mach-O, so darwin cross-compiles swap this in as `cfg.strip`.
*/
llvmStrip: string | undefined;
dsymutil: string | undefined;
bun: string;
jsRuntime: string;
esbuild: string;
ccache: string | undefined;
cmake: string;
/** Cargo executable. Required only if a rust dep (lolhtml) is being built. */
cargo: string | undefined;
/** CARGO_HOME. Set alongside cargo; undefined when cargo is unavailable. */
cargoHome: string | undefined;
/** RUSTUP_HOME. Set alongside cargo; undefined when cargo is unavailable. */
rustupHome: string | undefined;
/**
* Windows only: absolute path to MSVC's link.exe. Set as the cargo linker
* via CARGO_TARGET_<triple>_LINKER to prevent Git Bash's /usr/bin/link
* (the GNU hard-link utility) from shadowing the real linker in PATH.
*/
msvcLinker: string | undefined;
/**
* Windows only: llvm-rc (resource compiler). Passed to nested cmake
* as CMAKE_RC_COMPILER. cmake's own detection usually finds it, but
* that depends on PATH and cmake version — explicit is safer.
*/
rc: string | undefined;
/**
* Windows only: llvm-mt (manifest tool). Passed to nested cmake as
* CMAKE_MT. Optional — some LLVM distributions don't ship llvm-mt;
* when absent, cmake's STATIC_LIBRARY try-compile mode (set in
* source.ts) sidesteps the need.
*/
mt: string | undefined;
/**
* Windows only: nasm. BoringSSL's win-x64 assembly is NASM syntax;
* clang's integrated assembler can't read it. win-aarch64 uses gas
* .S files instead, so this is x64-only in practice.
*/
nasm: string | undefined;
}
/**
* Host platform detection. Only used for picking defaults.
*/
export function detectHost(): Host {
const plat = hostPlatform();
const os: OS =
plat === "linux"
? "linux"
: plat === "darwin"
? "darwin"
: plat === "win32"
? "windows"
: plat === "freebsd"
? "freebsd"
: plat === "haiku"
? "haiku"
: (() => {
throw new BuildError(`Unsupported host platform: ${plat}`, {
hint: "Bun builds on linux, darwin, windows, freebsd, or haiku",
});
})();
const a = hostArch();
const arch: Arch =
a === "x64"
? "x64"
: a === "arm64"
? "aarch64"
: (() => {
throw new BuildError(`Unsupported host architecture: ${a}`, { hint: "Bun builds on x64 or arm64" });
})();
// rustTriple is stamped later from Toolchain.rustHostTriple in resolveConfig
// (the rustc probe is authoritative — distinguishes glibc/musl host etc.).
return { os, arch, exeSuffix: os === "windows" ? ".exe" : "", rustTriple: undefined };
}
/**
* Detect linux ABI (gnu vs musl) by checking for /etc/alpine-release.
* Android is never auto-detected — it's always a cross-compile target,
* so it must be requested explicitly via --abi=android.
*/
export function detectLinuxAbi(): Abi {
return existsSync("/etc/alpine-release") ? "musl" : "gnu";
}
/**
* Minimum Android API level we target. 28 = Android 9 (2018), the oldest
* release with the bionic syscall wrappers we rely on without raw-syscall
* fallbacks. Covers ~96% of active devices as of 2026.
*/
export const ANDROID_API_LEVEL_DEFAULT = 28;
/**
* FreeBSD release we target. 14.x is the current production series; 14.3
* is the oldest 14.x still on download.freebsd.org. Building against 14.3
* produces binaries that run on 14.3+ (FreeBSD guarantees forward ABI
* compat within a major).
*/
export const FREEBSD_VERSION_DEFAULT = "14.3";
/**
* Locate a FreeBSD sysroot (extracted base.txz). Checks env var then
* well-known install paths. The sysroot is arch-specific (different
* crt/libc for amd64 vs arm64), so when cross-compiling for arm64 we
* look for the `-arm64` variant first. Returns undefined if none found.
*/
export function detectFreebsdSysroot(arch: Arch): string | undefined {
const env = process.env.FREEBSD_SYSROOT;
if (env && existsSync(join(env, "usr", "include", "sys", "param.h"))) return env;
const candidates =
arch === "aarch64"
? ["/opt/freebsd-sysroot-arm64", "/opt/freebsd-sysroot"]
: ["/opt/freebsd-sysroot", "/opt/freebsd-sysroot-amd64"];
for (const p of candidates) {
if (existsSync(join(p, "usr", "include", "sys", "param.h"))) return p;
}
return undefined;
}
/**
* Locate the linux-gnu sysroot: ubuntu:20.04 (glibc 2.31) + gcc-13 libstdc++,
* matching the WebKit prebuilt's build environment. Arch-specific. See
* install_linux_glibc_sysroot() in scripts/bootstrap.sh.
*/
export function detectLinuxGlibcSysroot(arch: Arch): string | undefined {
const looksValid = (p: string) => existsSync(join(p, "usr", "include", "c++", "13"));
const env = process.env.LINUX_GLIBC_SYSROOT;
if (env && looksValid(env)) return env;
const candidate = arch === "aarch64" ? "/opt/linux-sysroot-glibc-arm64" : "/opt/linux-sysroot-glibc";
return looksValid(candidate) ? candidate : undefined;
}
/**
* Locate a linux-musl sysroot — alpine rootfs with musl + modern libstdc++;
* see install_linux_musl_sysroot() in scripts/bootstrap.sh. Checks env var then
* well-known install paths. Arch-specific. Returns undefined if none found.
*/
export function detectLinuxMuslSysroot(arch: Arch): string | undefined {
const looksValid = (p: string) => existsSync(join(p, "usr", "lib", "libc.so"));
const env = process.env.LINUX_MUSL_SYSROOT;
if (env && looksValid(env)) return env;
const candidate = arch === "aarch64" ? "/opt/linux-sysroot-musl-arm64" : "/opt/linux-sysroot-musl";
return looksValid(candidate) ? candidate : undefined;
}
/**
* Locate a Windows sysroot (xwin splat of the MSVC CRT/STL + Windows SDK in
* Visual Studio layout). Checks the env var then well-known install paths.
* The splat contains both x64 and arm64 CRT/SDK libs, so unlike FreeBSD
* there's no per-arch variant. Returns undefined if none found.
*/
export function detectWindowsSysroot(): string | undefined {
// Case-tolerant: a real VS/SDK copy uses "Include", an xwin splat in
// winsysroot-style mode writes "include" (winsysroot.ts adds the
// title-case alias the LLVM toolchain needs at configure time).
const looksValid = (p: string) =>
existsSync(join(p, "Windows Kits", "10", "Include")) || existsSync(join(p, "Windows Kits", "10", "include"));
const env = process.env.WINDOWS_SYSROOT;
if (env && looksValid(env)) return env;
for (const p of ["/opt/winsysroot", "/opt/xwin"]) {
if (looksValid(p)) return p;
}
return undefined;
}
/**
* Locate the Android NDK. Checks the conventional env vars in priority
* order, then a couple of well-known install paths. Returns undefined if
* none found — caller decides whether to error.
*/
export function detectAndroidNdk(): string | undefined {
for (const v of ["ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_NDK"]) {
const p = process.env[v];
if (p && existsSync(join(p, "toolchains"))) return p;
}
for (const p of ["/opt/android-ndk", "/usr/local/android-ndk"]) {
if (existsSync(join(p, "toolchains"))) return p;
}
// Android Studio's sdkmanager puts NDKs under $ANDROID_HOME/ndk/<version>.
// We don't pick one automatically — too easy to get a stale version.
return undefined;
}
/**
* NDK toolchain prebuilt directory for the current build host. The NDK
* ships one prebuilt per host OS (always x86_64; arm64 macOS runs it
* under Rosetta).
*/
function ndkHostTag(host: Host): string {
switch (host.os) {
case "linux":
return "linux-x86_64";
case "darwin":
return "darwin-x86_64";
case "windows":
return "windows-x86_64";
case "freebsd":
throw new BuildError("Android NDK does not ship FreeBSD prebuilts", {
hint: "Cross-compile to Android from a Linux host",
});
}
}
/**
* Make the host clang able to link Android binaries by symlinking the
* NDK's compiler-rt builtins + libunwind into clang's resource dir.
*
* clang's driver emits a FULL PATH to `<resource-dir>/lib/<triple>/
* libclang_rt.builtins.a` — there's no `-L`-style search, so the file
* must exist at exactly that path. Our host clang has no Android-target
* compiler-rt; the NDK does. This is the standard "bring your own clang"
* setup for NDK cross-builds (Chromium does the same).
*
* Idempotent. Warns with a sudo hint if the resource dir isn't writable
* (CI build images create the symlinks as root in bootstrap.sh/Dockerfile).
*/
function linkNdkRuntimesIntoClang(cc: string, ndk: string, host: Host, triple: string): void {
const resourceDir = execSync(`"${cc}" -print-resource-dir`, { encoding: "utf8" }).trim();
const targetDir = join(resourceDir, "lib", triple);
// NDK r23+ layout: <prebuilt>/lib/clang/<ver>/lib/linux/<arch>/ for
// libunwind.a + new-style libclang_rt.builtins.a
const ndkPrebuilt = join(ndk, "toolchains", "llvm", "prebuilt", ndkHostTag(host));
const ndkClangLib = join(ndkPrebuilt, "lib", "clang");
// NDK ships exactly one clang version per release.
const ndkClangVer = readdirSync(ndkClangLib)[0];
if (ndkClangVer === undefined) {
throw new BuildError(`NDK clang resource dir not found under ${ndkClangLib}`);
}
const arch = triple.startsWith("x86_64") ? "x86_64" : "aarch64";
const ndkRtLinux = join(ndkClangLib, ndkClangVer, "lib", "linux");
// Populate BOTH layouts: apt.llvm.org clang uses old-style flat
// (lib/linux/libclang_rt.builtins-<arch>-android.a) while tarball builds use
// per-triple (lib/<triple>/libclang_rt.builtins.a). NDK r27 keeps builtins in
// the flat dir but libunwind in the per-arch subdir.
const flatDir = join(resourceDir, "lib", "linux");
const links = {
[join(targetDir, "libclang_rt.builtins.a")]: join(ndkRtLinux, `libclang_rt.builtins-${arch}-android.a`),
[join(targetDir, "libunwind.a")]: join(ndkRtLinux, arch, "libunwind.a"),
[join(flatDir, `libclang_rt.builtins-${arch}-android.a`)]: join(
ndkRtLinux,
`libclang_rt.builtins-${arch}-android.a`,
),
[join(flatDir, arch, "libunwind.a")]: join(ndkRtLinux, arch, "libunwind.a"),
};
if (Object.keys(links).every(dst => existsSync(dst))) return;
try {
mkdirSync(targetDir, { recursive: true });
mkdirSync(join(flatDir, arch), { recursive: true });
for (const [dst, src] of Object.entries(links)) {
if (!existsSync(dst)) symlinkSync(src, dst);
}
} catch (cause) {
// Don't throw — rust-only mode doesn't need these, and on CI bootstrap.sh
// creates them as root during image build. The actual link step will fail
// loudly later if they're genuinely missing where needed.
const lnCmds = Object.entries(links)
.map(([dst, src]) => `sudo ln -sf "${src}" "${dst}"`)
.join(" && ");
console.warn(
`warning: could not link NDK compiler-rt into ${resourceDir} (${(cause as NodeJS.ErrnoException).code}). ` +
`If the final link fails on libclang_rt.builtins.a, run: sudo mkdir -p "${targetDir}" "${join(flatDir, arch)}" && ${lnCmds}`,
);
}
}
/**
* Resolve a PartialConfig into a full Config.
*
* This is where all the "X defaults to Y unless Z" chains get resolved into
* concrete values. After this runs, everything downstream sees plain booleans.
*/
export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Config {
const host = detectHost();
host.rustTriple = toolchain.rustHostTriple;
// ─── Target platform ───
const os = partial.os ?? host.os;
// Windows hosts: process.arch can be wrong under emulation (x64 bun on
// arm64 hardware). Ask the compiler what it targets — CMake does the same
// in project() to set CMAKE_SYSTEM_PROCESSOR. The found clang's default
// target is what we actually build for. Cross-compiles from a unix host
// skip this (the host clang-cl's default arch is just the host's).
const compilerArch = os === "windows" && host.os === "windows" ? clangTargetArch(toolchain.cc) : undefined;
const arch = partial.arch ?? compilerArch ?? host.arch;
const abi: Abi | undefined = os === "linux" ? (partial.abi ?? detectLinuxAbi()) : undefined;
const linux = os === "linux";
const darwin = os === "darwin";
const windows = os === "windows";
const freebsd = os === "freebsd";
const haiku = os === "haiku";
const unix = linux || darwin || freebsd || haiku;
const kqueue = darwin || freebsd;
const x64 = arch === "x64";
const arm64 = arch === "aarch64";
// Darwin target on a non-darwin host (Linux CI box building macOS
// binaries). Same host-clang + --target/-isysroot model as Android/FreeBSD,
// with ld64.lld doing the Mach-O link. See the cross block further down.
const darwinCross = darwin && host.os !== "darwin";
// Windows target on a non-Windows host (clang-cl + lld-link + xwin
// sysroot). See the cross block further down.
// Platform file conventions — MSVC style on Windows, Unix everywhere else.
const exeSuffix = windows ? ".exe" : "";
const objSuffix = windows ? ".obj" : ".o";
const libPrefix = windows ? "" : "lib";
const libSuffix = windows ? ".lib" : ".a";
// ─── Build type ───
const buildType = partial.buildType ?? "Debug";
const debug = buildType === "Debug";
const release = buildType === "Release" || buildType === "RelWithDebInfo" || buildType === "MinSizeRel";
const smol = buildType === "MinSizeRel";
// ─── Environment ───
// Explicit (not auto-detected from env) — matches CMake's optionx(CI DEFAULT OFF).
// The ci-* profiles set these. Affects build semantics: LTO default, PCH
// skip, macOS min SDK. Log-group/annotation decisions use the runtime env
// detection in ci.ts instead, so running a non-CI profile on a CI machine
// still gets collapsible logs but not CI build flags.
const ci = partial.ci ?? false;
const buildkite = partial.buildkite ?? false;
// ─── Features ───
// Each is resolved exactly once here.
// ASAN: default on for debug builds on arm64 macOS or linux
const asanDefault = debug && ((darwin && arm64) || linux);
// Android: force off. NDK ASAN deployment needs wrap.sh + runtime .so
// shipping alongside the binary; UBSan likewise. Not worth the matrix.
// FreeBSD: force off. Cross-compiled — we'd need to ship FreeBSD's
// libclang_rt.asan, and there's no -asan WebKit prebuilt for it.
// Darwin cross: force off. The Linux LLVM toolchain doesn't ship the
// darwin ASAN/UBSan runtime dylibs (libclang_rt.*_osx_dynamic.dylib).
// Windows cross: force off. The host clang doesn't ship the windows
// clang_rt.asan runtime libs, so the link would fail.
const asan =
abi === "android" || freebsd || darwinCross || (windows && host.os !== "windows")
? false
: (partial.asan ?? asanDefault);
// Assertions: default on in debug OR asan. ASAN coupling is ABI-critical:
// the -asan WebKit prebuilt is built with ASSERT_ENABLED=1, which gates
// struct fields (RefCountDebugger etc). If bun's C++ isn't also compiled
// with ASSERT_ENABLED=1, the struct layouts mismatch → crashes. CMake's
// build:asan always set ENABLE_ASSERTIONS=ON for this reason.
const assertions = partial.assertions ?? (debug || asan);
// LTO: default on for CI release non-asan non-assertions builds across
// linux, darwin-cross, and windows-cross. All three use ThinLTO (the JSC
// ThinLTO miscompile was fixed upstream). The -lto WebKit prebuilts only
// exist for the cross toolchain, so native windows/darwin stay non-LTO.
const windowsCross = windows && host.os !== "windows";
const ltoDefault = release && (linux || darwinCross || windowsCross) && ci && !assertions && !asan;
let lto = partial.lto ?? ltoDefault;
// ASAN and LTO don't mix — ASAN wins (silently, no warn — config is explicit).
// Android: no LTO prebuilt WebKit exists; force off so the right tarball is fetched.
// Windows arm64: oven-sh/WebKit ships no bun-webkit-windows-arm64-lto
// (LLVM's CodeView emitter aborts on ARM64 NEON tuple registers).
if ((asan && lto) || abi === "android" || (windows && arm64)) {
lto = false;
}
// Cross-language LTO normally tracks `lto`. Gated off only for native
// Windows hosts — there `ld` is the host LLVM's lld-link and no rust-lld
// swap is wired up, so rustc's newer-LLVM bitcode would be unreadable at
// link time. Both halves still LTO independently when this is false — only
// the Rust↔C++ inlining is lost.
// (aarch64-musl used to be gated too: LLVM's `globalopt` segfaulted on the
// per-crate `bun_runtime` bitcode module during the merged link, CI build
// #53109. That bitcode shape no longer exists — the Rust side is one fat,
// pre-merged module since the CARGO_PROFILE_RELEASE_LTO=fat switch — so
// the gate was lifted; see the deleted "globalopt-crash-aarch64-musl"
// workarounds.ts entry if it ever needs to come back.)
// Darwin cross uses the same rust-lld swap as ELF: rustc's sysroot ships
// `gcc-ld/ld64.lld` (rust-lld in the Mach-O flavor, built against rustc's
// LLVM), which findRustLld() already resolves for darwin targets, so the
// newer-LLVM bitcode rustc emits under -Clinker-plugin-lto is readable at
// link time. Windows cross does the same with the `gcc-ld/lld-link`
// sibling (COFF flavor) — see the wantRustLld swap below.
const crossLangLto = lto && !(windows && host.os === "windows");
// Cross-language LTO bitcode-version skew: `-Clinker-plugin-lto` makes
// rustc emit raw LLVM bitcode into libbun_rust.a. LLVM bitcode is
// forward-compatible only (newer reader, older writer), so when rustc's
// bundled LLVM is ahead of clang's, clang's ld.lld rejects the rust .o
// files ("Unknown attribute kind"). rust-lld is built against rustc's
// LLVM, so it reads both rustc's bitcode (same version) and clang's
// (older, hence readable). Swap it in as `ld` for the whole build —
// it's a stock lld, just newer, so non-LTO objects and nested cmake
// deps link the same as before.
//
// Tracked in workarounds.ts ("rust-lld-for-crosslang-lto") so this
// branch self-obsoletes once clang's LLVM catches up to rustc's.
let ld = toolchain.ld;
const clangMajor = majorOf(toolchain.clangVersion);
const rustLlvmMajor = majorOf(toolchain.rustLlvmVersion);
// Shared with the darwin-cross ld64 swap below: for darwin targets
// findRustLld() resolves rustc's `gcc-ld/ld64.lld` (the Mach-O flavor of
// the same rust-lld), so the swap composes with the cross toolchain.
const wantRustLld =
crossLangLto &&
toolchain.rustLld !== undefined &&
clangMajor !== undefined &&
rustLlvmMajor !== undefined &&
rustLlvmMajor > clangMajor;
if (wantRustLld) {
if (windows) {
// Windows cross: `ld` must stay a COFF driver. `toolchain.rustLld` is
// the flavor matching the *host* (gcc-ld/ld.lld on a Linux box);
// rustc's gcc-ld/ directory ships every flavor of the same rust-lld,
// so use the lld-link sibling. If rustc ever stops shipping it, fall
// back to the host LLVM's lld-link — validateBunConfig() then fails
// at configure time with the bitcode-version-skew message instead of
// an opaque "Invalid record" at link time.
const rustLldLink = join(dirname(toolchain.rustLld!), "lld-link");
if (existsSync(rustLldLink)) {
ld = rustLldLink;
}
} else {
ld = toolchain.rustLld!;
}
}
// PGO: paths resolved to absolute. generate/use are mutually exclusive.
const pgoGenerate = partial.pgoGenerate ? resolve(partial.pgoGenerate) : undefined;
const pgoUse = partial.pgoUse ? resolve(partial.pgoUse) : undefined;
if (pgoGenerate && pgoUse) {
throw new BuildError("--pgo-generate and --pgo-use are mutually exclusive");
}
// Logs: on by default in debug non-test
const logs = partial.logs ?? debug;
const baseline = partial.baseline ?? x64;
const canary = partial.canary ?? true;
const canaryRevision = canary ? "1" : "0";
// Whether bun:sqlite and node:sqlite link the bundled sqlite3 directly
// (LAZY_LOAD_SQLITE=0) or dlopen the system library at runtime. macOS
// defaults to dlopen so both APIs share Apple's libsqlite3 (one library,
// one POSIX-lock inode map — howtocorrupt.html §2.2.1); Linux/Windows
// link the bundled amalgamation.
const staticSqlite = partial.staticSqlite ?? !darwin;
// Static libatomic: on by default. Arch/Manjaro don't ship libatomic.a —
// those users pass --static-libatomic=off. Not auto-detected: the link
// failure is loud ("cannot find -l:libatomic.a") and the fix is obvious.
const staticLibatomic = partial.staticLibatomic ?? true;
// TinyCC: off on Android (no upstream bionic support; FFI cc() falls back
// to dlopen-only) and FreeBSD (oven-sh/tinycc has no FreeBSD target).
const tinycc = partial.tinycc ?? !(abi === "android" || freebsd || haiku);
const valgrind = partial.valgrind ?? false;
const fuzzilli = partial.fuzzilli ?? false;
// Default follows asan: on for local debug (Linux / arm64 macOS) and CI
// release-asan, off everywhere else. The fuzz tests are most useful when
// memory errors are detectable, and the disarmed-hot-path cost (one acquire
// atomic load) is acceptable in asan builds but not in shipped release.
const socketFaultInjection = partial.socketFaultInjection ?? asan;
// ─── Paths ───
const cwd = findRepoRoot();
// Windows cross-compiles get their own default build dir — the native
// build of the same profile (build/debug, build/release) already holds
// host-target objects at the same obj/ paths, and mixing COFF into an ELF
// build dir (or vice versa) forces a full rebuild each time you switch.
const crossWindowsSuffix = windows && host.os !== "windows" ? `-windows-${arch}` : "";
const defaultBuildDirName = computeBuildDirName({ debug, release, asan, assertions }) + crossWindowsSuffix;
const buildDir =
partial.buildDir !== undefined
? isAbsolute(partial.buildDir)
? partial.buildDir
: resolve(cwd, partial.buildDir)
: resolve(cwd, "build", defaultBuildDirName);
const codegenDir = resolve(buildDir, "codegen");
// Local builds share $BUN_INSTALL/build-cache across checkouts and profiles
// so ccache/tarballs/webkit reuse one another's work. CI stays per-build
// so runners remain hermetic and `rm -rf build/` is a full reset.
// Relative BUN_INSTALL is anchored to repo root (not process.cwd()) so the
// ninja regen rule — which runs from buildDir — resolves the same path.
const bunInstall = process.env.BUN_INSTALL ? resolve(cwd, process.env.BUN_INSTALL) : join(homedir(), ".bun");
const cacheDir =
partial.cacheDir !== undefined
? isAbsolute(partial.cacheDir)
? partial.cacheDir
: resolve(cwd, partial.cacheDir)
: ci
? resolve(buildDir, "cache")
: resolve(bunInstall, "build-cache");
const vendorDir = resolve(cwd, "vendor");
// ─── Validation ───
assert(!baseline || x64, "baseline=true requires arch=x64 (baseline disables AVX which is x64-only)");
assert(!valgrind || linux, "valgrind=true requires os=linux");
assert(!(asan && valgrind), "Cannot enable both asan and valgrind simultaneously");
assert(os !== "linux" || abi !== undefined, "Linux builds require an abi (gnu, musl, or android)");
// ─── Cross-compilation (Android) ───
// We keep using the host's clang (same version everywhere) and pass
// --target/--sysroot. The NDK is needed only for its bionic sysroot,
// libc++, and compiler-rt — not for its bundled clang.
let crossTarget: string | undefined;
let sysroot: string | undefined;
let androidNdk: string | undefined;
let androidApiLevel: number | undefined;
let androidNdkRuntimeDir: string | undefined;
if (abi === "android") {
androidNdk =
partial.androidNdk !== undefined
? isAbsolute(partial.androidNdk)
? partial.androidNdk
: resolve(cwd, partial.androidNdk)
: detectAndroidNdk();
if (androidNdk === undefined) {
throw new BuildError("--abi=android requires the Android NDK", {
hint: "Set ANDROID_NDK_ROOT or pass --android-ndk=<path>. Download: https://developer.android.com/ndk/downloads",
});
}
androidApiLevel = partial.androidApiLevel ?? ANDROID_API_LEVEL_DEFAULT;
const ndkPrebuilt = join(androidNdk, "toolchains", "llvm", "prebuilt", ndkHostTag(host));
sysroot = join(ndkPrebuilt, "sysroot");
if (!existsSync(sysroot)) {
throw new BuildError(`Android NDK sysroot not found at ${sysroot}`, {
hint: `Is ANDROID_NDK_ROOT (${androidNdk}) a valid NDK? Expected r26 or newer.`,
});
}
// NDK ships exactly one clang version per release.
const ndkClangLib = join(ndkPrebuilt, "lib", "clang");
const ndkClangVer = readdirSync(ndkClangLib)[0];
if (ndkClangVer === undefined) {
throw new BuildError(`NDK clang resource dir not found under ${ndkClangLib}`);
}
androidNdkRuntimeDir = join(ndkClangLib, ndkClangVer, "lib", "linux");
const llvmArch = arch === "x64" ? "x86_64" : "aarch64";
crossTarget = `${llvmArch}-unknown-linux-android${androidApiLevel}`;
linkNdkRuntimesIntoClang(toolchain.cc, androidNdk, host, crossTarget);
}
// ─── Cross-compilation (FreeBSD) ───
// Same pattern as Android: host clang + --target/--sysroot. The sysroot
// is an extracted base.txz (libc, libc++, headers, crt files). When
// building ON FreeBSD, no cross flags are needed.
let freebsdVersion: string | undefined;
if (freebsd) {
freebsdVersion = partial.freebsdVersion ?? FREEBSD_VERSION_DEFAULT;
if (host.os !== "freebsd") {
sysroot =
partial.freebsdSysroot !== undefined
? isAbsolute(partial.freebsdSysroot)
? partial.freebsdSysroot
: resolve(cwd, partial.freebsdSysroot)
: detectFreebsdSysroot(arch);
if (sysroot === undefined) {
const dlArch = arch === "x64" ? "amd64" : "arm64";
const sysrootPath = arch === "x64" ? "/opt/freebsd-sysroot" : "/opt/freebsd-sysroot-arm64";
throw new BuildError("--os=freebsd requires a FreeBSD sysroot when cross-compiling", {
hint: `Set FREEBSD_SYSROOT or pass --freebsd-sysroot=<path>. Create one with: mkdir -p ${sysrootPath} && curl -L https://download.freebsd.org/releases/${dlArch}/${freebsdVersion}-RELEASE/base.txz | tar -C ${sysrootPath} -xJf - ./usr/include ./usr/lib ./lib`,
});
}
const llvmArch = arch === "x64" ? "x86_64" : "aarch64";
crossTarget = `${llvmArch}-unknown-freebsd${freebsdVersion}`;
// No compiler-rt symlinking needed (unlike Android): FreeBSD's base
// ships libgcc.a (which IS compiler-rt builtins, renamed for compat)
// in /usr/lib, and clang's freebsd driver finds it via --sysroot.
}
}
// ─── Linux-gnu/musl sysroot + target ───
// Every CI linux-gnu build (native AND cross-arch) uses the ubuntu:20.04 +
// gcc-13 sysroot so the glibc verneed matches what the --wrap list covers
// and the libstdc++ ABI matches the WebKit prebuilt. musl uses an
// alpine-derived sysroot. Local dev without a sysroot builds native.
if (linux && abi !== "android" && crossTarget === undefined) {
const llvmArch = x64 ? "x86_64" : "aarch64";
const hostAbi = host.os === "linux" ? detectLinuxAbi() : undefined;
const isCross = arch !== host.arch || abi !== hostAbi;
if (abi === "musl") {
sysroot = detectLinuxMuslSysroot(arch);
if (sysroot !== undefined || isCross) {
crossTarget = `${llvmArch}-alpine-linux-musl`;
if (sysroot === undefined) {
const p = arch === "aarch64" ? "/opt/linux-sysroot-musl-arm64" : "/opt/linux-sysroot-musl";
throw new BuildError(`--os=linux --arch=${arch} --abi=musl requires a musl sysroot when cross-compiling`, {
hint: `Set LINUX_MUSL_SYSROOT or provision ${p} (see install_linux_musl_sysroot() in scripts/bootstrap.sh).`,
});
}
}
} else {
sysroot =
partial.linuxSysroot !== undefined
? isAbsolute(partial.linuxSysroot)
? partial.linuxSysroot
: resolve(cwd, partial.linuxSysroot)
: detectLinuxGlibcSysroot(arch);
if (sysroot !== undefined || isCross) {
crossTarget = `${llvmArch}-linux-gnu`;
if (sysroot === undefined) {
const p = arch === "aarch64" ? "/opt/linux-sysroot-glibc-arm64" : "/opt/linux-sysroot-glibc";
throw new BuildError(`--os=linux --arch=${arch} --abi=gnu cross-compile requires a glibc sysroot`, {
hint: `Set LINUX_GLIBC_SYSROOT or provision ${p} (see install_linux_glibc_sysroot() in scripts/bootstrap.sh).`,
});
}
}
}
}
// ─── Cross-compilation (Windows) ───
// Same pattern as Android/FreeBSD, with the MSVC spin: the host LLVM's
// clang-cl/lld-link/llvm-lib/llvm-rc are used (tools.ts picks them by
// target), and the "sysroot" is an xwin splat of the MSVC CRT/STL +
// Windows SDK in Visual Studio layout, passed via /winsysroot instead of
// --sysroot. Building ON Windows needs none of this — the VS dev shell
// provides INCLUDE/LIB.
let winsysroot: string | undefined;
if (windows && host.os !== "windows") {
winsysroot =
partial.winsysroot !== undefined
? isAbsolute(partial.winsysroot)
? partial.winsysroot
: resolve(cwd, partial.winsysroot)
: detectWindowsSysroot();
if (winsysroot === undefined) {
if (ci || buildkite) {
// CI always fetches its own sysroot into the per-build cache (see
// winsysroot.ts `ensureWindowsSysroot`, called from configure.ts
// before the graph is emitted) instead of relying on agent image
// provisioning.
winsysroot = resolve(cacheDir, "winsysroot");
} else {
throw new BuildError("--os=windows requires a Windows sysroot (MSVC CRT + Windows SDK) when cross-compiling", {
hint:
"Set WINDOWS_SYSROOT or pass --winsysroot=<path>. Create one with xwin (https://github.com/Jake-Shadle/xwin):\n" +
" cargo install xwin (or download a release binary)\n" +
// Keep the pinned versions in sync with WINDOWS_SDK_VERSION / MSVC_CRT_VERSION in winsysroot.ts.
" xwin --accept-license --arch x86_64,aarch64 --sdk-version 10.0.26100 --crt-version 14.44.17.14 --include-atl splat --use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs --output /opt/winsysroot",
});
}
}
if (partial.webkit === "local") {
throw new BuildError("Cross-compiling for Windows requires the prebuilt WebKit (webkit=local needs msbuild)", {
hint: "Drop --webkit=local or build on a Windows host.",
});
}
const llvmArch = arch === "x64" ? "x86_64" : "aarch64";
crossTarget = `${llvmArch}-pc-windows-msvc`;
}
// ─── Versioning ───
const pkgJsonPath = resolve(cwd, "package.json");
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { version: string };
const version = pkgJson.version;
const revision = getGitRevision(cwd, debug && !ci ? buildDir : undefined);
// Defaults from versions.ts. Override via --webkit-version=<hash> etc.
// to test a branch before bumping the pinned default.
const nodejsVersion = partial.nodejsVersion ?? versionDefaults.nodejsVersion;
const nodejsAbiVersion = partial.nodejsAbiVersion ?? versionDefaults.nodejsAbiVersion;
const nodejsV8Version = partial.nodejsV8Version ?? versionDefaults.nodejsV8Version;
const webkitVersion = partial.webkitVersion ?? versionDefaults.webkitVersion;
// ─── macOS SDK ───
// Must be passed to nested cmake builds or they'll pick the wrong SDK.
// Native darwin: ask xcode-select/xcrun. Cross-compiling from a non-darwin
// host: an extracted MacOSX*.sdk — explicit path, well-known install, or
// auto-downloaded into the cache dir (see macos-sdk.ts / ensureMacosSdk()).
let osxDeploymentTarget: string | undefined;
let osxSysroot: string | undefined;
if (darwin && host.os === "darwin") {
({ osxDeploymentTarget, osxSysroot } = detectMacosSdk(ci));
if (partial.osxDeploymentTarget !== undefined) osxDeploymentTarget = partial.osxDeploymentTarget;
}
// ─── Cross-compilation (macOS from a non-darwin host) ───
// Host clang + `--target=<arch>-apple-macosx` + `-isysroot <SDK>`, linked
// with lld's Mach-O port (ld64.lld) and post-processed with llvm-strip /
// dsymutil — all of which ship in the same LLVM install we already require.
// The deployment target defaults to the CI floor (the SDK itself can be
// newer; only `-mmacosx-version-min` decides what the binary runs on).
let ld64StripSwap: { ld: string; strip: string } | undefined;
if (darwinCross) {
crossTarget = `${arm64 ? "arm64" : "x86_64"}-apple-macosx`;
osxDeploymentTarget = partial.osxDeploymentTarget ?? MIN_OSX_DEPLOYMENT_TARGET;
// rust-only mode never compiles C/C++ or links, so it doesn't need the
// SDK — skip resolution so a rust-only build doesn't download
// a ~730 MB sysroot it never reads.
if ((partial.mode ?? "full") !== "rust-only") {
osxSysroot = resolveMacosSdkPath(partial.macosSdk, cacheDir, cwd);
if (toolchain.ld64Lld === undefined) {
throw new BuildError("Cross-compiling for macOS requires ld64.lld (lld's Mach-O port)", {
hint: "Install lld for the same LLVM version as clang: apt install lld-21 (or equivalent).",
});
}
if (toolchain.llvmStrip === undefined) {
throw new BuildError("Cross-compiling for macOS requires llvm-strip (GNU strip can't read Mach-O)", {
hint: "Install llvm for the same version as clang: apt install llvm-21 (or equivalent).",
});
}
if (toolchain.clangResourceDir === undefined) {
throw new BuildError("Cross-compiling for macOS requires clang's resource directory", {
hint: "`clang -print-resource-dir` failed — is the discovered clang runnable?",
});
}
if (toolchain.dsymutil === undefined) {
throw new BuildError("Cross-compiling for macOS requires LLVM dsymutil", {
hint: "Install llvm for the same version as clang: apt install llvm-21 (or equivalent).",
});
}
// The Mach-O flavor of whichever lld the rest of the config picked.
// `toolchain.rustLld` is the flavor matching the *host* (gcc-ld/ld.lld
// on a Linux box); rustc's gcc-ld/ directory ships every flavor of the
// same rust-lld, so when the cross-language-LTO bitcode skew applies
// (see wantRustLld above) the Mach-O link uses the ld64.lld sibling.
// Falls back to clang's ld64.lld if rustc ever stops shipping it — the
// configure-time assert in validateBunConfig catches the resulting
// bitcode-version mismatch with a clear message.
const rustLd64Lld =
wantRustLld && toolchain.rustLld !== undefined ? join(dirname(toolchain.rustLld), "ld64.lld") : undefined;
ld64StripSwap = {
ld: rustLd64Lld !== undefined && existsSync(rustLd64Lld) ? rustLd64Lld : toolchain.ld64Lld,
strip: toolchain.llvmStrip,
};
}
}
return {
os,
arch,
abi,
linux,
darwin,
windows,
freebsd,
haiku,
unix,
kqueue,
x64,
arm64,
host,
canRunOnHost: os === host.os && arch === host.arch && (!linux || abi === (detectLinuxAbi() ?? abi)),
exeSuffix,
objSuffix,
libPrefix,
libSuffix,
buildType,
debug,
release,
mode: partial.mode ?? "full",
lto,
crossLangLto,
pgoGenerate,
pgoUse,
asan,
assertions,
logs,
baseline,
canary,
smol,
staticSqlite,
staticLibatomic,
tinycc,
valgrind,
fuzzilli,
socketFaultInjection,
unifiedSources: partial.unifiedSources ?? true,
archiveDeps: partial.archiveDeps ?? false,
timeTrace: partial.timeTrace ?? false,
ci,
buildkite,
webkit: partial.webkit ?? "prebuilt",
localDeps: parseLocalDeps(partial.localDeps, cwd),
cwd,
buildDir,
codegenDir,
cacheDir,
vendorDir,
cc: toolchain.cc,
cxx: toolchain.cxx,
hostCc: toolchain.hostCc ?? toolchain.cc,
hostCxx: toolchain.hostCxx ?? toolchain.cxx,
clangVersion: toolchain.clangVersion,
clangResourceDir: toolchain.clangResourceDir,
ar: toolchain.ar,
ranlib: toolchain.ranlib,
ld: ld64StripSwap?.ld ?? ld,
rustLld: toolchain.rustLld,
rustLlvmVersion: toolchain.rustLlvmVersion,
rustSysroot: toolchain.rustSysroot,
// Cross strips: linux-gnu uses <triple>-strip (GNU, handles -R .eh_frame
// fully; host strip rejects foreign-arch ELF); other cross targets use
// llvm-strip.
strip:
ld64StripSwap?.strip ??
(crossTarget !== undefined
? linux && abi === "gnu" && existsSync(`/usr/bin/${crossTarget}-strip`)
? `/usr/bin/${crossTarget}-strip`
: (toolchain.llvmStrip ?? toolchain.strip)
: toolchain.strip),
dsymutil: toolchain.dsymutil,
bun: toolchain.bun,
jsRuntime: toolchain.jsRuntime,
esbuild: toolchain.esbuild,
ccache: toolchain.ccache,
cmake: toolchain.cmake,
cargo: toolchain.cargo,
cargoHome: toolchain.cargoHome,
rustupHome: toolchain.rustupHome,
rustToolchain: readRustToolchainChannel(cwd),
// Cargo-driven links (the bun_shim_impl.exe edge, any future target
// cdylib) must keep using a real lld-link/link.exe, not the gcc-ld/
// lld-link wrapper `ld` may have been swapped to above: rustc treats a
// linker living in its own sysroot's gcc-ld/ as the bundled rust-lld and
// prepends `-flavor link`, which the wrapper forwards into the COFF
// driver as bogus input args ("could not open 'link'"). Those links have
// no LLVM bitcode in them, so the host LLVM's lld-link is always
// sufficient — only the final clang-cl-driven bun.exe link needs the
// newer rust-lld (and reaches it via the link rule's /clang:-B).
msvcLinker: toolchain.msvcLinker ?? (windows && ld !== toolchain.ld ? toolchain.ld : undefined),
rc: toolchain.rc,
mt: toolchain.mt,
nasm: toolchain.nasm,
osxDeploymentTarget,
osxSysroot,
crossTarget,
sysroot,
winsysroot,
androidNdkRuntimeDir,
version,
revision,
nodejsVersion,
nodejsV8Version,
nodejsAbiVersion,
canaryRevision,
webkitVersion,
};
}
/** Minimum macOS SDK version we support. */
const MIN_OSX_DEPLOYMENT_TARGET = "13.0";
/**
* Detect macOS SDK paths.
*
* - CI: always target the minimum (reproducible builds).
* - Local: target the installed SDK's major version (avoids linker warnings
* about object files built for newer macOS than target).
*
* Fast path: `xcode-select -p` (~5ms) gives the developer dir; from there
* we construct the SDK path and parse the version from the resolved
* symlink. Avoids `xcrun` (~100ms × 2 spawns). Falls back to xcrun only if
* the constructed path doesn't exist (exotic installs).
*/
function detectMacosSdk(ci: boolean): { osxDeploymentTarget: string; osxSysroot: string } {
// xcode-select -p prints the active developer dir (respects
// `xcode-select --switch` and DEVELOPER_DIR). It's a tiny C binary —
// fast enough to be negligible, unlike xcrun which does a bunch of
// environment discovery.
let devDir: string;
try {
devDir = (process.env.DEVELOPER_DIR ?? execSync("xcode-select -p", { encoding: "utf8" })).trim();
} catch (cause) {
throw new BuildError("xcode-select failed — command line tools not installed?", {
hint: "Run: xcode-select --install",
cause,
});
}
// For full Xcode the dev dir is ".../Developer"; for CLT it's
// "/Library/Developer/CommandLineTools". SDK layout differs:
// Xcode: <dev>/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
// CLT: <dev>/SDKs/MacOSX.sdk
//
// Return the SYMLINK path as sysroot (matches what xcrun returns, and
// what ends up in build.ninja — so swapping SDKs doesn't cause a
// spurious full rebuild). But follow the link to PARSE the version
// from the real basename (e.g. MacOSX14.2.sdk → "14").
const candidates = [`${devDir}/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`, `${devDir}/SDKs/MacOSX.sdk`];
let osxSysroot: string | undefined;
let sdkVersionFromPath: string | undefined;
for (const path of candidates) {
if (existsSync(path)) {
osxSysroot = path; // symlink — matches xcrun's output
const resolved = realpathSync(path);
const m = resolved.match(/MacOSX(\d+)(?:\.\d+)*\.sdk$/);
if (m) sdkVersionFromPath = m[1];
break;
}
}
// Neither layout matched — fall back to xcrun. Rare (custom SDK
// locations via SDKROOT env or similar).
if (osxSysroot === undefined) {
try {
osxSysroot = execSync("xcrun --sdk macosx --show-sdk-path", { encoding: "utf8" }).trim();
} catch (cause) {
throw new BuildError("Failed to find macOS SDK path", {
hint: "Run: xcode-select --install",
cause,
});
}
}
let osxDeploymentTarget: string;
if (ci) {
osxDeploymentTarget = MIN_OSX_DEPLOYMENT_TARGET;
} else if (sdkVersionFromPath !== undefined) {
osxDeploymentTarget = sdkVersionFromPath;
} else {
// Couldn't parse from path (unversioned symlink target?) — ask xcrun.
let sdkVersion: string;
try {
sdkVersion = execSync("xcrun --sdk macosx --show-sdk-version", { encoding: "utf8" }).trim();
} catch (cause) {
throw new BuildError("Failed to find macOS SDK version", {
hint: "Run: xcode-select --install",
cause,
});
}
const major = sdkVersion.match(/^(\d+)/)?.[1];
assert(major !== undefined, `Could not parse macOS SDK version: ${sdkVersion}`);
osxDeploymentTarget = major;
}
// Floor at minimum
if (compareVersionStrings(osxDeploymentTarget, MIN_OSX_DEPLOYMENT_TARGET) < 0) {
throw new BuildError(
`macOS SDK ${osxDeploymentTarget} is older than minimum supported ${MIN_OSX_DEPLOYMENT_TARGET}`,
{ hint: "Update Xcode or Xcode Command Line Tools" },
);
}
return { osxDeploymentTarget, osxSysroot };
}
/** Simple X.Y version comparison. Returns -1, 0, 1. */
function compareVersionStrings(a: string, b: string): number {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
const ai = pa[i] ?? 0;
const bi = pb[i] ?? 0;
if (ai !== bi) return ai < bi ? -1 : 1;
}
return 0;
}
/**
* Find the repository root by walking up from cwd looking for package.json
* with name "bun". Exported so `resolveToolchain()` in configure.ts can
* resolve paths correctly when invoked from ninja (where cwd = build dir).
*/
export function findRepoRoot(): string {
let dir = process.cwd();
while (true) {
const pkgPath = join(dir, "package.json");
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string };
if (pkg.name === "bun") {
return dir;
}
} catch {
// Invalid JSON, keep walking
}
}
const parent = resolve(dir, "..");
if (parent === dir) {
throw new BuildError("Could not find bun repository root", { hint: "Run this from within the bun repository" });
}
dir = parent;
}
}
/**
* Parse `--local-deps=name=path[,name=path...]` into name → absolute path.
* Names are checked against `allDeps` later (bun.ts) where the dep list is
* in scope; here we only validate shape and resolve paths.
*/
function parseLocalDeps(spec: string | undefined, cwd: string): Record<string, string> {
// Null prototype: any name (even `__proto__`) is stored as a plain entry and
// reaches the unknown-dep check in validateBunConfig.
const out = Object.create(null) as Record<string, string>;
if (spec === undefined || spec === "") return out;
for (const entry of spec.split(",")) {
const eq = entry.indexOf("=");
if (eq <= 0 || eq === entry.length - 1) {
throw new BuildError(`--local-deps: expected name=path, got '${entry}'`, {
hint: "Example: --local-deps=mimalloc=~/code/mimalloc",
});
}
const name = entry.slice(0, eq);
let path = entry.slice(eq + 1);
if (path === "~" || path.startsWith("~/")) path = join(homedir(), path.slice(1));
out[name] = resolve(cwd, path);
}
return out;
}
/**
* Get the current git revision (HEAD sha).
*
* Uses `git rev-parse` rather than reading .git/HEAD directly — the sha
* is baked into the binary and surfaces in bug reports, so correctness
* matters more than the ~20ms spawn. Git's plumbing has edge cases
* (packed-refs, worktrees, symbolic refs) that rev-parse handles for free.
*/
/**
* Parse the major component out of an X.Y.Z version string.
* Returns undefined for undefined/unparseable input so callers can
* compare without `!` assertions.
*/
function majorOf(version: string | undefined): number | undefined {
if (version === undefined) return undefined;
const m = version.match(/^(\d+)\./);
return m ? Number(m[1]) : undefined;
}
/**
* Read `channel` from `rust-toolchain.toml`. Passed as `RUSTUP_TOOLCHAIN` to
* cargo invocations so vendored Rust deps and the workspace staticlib are
* built with the same nightly — see `Config.rustToolchain` for why rustup's
* own directory walk isn't sufficient when `vendor/` is a worktree-shared
* symlink.
*
* Returns undefined if the file is missing (rustup then falls back to its
* normal lookup, which is correct for the workspace build's cwd).
*/
function readRustToolchainChannel(cwd: string): string | undefined {
const path = resolve(cwd, "rust-toolchain.toml");
if (!existsSync(path)) return undefined;
const m = /^\s*channel\s*=\s*"([^"]+)"/m.exec(readFileSync(path, "utf8"));
return m?.[1];
}
function getGitRevision(cwd: string, pinDir: string | undefined): string {
// CI env first — authoritative and zero-cost.
const envSha = process.env.BUILDKITE_COMMIT ?? process.env.GITHUB_SHA ?? process.env.GIT_SHA;
if (envSha !== undefined && envSha.length > 0) {
return envSha;
}
// Local debug builds pin the sha at first configure: it is a const in `bun_core`, so tracking HEAD would recompile every Rust crate on each commit/checkout/pull.
const pinFile = pinDir === undefined ? undefined : resolve(pinDir, "git-revision");
if (pinFile !== undefined && existsSync(pinFile)) {
const pinned = readFileSync(pinFile, "utf8").trim();
if (/^[0-9a-f]{40}$/.test(pinned)) return pinned;
}
let sha: string;
try {
sha = execSync("git rev-parse HEAD", { cwd, encoding: "utf8" }).trim();
} catch {
return "unknown";
}
if (pinFile !== undefined) {
mkdirSync(pinDir!, { recursive: true });
writeFileSync(pinFile, sha + "\n");
}
return sha;
}
/**
* Compute build directory name based on config.
* Matches the pattern used by package.json scripts.
*/
function computeBuildDirName(c: { debug: boolean; release: boolean; asan: boolean; assertions: boolean }): string {
if (c.debug) return "debug";
if (c.asan) return "release-asan";
if (c.assertions) return "release-assertions";
return "release";
}
/**
* Name of the output executable (no suffix).
*
* Debug builds: bun-debug. Release with ASAN: bun-asan. Etc.
* The plain `bun` name (without -profile) only exists post-strip.
*
* Lives here (not bun.ts) so flags.ts can use it for linker-map filename
* without a circular import.
*/
export function bunExeName(cfg: Config): string {
if (cfg.debug) return "bun-debug";
// Release variants — suffix encodes which features differ from plain release.
// First match wins.
if (cfg.asan && cfg.valgrind) return "bun-asan-valgrind";
if (cfg.asan) return "bun-asan";
if (cfg.valgrind) return "bun-valgrind";
if (cfg.assertions) return "bun-assertions";
// Plain release: called bun-profile (the stripped one is `bun`).
return "bun-profile";
}
/**
* Whether this config produces a stripped `bun` alongside `bun-profile`.
*
* Only plain release builds strip — not debug (you want symbols), not
* asan/valgrind (strip interferes), not assertions (usually debugging).
*/
export function shouldStrip(cfg: Config): boolean {
return !cfg.debug && !cfg.asan && !cfg.valgrind && !cfg.assertions;
}
const c = { dim, cyan, green };
/**
* Format a config for display (used at configure time).
* `exe` is the output binary name (e.g. "bun-debug" or "bun-profile → bun (stripped)").
*/
export function formatConfig(cfg: Config, exe: string): string {
const label = (s: string) => c.dim(s.padEnd(12));
// Relative build dir with ./ prefix — shorter, copy-pastable.
const relBuildDir = `.${sep}${relative(cfg.cwd, cfg.buildDir)}`;
const lines: string[] = [
`[configured] ${c.green(exe)}`,
` ${label("target")} ${cfg.os}-${cfg.arch}${cfg.abi !== undefined ? "-" + cfg.abi : ""}`,
` ${label("build type")} ${cfg.buildType}`,
` ${label("build dir")} ${relBuildDir}`,
` ${label("revision")} ${cfg.revision === "unknown" ? "unknown" : cfg.revision.slice(0, 10)}${cfg.debug && !cfg.ci ? " (pinned; rm <build dir>/git-revision to refresh)" : ""}`,
];
const features: string[] = [];
if (cfg.lto) features.push("lto");
if (cfg.pgoGenerate) features.push("pgo-gen");
if (cfg.pgoUse) features.push("pgo-use");
if (cfg.asan) features.push("asan");
if (cfg.assertions) features.push("assertions");
if (cfg.logs) features.push("logs");
if (cfg.baseline) features.push("baseline");
if (cfg.valgrind) features.push("valgrind");
if (cfg.fuzzilli) features.push("fuzzilli");
if (cfg.socketFaultInjection !== cfg.asan) {
features.push(`socket-fault-injection:${cfg.socketFaultInjection ? "on" : "off"}`);
}
if (!cfg.canary) features.push("canary:off");
// Non-default modes — show so you notice when a build is unusual.
if (cfg.webkit !== "prebuilt") features.push(`webkit:${cfg.webkit}`);
for (const name of Object.keys(cfg.localDeps)) features.push(`local:${name}`);
if (cfg.mode !== "full") features.push(`mode:${cfg.mode}`);
// Version pin overrides — show an identifying value so you catch "forgot
// to revert my WebKit test branch" before the build goes weird. Strip the
// autobuild- prefix so preview tags show their sha instead of the prefix.
if (cfg.webkitVersion !== versionDefaults.webkitVersion) {
const v = cfg.webkitVersion.startsWith("autobuild-")
? cfg.webkitVersion.slice("autobuild-".length)
: cfg.webkitVersion;
features.push(`webkit-version:${/^[0-9a-f]{40}$/.test(v) ? v.slice(0, 10) : v}`);
}
if (cfg.nodejsVersion !== versionDefaults.nodejsVersion) features.push(`nodejs:${cfg.nodejsVersion}`);
lines.push(` ${label("features")} ${features.length > 0 ? c.cyan(features.join(", ")) : c.dim("(none)")}`);
return lines.join("\n");
}
/**
* One-line "nothing changed" configure message. Bracketed to match the
* [name] prefix style used by deps.
*/
export function formatConfigUnchanged(exe: string, elapsed: number): string {
return `[configured] ${c.green(exe)} in ${elapsed}ms ${c.dim("(unchanged)")}`;
}