Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions .buildkite/ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -725,10 +725,17 @@ function getVerifyBaselineStep(platform, options) {
*
* linux-aarch64 is absent because its build lane runs on the aarch64 host and
* traces itself; `packageAndUpload()` is its sole publisher.
*
* The `on` platforms are entries of `testPlatforms`, so the step runs on an
* image that exists. The windows tracer is built on the test VM for whichever
* architecture it is running on (scripts/orderfile/functrace-windows.c), so each
* windows target traces on its own arch's fleet.
*/
const traceOrderTargets = [
{ os: "darwin", arch: "aarch64", on: { os: "darwin", arch: "aarch64", release: "26", tier: "latest" } },
{ os: "linux", arch: "x64", on: { os: "linux", arch: "x64", distro: "debian", release: "13" } },
{ os: "windows", arch: "x64", on: { os: "windows", arch: "x64", release: "2019", tier: "oldest" } },
{ os: "windows", arch: "aarch64", on: { os: "windows", arch: "aarch64", release: "11", tier: "latest" } },
];

/**
Expand All @@ -744,15 +751,24 @@ const traceOrderTargets = [
* Non-PR only — `orderFileEligible()` ignores PR builds, so a trace there has
* no consumer. Soft-fail: the order file is an optimization, and a broken
* tracer must not fail a build.
*
* Windows agents run commands under cmd.exe (see getVerifyBaselineStep for the
* `|| exit /b 1` convention). The generator compiles the tracer there, which
* takes clang-cl or a Visual Studio environment; the image has both, and
* vs-shell.ps1 provides the latter the same way it does for the test runner.
* The profile zip carries the two maps the generator resolves addresses with
* (packageAndUpload in scripts/build/ci.ts; scripts/orderfile/windows-symbols.ts).
* @param {Target} target
* @param {Platform} tracePlatform
* @param {PipelineOptions} options
* @returns {CommandStep}
*/
function getTraceOrderStep(target, tracePlatform, options) {
const { os } = target;
const targetKey = getTargetKey(target);
const triplet = getTargetTriplet(target);
const profileDir = `${triplet}-profile`;
const generate = `scripts/orderfile/generate.ts --build-dir=${profileDir} --out=${triplet}.order`;
return {
key: `${targetKey}-trace-order`,
label: `${getTargetLabel(target)} - trace-order`,
Expand All @@ -762,13 +778,21 @@ function getTraceOrderStep(target, tracePlatform, options) {
cancel_on_build_failing: isMergeQueue(),
soft_fail: true,
timeout_in_minutes: 15,
command: [
`buildkite-agent artifact download '${profileDir}.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${profileDir}.zip'`,
`chmod +x ${profileDir}/bun-profile`,
`./${profileDir}/bun-profile scripts/orderfile/generate.ts --build-dir=${profileDir} --out=${triplet}.order`,
`buildkite-agent artifact upload '${triplet}.order'`,
],
command:
os === "windows"
? [
`buildkite-agent artifact download ${profileDir}.zip . --step ${targetKey}-build-bun || exit /b 1`,
`tar -xf ${profileDir}.zip || exit /b 1`,
`pwsh -NoProfile -File .\\scripts\\vs-shell.ps1 .\\${profileDir}\\bun-profile.exe ${generate} || exit /b 1`,
`buildkite-agent artifact upload ${triplet}.order`,
]
: [
`buildkite-agent artifact download '${profileDir}.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${profileDir}.zip'`,
`chmod +x ${profileDir}/bun-profile`,
`./${profileDir}/bun-profile ${generate}`,
`buildkite-agent artifact upload '${triplet}.order'`,
],
};
}

Expand Down
15 changes: 7 additions & 8 deletions scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ import { allDeps } from "./deps/index.ts";
import { lolhtml } from "./deps/lolhtml.ts";
import { rustArgon2 } from "./deps/rust-argon2.ts";
import { assert } from "./error.ts";
import { bunIncludes, computeFlags, extraFlagsFor, linkDepends } from "./flags.ts";
import { bunIncludes, computeFlags, extraFlagsFor, linkDepends, linkerMapOutputs } from "./flags.ts";
import { writeIfChanged } from "./fs.ts";
import type { BuildNode, Ninja } from "./ninja.ts";
import { emitRust, linkerMapPath, rustLibPath, rustLtoLinkInputs } from "./rust.ts";
import { emitRust, rustLibPath, rustLtoLinkInputs } from "./rust.ts";
import { quote, slash } from "./shell.ts";
import { emitShims, machoPostlinkCommand, machoPostlinkImplicitInputs } from "./shims.ts";
import { computeDepLibs, resolveDep, type ResolvedDep } from "./source.ts";
Expand Down Expand Up @@ -511,10 +511,9 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
libs: depLibs,
flags: ldflags,
implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs, ...depChecks],
// Declare the `-Wl,-Map=` side-product so `perf` symbolication picks it
// up. Linux release only — the map flag itself is gated identically in
// flags.ts.
linkerMapOutput: cfg.linux && cfg.release && !cfg.asan && !cfg.valgrind ? linkerMapPath(cfg) : undefined,
// Declare the maps the release link writes as side-products (`perf`
// symbolication on linux; the order file tracer's symbol table on windows).
linkerMapOutputs: linkerMapOutputs(cfg),
});

// ─── Step 7: post-link (strip, dsymutil, smoke test) ───
Expand Down Expand Up @@ -658,7 +657,7 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput {
libs: depLibs,
flags: ldflags,
implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs],
linkerMapOutput: cfg.linux && cfg.release && !cfg.asan && !cfg.valgrind ? linkerMapPath(cfg) : undefined,
linkerMapOutputs: linkerMapOutputs(cfg),
});

// Strip + smoke test — same as full mode.
Expand Down Expand Up @@ -735,7 +734,7 @@ function emitRustAndLink(n: Ninja, cfg: Config, sources: Sources): BunOutput {
libs: depLibs,
flags: ldflags,
implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs],
linkerMapOutput: cfg.linux && cfg.release && !cfg.asan && !cfg.valgrind ? linkerMapPath(cfg) : undefined,
linkerMapOutputs: linkerMapOutputs(cfg),
});

const { strippedExe, dsym } = emitPostLink(n, cfg, exe, exeName, flags.stripflags);
Expand Down
44 changes: 23 additions & 21 deletions scripts/build/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import {
} from "node:fs";
import { basename, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { generateOrderFile } from "../orderfile/generate.ts";
import { generateOrderFile, readTextSymbols } from "../orderfile/generate.ts";
// @ts-ignore — utils.mjs has JSDoc types but no .d.ts
import * as utils from "../utils.mjs";
import { bunExeName, shouldStrip, type BunOutput } from "./bun.ts";
import type { Config } from "./config.ts";
import { webkitTestFFIPath } from "./deps/webkit.ts";
import { BuildError } from "./error.ts";
import { crossFeaturesJson } from "./features-json.ts";
import { orderFilePath, usesOrderFile } from "./flags.ts";
import { linkerMapOutputs, orderFilePath, usesOrderFile } from "./flags.ts";

/** True if running under any CI (env: CI, BUILDKITE, or GITHUB_ACTIONS). */
export const isCI: boolean = utils.isCI;
Expand Down Expand Up @@ -342,7 +342,10 @@ function upload(paths: string[], cwd: string): void {
// ├── bun-profile[.exe]
// ├── testFFI[.exe] (WebKit FFI test binary, when shipped)
// ├── features.json
// ├── bun-profile.linker-map (linux/mac non-asan)
// ├── bun-profile.linker-map (linkerMapOutputs: release, non-asan)
// ├── bun-profile.map (windows; with the above, what the
// │ trace-order step resolves addresses with)
// ├── linker.order (the order file this binary was linked with, if any)
// ├── bun-profile.pdb (windows)
// └── bun-profile.dSYM (mac)
//
Expand Down Expand Up @@ -434,10 +437,10 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void {
} else if (cfg.darwin) {
files.push(`${exeName}.dSYM`);
}
// Linker map: posix non-asan (cmake gate: (APPLE OR LINUX) AND NOT ENABLE_ASAN).
if (cfg.unix && !cfg.asan) {
files.push(`${exeName}.linker-map`);
}
// Linker map(s). On windows they are also what the trace-order step
// (.buildkite/ci.mjs) resolves traced addresses against, the PE itself
// having no symbol table, so without them that step has nothing to work from.
files.push(...linkerMapOutputs(cfg).map(map => basename(map)));
// The symbol ordering file this binary was linked with, next to the linker
// map. Skip the seeded placeholder — it has no functions in it.
const hasOrderFile = usesOrderFile(cfg) && orderFileFunctionCount(cfg) > 0;
Expand Down Expand Up @@ -993,24 +996,21 @@ export function verifyOrderFileApplied(cfg: Config, ctx: OrderFileContext, exe:
return;
}

// Same resolution as generate.ts: honor NM, else llvm-nm, else nm.
let nm = { status: null, stdout: "" } as { status: number | null; stdout: string };
for (const tool of [process.env.NM, "llvm-nm", "nm"].filter(Boolean) as string[]) {
nm = spawnSync(tool, ["--defined-only", exe], { encoding: "utf8", maxBuffer: 1 << 29 });
if (nm.status === 0) break;
}
if (nm.status !== 0) {
console.log("~ symbol order: no working nm — skipping verification");
// The same names the generator traces against: nm's, or on windows the link's maps'.
let symbols: Map<number, string[]>;
try {
symbols = readTextSymbols(exe);
} catch (error) {
console.log(
`~ symbol order: cannot read the binary's symbols — skipping verification (${(error as Error).message})`,
);
return;
}

const addresses = new Map<string, number>();
let textBase = Number.MAX_SAFE_INTEGER;
for (const line of nm.stdout.split("\n")) {
const m = /^([0-9a-f]+) ([tT]) (\S+)$/.exec(line);
if (!m) continue;
const address = parseInt(m[1]!, 16);
addresses.set(m[3]!, address);
for (const [address, names] of symbols) {
for (const name of names) addresses.set(name, address);
if (address < textBase) textBase = address;
}

Expand Down Expand Up @@ -1055,7 +1055,9 @@ export function verifyOrderFileApplied(cfg: Config, ctx: OrderFileContext, exe:
`the order file had no effect: hot functions sit at ${mb(hot)}, a typical one at ${mb(control)}`,
cfg.darwin
? "Apple ld ignored it — check -order_file and that the names match nm's"
: "lld ignored it — check --symbol-ordering-file and that -ffunction-sections survived",
: cfg.windows
? "lld-link ignored it — check /order and that /Gy survived"
: "lld ignored it — check --symbol-ordering-file and that -ffunction-sections survived",
);
return;
}
Expand Down
11 changes: 4 additions & 7 deletions scripts/build/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,8 @@ export interface LinkOpts {
* Editing these should trigger relink (cmake's LINK_DEPENDS equivalent).
*/
implicitInputs?: string[];
/** Output linker map to this path (for debugging symbol bloat). */
linkerMapOutput?: string | undefined;
/** Map files the link's flags make it write alongside the executable (flags.ts linkerMapOutputs). */
linkerMapOutputs?: string[];
}

/**
Expand All @@ -461,11 +461,8 @@ export interface LinkOpts {
export function link(n: Ninja, cfg: Config, out: string, objects: string[], opts: LinkOpts): string {
const absOut = resolve(cfg.buildDir, out + cfg.exeSuffix);

// Linker map is an implicit output (ninja tracks it but not in $out)
const implicitOutputs: string[] = [];
if (opts.linkerMapOutput !== undefined) {
implicitOutputs.push(resolve(cfg.buildDir, opts.linkerMapOutput));
}
// Linker maps are implicit outputs (ninja tracks them but they're not in $out)
const implicitOutputs = (opts.linkerMapOutputs ?? []).map(map => resolve(cfg.buildDir, map));

const node: BuildNode = {
outputs: [absOut],
Expand Down
105 changes: 85 additions & 20 deletions scripts/build/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,32 @@ export const linkerFlags: Flag[] = [
when: c => c.windows && c.release,
desc: "Release link opts + delay-load non-critical DLLs (faster startup)",
},
{
// A PE carries no symbol table — lld-link puts the names in the PDB — so
// these two maps stand in for it on the order file's behalf (see
// scripts/orderfile/windows-symbols.ts): the MSVC-style one lists every
// symbol by final address under the exact name /order takes, and lld's own
// says where each input chunk was placed, which is what tells a function
// apart from the labels the MSVC CRT leaves on data inside its code. They
// ship in the profile zip beside the binary, for the trace-order step
// (.buildkite/ci.mjs) and for verifyOrderFileApplied() in scripts/build/ci.ts.
flag: c => [`/lldmap:${slash(linkerMapPath(c))}`, `/map:${slash(symbolMapPath(c))}`],
when: c => c.windows && writesLinkerMap(c),
desc: "Linker maps: the order file tracer's symbol table (see windows-symbols.ts)",
},
{
// lld-link's spelling of the symbol ordering file (the Linux entry below
// explains it): one COMDAT leader name per line, laid out first in .text.
// Every function is its own COMDAT here — /Gy for bun's C++ and for the
// WebKit prebuilt, rustc's default function sections for the Rust side, and
// LTO output is per-function regardless — so one trace reorders all of
// them. /ignore:4037 is --no-warn-symbol-ordering's counterpart: the names
// were traced from an earlier build's binary (see usesOrderFile), and each
// one that no longer exists would otherwise be an LNK4037 warning.
flag: c => [`/order:@${slash(orderFilePath(c))}`, "/ignore:4037"],
when: c => c.windows && usesOrderFile(c),
desc: "Sort startup-hot functions to the front of .text (cuts resident binary pages)",
},

// ─── macOS ───
{
Expand Down Expand Up @@ -1107,7 +1133,7 @@ export const linkerFlags: Flag[] = [
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`],
flag: c => ["-dead_strip", "-dead_strip_dylibs", `-Wl,-map,${linkerMapPath(c)}`],
when: c => c.darwin && c.release,
desc: "Dead-code strip + emit linker map",
},
Expand Down Expand Up @@ -1293,7 +1319,7 @@ export const linkerFlags: Flag[] = [
// 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`],
flag: c => ["-Wl,-icf=safe", `-Wl,-Map=${linkerMapPath(c)}`],
when: c => c.linux && c.release && !c.asan && !c.valgrind,
desc: "Identical-code-folding (safe; perf symbolication uses the linker-map)",
},
Expand Down Expand Up @@ -1426,13 +1452,18 @@ export const linkerFlags: Flag[] = [
/**
* 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.
* ld and ld64.lld take, lld-link `/order` on windows). 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.
* inherits an earlier build's file instead and still links ordered; the
* trace-order step in .buildkite/ci.mjs produces that file on the target's test
* fleet. Both windows targets work this way: their tracer is a debugger
* (scripts/orderfile/functrace-windows.c), so it needs no preload mechanism,
* and it plants INT3 or BRK depending on which architecture it is built for.
*
* 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
Expand All @@ -1442,38 +1473,72 @@ export const linkerFlags: Flag[] = [
* 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">,
cfg: Pick<Config, "linux" | "darwin" | "windows" | "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;
return cfg.windows;
}

/** 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");
}

/**
* Whether the link writes its map(s); mirrors the map flags above (linux: the
* `-icf=safe` entry, darwin: `-dead_strip`, windows: `/lldmap` + `/map`).
* `linkerMapOutputs()` names them: bun.ts declares those to ninja as the link's
* outputs, and ci.ts ships them in the profile zip — on windows the trace-order
* step cannot work without them (see the `/lldmap` entry).
*/
export function writesLinkerMap(
cfg: Pick<Config, "linux" | "darwin" | "windows" | "release" | "asan" | "valgrind">,
): boolean {
if (!cfg.release) return false;
if (cfg.linux) return !cfg.asan && !cfg.valgrind;
return cfg.darwin || cfg.windows;
}

/** `<buildDir>/bun-profile.linker-map`: the linker's own map — lld's `-Map`, ld64's `-map`, lld-link's `/lldmap`. */
export function linkerMapPath(cfg: Config): string {
return join(cfg.buildDir, `${bunExeName(cfg)}.linker-map`);
}

/**
* `<buildDir>/bun-profile.map`: lld-link's MSVC-style `/map`, every symbol by
* address. Windows only; the same name scripts/orderfile/windows-symbols.ts
* derives from the binary's.
*/
export function symbolMapPath(cfg: Config): string {
return join(cfg.buildDir, `${bunExeName(cfg)}.map`);
}

/** The map files the link writes (see writesLinkerMap), or none. */
export function linkerMapOutputs(cfg: Config): string[] {
if (!writesLinkerMap(cfg)) return [];
return cfg.windows ? [linkerMapPath(cfg), symbolMapPath(cfg)] : [linkerMapPath(cfg)];
}

/**
* 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.
*
* The release symbol ordering file is one of them on every target that uses
* it: listing it here is what makes regenerating (or inheriting) it relink, and
* only relink.
*/
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;
const depends = cfg.windows
? [join(cfg.cwd, "src/symbols.def")]
: cfg.darwin
? [join(cfg.cwd, "src/symbols.txt")]
: [join(cfg.cwd, "src/symbols.dyn"), join(cfg.cwd, "src/linker.lds")]; // linux: ELF dynamic-list + version script
if (usesOrderFile(cfg)) depends.push(orderFilePath(cfg));
return depends;
}

// ═══════════════════════════════════════════════════════════════════════════
Expand Down
Loading
Loading