Files
2026-08-27 21:09:14 +00:00

1615 lines
68 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)")}`;
}