Files

174 lines
8.2 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
/**
* Rust regular-LTO summary fix-up — the ninja build-time CLI for the
* `rust_lto_fix` rule (see `rustLtoLinkInputs()` in rust.ts and the
* `rustc-no-regular-lto-summary` entry in workarounds.ts).
*
* ## Why this exists
*
* The ELF release link is full (regular) LTO: every C/C++ object — ours,
* the direct deps', the WebKit `-lto` prebuilts' — is clang full-LTO
* bitcode, and clang unconditionally writes a per-module *regular-LTO
* summary* with `EnableSplitLTOUnit=1` into such objects on ELF
* (`shouldEmitRegularLTOSummary()` in clang's BackendUtil; neither
* `-fno-split-lto-unit` nor any other driver flag turns that off).
*
* The Rust side reaches the link as `-Clinker-plugin-lto` + `lto = "fat"`
* bitcode: one merged module with *no* summary at all. lld's
* `getLTOInfo()` reports a summary-less module as `EnableSplitLTOUnit=0`,
* the link becomes "partially split", and because `-fwhole-program-vtables`
* puts `llvm.type.test` calls in the merged C++ module,
* `LTO::checkPartiallySplit()` aborts the link with
* "inconsistent LTO Unit splitting (recompile with -fsplit-lto-unit)".
* rustc has no option to emit a regular-LTO summary, so this step bolts
* one on:
*
* 1. extract the bitcode member(s) from `libbun_rust.a`,
* 2. `llvm-link` in a stub that adds the `ThinLTO=0` module flag — that
* flag is what makes the bitcode writer emit a FULL_LTO summary block
* instead of a ThinLTO one,
* 3. re-emit with `opt --module-summary`, which builds the per-module
* summary from the IR. Its `EnableSplitLTOUnit` bit is copied from the
* module flag that `-Zsplit-lto-unit` stamped on every CGU (rust.ts
* passes it on ELF for exactly this reason), so the result matches the
* clang objects and the consistency check passes.
*
* The tools must come from rustc's own LLVM (the rustup `llvm-tools`
* component, installed next to rust-lld) — clang's older LLVM cannot read
* rustc's newer bitcode. If the component is missing, this script installs
* it (`rustup component add llvm-tools`), mirroring how the
* `rust_build_cross` rule self-heals missing `rust-std` targets on CI
* agents that pin the toolchain via `RUSTUP_TOOLCHAIN`.
*
* argv: [node, rust-lto-fix-cli.ts, <libbun_rust.a>, <out.o>, <llvm-bin-dir>, <ar>]
*/
import { spawnSync } from "node:child_process";
import { closeSync, existsSync, mkdirSync, openSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { BuildError, assert } from "./error.ts";
/** Absolute path to this file — referenced by the `rust_lto_fix` ninja rule. */
export const rustLtoFixCliPath: string = import.meta.filename;
/** Run a tool, streaming its output; throw a BuildError on failure. */
function run(cmd: string, args: string[], cwd?: string): void {
const res = spawnSync(cmd, args, { stdio: "inherit", cwd });
if (res.error !== undefined || res.status !== 0) {
throw new BuildError(`${cmd} ${args.join(" ")} failed${res.status !== null ? ` (exit ${res.status})` : ""}`, {
cause: res.error,
});
}
}
/** First bytes of an LLVM bitcode file: 'BC\xC0\xDE', or the wrapper magic 0x0B17C0DE (LE). */
function isBitcode(path: string): boolean {
const buf = Buffer.alloc(4);
const fd = openSync(path, "r");
try {
if (readSync(fd, buf, 0, 4, 0) < 4) return false;
} finally {
closeSync(fd);
}
if (buf[0] === 0x42 && buf[1] === 0x43 && buf[2] === 0xc0 && buf[3] === 0xde) return true;
return buf[0] === 0xde && buf[1] === 0xc0 && buf[2] === 0x17 && buf[3] === 0x0b;
}
/**
* Make sure llvm-link/opt/llvm-as exist in rustc's host tool dir. They ship
* with the rustup `llvm-tools` component (rust-toolchain.toml lists it, but
* CI agents pin via `RUSTUP_TOOLCHAIN` which bypasses that file's component
* list), so install it on demand.
*/
function ensureLlvmTools(llvmBin: string): void {
const needed = ["llvm-link", "opt", "llvm-as", "llvm-dis"];
const missing = () => needed.filter(t => !existsSync(join(llvmBin, t)));
if (missing().length === 0) return;
// `<...>/toolchains/<name>/lib/rustlib/<triple>/bin` → `<name>`.
const toolchain = /[\\/]toolchains[\\/]([^\\/]+)[\\/]/.exec(llvmBin)?.[1];
const args = ["component", "add", "llvm-tools"];
if (toolchain !== undefined) args.push("--toolchain", toolchain);
console.log(`rust-lto-fix: ${missing().join(", ")} not found in ${llvmBin}, running rustup ${args.join(" ")}`);
const res = spawnSync("rustup", args, { stdio: "inherit" });
assert(
res.error === undefined && res.status === 0 && missing().length === 0,
`missing ${missing().join(", ")} in ${llvmBin}`,
{
hint: `Install rustc's LLVM tools: rustup component add llvm-tools${toolchain !== undefined ? ` --toolchain ${toolchain}` : ""}`,
},
);
}
function main(): void {
const argv = process.argv.slice(2);
assert(
argv[0] !== undefined && argv[1] !== undefined && argv[2] !== undefined && argv[3] !== undefined,
"usage: rust-lto-fix-cli.ts <libbun_rust.a> <out.o> <llvm-bin-dir> <ar>",
);
// Ninja passes buildDir-relative $in/$out and runs us with cwd=buildDir,
// but the archive is extracted with cwd set to the scratch dir below —
// make them absolute first. The tool paths are already absolute.
const [rustLib, outObj, llvmBin, ar] = [resolve(argv[0]), resolve(argv[1]), argv[2], argv[3]];
assert(existsSync(rustLib), `${rustLib} does not exist`);
ensureLlvmTools(llvmBin);
// Scratch space next to the output; recreated from scratch every run.
const tmp = `${outObj}.tmp`;
rmSync(tmp, { recursive: true, force: true });
mkdirSync(tmp, { recursive: true });
try {
// Extract the archive and pick out the bitcode member(s). With
// `lto = "fat"` there is exactly one (the merged module); the rest are
// native objects (compiler_builtins) that stay in the archive.
run(ar, ["x", rustLib], tmp);
const bitcode = readdirSync(tmp)
.filter(f => isBitcode(join(tmp, f)))
.map(f => join(tmp, f));
assert(bitcode.length > 0, `no LLVM bitcode members found in ${rustLib}`, {
hint:
"The ELF cross-language LTO build expects cargo to emit fat bitcode " +
"(-Clinker-plugin-lto with CARGO_PROFILE_RELEASE_LTO=fat — see emitRust() in rust.ts).",
});
// The `ThinLTO=0` module flag is the bitcode writer's "this is a regular
// LTO module" marker — without it `--module-summary` writes a ThinLTO
// summary block and lld would send the module to a ThinLTO backend.
// Carry the module's target data layout on the stub too: without it the
// stub's empty layout mismatches the real module and llvm-link prints a
// "Linking two modules of different data layouts" warning on every link.
// llvm-dis streams the .ll header first, so a bounded read suffices.
const dis = spawnSync(join(llvmBin, "llvm-dis"), ["-o", "-", bitcode[0]], {
encoding: "utf8",
maxBuffer: 256 * 1024,
});
const dataLayout = /^target datalayout = "[^"]*"/m.exec(dis.stdout || "")?.[0];
const stubLl = join(tmp, "regular-lto-flag-stub.ll");
const stubBc = join(tmp, "regular-lto-flag-stub.bc");
writeFileSync(
stubLl,
`${dataLayout ? `${dataLayout}\n` : ""}!llvm.module.flags = !{!0}\n!0 = !{i32 1, !"ThinLTO", i32 0}\n`,
);
run(join(llvmBin, "llvm-as"), [stubLl, "-o", stubBc]);
const merged = join(tmp, "merged.bc");
// The stub goes FIRST: llvm-link uses the first module as the link
// destination, and IRMover silently inherits the data layout / target
// triple when the destination has none. With the stub last it is a
// *source* module whose empty layout differs from the destination's,
// and every build-bun job warns "Linking two modules of different data
// layouts". Same merged output either way (verified: the module flag and
// the real layout both survive).
run(join(llvmBin, "llvm-link"), [stubBc, ...bitcode, "-o", merged]);
run(join(llvmBin, "opt"), ["--module-summary", merged, "-o", outObj]);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
// Imported by rust.ts for `rustLtoFixCliPath`; only act as a CLI when ninja
// invokes this file directly.
if (process.argv[1] === import.meta.filename) {
main();
}