Skip to content
Open
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
34 changes: 23 additions & 11 deletions scripts/build/cargo-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
* lines from that discovered path rather than hardcoding one.
*
* For the `bun bd` / ninja build this file is purely advisory: `rust.ts`
* passes `CARGO_TARGET_<TRIPLE>_LINKER = cfg.cxx` (plus `CC`/`CXX`/`AR`) and
* `-Clink-arg=-fuse-ld=lld` directly on the cargo invocation's environment,
* which override anything here. The file matters for a contributor running
* passes `CARGO_TARGET_<TRIPLE>_LINKER = cfg.cxx` (plus `CC`/`CXX`/`AR`) and,
* when `rustForcesFuseLdLld(cfg)` holds, `-Clink-arg=-fuse-ld=lld` directly on
* the cargo invocation's environment, which override anything here. (Darwin
* omits that flag unless cross-language LTO is on — macOS uses ld64 by
* default; see rust.ts. The darwin sections below skip the `-fuse-ld=lld`
* rustflag for the same reason.) The file matters for a contributor running
* `cargo build` / `cargo check` directly, and for rust-analyzer.
*
* `writeIfChanged` semantics (precedent: `depVersionsHeader.ts`) so a
Expand Down Expand Up @@ -84,17 +87,26 @@ export function generateCargoConfig(cfg: Config): string {
lines.push("");
lines.push(`[target.${triple}]${triple === host ? " # host" : ""}`);
lines.push(`linker = ${JSON.stringify(linkerFor(triple, cfg))}`);
// -Qunused-arguments: rustc passes link args that don't apply to every
// artifact kind (e.g. `-no-pie` when it links the lol_html_c_api cdylib),
// and its `linker_messages` lint re-surfaces clang's "argument unused
// during compilation" complaint as a warning on every build-rust job.
// These config rustflags reach the cargo invocations that don't set
// -fuse-ld=lld forces clang++ to drive lld instead of its default linker.
// -Qunused-arguments + `-A linker_messages` quiet the `linker_messages`
// lint about link args that don't apply to every artifact kind (e.g.
// `-no-pie` when rustc links the lol_html_c_api cdylib). These config
// rustflags reach the cargo invocations that don't set
// CARGO_ENCODED_RUSTFLAGS themselves (the lolhtml dep edge, plain
// `cargo build`/`cargo check`, rust-analyzer); real linker errors still
// fail the link.
lines.push(
`rustflags = ["-C", "link-arg=-fuse-ld=lld", "-C", "link-arg=-Qunused-arguments", "-A", "linker_messages"]`,
);
//
// darwin omits `-fuse-ld=lld`: macOS uses `ld64` / the system linker, and
// a Homebrew `clang++` without the `lld` driver alias rejects it with
// "invalid linker name in argument '-fuse-ld=lld'", breaking plain
// `cargo check` / rust-analyzer on contributors' macs (#30870). The
// lint-quieting flags stay, matching `rustForcesFuseLdLld()` in rust.ts
// (which also keeps them on darwin while dropping the linker flag).
const rustflags =
tripleOs(triple) === "darwin"
? ["-C", "link-arg=-Qunused-arguments", "-A", "linker_messages"]
: ["-C", "link-arg=-fuse-ld=lld", "-C", "link-arg=-Qunused-arguments", "-A", "linker_messages"];
lines.push(`rustflags = [${rustflags.map(f => JSON.stringify(f)).join(", ")}]`);
}
lines.push("");

Expand Down
58 changes: 44 additions & 14 deletions scripts/build/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ export function rustCanCrossFromLinux(cfg: Config): boolean {
return false;
}

/**
* Whether the ninja build should force `-fuse-ld=lld` into the Rust target
* crates' link (via `CARGO_ENCODED_RUSTFLAGS`). Centralized so `emitRust` and
* the regression test share one source of truth.
*
* - Windows: never — the per-target linker is `link.exe` / `lld-link.exe`,
* which take `/X` args, not the GCC/clang `-fuse-ld=`.
* - darwin: only under cross-language LTO. macOS uses `ld64` / the system
* linker by default (`cfg.ld` is empty — config.ts; the C++ side doesn't
* pass `--ld-path=` either — flags.ts), and a Homebrew `clang++` without
* the `lld` driver alias rejects `-fuse-ld=lld` outright ("invalid linker
* name in argument '-fuse-ld=lld'"), breaking `bun run rust:check` /
* `bun bd` on contributors' macs (#30870). Under `--lto` the flag is
* required so rustc's bitcode link goes through the LTO-aware linker (the
* user must then have an `lld`-capable clang++, same as on linux).
* - linux / freebsd / android: always — the default `cc` driver picks BFD
* `/usr/bin/ld`, which doesn't match the semantics the C/C++ object set
* assumes (and under `-Clinker-plugin-lto` doesn't understand `-plugin-opt`).
*/
export function rustForcesFuseLdLld(cfg: Config): boolean {
if (cfg.windows) return false;
if (cfg.darwin) return cfg.crossLangLto;
return true;
}

/**
* All target triples CI builds. Exposed so `rust:check-all` can iterate
* `cargo check --target <t>` without re-deriving the list.
Expand Down Expand Up @@ -533,15 +558,17 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string
// `cfg.lto`, with the non-LTO build relying on `.cargo/config.toml`'s
// `rustflags`; but `CARGO_ENCODED_RUSTFLAGS` (always set below) *replaces*
// the config-file `rustflags` rather than merging, so the config entry was
// dead for any ninja build. Push it unconditionally so the ninja build's
// behavior doesn't depend on the generated `.cargo/config.toml` at all.
// dead for any ninja build. Push it here so the ninja build's behavior
// doesn't depend on the generated `.cargo/config.toml` at all.
//
// Not on Windows: the per-target linker there is `link.exe` / `lld-link.exe`
// (see `CARGO_TARGET_*_LINKER` below), which take `/X` args, not the GCC/clang
// `-fuse-ld=`. RUSTFLAGS only reach *target* crates when `--target` is given,
// and the `bun_bin` staticlib has no link step, so it's normally dead — but
// if a target cdylib ever appears it'd fail with "could not open '-fuse-ld=lld'".
if (!cfg.windows) rustflags.push(`-Clink-arg=-fuse-ld=lld`);
// `rustForcesFuseLdLld()` owns the per-platform decision: windows never
// (its linker takes `/X` args, not the GCC/clang `-fuse-ld=`); darwin only
// under cross-lang LTO (macOS defaults to ld64 and a Homebrew clang++ may
// reject the flag, #30870); linux/freebsd/android always. RUSTFLAGS only
// reach *target* crates when `--target` is given, and the `bun_bin`
// staticlib has no link step, so it's normally dead — but if a target
// cdylib ever appears it'd fail with "could not open '-fuse-ld=lld'".
if (rustForcesFuseLdLld(cfg)) rustflags.push(`-Clink-arg=-fuse-ld=lld`);
// Keep the clang driver quiet about link args that don't apply to a given
// artifact kind: rustc adds `-no-pie` under `-Crelocation-model=static`,
// which is meaningless when it links a target cdylib (lol_html_c_api), and
Expand Down Expand Up @@ -595,9 +622,11 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string
// regular-LTO summary it bolts onto the merged module — see
// rust-lto-fix-cli.ts.)
//
// (`-Clink-arg=-fuse-ld=lld` is pushed unconditionally above — under LTO
// it doubles as making rustc's bitcode link go through the LTO-aware
// linker our final link uses, not BFD `/usr/bin/ld`.)
// (`-Clink-arg=-fuse-ld=lld` is handled by `rustForcesFuseLdLld()` above —
// true for every non-darwin non-windows target, and for darwin only under
// cross-lang LTO, so it's always set inside this ELF-only block. Under LTO
// it doubles as routing rustc's bitcode link through the LTO-aware linker
// our final link uses, not BFD `/usr/bin/ld`.)
if (!cfg.darwin && !cfg.windows) {
rustflags.push("-Zsplit-lto-unit");

Expand Down Expand Up @@ -642,9 +671,10 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string
// (build scripts, proc-macros) — and on a native build, `--target` is the
// host triple, so this env var sets *their* linker too.
//
// Non-Windows: `cfg.cxx` (clang++) drives lld with the same flag dialect
// the C++ side uses. `-Clink-arg=-fuse-ld=lld` (pushed into rustflags
// below) selects lld for any rustc-driven cdylib link.
// Non-Windows: `cfg.cxx` (clang++) is the driver. Whether it drives lld
// depends on `rustForcesFuseLdLld(cfg)` above — true on linux/freebsd/
// android, and on darwin only under cross-lang LTO; otherwise the driver
// picks its default linker (ld64 on darwin).
//
// Windows: rustc's `*-msvc` linker flavor passes `link.exe`-style args
// directly (`/NOLOGO`, `/OUT:`, `/NATVIS:`, `/PDBALTPATH:`, …). `clang-cl`
Expand Down
78 changes: 76 additions & 2 deletions test/internal/macos-cross-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
*/
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isMacOS, tempDir } from "harness";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

import { generateCargoConfig } from "../../scripts/build/cargo-config.ts";
import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../scripts/build/config.ts";
import { webkit } from "../../scripts/build/deps/webkit.ts";
import { parsePackedFeaturesList } from "../../scripts/build/features-json.ts";
import { computeFlags, DARWIN_STACK_SIZE } from "../../scripts/build/flags.ts";
import { MACOS_SDK_VERSION, macosSdkCachePath, resolveMacosSdkPath } from "../../scripts/build/macos-sdk.ts";
import { rustCanCrossFromLinux, rustTarget } from "../../scripts/build/rust.ts";
import { rustCanCrossFromLinux, rustForcesFuseLdLld, rustTarget } from "../../scripts/build/rust.ts";
import { machoEntitlementsPlist, machoPostlinkCommand } from "../../scripts/build/shims.ts";

/** A fully-populated fake toolchain — resolveConfig never spawns any of these. */
Expand Down Expand Up @@ -334,3 +335,76 @@ describe("macOS SDK resolution", () => {
}
});
});

// Regression tests for https://github.com/oven-sh/bun/issues/30870.
//
// Before the fix, both the generated `.cargo/config.toml` (read by plain
// `cargo check` / rust-analyzer) and `CARGO_ENCODED_RUSTFLAGS` (the ninja
// build) forced `-fuse-ld=lld` on every non-windows target, darwin included.
// A Homebrew `clang++` without the `lld` driver alias rejects that flag
// ("invalid linker name in argument '-fuse-ld=lld'"), so `bun run rust:check`
// and `bun bd` broke on contributor macs. macOS uses ld64 by default — the
// flag is now skipped on darwin unless cross-language LTO is explicitly on.
describe("darwin rust linker: no forced -fuse-ld=lld (#30870)", () => {
/**
* Slice out one `[target.<triple>]` block from the generated TOML: from its
* header up to the next `[` section header or EOF. `.cargo/config.toml` is a
* flat list of sections, so substring scanning suffices — no TOML parse.
*/
function targetSection(toml: string, triple: string): string {
const header = `[target.${triple}]`;
const start = toml.indexOf(header);
if (start === -1) throw new Error(`missing section: ${header}`);
const next = toml.indexOf("\n[", start + header.length);
return next === -1 ? toml.slice(start) : toml.slice(start, next);
}

test("rustForcesFuseLdLld: darwin skips lld unless cross-lang LTO", () => {
// Resolve real configs via the same path the build uses. On a non-darwin
// host these are cross-compile configs; on darwin they're native. Either
// way the `-fuse-ld=lld` decision is platform-derived, so the assertions
// hold on every host.
const darwin = resolveConfig({ os: "darwin", arch: "aarch64", buildType: "Release" }, mockToolchain());
expect(darwin.crossLangLto).toBe(false); // LTO is off by default for darwin
expect(rustForcesFuseLdLld(darwin)).toBe(false);

// Explicit --lto on darwin re-enables it (needs an lld-capable clang++,
// same requirement as linux LTO).
const darwinLto = resolveConfig(
{ os: "darwin", arch: "aarch64", buildType: "Release", lto: true },
mockToolchain(),
);
expect(darwinLto.crossLangLto).toBe(true);
expect(rustForcesFuseLdLld(darwinLto)).toBe(true);

// Linux keeps forcing lld (the default `cc` driver would pick BFD ld).
const linux = resolveConfig({ os: "linux", arch: "x64", buildType: "Release" }, mockToolchain());
expect(rustForcesFuseLdLld(linux)).toBe(true);
});

test("generated .cargo/config.toml: darwin sections are lld-free, linux keeps the flag", () => {
using dir = tempDir("cargo-config-darwin", {});
// generateCargoConfig writes to `cfg.cwd/.cargo/config.toml` — point cwd at
// a scratch dir so the repo's real file is untouched. The file contains a
// section for every triple in `allRustTargets`, so one run covers both
// apple-darwin triples plus the linux regression guard.
const cfg = { ...resolveConfig({ os: "linux", arch: "x64" }, mockToolchain()), cwd: String(dir) } as Config;
const outPath = generateCargoConfig(cfg);
expect(outPath).toBe(join(String(dir), ".cargo", "config.toml"));
const toml = readFileSync(outPath, "utf8");

for (const triple of ["x86_64-apple-darwin", "aarch64-apple-darwin"]) {
const section = targetSection(toml, triple);
// linker is still the discovered clang++ driver — only the flag is gone.
expect(section).toContain("linker = ");
expect(section).not.toContain("-fuse-ld=lld");
// The lint-quieting rustflags stay (matches rust.ts keeping them on darwin).
expect(section).toContain("-Qunused-arguments");
expect(section).toContain("linker_messages");
}

// Linux must still force lld.
const linux = targetSection(toml, "x86_64-unknown-linux-gnu");
expect(linux).toContain("link-arg=-fuse-ld=lld");
});
});
Loading