1742 lines
71 KiB
TypeScript
1742 lines
71 KiB
TypeScript
/**
|
|
* Compiler, linker, and define flags for the bun target.
|
|
*
|
|
* Design: ONE flat table per flag category. Each entry has a `when` predicate
|
|
* and a `desc`. To find out why a flag is set, grep for it in this file.
|
|
* Related flags (e.g. `-fno-unwind-tables` + `--no-eh-frame-hdr` + strip
|
|
* `-R .eh_frame`) live adjacent so their coupling is obvious.
|
|
*
|
|
* Note: dependency include paths (WebKit, boringssl, etc.) are NOT here —
|
|
* they come from each dep's `Provides.includes`. This file covers only flags
|
|
* that apply uniformly to bun's own C/C++ sources.
|
|
*/
|
|
|
|
import { join } from "node:path";
|
|
import { bunExeName, type Config } from "./config.ts";
|
|
import { quote, slash } from "./shell.ts";
|
|
import { ucrtServicingLibDir } from "./winsysroot.ts";
|
|
|
|
export type FlagValue = string | string[] | ((cfg: Config) => string | string[]);
|
|
|
|
/**
|
|
* Main-thread stack size for darwin executables (18 MB — JSC's interpreter
|
|
* and the bundler's visitor recursion both go deep). Passed to the linker as
|
|
* `-Wl,-stack_size` on native darwin links; ld64.lld parses but doesn't
|
|
* implement that option, so cross links additionally patch LC_MAIN.stacksize
|
|
* post-link with the same value (shims/macho-postlink.c). Keep in sync with
|
|
* the Windows `/STACK:` reserve and the Linux `-z stack-size` below.
|
|
*/
|
|
export const DARWIN_STACK_SIZE = "0x1200000";
|
|
|
|
export interface Flag {
|
|
/** Flag(s) to emit. Can be a function for flags that interpolate config values. */
|
|
flag: FlagValue;
|
|
/** Predicate. Omitted = always. */
|
|
when?: (cfg: Config) => boolean;
|
|
/** Restrict to one language. Omitted = both C and C++. */
|
|
lang?: "c" | "cxx";
|
|
/** What this flag does. */
|
|
desc: string;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CPU TARGET FLAGS
|
|
// -march/-mcpu/-mtune. Split out so deps that manage their own optimization
|
|
// and sanitizer flags (WebKit) can still inherit the target arch without
|
|
// the rest of globalFlags.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export const cpuTargetFlags: Flag[] = [
|
|
{
|
|
flag: "-mcpu=apple-m1",
|
|
when: c => c.darwin && c.arm64,
|
|
desc: "Target Apple M1 (works on all Apple Silicon)",
|
|
},
|
|
{
|
|
flag: ["-march=armv8-a+crc", "-mtune=ampere1"],
|
|
when: c => (c.linux || c.freebsd) && c.arm64 && c.abi !== "android",
|
|
desc: "ARM64 Linux/FreeBSD: ARMv8-A base + CRC, tuned for Ampere (Graviton-like)",
|
|
},
|
|
{
|
|
flag: ["-march=armv8-a+crc", "-mtune=cortex-a78"],
|
|
when: c => c.linux && c.arm64 && c.abi === "android",
|
|
desc: "ARM64 Android: ARMv8-A base + CRC, tuned for Cortex-A78 (common big core)",
|
|
},
|
|
{
|
|
flag: ["/clang:-march=armv8-a+crc", "/clang:-mtune=ampere1"],
|
|
when: c => c.windows && c.arm64,
|
|
desc: "ARM64 Windows: clang-cl prefix required (/clang: passes to clang)",
|
|
},
|
|
{
|
|
flag: "-march=nehalem",
|
|
when: c => c.x64,
|
|
desc: "x64: Nehalem (2008) — no AVX, broadest compatibility",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// GLOBAL COMPILER FLAGS
|
|
// Applied to BOTH bun's own sources AND forwarded to vendored deps
|
|
// via -DCMAKE_C_FLAGS / -DCMAKE_CXX_FLAGS.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export const globalFlags: Flag[] = [
|
|
// ─── Cross-compilation target/sysroot ───
|
|
// Generic — currently only Android sets these. Kept first so the
|
|
// target triple is in effect before any arch-dependent flags.
|
|
{
|
|
flag: c => `--target=${c.crossTarget!}`,
|
|
when: c => c.crossTarget !== undefined,
|
|
desc: "Cross-compile target triple (clang is inherently a cross-compiler)",
|
|
},
|
|
{
|
|
flag: c => `--sysroot=${c.sysroot!}`,
|
|
when: c => c.sysroot !== undefined,
|
|
desc: "Cross-compile sysroot (target libc headers + libs)",
|
|
},
|
|
{
|
|
// Windows cross-compile: clang-cl can't read the VS dev shell's INCLUDE
|
|
// env on a non-Windows host. /winsysroot points it at an xwin-style
|
|
// splat laid out like a VS install (VC/Tools/MSVC + Windows Kits/10),
|
|
// covering the MSVC CRT/STL and Windows SDK headers + import libs.
|
|
// The lld-link equivalent (/winsysroot:) is added in linkerFlags below.
|
|
flag: c => ["/winsysroot", quote(c.winsysroot!, false)],
|
|
when: c => c.windows && c.winsysroot !== undefined,
|
|
desc: "Windows cross-compile: MSVC CRT + Windows SDK root (xwin splat)",
|
|
},
|
|
{
|
|
// Same host-GCC #include_next leak as the FreeBSD block below: on
|
|
// amazonlinux, clang's driver injects /usr/include/c++/N even with
|
|
// --sysroot. -nostdlibinc drops all default system include paths
|
|
// (keeping clang's resource dir) and we add back only the NDK's three
|
|
// dirs in the order #include_next expects: libc++ → per-arch bionic
|
|
// → generic bionic.
|
|
flag: c => [
|
|
"-nostdlibinc",
|
|
"-isystem",
|
|
join(c.sysroot!, "usr", "include", "c++", "v1"),
|
|
"-isystem",
|
|
join(c.sysroot!, "usr", "include", c.arm64 ? "aarch64-linux-android" : "x86_64-linux-android"),
|
|
"-isystem",
|
|
join(c.sysroot!, "usr", "include"),
|
|
],
|
|
when: c => c.abi === "android",
|
|
lang: "cxx",
|
|
desc: "Android: explicit NDK include paths only (suppress host GCC C++ detection)",
|
|
},
|
|
{
|
|
flag: ["-DANDROID", "-D_FILE_OFFSET_BITS=64"],
|
|
when: c => c.abi === "android",
|
|
desc: "Android: platform define + 64-bit off_t (bionic defaults to 32-bit on LP32)",
|
|
},
|
|
{
|
|
// On hosts with a GCC install (amazonlinux), clang's driver auto-detects
|
|
// it and injects /usr/include/c++/N into the search list — even with
|
|
// --sysroot, --gcc-toolchain, and -nostdinc++. That breaks #include_next
|
|
// in the sysroot's libc++ headers. -nostdlibinc drops ALL standard system
|
|
// include paths (host AND sysroot defaults) while keeping clang's own
|
|
// resource dir (stddef.h etc.); we then add back only the two sysroot
|
|
// dirs in the correct order so #include_next from c++/v1 finds usr/include.
|
|
flag: c => [
|
|
"-nostdlibinc",
|
|
"-isystem",
|
|
join(c.sysroot!, "usr", "include", "c++", "v1"),
|
|
"-isystem",
|
|
join(c.sysroot!, "usr", "include"),
|
|
],
|
|
when: c => c.freebsd && c.sysroot !== undefined,
|
|
lang: "cxx",
|
|
desc: "FreeBSD: explicit sysroot include paths only (suppress host GCC C++ detection)",
|
|
},
|
|
{
|
|
// C compiles don't have the host-C++-path leak, so --sysroot's default
|
|
// search is fine — but it omits __BSD_VISIBLE when no feature-test macro
|
|
// is set on some clang configs. Match what FreeBSD's own cc does.
|
|
flag: "-D__BSD_VISIBLE=1",
|
|
when: c => c.freebsd,
|
|
desc: "FreeBSD: expose BSD typedefs (u_long etc.) in <sys/types.h>",
|
|
},
|
|
{
|
|
// macOS cross-compile: C++ must see Apple's libc++ from the SDK and
|
|
// nothing from the build host. Left to its defaults, clang prefers its
|
|
// own toolchain libc++ headers (<llvm>/include/c++/v1) — a newer libc++
|
|
// than the OS dylib the SDK's libc++.tbd describes, so compiles pick up
|
|
// out-of-line symbols (e.g. std::__hash_memory, added in libc++ 19) that
|
|
// Apple's libc++ never exports → undefined symbols at link. On CI images
|
|
// with a host GCC install the driver also injects the host's
|
|
// /usr/include/c++/N and /usr/include into the search list (the FreeBSD
|
|
// block above documents the same behaviour; -nostdlibinc alone did not
|
|
// stop it there either), which breaks the SDK headers' #include_next
|
|
// chains. So drop every default include dir (-nostdinc) and rebuild the
|
|
// exact list Apple's own driver uses: SDK libc++ → clang builtins → SDK
|
|
// libc → SDK frameworks. Native darwin builds keep the default search.
|
|
flag: c => [
|
|
"-nostdinc",
|
|
"-isystem",
|
|
join(c.osxSysroot!, "usr", "include", "c++", "v1"),
|
|
"-isystem",
|
|
join(c.clangResourceDir!, "include"),
|
|
"-isystem",
|
|
join(c.osxSysroot!, "usr", "include"),
|
|
"-iframework",
|
|
join(c.osxSysroot!, "System", "Library", "Frameworks"),
|
|
],
|
|
when: c =>
|
|
c.darwin && c.crossTarget !== undefined && c.osxSysroot !== undefined && c.clangResourceDir !== undefined,
|
|
lang: "cxx",
|
|
desc: "macOS cross: only the SDK's (Apple) libc++/libc/framework headers + clang builtins",
|
|
},
|
|
|
|
{
|
|
// Emit the address-significance table (__DATA,__llvm_addrsig) so
|
|
// ld64.lld's --icf=safe knows which functions never have their address
|
|
// taken and can be folded without breaking pointer-identity comparisons
|
|
// (the same table -faddrsig emits by default on ELF for the Linux
|
|
// -Wl,-icf=safe). Cross-only: lld consumes the section and drops it
|
|
// from the output, but Apple's ld doesn't know it and would copy ~8
|
|
// bytes of dead __DATA per TU into the native binary — and `strip`
|
|
// won't remove a regular data section post-link. Flip the predicate to
|
|
// plain `c.darwin` once someone confirms on a Mac that Apple's ld
|
|
// discards it.
|
|
flag: "-faddrsig",
|
|
when: c => c.darwin && c.crossTarget !== undefined,
|
|
desc: "macOS cross: address-significance table for the linker's safe ICF",
|
|
},
|
|
|
|
// ─── CPU target ───
|
|
...cpuTargetFlags,
|
|
{
|
|
// CMake auto-added these via CMAKE_OSX_DEPLOYMENT_TARGET/CMAKE_OSX_SYSROOT;
|
|
// we must add explicitly. Without this, clang/ld64 default to the host SDK
|
|
// version — CI builds get minos=15.0, breaking macOS 13/14 users at launch.
|
|
flag: c => [`-mmacosx-version-min=${c.osxDeploymentTarget!}`, "-isysroot", c.osxSysroot!],
|
|
when: c => c.darwin && c.osxDeploymentTarget !== undefined && c.osxSysroot !== undefined,
|
|
desc: "macOS deployment target + SDK (sets LC_BUILD_VERSION minos)",
|
|
},
|
|
|
|
// ─── MSVC runtime (Windows) ───
|
|
{
|
|
flag: "/MTd",
|
|
when: c => c.windows && c.debug,
|
|
desc: "Static debug MSVC runtime",
|
|
},
|
|
{
|
|
flag: "/MT",
|
|
when: c => c.windows && c.release,
|
|
desc: "Static MSVC runtime",
|
|
},
|
|
{
|
|
flag: "/U_DLL",
|
|
when: c => c.windows,
|
|
desc: "Undefine _DLL (we link statically)",
|
|
},
|
|
|
|
// ─── Optimization ───
|
|
{
|
|
// cmake's Release/RelWithDebInfo build types append this to
|
|
// CMAKE_<LANG>_FLAGS_<TYPE> automatically; nested-cmake deps got it
|
|
// from there. Direct deps only see globalFlags, so it must be here
|
|
// too — otherwise every assert() in zstd/boringssl/mimalloc/etc.
|
|
// stays live in release. (bun's own NDEBUG in `defines` below is
|
|
// redundant after this, but harmless.)
|
|
flag: "-DNDEBUG",
|
|
when: c => c.release,
|
|
desc: "Disable libc assert() (release builds, direct deps included)",
|
|
},
|
|
{
|
|
flag: "-O0",
|
|
when: c => c.unix && c.debug,
|
|
desc: "No optimization (debug)",
|
|
},
|
|
{
|
|
flag: "/Od",
|
|
when: c => c.windows && c.debug,
|
|
desc: "No optimization (debug)",
|
|
},
|
|
{
|
|
flag: "-Os",
|
|
when: c => c.unix && c.smol,
|
|
desc: "Optimize for size (MinSizeRel)",
|
|
},
|
|
{
|
|
flag: "/Os",
|
|
when: c => c.windows && c.smol,
|
|
desc: "Optimize for size (MinSizeRel)",
|
|
},
|
|
{
|
|
flag: "-O3",
|
|
when: c => c.unix && c.release && !c.smol,
|
|
desc: "Optimize for speed",
|
|
},
|
|
{
|
|
flag: "/O2",
|
|
when: c => c.windows && c.release && !c.smol,
|
|
desc: "Optimize for speed (MSVC /O2 ≈ clang -O2)",
|
|
},
|
|
|
|
// ─── Debug info ───
|
|
{
|
|
flag: "/Z7",
|
|
when: c => c.windows,
|
|
desc: "Emit debug info into .obj (no .pdb during compile)",
|
|
},
|
|
{
|
|
flag: "-gdwarf-4",
|
|
when: c => c.darwin,
|
|
desc: "DWARF 4 debug info (dsymutil-compatible)",
|
|
},
|
|
{
|
|
// Must stay ahead of the -gN entries below: every -g flag clang sees
|
|
// resets the debug-info level, and -glldb (like plain -g) selects the
|
|
// full standalone kind. With it after -g1, release cc1 invocations got
|
|
// -debug-info-kind=standalone (check with -###) and every release TU,
|
|
// deps included, emitted full DWARF: 98% of the ~1.7 GB of objects bun's
|
|
// own C++ produced in a release build, 740 MB of .debug_* in bun-profile.
|
|
// test/internal/build-debug-info-flags.test.ts pins the order.
|
|
flag: "-glldb",
|
|
when: c => c.unix,
|
|
desc: "Tune debug info for LLDB (before the level flags; the last -g flag wins)",
|
|
},
|
|
{
|
|
// Nix LLVM doesn't support zstd — but we target standard distros.
|
|
// Nix users can override via profile if needed.
|
|
flag: ["-g3", "-gz=zstd"],
|
|
when: c => c.unix && c.debug,
|
|
desc: "Full debug info, zstd-compressed",
|
|
},
|
|
{
|
|
flag: ["-g", "-gz=zstd"],
|
|
when: c => c.unix && c.release && !c.lto,
|
|
desc: "Full debug info (types and variables) where no LTO link has to carry it: local release, asan, the non-LTO CI lanes",
|
|
},
|
|
{
|
|
// -glldb implies -fstandalone-debug: every TU emits the definition of
|
|
// every type it can see, i.e. the whole JSC/WTF universe the PCH pulls
|
|
// in, again. Homing (clang's default on Linux) emits a type once, in the
|
|
// TU that defines its constructor or vtable, so bun's types come from
|
|
// bun's own objects and JSC's from the WebKit prebuilt, which carries its
|
|
// own DWARF. Same types and variables in the debugger; BunObject.cpp
|
|
// measured 8.2s -> 5.35s to compile (the line-tables-only build of the
|
|
// same file is 5.25s), object 9.0 MB -> 5.2 MB. Irrelevant to the LTO
|
|
// builds: line tables (-g1 below) carry no type definitions either way.
|
|
flag: "-fno-standalone-debug",
|
|
when: c => c.unix && (c.debug || !c.lto),
|
|
desc: "Emit each type's debug info once, where it is defined, instead of in every TU",
|
|
},
|
|
{
|
|
// The bitcode carries the debug info through the ThinLTO link, which is the longest step of a CI build; the -lto WebKit prebuilts make the same trade.
|
|
flag: "-g1",
|
|
when: c => c.unix && c.release && c.lto,
|
|
desc: "Line tables only for LTO builds (backtraces and symbolication; no types)",
|
|
},
|
|
|
|
// ─── ASAN (global — passed to deps so they link against the same runtime) ───
|
|
// Unlike UBSan (bun-target-only below), ASAN must be global: the runtime
|
|
// library has to match across all linked objects or you get crashes at init.
|
|
{
|
|
flag: "-fsanitize=address",
|
|
when: c => c.asan,
|
|
desc: "AddressSanitizer (also forwarded to deps for ABI consistency)",
|
|
},
|
|
|
|
// ─── C++ language behavior ───
|
|
{
|
|
flag: "-fno-exceptions",
|
|
when: c => c.unix,
|
|
desc: "Disable C++ exceptions",
|
|
},
|
|
{
|
|
flag: "/EHs-c-",
|
|
when: c => c.windows,
|
|
desc: "Disable C++ exceptions (MSVC: s- disables C++, c- disables C)",
|
|
},
|
|
{
|
|
flag: "-fno-c++-static-destructors",
|
|
when: c => c.unix,
|
|
lang: "cxx",
|
|
desc: "Skip static destructors at exit (JSC assumes this)",
|
|
},
|
|
{
|
|
// /clang: (not -Xclang): single token survives CMAKE_CXX_FLAGS
|
|
// re-tokenization in nested dep configures. -Xclang <arg> pair gets
|
|
// split by cmake's try_compile → "unknown argument". Also more correct:
|
|
// /clang: passes to the driver, -Xclang to cc1.
|
|
flag: "/clang:-fno-c++-static-destructors",
|
|
when: c => c.windows,
|
|
lang: "cxx",
|
|
desc: "Skip static destructors at exit (clang-cl syntax)",
|
|
},
|
|
{
|
|
flag: "-fno-rtti",
|
|
when: c => c.unix,
|
|
desc: "Disable RTTI (no dynamic_cast/typeid)",
|
|
},
|
|
{
|
|
flag: "/GR-",
|
|
when: c => c.windows,
|
|
desc: "Disable RTTI (MSVC syntax)",
|
|
},
|
|
|
|
// ─── Frame pointers (needed for profiling/backtraces) ───
|
|
{
|
|
flag: ["-fno-omit-frame-pointer", "-mno-omit-leaf-frame-pointer"],
|
|
when: c => c.unix,
|
|
desc: "Keep frame pointers (for profiling and backtraces)",
|
|
},
|
|
{
|
|
flag: "/Oy-",
|
|
when: c => c.windows,
|
|
desc: "Keep frame pointers",
|
|
},
|
|
|
|
// ─── Visibility ───
|
|
{
|
|
flag: ["-fvisibility=hidden", "-fvisibility-inlines-hidden"],
|
|
when: c => c.unix,
|
|
desc: "Hidden symbol visibility (explicit exports only)",
|
|
},
|
|
|
|
// ─── Unwinding / exception tables ───
|
|
// These go together: -fno-unwind-tables at compile, --no-eh-frame-hdr at
|
|
// link (release glibc), and strip -R .eh_frame at post-link (release glibc).
|
|
// See linkerFlags and stripFlags below — kept adjacent intentionally.
|
|
{
|
|
flag: ["-fno-unwind-tables", "-fno-asynchronous-unwind-tables"],
|
|
when: c => c.unix,
|
|
desc: "Skip unwind tables (we don't use C++ exceptions)",
|
|
},
|
|
{
|
|
// libuv stubs use C23 anonymous parameters
|
|
flag: "-Wno-c23-extensions",
|
|
when: c => c.unix,
|
|
desc: "Allow C23 extensions (libuv stubs use anonymous parameters)",
|
|
},
|
|
|
|
// ─── Sections (enables dead-code stripping at link) ───
|
|
{
|
|
flag: "-ffunction-sections",
|
|
when: c => c.unix,
|
|
desc: "One section per function (for --gc-sections)",
|
|
},
|
|
{
|
|
flag: "/Gy",
|
|
when: c => c.windows,
|
|
desc: "One section per function (COMDAT folding)",
|
|
},
|
|
{
|
|
flag: "-fdata-sections",
|
|
when: c => c.unix,
|
|
desc: "One section per data item (for --gc-sections)",
|
|
},
|
|
{
|
|
flag: "/Gw",
|
|
when: c => c.windows,
|
|
desc: "One section per data item",
|
|
},
|
|
{
|
|
// Address-significance table: enables safe ICF at link.
|
|
// Macos debug mode + this flag breaks libarchive configure ("pid_t doesn't exist").
|
|
// darwin cross targets get this from their own entry above (debug and
|
|
// release), so skip them here rather than emit the flag twice.
|
|
flag: "-faddrsig",
|
|
when: c => (c.debug && c.linux) || (c.release && c.unix && !(c.darwin && c.crossTarget !== undefined)),
|
|
desc: "Emit address-significance table (enables safe ICF)",
|
|
},
|
|
|
|
// ─── Windows-specific codegen ───
|
|
{
|
|
flag: "/GF",
|
|
when: c => c.windows,
|
|
desc: "String pooling (merge identical string literals)",
|
|
},
|
|
{
|
|
flag: "/GA",
|
|
when: c => c.windows,
|
|
desc: "Optimize TLS access (assume vars defined in executable)",
|
|
},
|
|
|
|
// ─── Linux-specific codegen ───
|
|
{
|
|
flag: "-fno-semantic-interposition",
|
|
when: c => c.linux,
|
|
desc: "Assume no symbol interposition (enables more inlining across TUs)",
|
|
},
|
|
|
|
// ─── Hardening (assertions builds) ───
|
|
{
|
|
flag: "-fno-delete-null-pointer-checks",
|
|
when: c => c.assertions,
|
|
desc: "Don't optimize out null checks (hardening)",
|
|
},
|
|
|
|
// ─── Diagnostics ───
|
|
{
|
|
flag: "-fdiagnostics-color=always",
|
|
when: c => c.unix,
|
|
desc: "Colored compiler errors",
|
|
},
|
|
{
|
|
flag: "-ferror-limit=100",
|
|
when: c => c.unix,
|
|
desc: "Stop after 100 errors",
|
|
},
|
|
{
|
|
flag: "/clang:-ferror-limit=100",
|
|
when: c => c.windows,
|
|
desc: "Stop after 100 errors (clang-cl syntax)",
|
|
},
|
|
|
|
// ─── LTO (compile-side) ───
|
|
{
|
|
// Thin, not full: each .o carries a per-module summary so the link runs
|
|
// the LTO backends in parallel (and could cache them) instead of merging
|
|
// every module into one serial multi-gigabyte regular-LTO partition. The
|
|
// WebKit macos -lto prebuilts and rustc's -Clinker-plugin-lto bitcode are
|
|
// both ThinLTO-summaried, so this makes the whole link one uniform
|
|
// ThinLTO graph with cross-module importing across C++/Rust/JSC
|
|
// boundaries. All platforms now use ThinLTO (the linux JSC ThinLTO
|
|
// miscompile was fixed in the WebKit prebuilt).
|
|
flag: "-flto=thin",
|
|
when: c => c.unix && c.lto,
|
|
desc: "Thin link-time optimization",
|
|
},
|
|
{
|
|
// Windows (cross) uses ThinLTO like darwin: clang-cl accepts -flto=thin
|
|
// directly (core option), the WebKit windows-amd64-lto prebuilt is
|
|
// ThinLTO-summaried bitcode, and rustc's -Clinker-plugin-lto bitcode is
|
|
// too, so lld-link runs one uniform ThinLTO graph with cross-language
|
|
// importing. lld-link does LTO automatically when it sees bitcode
|
|
// inputs — no link-side -flto spelling exists or is needed there.
|
|
flag: "-flto=thin",
|
|
when: c => c.windows && c.lto,
|
|
desc: "Thin link-time optimization (clang-cl)",
|
|
},
|
|
{
|
|
// Unix only (not windows): on COFF, whole-program vtable opt drops
|
|
// vtable symbols that associative COMDAT sections still name as their
|
|
// parent and the LTO codegen aborts ("Associative COMDAT symbol
|
|
// '??_7...' does not exist"). The WebKit windows-amd64-lto prebuilt is
|
|
// built without it for the same reason.
|
|
flag: ["-fforce-emit-vtables", "-fwhole-program-vtables"],
|
|
when: c => c.unix && c.lto,
|
|
lang: "cxx",
|
|
desc: "Enable devirtualization across whole program (LTO only)",
|
|
},
|
|
{
|
|
// Every summaried bitcode module in the link must agree on the
|
|
// EnableSplitLTOUnit flag or lld dies with "inconsistent LTO Unit
|
|
// splitting". -fwhole-program-vtables (above) defaults the split ON for
|
|
// C++ on ELF targets but it's a cxx-only flag, so the C modules (zlib,
|
|
// c-ares, mimalloc, boringssl's .c files, ...) would disagree. Force it
|
|
// OFF everywhere instead of on: split LTO units shunt every module's
|
|
// vtables + type metadata into a merged regular-LTO half — a serial
|
|
// merged module, which is exactly what ThinLTO is supposed to avoid.
|
|
// The type hierarchy goes into the per-module ThinLTO summaries instead
|
|
// (typeidCompatibleVTable entries) and whole-program devirtualization
|
|
// runs in index-based mode via --lto-whole-program-visibility at link
|
|
// time. 0 is also the default for rustc, for Apple targets, and for the
|
|
// WebKit -lto prebuilts, so this is the configuration that can't drift.
|
|
flag: "-fno-split-lto-unit",
|
|
when: c => c.lto,
|
|
desc: "Index-based WPD: keep type metadata in the ThinLTO summaries, no regular-LTO half",
|
|
},
|
|
|
|
// ─── PGO (compile-side) ───
|
|
{
|
|
flag: c => `-fprofile-generate=${c.pgoGenerate}`,
|
|
when: c => c.unix && !!c.pgoGenerate,
|
|
desc: "IR PGO: instrument for profile generation",
|
|
},
|
|
{
|
|
flag: c => [
|
|
`-fprofile-use=${c.pgoUse}`,
|
|
"-Wno-profile-instr-out-of-date",
|
|
"-Wno-profile-instr-unprofiled",
|
|
"-Wno-backend-plugin",
|
|
],
|
|
when: c => c.unix && !!c.pgoUse,
|
|
desc: "IR PGO: optimize with profile data",
|
|
},
|
|
|
|
// ─── Path remapping (CI reproducibility) ───
|
|
{
|
|
flag: c => [
|
|
`-ffile-prefix-map=${c.cwd}=.`,
|
|
`-ffile-prefix-map=${c.vendorDir}=vendor`,
|
|
`-ffile-prefix-map=${c.cacheDir}=cache`,
|
|
],
|
|
when: c => c.unix && c.ci,
|
|
desc: "Remap source paths in debug info (reproducible builds)",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// BUN-ONLY COMPILER FLAGS
|
|
// Applied ONLY to bun's own .c/.cpp files, NOT forwarded to deps.
|
|
// This is where -Werror, sanitizer flags, and bun-specific tweaks live.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export const bunOnlyFlags: Flag[] = [
|
|
// ─── Build profiling ───
|
|
{
|
|
flag: "-ftime-trace",
|
|
when: c => c.timeTrace,
|
|
lang: "cxx",
|
|
desc: "Emit per-TU Chrome-trace JSON next to each .o (analyze with ClangBuildAnalyzer)",
|
|
},
|
|
|
|
// ─── Language standard ───
|
|
// WebKit uses gnu++ extensions on Linux; if we don't match, the first
|
|
// memory allocation crashes (ABI mismatch in sized delete).
|
|
// Not in globalFlags because deps set their own standard.
|
|
{
|
|
flag: "-std=gnu++23",
|
|
when: c => c.linux || c.freebsd,
|
|
lang: "cxx",
|
|
desc: "C++23 with GNU extensions (required to match WebKit's ABI on Linux/FreeBSD)",
|
|
},
|
|
{
|
|
flag: "-std=c++23",
|
|
when: c => c.darwin,
|
|
lang: "cxx",
|
|
desc: "C++23 standard",
|
|
},
|
|
{
|
|
flag: "/std:c++23preview",
|
|
when: c => c.windows,
|
|
lang: "cxx",
|
|
desc: "C++23 standard (MSVC flag for clang-cl)",
|
|
},
|
|
// C standard: gnu17 on unix (cmake: C_STANDARD 17 with C_EXTENSIONS
|
|
// default ON → -std=gnu17). Can't go to C23 — MSVC doesn't support it.
|
|
// Most .c files are usockets/llhttp which would compile fine without,
|
|
// but explicit is better than compiler-default drift.
|
|
{
|
|
flag: "-std=gnu17",
|
|
when: c => c.unix,
|
|
lang: "c",
|
|
desc: "C17 with GNU extensions (matches cmake's C_STANDARD 17 + default C_EXTENSIONS)",
|
|
},
|
|
{
|
|
flag: "/std:c17",
|
|
when: c => c.windows,
|
|
lang: "c",
|
|
desc: "C17 standard (MSVC-mode flag for clang-cl; no GNU extensions in MSVC mode)",
|
|
},
|
|
|
|
// ─── Sanitizers (bun only — deps would break with -Werror + UBSan) ───
|
|
// Note: -fsanitize=address is in globalFlags (deps need ABI consistency).
|
|
// UBSan is bun-only because it's stricter and vendored code often violates it.
|
|
// Enabled: debug builds (non-musl — musl's implementation hits false positives),
|
|
// and release-asan builds (if you're debugging memory you want UBSan too).
|
|
// Darwin cross-compiles are excluded: the UBSan runtime dylib for macOS
|
|
// isn't shipped by the Linux LLVM toolchain, so the link would fail.
|
|
{
|
|
flag: [
|
|
"-fsanitize=null",
|
|
"-fno-sanitize-recover=all",
|
|
"-fsanitize=bounds",
|
|
"-fsanitize=return",
|
|
"-fsanitize=nullability-arg",
|
|
"-fsanitize=nullability-assign",
|
|
"-fsanitize=nullability-return",
|
|
"-fsanitize=returns-nonnull-attribute",
|
|
"-fsanitize=unreachable",
|
|
],
|
|
when: c =>
|
|
c.unix &&
|
|
!(c.darwin && c.crossTarget !== undefined) &&
|
|
((c.debug && c.abi !== "musl" && c.abi !== "android" && !c.freebsd) || (c.release && c.asan)),
|
|
desc: "Undefined-behavior sanitizers",
|
|
},
|
|
{
|
|
flag: ["-fsanitize-coverage=trace-pc-guard", "-DFUZZILLI_ENABLED"],
|
|
when: c => c.fuzzilli,
|
|
desc: "Fuzzilli coverage instrumentation",
|
|
},
|
|
|
|
// ─── Bun-target-specific ───
|
|
{
|
|
flag: ["-fconstexpr-steps=6000000", "-fconstexpr-depth=54"],
|
|
when: c => c.unix,
|
|
lang: "cxx",
|
|
desc: "Raise constexpr limits (JSC uses heavy constexpr; under ASSERT_ENABLED, ASCIILiteral::fromLiteralUnsafe constexpr-validates the largest embedded builtin source in InternalModuleRegistryConstants.h char by char)",
|
|
},
|
|
{
|
|
flag: ["-fno-pic", "-fno-pie"],
|
|
when: c => c.unix && c.abi !== "android",
|
|
desc: "No position-independent code (we're a final executable)",
|
|
},
|
|
{
|
|
flag: "-fPIC",
|
|
when: c => c.abi === "android",
|
|
desc: "Android requires PIE since API 21; bionic's loader rejects non-PIE",
|
|
},
|
|
|
|
// ─── Warnings-as-errors (unix) ───
|
|
{
|
|
flag: [
|
|
"-Werror=return-type",
|
|
"-Werror=return-stack-address",
|
|
"-Werror=implicit-function-declaration",
|
|
"-Werror=uninitialized",
|
|
"-Werror=conditional-uninitialized",
|
|
"-Werror=suspicious-memaccess",
|
|
"-Werror=int-conversion",
|
|
"-Werror=nonnull",
|
|
"-Werror=move",
|
|
"-Werror=sometimes-uninitialized",
|
|
"-Wno-c++23-lambda-attributes",
|
|
"-Wno-nullability-completeness",
|
|
"-Wno-character-conversion",
|
|
"-Werror",
|
|
],
|
|
when: c => c.unix,
|
|
desc: "Treat most warnings as errors; suppress known-noisy ones",
|
|
},
|
|
{
|
|
// Debug adds -Werror=unused; release omits it (vars used only in ASSERT)
|
|
flag: ["-Werror=unused", "-Wno-unused-function"],
|
|
when: c => c.unix && c.debug,
|
|
desc: "Warn on unused vars in debug (catches dead code)",
|
|
},
|
|
{
|
|
// Windows: suppress noisy warnings from headers we don't control
|
|
flag: [
|
|
"-Wno-nullability-completeness",
|
|
"-Wno-inconsistent-dllimport",
|
|
"-Wno-incompatible-pointer-types",
|
|
"-Wno-deprecated-declarations",
|
|
"-Wno-character-conversion",
|
|
],
|
|
when: c => c.windows,
|
|
desc: "Suppress noisy warnings from system/dependency headers",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PREPROCESSOR DEFINES
|
|
// -D flags passed to every bun compilation unit.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export const defines: Flag[] = [
|
|
// ─── Always on ───
|
|
{
|
|
flag: [
|
|
"_HAS_EXCEPTIONS=0",
|
|
"LIBUS_USE_OPENSSL=1",
|
|
"STATICALLY_LINKED_WITH_JavaScriptCore=1",
|
|
"BUILDING_WITH_CMAKE=1",
|
|
"JSC_OBJC_API_ENABLED=0",
|
|
"NAPI_EXPERIMENTAL=ON",
|
|
"NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT=1",
|
|
"NOMINMAX",
|
|
"BUILDING_JSCONLY__",
|
|
],
|
|
desc: "Core bun defines (always on)",
|
|
},
|
|
{
|
|
// Shell-escaped quotes so clang receives literal quotes in the define
|
|
// (the preprocessor needs the string to be "26.3.0", not bare 26.3.0).
|
|
flag: c => `REPORTED_NODEJS_VERSION=\\"${c.nodejsVersion}\\"`,
|
|
desc: "Node.js version string reported by process.version",
|
|
},
|
|
{
|
|
flag: c => `REPORTED_NODEJS_ABI_VERSION=${c.nodejsAbiVersion}`,
|
|
desc: "Node.js ABI version (process.versions.modules)",
|
|
},
|
|
{
|
|
flag: c => `REPORTED_NODEJS_V8_VERSION=\\"${c.nodejsV8Version}\\"`,
|
|
desc: "V8 version string (process.versions.v8)",
|
|
},
|
|
{
|
|
// Hardcoded ON — experimental flag not exposed in config
|
|
flag: "USE_BUN_MIMALLOC=1",
|
|
desc: "Use mimalloc as default allocator",
|
|
},
|
|
|
|
// ─── Config-dependent ───
|
|
{
|
|
flag: "ASSERT_ENABLED=1",
|
|
when: c => c.assertions,
|
|
desc: "Enable runtime assertions",
|
|
},
|
|
{
|
|
flag: "BUN_DEBUG=1",
|
|
when: c => c.debug,
|
|
desc: "Enable debug-only code paths",
|
|
},
|
|
{
|
|
flag: "LIBUS_SOCKET_FAULT_INJECTION=1",
|
|
when: c => c.socketFaultInjection,
|
|
desc: "Compile usockets bsd_* syscall fault-injection hooks (runtime-armed via bun:internal-for-testing)",
|
|
},
|
|
{
|
|
// slash(): path becomes a C string literal — `\U` would be a unicode escape.
|
|
flag: c => `BUN_DYNAMIC_JS_LOAD_PATH=\\"${slash(join(c.buildDir, "js"))}\\"`,
|
|
when: c => c.debug && !c.ci,
|
|
desc: "Hot-reload built-in JS from build dir (dev convenience)",
|
|
},
|
|
{
|
|
// Standard define that disables assert() in libc headers. CMake adds
|
|
// this automatically for Release builds; we do it explicitly.
|
|
flag: "NDEBUG",
|
|
when: c => c.release,
|
|
desc: "Disable libc assert() (release builds)",
|
|
},
|
|
|
|
// ─── Platform ───
|
|
{
|
|
flag: "_DARWIN_NON_CANCELABLE=1",
|
|
when: c => c.darwin,
|
|
desc: "Use non-cancelable POSIX calls on Darwin",
|
|
},
|
|
{
|
|
flag: ["WIN32", "_WINDOWS", "WIN32_LEAN_AND_MEAN=1", "_CRT_SECURE_NO_WARNINGS", "BORINGSSL_NO_CXX=1"],
|
|
when: c => c.windows,
|
|
desc: "Standard Windows defines + disable CRT security warnings",
|
|
},
|
|
{
|
|
flag: "U_STATIC_IMPLEMENTATION",
|
|
when: c => c.windows,
|
|
desc: "ICU static linkage (without this: ABI mismatch → STATUS_STACK_BUFFER_OVERRUN)",
|
|
},
|
|
{
|
|
flag: "U_DISABLE_RENAMING=1",
|
|
when: c => c.darwin,
|
|
desc: "Disable ICU symbol renaming (using system ICU)",
|
|
},
|
|
|
|
// ─── Feature toggles ───
|
|
{
|
|
flag: "LAZY_LOAD_SQLITE=0",
|
|
when: c => c.staticSqlite,
|
|
desc: "SQLite statically linked",
|
|
},
|
|
{
|
|
flag: "LAZY_LOAD_SQLITE=1",
|
|
when: c => !c.staticSqlite,
|
|
desc: "SQLite loaded at runtime",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// LINKER FLAGS
|
|
// For the final bun executable link step only.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export const linkerFlags: Flag[] = [
|
|
// ─── Sanitizers ───
|
|
{
|
|
flag: "-fsanitize=address",
|
|
when: c => c.unix && c.asan,
|
|
desc: "Link ASAN runtime",
|
|
},
|
|
{
|
|
flag: "-fsanitize=null",
|
|
when: c =>
|
|
c.unix &&
|
|
c.debug &&
|
|
c.abi !== "musl" &&
|
|
c.abi !== "android" &&
|
|
!c.freebsd &&
|
|
!(c.darwin && c.crossTarget !== undefined),
|
|
desc: "Link UBSan runtime",
|
|
},
|
|
{
|
|
flag: "-fsanitize=null",
|
|
when: c => c.unix && c.release && c.asan,
|
|
desc: "Link UBSan runtime (release-asan)",
|
|
},
|
|
{
|
|
flag: "-fsanitize-coverage=trace-pc-guard",
|
|
when: c => c.fuzzilli,
|
|
desc: "Link fuzzilli coverage runtime",
|
|
},
|
|
|
|
// ─── LTO (link-side) ───
|
|
{
|
|
// Whole-program devirtualization needs two things: the type hierarchy in
|
|
// the ThinLTO summaries (compile-side -fwhole-program-vtables with
|
|
// -fno-split-lto-unit) and the linker's assertion that every derived
|
|
// class is visible in this link — without the visibility upgrade WPD
|
|
// only fires for classes explicitly annotated [[clang::lto_visibility]],
|
|
// i.e. never. A static executable that only dlopens C-ABI addons (NAPI)
|
|
// satisfies the whole-program assumption. ld64.lld has no named option
|
|
// for this; -mllvm reaches the underlying cl::opt directly.
|
|
flag: ["-Wl,-mllvm,-whole-program-visibility"],
|
|
when: c => c.darwin && c.lto,
|
|
desc: "Enable index-based whole-program devirtualization at link time",
|
|
},
|
|
{
|
|
// ELF spelling of the entry above. The WebKit -lto prebuilts carry the
|
|
// !type/!vcall_visibility metadata (built with -fwhole-program-vtables),
|
|
// so this upgrades JSC/WTF's exported classes to hidden LTO visibility
|
|
// and lets WPD fire on them, not just on our -fvisibility=hidden classes.
|
|
flag: ["-Wl,--lto-whole-program-visibility"],
|
|
when: c => c.unix && !c.darwin && c.lto,
|
|
desc: "Enable index-based whole-program devirtualization at link time (lld ELF)",
|
|
},
|
|
{
|
|
flag: ["-flto=thin", "-fwhole-program-vtables", "-fforce-emit-vtables"],
|
|
when: c => c.unix && c.lto,
|
|
desc: "LTO at link time (matches compile-side -flto=thin)",
|
|
},
|
|
{
|
|
// Without -O at link time, clang's driver defaults LTO codegen to -O2.
|
|
// CMake implicitly forwarded CMAKE_CXX_FLAGS (incl. -O2) to the link line;
|
|
// we must do so explicitly. Dropping this cost ~5 MB of .text on linux-x64
|
|
// (less unrolling/inlining in JSC — measurable in Yarr, DFG, BuiltinNames).
|
|
// ELF only: the driver forwards this to lld as -plugin-opt=O2. The Darwin
|
|
// driver forwards no opt-level flag at all — see the next entry.
|
|
flag: "-O2",
|
|
when: c => c.unix && !c.darwin && c.lto && c.release && !c.smol,
|
|
desc: "LTO codegen at -O2 (ELF: forwarded to lld as -plugin-opt=O2)",
|
|
},
|
|
{
|
|
// The Darwin driver drops a bare -O at link time (`clang++ -### …` shows
|
|
// no opt-level flag reaching the linker), so Mach-O LTO would codegen at
|
|
// ld64.lld's built-in defaults: --lto-O2 for the IR pipeline and
|
|
// --lto-CGO2 (CodeGenOptLevel::Default) for instruction selection —
|
|
// inline threshold 225 and default isel, while the per-TU build codegens
|
|
// everything at -O3 + CodeGenOptLevel::Aggressive (threshold 275). Pass
|
|
// ld64.lld's own options so LTO codegen matches the compile side.
|
|
// Cross links only: --lto-O/--lto-CGO are lld-specific, and only the
|
|
// darwin cross link uses lld's Mach-O port (ld64.lld). Native darwin
|
|
// links go through Apple's ld, which rejects unknown double-dash options,
|
|
// so they keep the driver's default LTO codegen level.
|
|
// arm64 only: O3 codegen costs +0.3 MB there but +3.1 MB on x64 (the
|
|
// higher inline threshold is much more expensive in x86-64's
|
|
// variable-length encoding); x64 stays at lld's default --lto-O2/CGO2.
|
|
flag: ["-Wl,--lto-O3", "-Wl,--lto-CGO3"],
|
|
when: c => c.darwin && c.arm64 && c.crossTarget !== undefined && c.lto && c.release && !c.smol,
|
|
desc: "LTO codegen at -O3 + aggressive isel (Darwin driver forwards no -O to ld64.lld)",
|
|
},
|
|
{
|
|
flag: "-Os",
|
|
when: c => c.unix && c.lto && c.smol,
|
|
desc: "LTO codegen at -Os (matches compile-side opt level)",
|
|
},
|
|
|
|
// ─── PGO (link-side) ───
|
|
{
|
|
flag: c => `-fprofile-generate=${c.pgoGenerate}`,
|
|
when: c => c.unix && !!c.pgoGenerate,
|
|
desc: "IR PGO: link profiling runtime",
|
|
},
|
|
{
|
|
flag: c => `-fprofile-use=${c.pgoUse}`,
|
|
when: c => c.unix && !!c.pgoUse,
|
|
desc: "IR PGO: LTO+PGO at link time",
|
|
},
|
|
|
|
// ─── Windows ───
|
|
{
|
|
// Explicit machine type — clang-cl's driver does not reliably forward
|
|
// its default target to lld-link when invoked as a pure link driver
|
|
// (no source inputs, /link separator), so on arm64 lld-link would
|
|
// autodetect x64 and reject every arm64 input. CMake's Windows-MSVC
|
|
// platform module always set /machine: from CMAKE_SYSTEM_PROCESSOR,
|
|
// which is why the pre-ninja build never needed this in BuildBun.cmake.
|
|
flag: c => `/machine:${c.arm64 ? "arm64" : "x64"}`,
|
|
when: c => c.windows,
|
|
desc: "Target machine type for lld-link (required on arm64; x64 hosts default correctly but explicit is harmless)",
|
|
},
|
|
{
|
|
// Serviced UCRT overlay: an explicit /libpath: is searched before the
|
|
// /winsysroot-derived paths, so its libucrt.lib/ucrt.lib win over the
|
|
// splat's stale copies (see UCRT_SERVICING_VERSION in winsysroot.ts —
|
|
// the VS-manifest payload xwin downloads carries an ancient arm64 UCRT
|
|
// with broken printf formatting).
|
|
flag: c => quote(`/libpath:${ucrtServicingLibDir(c)!}`, false),
|
|
when: c => c.windows && c.host.os !== "windows",
|
|
desc: "Windows cross-compile: serviced Universal CRT static libs (SDK NuGet) override the splat's",
|
|
},
|
|
{
|
|
// Windows cross-compile: these ldflags go after /link, straight to
|
|
// lld-link, which doesn't see the compile-side `/winsysroot` from
|
|
// globalFlags — repeat it in lld-link's own spelling so the MSVC CRT
|
|
// and Windows SDK import libraries (libcmt, kernel32, ...) are found
|
|
// without a VS dev shell's LIB env.
|
|
flag: c => quote(`/winsysroot:${c.winsysroot!}`, false),
|
|
when: c => c.windows && c.winsysroot !== undefined,
|
|
desc: "Windows cross-compile: MSVC CRT + Windows SDK library search root (xwin splat)",
|
|
},
|
|
{
|
|
flag: ["/STACK:0x1200000,0x200000", "/errorlimit:0"],
|
|
when: c => c.windows,
|
|
desc: "18MB stack reserve (JSC uses deep recursion), no error limit",
|
|
},
|
|
{
|
|
flag: "/DEBUG:FULL",
|
|
when: c => c.windows && c.debug,
|
|
desc: "Emit PDB so the crash handler can symbolize stack traces",
|
|
},
|
|
{
|
|
flag: [
|
|
"/LTCG",
|
|
"/OPT:REF",
|
|
// SAFEICF (lld-specific) only folds functions whose address is never
|
|
// taken (it honours .llvm_addrsig; objects without one — MSVC CRT
|
|
// import libs, the prebuilt ICU data — are treated conservatively), so
|
|
// JSC ClassInfo native constructors — stored as pointers and compared
|
|
// for identity — stay distinct. /OPT:ICF (aggressive) folded
|
|
// callBigIntConstructor with constructBigInt → "not a constructor",
|
|
// and broke expect.any(Constructor); see commit 218430c731. Mirrors
|
|
// Linux `-Wl,-icf=safe`.
|
|
"/OPT:SAFEICF",
|
|
// String-literal tail merging (lld-specific; MSVC link.exe has no
|
|
// equivalent). Helps .rdata the same way --icf handles .rodata.cst on ELF.
|
|
"/OPT:lldtailmerge",
|
|
// 512-byte section file alignment (default is 4 KB). Was present in
|
|
// the pre-ninja CMake config; harmless to page-in cost since sections
|
|
// are few and large.
|
|
"/FILEALIGN:0x200",
|
|
"/DEBUG:FULL",
|
|
"/delayload:ole32.dll",
|
|
"/delayload:WINMM.dll",
|
|
"/delayload:dbghelp.dll",
|
|
"/delayload:WS2_32.dll",
|
|
"/delayload:WSOCK32.dll",
|
|
"/delayload:ADVAPI32.dll",
|
|
"/delayload:IPHLPAPI.dll",
|
|
"/delayload:CRYPT32.dll",
|
|
],
|
|
when: c => c.windows && c.release,
|
|
desc: "Release link opts + delay-load non-critical DLLs (faster startup)",
|
|
},
|
|
|
|
// ─── macOS ───
|
|
{
|
|
flag: ["-Wl,-no_compact_unwind", `-Wl,-stack_size,${DARWIN_STACK_SIZE}`, "-fno-keep-static-consts"],
|
|
when: c => c.darwin,
|
|
desc: "18MB stack, skip compact unwind",
|
|
},
|
|
{
|
|
// Force the linker to reserve + emit LC_CODE_SIGNATURE on arm64 cross
|
|
// links so the post-link fixup (shims/macho-postlink.c) has a signature
|
|
// to replace after patching the stack size — arm64 macOS refuses to exec
|
|
// unsigned binaries. x64 deliberately ships UNSIGNED, matching the
|
|
// native x64 build (Apple's ld only auto-signs arm64; x64 macOS runs
|
|
// unsigned binaries fine, an ad-hoc signature buys nothing there, and
|
|
// the CodeDirectory costs 32 bytes per 4 KB page ≈ 0.8% of the binary).
|
|
flag: "-Wl,-adhoc_codesign",
|
|
when: c => c.darwin && c.crossTarget !== undefined && c.arm64,
|
|
desc: "macOS arm64 cross-link: emit an ad-hoc LC_CODE_SIGNATURE for macho-postlink to replace",
|
|
},
|
|
{
|
|
// `bun build --compile` grows the __BUN placeholder segment in place and
|
|
// can only shift the one segment nothing references by address —
|
|
// __LINKEDIT — so __BUN must be the last content segment. Apple's ld
|
|
// orders known segments canonically (…, __DATA_DIRTY, __BUN, __LINKEDIT),
|
|
// but ld64.lld orders unknown segments by creation order, and the
|
|
// -sectcreate'd __BUN is created before the input objects' __DATA_DIRTY
|
|
// is encountered — leaving __DATA_DIRTY *between* __BUN and __LINKEDIT.
|
|
// Growing __BUN then overlaps it (`OverlappingSegments` from
|
|
// src/exe_format/macho.rs). Fold __DATA_DIRTY into __DATA instead: it
|
|
// holds a single 8-byte JSC::SourceProfiler hook and is only meaningful
|
|
// as a dyld-shared-cache page-grouping hint, which executables don't use.
|
|
flag: "-Wl,-rename_segment,__DATA_DIRTY,__DATA",
|
|
when: c => c.darwin && c.crossTarget !== undefined,
|
|
desc: "macOS cross-link: keep __BUN as the last content segment so `bun build --compile` can grow it",
|
|
},
|
|
{
|
|
// Identical-code folding, on top of -dead_strip: dead_strip removes
|
|
// unreferenced functions, ICF merges duplicate ones (template/bindgen
|
|
// instantiations, mostly). `safe` only folds functions whose address is
|
|
// never taken, per the -faddrsig table in globalFlags — the aggressive
|
|
// mode is what folded callBigIntConstructor into constructBigInt on
|
|
// Windows (/OPT:ICF) and broke `expect.any()`. Matches the Linux
|
|
// release link's -Wl,-icf=safe. lld-only: Apple's ld has no ICF, so
|
|
// this is a cross-only divergence (smaller binary than native). The
|
|
// prebuilt WebKit archives are compiled with -faddrsig too, so WebKit
|
|
// code participates in the folding.
|
|
flag: "-Wl,--icf=safe",
|
|
when: c => c.darwin && c.crossTarget !== undefined && c.release,
|
|
desc: "macOS cross-link: fold identical address-insignificant functions",
|
|
},
|
|
{
|
|
// -ld_new selects Apple's new linker — only meaningful (and only
|
|
// understood) when Apple's ld driver does the link. ld64.lld (the
|
|
// cross-link path) parses it as `-l d_new` and fails.
|
|
flag: "-Wl,-ld_new",
|
|
when: c => c.darwin && c.crossTarget === undefined,
|
|
desc: "Use new Apple linker (native darwin links only)",
|
|
},
|
|
{
|
|
// Cross-link from a non-darwin host: same pattern as Android/FreeBSD —
|
|
// target triple + explicit linker. -isysroot is added by the deployment-
|
|
// target flag below; the clang driver forwards it to ld64.lld as
|
|
// -syslibroot. -mlinker-version≥520 makes the driver emit the modern
|
|
// -platform_version argument ld64.lld requires (without it the driver
|
|
// assumes an ancient host ld64 and emits nothing usable); the exact
|
|
// value only gates driver behavior, so track a recent ld64 release.
|
|
flag: c => [`--target=${c.crossTarget!}`, "-mlinker-version=705", `--ld-path=${c.ld}`],
|
|
when: c => c.darwin && c.crossTarget !== undefined,
|
|
desc: "macOS cross-link: target triple + ld64.lld + modern linker arg style",
|
|
},
|
|
{
|
|
// The `__BUN,__bun` standalone-graph placeholder (c-bindings.cpp) is a
|
|
// 16 KB-aligned section. On x86_64, ld64.lld 16K-aligns its FILE offset
|
|
// inside a 4K-aligned segment but not its VM span, producing
|
|
// filesize > vmsize for the __BUN segment — an invalid Mach-O that
|
|
// llvm-strip/dsymutil (and codesign) reject. Capping the section's
|
|
// alignment at the x86_64 page size removes the padding asymmetry; the
|
|
// placeholder only needs 8-byte alignment at runtime, and
|
|
// `bun build --compile` rewrites the segment at 16 KB alignment anyway
|
|
// (exe_format/macho.rs). arm64 uses 16 KB pages, so it's unaffected.
|
|
flag: ["-Wl,-sectalign,__BUN,__bun,0x1000"],
|
|
when: c => c.darwin && c.crossTarget !== undefined && c.x64,
|
|
desc: "macOS x64 cross-link: keep the __BUN segment's filesize ≤ vmsize under ld64.lld",
|
|
},
|
|
{
|
|
// Must also be passed at link: ld64 reads this to write LC_BUILD_VERSION.minos.
|
|
// Without it, ld64 defaults to the SDK version (15.0 on CI) → binary refuses
|
|
// to launch on macOS 13/14. globalFlags doesn't flow to ldflags, so repeat here.
|
|
flag: c => [`-mmacosx-version-min=${c.osxDeploymentTarget!}`, "-isysroot", c.osxSysroot!],
|
|
when: c => c.darwin && c.osxDeploymentTarget !== undefined && c.osxSysroot !== undefined,
|
|
desc: "macOS deployment target at link (sets LC_BUILD_VERSION minos)",
|
|
},
|
|
{
|
|
flag: "-Wl,-w",
|
|
when: c => c.darwin && c.debug,
|
|
desc: "Suppress all linker warnings (workaround: no selective suppress for alignment warnings as of 2025-07)",
|
|
},
|
|
{
|
|
flag: c => ["-dead_strip", "-dead_strip_dylibs", `-Wl,-map,${c.buildDir}/${bunExeName(c)}.linker-map`],
|
|
when: c => c.darwin && c.release,
|
|
desc: "Dead-code strip + emit linker map",
|
|
},
|
|
{
|
|
// Mach-O keeps DWARF in the input .o files and records their paths in
|
|
// the linked binary's debug map (N_OSO stabs); dsymutil follows the map
|
|
// to build the .dSYM. Under LTO the "input objects" are temporaries the
|
|
// linker deletes after the link, which would leave the debug map dangling
|
|
// and the dSYM empty. -object_path_lto persists the LTO-codegen'd object
|
|
// at a stable path inside the build dir and points the debug map at it.
|
|
flag: c => `-Wl,-object_path_lto,${c.buildDir}/${bunExeName(c)}.lto.o`,
|
|
when: c => c.darwin && c.lto,
|
|
desc: "Persist the LTO-generated object so dsymutil can extract its DWARF into the dSYM",
|
|
},
|
|
{
|
|
// Mach-O counterpart to lld's --symbol-ordering-file below:
|
|
// <buildDir>/linker.order lists the functions bun actually executes while
|
|
// starting up, and Apple's linker sorts them to the front of __text. The
|
|
// file is a build artifact, never committed: configure seeds an empty one
|
|
// so both link passes share one build.ninja — a release build regenerates
|
|
// it from its own pass-1 binary and reruns ninja, which relinks and
|
|
// nothing else. Unknown names are silently skipped, so a stale file only
|
|
// costs part of the win.
|
|
flag: c => `-Wl,-order_file,${orderFilePath(c)}`,
|
|
when: c => c.darwin && usesOrderFile(c),
|
|
desc: "Sort startup-hot functions to the front of __text (cuts resident binary pages)",
|
|
},
|
|
|
|
// ─── Linux ───
|
|
{
|
|
flag: c => [`--target=${c.crossTarget!}`, `--sysroot=${c.sysroot!}`],
|
|
when: c => c.linux && c.abi !== "android" && c.crossTarget !== undefined && c.sysroot !== undefined,
|
|
desc: "linux sysroot link (gnu: ubuntu:20.04+gcc-13; musl: alpine)",
|
|
},
|
|
{
|
|
// Wrap glibc symbols whose default version on the sysroot's glibc (2.31)
|
|
// is > 2.17. Each __wrap_X in workaround-missing-symbols.cpp pins to the
|
|
// 2.2.5/2.17 compat version (or a raw syscall) so the binary's verneed
|
|
// never exceeds the floor.
|
|
flag: [
|
|
"exp",
|
|
"exp2",
|
|
"expf",
|
|
"fcntl64",
|
|
"getrandom",
|
|
"gettid",
|
|
"log",
|
|
"log2",
|
|
"log2f",
|
|
"logf",
|
|
"pow",
|
|
"powf",
|
|
"quick_exit",
|
|
// libpthread/libdl → libc.so merge (2.32/2.34)
|
|
"__libc_start_main",
|
|
"__pthread_key_create",
|
|
"dladdr",
|
|
"dlerror",
|
|
"dlsym",
|
|
"dlvsym",
|
|
"pthread_attr_setstack",
|
|
"pthread_attr_setstacksize",
|
|
"pthread_getattr_np",
|
|
"pthread_getspecific",
|
|
"pthread_key_create",
|
|
"pthread_key_delete",
|
|
"pthread_kill",
|
|
"pthread_mutex_trylock",
|
|
"pthread_mutexattr_destroy",
|
|
"pthread_mutexattr_init",
|
|
"pthread_mutexattr_settype",
|
|
"pthread_once",
|
|
"pthread_rwlock_destroy",
|
|
"pthread_rwlock_rdlock",
|
|
"pthread_rwlock_unlock",
|
|
"pthread_rwlock_wrlock",
|
|
"pthread_setspecific",
|
|
// stat-family inline → real symbol (2.33)
|
|
"fstat",
|
|
"fstat64",
|
|
"fstatat",
|
|
"fstatat64",
|
|
"mknod",
|
|
// syscall wrappers (2.27/2.28)
|
|
"copy_file_range",
|
|
"memfd_create",
|
|
"statx",
|
|
// no older version (2.29/2.35)
|
|
"_dl_find_object",
|
|
"posix_spawn_file_actions_addchdir_np",
|
|
].map(s => `-Wl,--wrap=${s}`),
|
|
when: c => c.linux && c.abi === "gnu",
|
|
desc: "Wrap glibc 2.18+ symbols (portable down to glibc 2.17)",
|
|
},
|
|
{
|
|
flag: ["-static-libstdc++", "-static-libgcc"],
|
|
when: c => c.linux && c.abi === "gnu",
|
|
desc: "Static C++ runtime (don't depend on host libstdc++)",
|
|
},
|
|
{
|
|
flag: ["-lstdc++", "-lgcc"],
|
|
when: c => c.linux && c.abi === "musl",
|
|
desc: "Dynamic C++ runtime on musl (static unavailable)",
|
|
},
|
|
{
|
|
flag: c => [
|
|
`--target=${c.crossTarget!}`,
|
|
`--sysroot=${c.sysroot!}`,
|
|
"--rtlib=compiler-rt",
|
|
"--unwindlib=libunwind",
|
|
"-stdlib=libc++",
|
|
"-static-libstdc++",
|
|
// -l:libunwind.a (driver-emitted) searches -L paths; point at the NDK's
|
|
// own per-arch runtime dir so it resolves regardless of resource-dir layout.
|
|
`-L${join(c.androidNdkRuntimeDir!, c.arm64 ? "aarch64" : "x86_64")}`,
|
|
],
|
|
when: c => c.linux && c.abi === "android",
|
|
desc: "Android link: target/sysroot + compiler-rt/libunwind + static libc++",
|
|
},
|
|
{
|
|
// Paired with compile-side -fno-unwind-tables above.
|
|
// Gated on release (not LTO): the workspace is `panic = "abort"` and
|
|
// C++ is `-fno-exceptions`/`-fno-unwind-tables`, so nothing unwinds at
|
|
// runtime regardless of LTO. Backtraces walk frame pointers (forced on
|
|
// both sides). Debug builds keep .eh_frame for gdb/libunwind.
|
|
flag: "-Wl,--no-eh-frame-hdr",
|
|
when: c => c.linux && c.abi === "gnu" && c.release,
|
|
desc: "Omit eh_frame header (release; size/RSS opt; see stripFlags for matching -R .eh_frame)",
|
|
},
|
|
{
|
|
flag: "-Wl,--eh-frame-hdr",
|
|
when: c => c.linux && !(c.abi === "gnu" && c.release),
|
|
desc: "Keep eh_frame header (debug/musl/android; needed for DWARF backtraces)",
|
|
},
|
|
{
|
|
flag: c => `--ld-path=${c.ld}`,
|
|
when: c => c.linux,
|
|
desc: "Use lld instead of system ld",
|
|
},
|
|
{
|
|
flag: ["-fno-pic", "-Wl,-no-pie"],
|
|
when: c => c.linux && c.abi !== "android",
|
|
desc: "No PIE (we don't need ASLR; simpler codegen)",
|
|
},
|
|
{
|
|
flag: ["-fPIC", "-pie"],
|
|
when: c => c.abi === "android",
|
|
desc: "Android: bionic loader requires PIE",
|
|
},
|
|
{
|
|
flag: [
|
|
"-Wl,--as-needed",
|
|
"-Wl,-z,stack-size=12800000",
|
|
"-Wl,-z,lazy",
|
|
"-Wl,-z,norelro",
|
|
// (no --pack-dyn-relocs=relr: DT_RELR needs glibc ≥ 2.36 to load,
|
|
// and we wrap symbols for portability down to 2.17. With -no-pie
|
|
// there are <500 R_*_RELATIVE entries anyway — not worth the compat
|
|
// break.)
|
|
"-Wl,-O2",
|
|
"-Wl,--gdb-index",
|
|
"-Wl,-z,combreloc",
|
|
// NOTE: --sort-section=name was here historically; lld ignores it
|
|
// for the default `.text` rule. We also deliberately do NOT pass
|
|
// `-z keep-text-section-prefix`: without a PGO profile, LLVM's *static*
|
|
// `.unlikely` heuristic mislabels a lot of error/panic/bring-up code
|
|
// that actually runs on the `bun <file>` startup path; segregating it
|
|
// into `.text.unlikely` made ~84% of that section resident at startup
|
|
// anyway, so the monolithic default `.text` has *better* RSS locality.
|
|
"-Wl,--hash-style=both",
|
|
"-Wl,--build-id=sha1",
|
|
],
|
|
when: c => c.linux,
|
|
desc: "Linux linker tuning: lazy binding, large stack, fast gdb loading",
|
|
},
|
|
{
|
|
flag: "-Wl,--gc-sections",
|
|
when: c => c.linux && c.release,
|
|
desc: "Garbage-collect unused sections (release only; debug keeps Zig dbHelper symbols)",
|
|
},
|
|
{
|
|
// Always icf=safe in release. The stripped `bun` shares its build-id
|
|
// with `bun-profile`, so disabling ICF on the profile binary "for perf
|
|
// symbolication" would also bloat the shipped binary's .text — and
|
|
// `perf` symbolicates folded functions fine via the linker-map anyway.
|
|
flag: c => ["-Wl,-icf=safe", `-Wl,-Map=${c.buildDir}/${bunExeName(c)}.linker-map`],
|
|
when: c => c.linux && c.release && !c.asan && !c.valgrind,
|
|
desc: "Identical-code-folding (safe; perf symbolication uses the linker-map)",
|
|
},
|
|
{
|
|
// When a PGO profile is loaded (`--pgo-use`, e.g. the two-stage `btg`
|
|
// build driven by scripts/build-pgo.ts) clang AND rustc emit `.text.hot` /
|
|
// `.text.unlikely` section prefixes from *measured* execution counts.
|
|
// Tell lld to keep those prefixes (it merges them into one `.text` by
|
|
// default) so the hot cold-start working set — clap → CLI dispatch →
|
|
// module loader → js_parser/js_printer bring-up → JSC VM init — lands in
|
|
// one contiguous run of pages instead of being scattered across the ~54 MB
|
|
// `.text` (each hot fn otherwise drags in a 64 KB fault-around window of
|
|
// cold neighbours: ~+1.3 MB resident `.text` vs the PGO+BOLT'd shipped
|
|
// binary).
|
|
// Gated strictly on `pgoUse`: without a real profile this flag is harmful
|
|
// (the static `.unlikely` heuristic is wrong for our startup path — see
|
|
// the linker-tuning block above).
|
|
flag: "-Wl,-z,keep-text-section-prefix",
|
|
when: c => c.linux && c.release && !!c.pgoUse && !c.asan && !c.valgrind,
|
|
desc: "Keep .text.hot/.text.unlikely prefixes from the PGO profile (cluster hot startup .text)",
|
|
},
|
|
{
|
|
// Same goal as keep-text-section-prefix, without needing a PGO profile:
|
|
// <buildDir>/linker.order lists the functions bun actually executes while
|
|
// starting up, and lld sorts their input sections to the front of `.text`.
|
|
// Starting up only touches ~8.5 MB of pages, but they are scattered over a
|
|
// ~50 MB `.text` and the kernel faults in 64 KB around each one, so the
|
|
// binary ends up with ~27 MB resident for `bun -e 'console.log(1)'`.
|
|
// Packing them together cuts that by a third for a same-size binary.
|
|
//
|
|
// The file is a build artifact, never committed: configure seeds an empty one
|
|
// (a no-op for lld) so this flag is unconditional and both link passes share
|
|
// one build.ninja — a release build regenerates it from its own pass-1 binary
|
|
// and reruns ninja, which relinks and nothing else. Symbols lld cannot find
|
|
// are skipped, so a stale file only costs part of the win.
|
|
//
|
|
// A local `bun run build:release` therefore links unordered until you run
|
|
// `bun run orderfile` and build again.
|
|
flag: c => [`-Wl,--symbol-ordering-file=${orderFilePath(c)}`, "-Wl,--no-warn-symbol-ordering"],
|
|
when: c => c.linux && usesOrderFile(c),
|
|
desc: "Sort startup-hot functions to the front of .text (cuts resident binary pages)",
|
|
},
|
|
|
|
// ─── Symbols / exports ───
|
|
// These reference files on disk — linkDepends() lists the same paths
|
|
// so ninja relinks when they change (cmake's LINK_DEPENDS equivalent).
|
|
{
|
|
flag: c => `/DEF:${slash(join(c.cwd, "src/symbols.def"))}`,
|
|
when: c => c.windows,
|
|
desc: "Exported symbol definition (.def format)",
|
|
},
|
|
{
|
|
flag: c => ["-exported_symbols_list", `${c.cwd}/src/symbols.txt`],
|
|
when: c => c.darwin,
|
|
desc: "Exported symbol list",
|
|
},
|
|
{
|
|
flag: c => [
|
|
"-Wl,-Bsymbolic-functions",
|
|
"-rdynamic",
|
|
`-Wl,--dynamic-list=${c.cwd}/src/symbols.dyn`,
|
|
`-Wl,--version-script=${c.cwd}/src/linker.lds`,
|
|
],
|
|
when: c => c.linux,
|
|
desc: "Dynamic symbol list + version script",
|
|
},
|
|
// ─── FreeBSD ───
|
|
{
|
|
flag: c => [`--target=${c.crossTarget!}`, `--sysroot=${c.sysroot!}`, "-stdlib=libc++"],
|
|
when: c => c.freebsd && c.crossTarget !== undefined,
|
|
desc: "FreeBSD cross-link: target/sysroot + libc++ (FreeBSD base ships libc++)",
|
|
},
|
|
{
|
|
flag: c => `--ld-path=${c.ld}`,
|
|
when: c => c.freebsd,
|
|
desc: "Use lld instead of system ld",
|
|
},
|
|
{
|
|
flag: ["-fno-pic", "-Wl,-no-pie"],
|
|
when: c => c.freebsd,
|
|
desc: "FreeBSD 13+ clang defaults to PIE; opt out (matches Linux, avoids -fPIC rebuild of WebKit/deps)",
|
|
},
|
|
{
|
|
flag: [
|
|
"-Wl,-O2",
|
|
"-Wl,--as-needed",
|
|
"-Wl,-z,stack-size=12800000",
|
|
"-Wl,-z,lazy",
|
|
"-Wl,-z,norelro",
|
|
"-Wl,--gdb-index",
|
|
"-Wl,-z,combreloc",
|
|
"-Wl,--hash-style=both",
|
|
"-Wl,--build-id=sha1",
|
|
],
|
|
when: c => c.freebsd,
|
|
desc: "FreeBSD linker tuning (same as Linux ELF; here -z stack-size also sizes the main thread's stack)",
|
|
},
|
|
{
|
|
// rust-lang/llvm-project doesn't enable `LLVM_ENABLE_ZLIB` (or `_ZSTD`) for
|
|
// the lld they bundle as `rust-lld`, so this flag hard-fails there:
|
|
// `rust-lld: error: --compress-debug-sections: LLVM was not built with
|
|
// LLVM_ENABLE_ZLIB or did not find zlib at build time`.
|
|
// We only fall onto rust-lld for cross-language LTO when rustc's LLVM is
|
|
// newer than the system clang/lld (see config.ts `cfg.ld` selection); in
|
|
// that case the link-time flag is dropped and llvm-objcopy compresses
|
|
// post-link instead (shims.ts elfDebugCompressPostlinkCommand) — an
|
|
// uncompressed bun-profile is ~2x larger and every `--compile` test
|
|
// copies it, so leaving it uncompressed times CI out.
|
|
flag: "-Wl,--compress-debug-sections=zlib",
|
|
when: c => (c.linux || c.freebsd) && c.ld !== c.rustLld,
|
|
desc: "Compress ELF debug sections (post-link via llvm-objcopy with rust-lld — built without zlib)",
|
|
},
|
|
{
|
|
flag: "-Wl,--gc-sections",
|
|
when: c => c.freebsd && c.release,
|
|
desc: "Garbage-collect unused sections",
|
|
},
|
|
{
|
|
flag: c => [
|
|
"-Wl,-Bsymbolic-functions",
|
|
"-rdynamic",
|
|
`-Wl,--dynamic-list=${c.cwd}/src/symbols.dyn`,
|
|
`-Wl,--version-script=${c.cwd}/src/linker-freebsd.lds`,
|
|
],
|
|
when: c => c.freebsd,
|
|
desc: "Dynamic symbol list + version script (FreeBSD adds environ/__progname)",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Whether this target links with a symbol ordering file (lld
|
|
* `--symbol-ordering-file` on linux, `-order_file` on darwin, which both Apple
|
|
* ld and ld64.lld take). Only where the startup win is worth a relink: release
|
|
* builds, not under a sanitizer — the tracer swaps `.text` out for a private
|
|
* copy, and nobody measures startup RSS on an ASAN build anyway.
|
|
*
|
|
* This says where the order file is CONSUMED, not where it is produced. A
|
|
* cross-compiled lane cannot trace its own binary (`canTraceOrderFile`), so it
|
|
* inherits an earlier build's file instead and still links ordered.
|
|
*
|
|
* linux gnu only: musl links statically, so LD_PRELOAD cannot load the tracer,
|
|
* and no musl test host exists to trace on either. android has no order-file
|
|
* linker support. Neither can produce or consume one.
|
|
*
|
|
* darwin arm64 only: the BRK-based tracer is arm64-only, so x64 has nothing to
|
|
* inherit and linking with an always-empty order file just adds noise.
|
|
*/
|
|
export function usesOrderFile(
|
|
cfg: Pick<Config, "linux" | "darwin" | "abi" | "arm64" | "release" | "asan" | "valgrind">,
|
|
): boolean {
|
|
if (!cfg.release || cfg.asan || cfg.valgrind) return false;
|
|
if (cfg.linux) return cfg.abi === "gnu";
|
|
if (cfg.darwin) return cfg.arm64;
|
|
return false;
|
|
}
|
|
|
|
/** The order file lives in the build directory — it is generated, never committed. */
|
|
export function orderFilePath(cfg: Pick<Config, "buildDir">): string {
|
|
return join(cfg.buildDir, "linker.order");
|
|
}
|
|
|
|
/**
|
|
* Files the linker reads via flags above. Return as implicit inputs so
|
|
* ninja relinks when exported symbols / version script change.
|
|
* CMake tracks these via set_target_properties LINK_DEPENDS.
|
|
*/
|
|
export function linkDepends(cfg: Config): string[] {
|
|
if (cfg.freebsd) return [join(cfg.cwd, "src/symbols.dyn"), join(cfg.cwd, "src/linker-freebsd.lds")];
|
|
if (cfg.windows) return [join(cfg.cwd, "src/symbols.def")];
|
|
// The release symbol ordering file: listing it here is what makes
|
|
// regenerating it relink, and only relink.
|
|
if (cfg.darwin) {
|
|
const darwin = [join(cfg.cwd, "src/symbols.txt")];
|
|
if (usesOrderFile(cfg)) darwin.push(orderFilePath(cfg));
|
|
return darwin;
|
|
}
|
|
// linux: ELF dynamic-list + version script.
|
|
const linux = [join(cfg.cwd, "src/symbols.dyn"), join(cfg.cwd, "src/linker.lds")];
|
|
if (usesOrderFile(cfg)) linux.push(orderFilePath(cfg));
|
|
return linux;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// STRIP FLAGS
|
|
// For the post-link strip step (release only).
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Strip step only runs for plain release builds (bun-profile → bun).
|
|
* Not for debug/asan/valgrind/assertions variants — those keep symbols.
|
|
*
|
|
* Always: --strip-all --strip-debug --discard-all.
|
|
* Platform extras remove unwind/exception sections we compile without
|
|
* (no -fexceptions, lolhtml built with panic=abort).
|
|
*
|
|
* Linux section removal: CMake notes llvm-strip doesn't fully delete
|
|
* these (leaves [LOAD #2 [R]]), GNU strip does. If size matters and
|
|
* llvm-strip's output is larger, swap to /usr/bin/strip for this step.
|
|
*/
|
|
export const stripFlags: Flag[] = [
|
|
{
|
|
// Core strip: symbols + debug info + local symbols.
|
|
flag: ["--strip-all", "--strip-debug", "--discard-all"],
|
|
desc: "Remove symbols, debug info, local symbols",
|
|
},
|
|
{
|
|
flag: [
|
|
"--remove-section=__TEXT,__eh_frame",
|
|
"--remove-section=__TEXT,__unwind_info",
|
|
"--remove-section=__TEXT,__gcc_except_tab",
|
|
],
|
|
when: c => c.darwin,
|
|
desc: "Remove unwind/exception sections (we compile with -fno-exceptions; these come from lolhtml etc., built with panic=abort)",
|
|
},
|
|
{
|
|
// musl: no eh_frame handling differences, but CMake gates on NOT musl so we do too.
|
|
//
|
|
// Gated on release to match -Wl,--no-eh-frame-hdr in linkerFlags above
|
|
// (both fire on `c.linux && c.abi === "gnu" && c.release`). GNU strip
|
|
// does not rewrite the program header table, so the PT_GNU_EH_FRAME
|
|
// phdr must already be absent at link time — which the matching
|
|
// --no-eh-frame-hdr above guarantees. Nothing unwinds at runtime
|
|
// (`panic = "abort"`, `-fno-exceptions`); release backtraces use frame
|
|
// pointers. Saves ~962 KB of R-segment (.eh_frame 806 KB +
|
|
// .eh_frame_hdr 142 KB + .gcc_except_table 13 KB) that otherwise gets
|
|
// dragged into RSS via 64 KB fault-around on adjacent .rodata reads.
|
|
flag: ["-R", ".eh_frame", "-R", ".eh_frame_hdr", "-R", ".gcc_except_table"],
|
|
when: c => c.linux && c.abi === "gnu" && c.release,
|
|
desc: "Remove unwind sections (GNU strip required — llvm-strip leaves [LOAD #2 [R]])",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// INCLUDE DIRECTORIES
|
|
// Bun's own source tree + build-time generated code.
|
|
// Dependency includes (WebKit, boringssl, ...) come from resolveDep().
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Bun's source-tree include paths. These are the -I dirs for bun's own code
|
|
* (not vendored deps — those come from each dep's `Provides.includes`).
|
|
*/
|
|
export function bunIncludes(cfg: Config): string[] {
|
|
const { cwd, codegenDir, vendorDir } = cfg;
|
|
const includes: string[] = [
|
|
// Release builds shadow <iostream> with a #error shim. A single <iostream>
|
|
// include anywhere (our headers, bun-uws, or a WebKit header pulled into a
|
|
// Bun TU) drags libstdc++'s globals_io.o into the link and runs
|
|
// std::ios_base::Init + the full locale facet set before main. Debug builds
|
|
// keep the real header available for ad-hoc printf-debugging.
|
|
...(cfg.release ? [join(cwd, "src/banned-includes")] : []),
|
|
join(cwd, "packages"),
|
|
join(cwd, "packages/bun-usockets"),
|
|
join(cwd, "packages/bun-usockets/src"),
|
|
join(cwd, "src/jsc/bindings"),
|
|
join(cwd, "src/jsc/bindings/webcore"),
|
|
join(cwd, "src/jsc/bindings/webcrypto"),
|
|
join(cwd, "src/jsc/bindings/node/crypto"),
|
|
join(cwd, "src/jsc/bindings/node/http"),
|
|
join(cwd, "src/jsc/bindings/sqlite"),
|
|
join(cwd, "src/jsc/bindings/v8"),
|
|
join(cwd, "src/jsc/modules"),
|
|
join(cwd, "src/js/builtins"),
|
|
join(cwd, "src/runtime/napi"),
|
|
join(cwd, "src/uws_sys"),
|
|
codegenDir,
|
|
vendorDir,
|
|
join(vendorDir, "picohttpparser"),
|
|
join(vendorDir, "zlib"),
|
|
// NODEJS_HEADERS_PATH comes from the nodejs dep; added separately
|
|
];
|
|
|
|
if (cfg.windows) {
|
|
includes.push(join(cwd, "src/jsc/bindings/windows"));
|
|
} else {
|
|
// libuv stubs for unix (real libuv used on windows)
|
|
includes.push(join(cwd, "src/jsc/bindings/libuv"));
|
|
}
|
|
|
|
// musl doesn't ship sys/queue.h (glibc-only BSDism). lshpack bundles
|
|
// a compat copy for this case.
|
|
if (cfg.linux && cfg.abi === "musl") {
|
|
includes.push(join(vendorDir, "lshpack/compat/queue"));
|
|
}
|
|
|
|
return includes;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PER-FILE OVERRIDES
|
|
// Exceptional files that need different flags than the global set.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export interface FileOverride {
|
|
/** Source path relative to cfg.cwd. */
|
|
file: string;
|
|
/** Extra flags appended after the global set. */
|
|
extraFlags: FlagValue;
|
|
when?: (cfg: Config) => boolean;
|
|
desc: string;
|
|
}
|
|
|
|
export const fileOverrides: FileOverride[] = [
|
|
{
|
|
file: "src/jsc/bindings/workaround-missing-symbols.cpp",
|
|
// -fwhole-program-vtables requires -flto; disabling one requires
|
|
// disabling the other or clang errors.
|
|
extraFlags: ["-fno-lto", "-fno-whole-program-vtables"],
|
|
when: c => c.linux && c.lto && c.abi === "gnu",
|
|
desc: "Disable LTO: LLD 21 emits glibc versioned symbols (exp@GLIBC_2.17) into .lto_discard which fails to parse '@'",
|
|
},
|
|
{
|
|
file: "src/jsc/bindings/windows/rescle.cpp",
|
|
extraFlags: "/EHsc",
|
|
when: c => c.windows,
|
|
desc: "Vendored electron/rcedit; VersionInfo ctor throws std::system_error caught in OnEnumResourceLanguage. Self-contained throw/catch — already excluded from PCH",
|
|
},
|
|
{
|
|
file: "src/jsc/bindings/highway_xml.cpp",
|
|
extraFlags: ["-O2"],
|
|
when: c => c.debug,
|
|
desc: "Same as highway_json.cpp below: the XML structural-index kernel must be optimized even in debug builds.",
|
|
},
|
|
{
|
|
file: "src/jsc/bindings/highway_json.cpp",
|
|
extraFlags: ["-O2"],
|
|
when: c => c.debug,
|
|
desc:
|
|
"Always optimize the JSON structural-index kernel (debug builds use -O0 globally). At -O0 every " +
|
|
"highway op is an outlined call taking 64-byte vectors by value through the stack, and clang's " +
|
|
"unoptimized codegen for that pattern raises #GP on an aligned zmm stack store under the debug " +
|
|
"sanitizer set; it would also make every JSON parse in debug builds pathologically slow. " +
|
|
"Covered by test/js/bun/jsonc/jsonc.test.ts and `scripts/bench-json-rust.sh --test`.",
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// COMPUTED OUTPUT
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export interface ComputedFlags {
|
|
/** C compiler flags (clang -c for .c files). */
|
|
cflags: string[];
|
|
/** C++ compiler flags (clang++ -c for .cpp files). */
|
|
cxxflags: string[];
|
|
/** Preprocessor defines, without -D prefix. */
|
|
defines: string[];
|
|
/** Linker flags for the final bun link. */
|
|
ldflags: string[];
|
|
/** Strip flags for post-link. */
|
|
stripflags: string[];
|
|
}
|
|
|
|
/**
|
|
* Resolve a FlagValue to its final string form(s) for a given config.
|
|
*/
|
|
function resolveFlagValue(v: FlagValue, cfg: Config): string[] {
|
|
const resolved = typeof v === "function" ? v(cfg) : v;
|
|
return Array.isArray(resolved) ? resolved : [resolved];
|
|
}
|
|
|
|
/**
|
|
* Evaluate a Flag table and push into c/cxx output arrays.
|
|
*/
|
|
function evalTable(table: Flag[], cfg: Config, c: string[], cxx: string[]): void {
|
|
for (const f of table) {
|
|
if (f.when && !f.when(cfg)) continue;
|
|
const flags = resolveFlagValue(f.flag, cfg);
|
|
if (f.lang === "c") {
|
|
c.push(...flags);
|
|
} else if (f.lang === "cxx") {
|
|
cxx.push(...flags);
|
|
} else {
|
|
c.push(...flags);
|
|
cxx.push(...flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evaluate all flag predicates for bun's own source files.
|
|
* Combines global flags + bun-only flags.
|
|
*/
|
|
export function computeFlags(cfg: Config): ComputedFlags {
|
|
const cflags: string[] = [];
|
|
const cxxflags: string[] = [];
|
|
const defs: string[] = [];
|
|
const ldflags: string[] = [];
|
|
const stripflags: string[] = [];
|
|
|
|
// Compile: global first, then bun-only
|
|
evalTable(globalFlags, cfg, cflags, cxxflags);
|
|
evalTable(bunOnlyFlags, cfg, cflags, cxxflags);
|
|
|
|
// Defines, linker, strip
|
|
for (const f of defines) {
|
|
if (f.when && !f.when(cfg)) continue;
|
|
defs.push(...resolveFlagValue(f.flag, cfg));
|
|
}
|
|
for (const f of linkerFlags) {
|
|
if (f.when && !f.when(cfg)) continue;
|
|
ldflags.push(...resolveFlagValue(f.flag, cfg));
|
|
}
|
|
for (const f of stripFlags) {
|
|
if (f.when && !f.when(cfg)) continue;
|
|
stripflags.push(...resolveFlagValue(f.flag, cfg));
|
|
}
|
|
|
|
return { cflags, cxxflags, defines: defs, ldflags, stripflags };
|
|
}
|
|
|
|
/**
|
|
* Flags forwarded to vendored dependencies via -DCMAKE_C_FLAGS/-DCMAKE_CXX_FLAGS.
|
|
* This is ONLY the global table — no -Werror, no bun-specific defines, no UBSan.
|
|
*/
|
|
export function computeDepFlags(cfg: Config): { cflags: string[]; cxxflags: string[] } {
|
|
const cflags: string[] = [];
|
|
const cxxflags: string[] = [];
|
|
evalTable(globalFlags, cfg, cflags, cxxflags);
|
|
return { cflags, cxxflags };
|
|
}
|
|
|
|
/**
|
|
* Just the -march/-mcpu/-mtune flags. For deps (WebKit) whose own build system
|
|
* sets -O/-g/sanitizer flags but never sets a CPU target, so without this they
|
|
* end up targeting generic x86-64 while the rest of bun targets nehalem.
|
|
*/
|
|
export function computeCpuTargetFlags(cfg: Config): string[] {
|
|
const out: string[] = [];
|
|
for (const f of cpuTargetFlags) {
|
|
if (f.when && !f.when(cfg)) continue;
|
|
out.push(...resolveFlagValue(f.flag, cfg));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Per-file extra flags lookup. Call after computeFlags() when compiling a
|
|
* specific source. Returns extra flags to append (may be empty).
|
|
*/
|
|
export function extraFlagsFor(cfg: Config, srcRelPath: string): string[] {
|
|
const key = srcRelPath.replaceAll("\\", "/");
|
|
for (const o of fileOverrides) {
|
|
if (o.file !== key) continue;
|
|
if (o.when && !o.when(cfg)) continue;
|
|
return resolveFlagValue(o.extraFlags, cfg);
|
|
}
|
|
return [];
|
|
}
|