Files
bun-src/scripts/build/workarounds.ts
T
2026-08-27 21:09:14 +00:00

249 lines
12 KiB
TypeScript

/**
* Self-obsoleting workaround registry.
*
* Workarounds accumulate as dead code because nobody remembers to remove
* them once the upstream fix ships. This file is the antidote: every
* workaround registers an `expectedToBeFixed` predicate that trips once the
* fix is available, and configure fails with cleanup instructions.
*
* Add an entry here whenever you land a workaround that's waiting on an
* upstream release (LLVM fix, macOS update, Zig release, vendored dep
* bump, etc.). The entry is the reminder.
*
* ## Writing an `expectedToBeFixed` predicate
*
* Typically a version check: `cfg.clangVersion >= FIXED_IN_LLVM`,
* macOS SDK version, a dep's commit hash, etc. When you know exactly
* which release has the fix, use that. When you don't — fix merged
* upstream but not released yet — pick your best guess for the likely
* release. The check might trip on a version that turns out not to
* have the fix; that's okay. The error message tells the dev to bump
* the threshold, which takes 30 seconds. That's cheaper than leaving
* the check blank and the workaround living forever.
*
* - Use `applies` to gate the check to configs where the workaround is
* actually exercised — no point failing a Linux build for a
* macOS-only workaround.
* - Tool/OS detection: if you can't reliably detect (e.g. Apple clang
* vs LLVM clang have different version schemes), exclude the
* ambiguous case.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { Config } from "./config.ts";
import { BuildError } from "./error.ts";
import { satisfiesRange } from "./tools.ts";
/** Read a crate's locked version out of the repo's Cargo.lock. */
function lockedCrateVersion(cfg: Config, name: string): string | undefined {
const lock = readFileSync(join(cfg.cwd, "Cargo.lock"), "utf8");
const m = lock.match(new RegExp(`\\nname = "${name}"\\nversion = "([^"]+)"`));
return m?.[1];
}
export interface Workaround {
/** Short slug — shows up in the error message. */
id: string;
/** Upstream tracker reference (issue URL, PR number, etc.). */
issue: string;
/** One-line: what's being worked around. */
description: string;
/**
* Gate the check to relevant configs. If false, `expectedToBeFixed` isn't
* evaluated — the workaround isn't exercised on this config so there's
* nothing to verify.
*/
applies: (cfg: Config) => boolean;
/**
* Return true once the upstream fix is available in the current
* toolchain/environment. Configure fails when this trips.
*/
expectedToBeFixed: (cfg: Config) => boolean;
/** What to remove once the fix ships. */
cleanup: string;
}
export const workarounds: Workaround[] = [
{
id: "asan-dyld-shim",
issue: "https://github.com/llvm/llvm-project/issues/182943",
description:
"macOS 26.4 Dyld.framework reimplemented dyld_shared_cache_iterate_text in Swift; " +
"the _Block_copy allocation deadlocks ASAN init re-entrantly",
applies: cfg => cfg.darwin && cfg.asan,
expectedToBeFixed: cfg => {
// Fix merged to LLVM main. Backport to release/22.x is
// https://github.com/llvm/llvm-project/pull/188913 — lower this
// threshold to the exact 22.1.x once it lands. Apple clang is
// already excluded: resolveLlvmToolchain only accepts Homebrew
// llvm (LLVM_VERSION_RANGE is >=21 <23), so cfg.clangVersion is
// always LLVM clang's version here.
const FIXED_IN_LLVM = "22.1.4";
return cfg.clangVersion !== undefined && satisfiesRange(cfg.clangVersion, `>=${FIXED_IN_LLVM}`);
},
cleanup: `Delete scripts/build/shims/asan-dyld-shim.c, scripts/build/shims.ts, the emitShims() calls in bun.ts, registerShimRules in rules.ts, and this entry.`,
},
{
id: "rustc-no-regular-lto-summary",
issue:
"https://github.com/rust-lang/rust/issues/ (none filed yet — rustc has no equivalent of clang's shouldEmitRegularLTOSummary())",
description:
'Under -Clinker-plugin-lto + lto = "fat", rustc emits the merged bitcode module without a ' +
"per-module summary, so lld reads it as EnableSplitLTOUnit=0 while every clang full-LTO " +
"object (ours and the WebKit -lto prebuilts) hardcodes 1 — the ELF release link aborts " +
'with "inconsistent LTO Unit splitting". rust-lto-fix-cli.ts re-emits the Rust bitcode ' +
"with a regular-LTO summary using rustc's own llvm-tools (rustLtoLinkInputs() in rust.ts).",
applies: cfg => cfg.crossLangLto && !cfg.darwin,
expectedToBeFixed: cfg => {
// Re-evaluate when the pinned rustc moves to its next LLVM major:
// either rustc grew a way to emit regular-LTO summaries (delete the
// fix-up), or linux moved to ThinLTO (it's moot), or neither — bump
// the threshold and keep it.
const RECHECK_AT_RUST_LLVM = "23.0.0";
return cfg.rustLlvmVersion !== undefined && satisfiesRange(cfg.rustLlvmVersion, `>=${RECHECK_AT_RUST_LLVM}`);
},
cleanup:
`Delete scripts/build/rust-lto-fix-cli.ts, the rust_lto_fix rule and rustLtoLinkInputs() in ` +
`rust.ts, unwrap its call sites in bun.ts, drop "llvm-tools" from rust-toolchain.toml's ` +
`components, and delete this entry.`,
},
{
id: "rust-lld-for-crosslang-lto",
issue: "https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html",
description:
"rustc's bundled LLVM is newer than clang's, so clang's ld.lld can't read " +
"-Clinker-plugin-lto bitcode (forward-compatible only). Link with rust-lld instead " +
"(and compress ELF debug sections post-link via llvm-objcopy, since rust-lld lacks zlib).",
applies: cfg => cfg.crossLangLto && cfg.rustLlvmVersion !== undefined && cfg.clangVersion !== undefined,
expectedToBeFixed: cfg => {
// Obsolete once clang's LLVM major catches up to (or passes) rustc's —
// at that point clang's own ld.lld reads rustc's bitcode and the
// rust-lld swap in resolveConfig() never fires.
const clangMajor = Number(cfg.clangVersion!.split(".")[0]);
const rustMajor = Number(cfg.rustLlvmVersion!.split(".")[0]);
return clangMajor >= rustMajor;
},
cleanup:
`Delete the rust-lld swap block in resolveConfig() (config.ts), findRustLld() and its call ` +
`in resolveLlvmToolchain() (tools.ts), the rustLld/rustLlvmVersion fields on Toolchain/Config, ` +
`and this entry.`,
},
{
id: "darwin-cross-cpu-model",
issue: "https://github.com/llvm/llvm-project/tree/main/compiler-rt/lib/builtins/cpu_model",
description:
"macOS x64 cross-links from Linux have no libclang_rt.osx.a, and the SDK's libSystem " +
"reexport (libcompiler_rt.tbd) doesn't provide the __builtin_cpu_supports globals " +
"(___cpu_model / ___cpu_indicator_init / ___cpu_features2). A vendored copy of " +
"compiler-rt's cpu_model/x86.c is compiled into the cross link instead.",
// Only exercised on x64 darwin cross links (arm64 never references these).
applies: cfg => cfg.darwin && cfg.crossTarget !== undefined && cfg.x64 && cfg.osxSysroot !== undefined,
expectedToBeFixed: cfg => {
// Obsolete if the SDK starts exporting the symbol from the libSystem
// umbrella (then the shim would be a duplicate definition waiting to
// happen). Checked against the .tbd text — cheap and version-agnostic.
const tbd = join(cfg.osxSysroot!, "usr", "lib", "system", "libcompiler_rt.tbd");
try {
return readFileSync(tbd, "utf8").includes("___cpu_model");
} catch {
return false;
}
},
cleanup:
`Delete scripts/build/shims/cpu_model/, needsDarwinCpuModelShim() and its blocks in ` +
`scripts/build/shims.ts, and this entry.`,
},
{
id: "darwin-cross-stack-size",
issue:
"https://github.com/llvm/llvm-project/blob/main/lld/MachO/Driver.cpp (OPT_stack_size in unimplemented warnings)",
description:
"ld64.lld parses `-stack_size` but doesn't implement it (\"is not yet implemented. Stay " +
'tuned..."), so darwin cross links keep the 8 MB default main-thread stack instead of the ' +
"18 MB JSC needs. shims/macho-postlink.c patches LC_MAIN.stacksize after the link instead.",
applies: cfg => cfg.darwin && cfg.crossTarget !== undefined,
expectedToBeFixed: cfg => {
// Not implemented as of LLVM 21 (lld/MachO/Driver.cpp keeps
// OPT_stack_size in the "unimplemented, warn and ignore" list).
// Re-test when the toolchain moves to LLVM 23: link a darwin cross
// build and check whether `ld64.lld ... -stack_size 0x1200000` still
// prints "is not yet implemented". If it does, bump this threshold.
// (A configure-time probe that spawned ld64.lld was tried first and
// reverted: the rust/cpp split steps configure on machines whose
// ld64.lld doesn't behave like the link machine's, and a probe that
// misfires there fails the whole lane.)
const FIXED_IN_LLVM = "23.0.0";
return cfg.clangVersion !== undefined && satisfiesRange(cfg.clangVersion, `>=${FIXED_IN_LLVM}`);
},
cleanup:
`Drop the --stack-size argument from machoPostlinkCommand() in scripts/build/shims.ts and ` +
`this entry. Keep macho-postlink.c itself — it still owns the entitlements embedding and ` +
`the post-edit re-sign.`,
},
{
id: "rust-lld-musl-crt-zlib",
issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled",
description:
"rust-lld is built without LLVM_ENABLE_ZLIB. Alpine's musl CRT objects ship with " +
"ELFCOMPRESS_ZLIB debug sections, which rust-lld rejects at input parse time. " +
"Decompress them via objcopy and prepend a -B search path.",
// Only exercised when the rust-lld swap actually fired on a musl link.
applies: cfg => cfg.linux && cfg.abi === "musl" && cfg.rustLld !== undefined && cfg.ld === cfg.rustLld,
expectedToBeFixed: cfg => {
// Obsolete the same instant the rust-lld swap above is — once clang's
// ld.lld (built with zlib) reads rustc's bitcode, we never select
// rust-lld and the compressed CRTs are a non-issue.
const clangMajor = Number(cfg.clangVersion!.split(".")[0]);
const rustMajor = Number(cfg.rustLlvmVersion!.split(".")[0]);
return clangMajor >= rustMajor;
},
cleanup:
`Delete needsMuslCrtDecompress(), MUSL_CRT_OBJECTS, the shim_crt_decompress rule, and the ` +
`musl block in emitShims() (scripts/build/shims.ts), and this entry.`,
},
{
id: "android-posix-spawn-setsid-const",
issue: "https://github.com/rust-lang/libc/pull/5104",
description:
"The libc crate doesn't expose POSIX_SPAWN_SETSID for target_os = android, so the " +
"linux+android cfg arm in spawn_sys hardcodes 0x80 (the value glibc/musl/bionic share).",
// Cleanup is a source-code change, not a build-config change — once
// Cargo.lock's libc has the constant, the local 0x80 can go regardless
// of which target is being built. Gate to android so the threshold-bump
// hint doesn't bother host-only builds.
applies: cfg => cfg.abi === "android",
expectedToBeFixed: cfg => {
// PR #5104 targets `main` with `stable-nominated`; a 0.2.x cherry-pick
// follows. Best guess for the first 0.2.x with it — bump if the
// constant isn't actually there yet.
const FIXED_IN_LIBC = "0.2.187";
const v = lockedCrateVersion(cfg, "libc");
return v !== undefined && satisfiesRange(v, `>=${FIXED_IN_LIBC}`);
},
cleanup:
`In src/spawn_sys/posix_spawn.rs (Attr::set) and src/spawn_sys/spawn_process.rs ` +
`(options.detached block), replace the local 0x80 with libc::POSIX_SPAWN_SETSID, ` +
`drop the explanatory comments, and delete this entry.`,
},
];
/**
* Check every workaround. Throws if any is obsolete on the current config.
* Call from configure.ts after Config is fully resolved.
*/
export function checkWorkarounds(cfg: Config): void {
for (const w of workarounds) {
if (!w.applies(cfg)) continue;
if (!w.expectedToBeFixed(cfg)) continue;
throw new BuildError(`Workaround '${w.id}' is obsolete — upstream fix is available`, {
hint:
`${w.description}\n` +
` Tracked: ${w.issue}\n\n` +
`${w.cleanup}\n\n` +
`If the issue still reproduces, bump the threshold in expectedToBeFixed() in scripts/build/workarounds.ts instead.`,
});
}
}