build: fat Rust bitcode for cross-language LTO on Linux (#31412)
### What
Rust↔C++ cross-language LTO on the Linux release links, without touching
the ThinLTO-miscompile situation: the Rust side now emits **fat**
(pre-merged) bitcode that joins the same regular-LTO partition as the
`-flto=full` C++ objects.
Two commits:
1. **`CARGO_PROFILE_RELEASE_LTO = fat` on ELF cross-language-LTO
builds** (darwin keeps `off` — its ThinLTO link needs the per-CGU
summaries from #31303). rustc pre-merges every crate (incl. prebuilt std
bitcode) into one regular-LTO module, so lld folds it into the C++
partition: cross-language inlining happens through the regular-LTO
merge, and **no ThinLTO backend runs anywhere in the link**.
2. **A regular-LTO-summary fix-up step** (`rust-lto-fix-cli.ts` + the
`rust_lto_fix` ninja edge). lld checks split-LTO-unit consistency via
each module's *summary*: clang full-LTO objects on ELF always carry
`EnableSplitLTOUnit=1` (hardcoded in `shouldEmitRegularLTOSummary()`),
while rustc's fat output has no summary at all → reads as 0 →
`LTO::checkPartiallySplit()` aborts with "inconsistent LTO Unit
splitting" (this is what the first CI round's 4 Linux link jobs hit).
rustc can't emit such a summary, so the step extracts the fat bitcode
member, adds the `ThinLTO=0` module flag, and re-emits it with `opt
--module-summary` from rustc's own llvm-tools. Metadata-only; no
optimization passes run. Registered in `workarounds.ts`
(`rustc-no-regular-lto-summary`, re-checked at the LLVM-23 rustc bump).
### Local validation (release+LTO build on Linux x64)
- Link succeeds; smoke test + eval/fs/`Bun.serve`+fetch + a real test
file (75/75) pass with the built binary.
- **Link: 763s** (vs ~14 min for the previous C++-only full-LTO link —
roughly unchanged). Fix-up step: **32s**. Stripped binary: **74.7MB**
(+0.9% vs the current release binary).
- **Cross-language inlining is real**: of the 2,508 extern "C" symbols
defined by the Rust side, **1,937 are fully inlined and eliminated**,
and only **607 out-of-line calls** (to 201 symbols) remain in the entire
binary — the survivors are large/cold targets (`Bun__panic`, finalizers,
init paths). Before this, every C++→Rust boundary call stayed
out-of-line (~15.7k calls total across the boundary, per the #31303
analysis).
### What to watch in CI
- Link-step wall time / memory on the Linux link boxes (locally it's on
par with the old link, but those boxes have 64 GB).
- The thin-LTO miscompile symptoms (`require("axobject-query")` DFG
repro, bundler-test hangs) must **not** appear — nothing thin runs in
this configuration.
- The `rust_lto_fix` step self-installs the `llvm-tools` rustup
component on agents that don't have it; first run on each link agent
does one `rustup component add`.
### Not changed
- macOS/darwin-cross links: exactly as #31303 shipped.
- aarch64-musl keeps its `crossLangLto` gate (globalopt segfault
workaround); lifting it is a candidate follow-up since the bitcode shape
that crashed no longer exists.
- Non-LTO and ASAN builds untouched; debug build graph identical.
### Known wart
The fix-up step exists because rustc has no equivalent of clang's
regular-LTO summary emission. The durable alternatives are upstreaming
that into rustc, or moving Linux to ThinLTO once the LLVM 22
thin-backend miscompile of JSC is isolated — either one deletes the
workaround (the `workarounds.ts` entry will nag at the LLVM-23 bump).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> J
Jarred Sumner committed
ec68d390140fbba80e4ae550415cea5c2b27cba6
Parent: 0b1e7e5
Committed by GitHub <noreply@github.com>
on 5/26/2026, 4:07:41 AM