Files
2026-08-27 21:09:14 +00:00

183 lines
7.7 KiB
TypeScript

/**
* The cargo half of the build (scripts/build/rust.ts), configure-time logic
* only: nothing here runs cargo.
*
* - `allRustTargets` is what `bun run rust:check-all` type-checks and what the
* generated .cargo/config.toml gets entries for, so a platform CI builds
* that is missing from it is one whose cfg-gated code nothing checks before
* the CI build itself. Its source of truth is the build matrix in
* .buildkite/ci.mjs, which can't be imported (it generates the pipeline on
* import), so it is read as text.
* - The rustflags put the Rust half of the binary on the CPU baseline the C++
* half is compiled for (`cpuTargetFlags` in scripts/build/flags.ts).
*/
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
resolveConfig,
type Abi,
type Arch,
type Config,
type OS,
type PartialConfig,
type Toolchain,
} from "../../../scripts/build/config.ts";
import {
allRustTargets,
cargoBuildInvocation,
cargoBuildStdArg,
rustTargetIsTier3,
rustTriple,
} from "../../../scripts/build/rust.ts";
const repoRoot = join(import.meta.dir, "..", "..", "..");
/** A fully-populated fake toolchain; resolveConfig() never spawns any of these. */
const mockToolchain: Toolchain = {
cc: "/fake/llvm/bin/clang",
cxx: "/fake/llvm/bin/clang++",
hostCc: "/fake/llvm/bin/clang",
hostCxx: "/fake/llvm/bin/clang++",
clangVersion: "21.1.8",
clangResourceDir: "/fake/llvm/lib/clang/21",
ar: "/fake/llvm/bin/llvm-ar",
ranlib: "/fake/llvm/bin/llvm-ranlib",
ld: "/fake/llvm/bin/ld.lld",
ld64Lld: "/fake/llvm/bin/ld64.lld",
rustLld: undefined,
rustLlvmVersion: "22.1.4",
rustSysroot: undefined,
rustHostTriple: undefined,
strip: "/fake/bin/strip",
llvmStrip: "/fake/llvm/bin/llvm-strip",
dsymutil: "/fake/llvm/bin/dsymutil",
bun: "/fake/bin/bun",
jsRuntime: "/fake/bin/bun",
esbuild: "/fake/bin/esbuild",
ccache: undefined,
cmake: "/fake/bin/cmake",
cargo: undefined,
cargoHome: undefined,
rustupHome: undefined,
msvcLinker: undefined,
rc: undefined,
mt: undefined,
nasm: undefined,
};
/**
* Resolve a release config for a target the way its CI lane does, with fake
* sysroots where cross-compiling needs one (nothing stats them at configure
* time).
*/
function resolve(partial: PartialConfig): Config {
return resolveConfig({ buildType: "Release", ...partial }, mockToolchain);
}
/** Retarget a resolved config to another linux abi; abi=android needs a real NDK to resolve from scratch. */
function withAbi(cfg: Config, abi: Abi): Config {
return { ...cfg, abi };
}
describe("allRustTargets", () => {
test("is exactly the set of triples .buildkite/ci.mjs builds", () => {
const ciScript = readFileSync(join(repoRoot, ".buildkite", "ci.mjs"), "utf8");
const matrix = /^const buildPlatforms = \[\n([\s\S]*?)^\];/m.exec(ciScript);
if (matrix === null) throw new Error("buildPlatforms not found in .buildkite/ci.mjs");
const entries =
matrix[1]!
.split("\n")
.filter(line => !line.trim().startsWith("//"))
.join("\n")
.match(/\{[^}]*\}/g) ?? [];
// CI builds x64 and aarch64 for each of linux, darwin, windows and
// freebsd; fewer entries than that means the extraction above broke.
expect(entries.length).toBeGreaterThanOrEqual(8);
const built = entries.map(entry => {
const field = (name: string) => new RegExp(`\\b${name}: "([^"]+)"`).exec(entry)?.[1];
const os = field("os") as OS;
const arch = field("arch") as Arch;
// ci.mjs passes `--abi=gnu` to the linux builds that don't name an abi.
const abi = os === "linux" ? ((field("abi") as Abi | undefined) ?? "gnu") : undefined;
return rustTriple(os, arch, abi);
});
const listed: string[] = [...allRustTargets].sort();
expect(listed).toEqual([...new Set(built)].sort());
});
test("rust-toolchain.toml preinstalls std for exactly the triples that have a prebuilt one", () => {
const { toolchain } = Bun.TOML.parse(readFileSync(join(repoRoot, "rust-toolchain.toml"), "utf8")) as {
toolchain: { targets: string[] };
};
const prebuilt = allRustTargets.filter(triple => !rustTargetIsTier3(triple));
expect([...toolchain.targets].sort()).toEqual([...prebuilt].sort());
// rust:check-all reaches the remaining triples with -Zbuild-std; that path
// is only exercised if the matrix really contains a Tier 3 triple.
expect(allRustTargets.filter(rustTargetIsTier3)).toEqual(["aarch64-unknown-freebsd"]);
});
test("a Tier 3 target builds std from source with the flag rust:check-all shares", () => {
// Debug: the only reason left to build std is the missing prebuilt.
const freebsdArm64 = cargoBuildInvocation(
resolve({ os: "freebsd", arch: "aarch64", freebsdSysroot: "/fake", buildType: "Debug" }),
);
expect(freebsdArm64.triple).toBe("aarch64-unknown-freebsd");
expect(freebsdArm64.args).toContain(cargoBuildStdArg);
const freebsdX64 = cargoBuildInvocation(
resolve({ os: "freebsd", arch: "x64", freebsdSysroot: "/fake", buildType: "Debug" }),
);
expect(freebsdX64.triple).toBe("x86_64-unknown-freebsd");
expect(freebsdX64.args).not.toContain(cargoBuildStdArg);
});
});
describe("CPU baseline", () => {
/** The rustflags that choose the ISA the build assumes. */
function cpuFlags(cfg: Config): string[] {
const rustflags = cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS!.split("\x1f");
return rustflags.filter(
flag =>
flag.startsWith("-Ctarget-cpu=") || flag.startsWith("-Ctarget-feature=") || flag.startsWith("-Ztune-cpu="),
);
}
test("arm64 linux, freebsd and windows assume armv8-a+crc tuned for Ampere, like the C++ side", () => {
// flags.ts: `-march=armv8-a+crc -mtune=ampere1`. Naming a CPU instead
// (this used to be `-Ctarget-cpu=cortex-a72`) also assumes that CPU's
// aes/sha2/pmuv3, which the C++ side doesn't, and gives every Rust
// function a different feature set from every C++ function, which LLVM's
// inliner treats as incompatible under cross-language LTO.
const expected = ["-Ctarget-cpu=generic", "-Ctarget-feature=+crc", "-Ztune-cpu=ampere1"];
const linuxGnu = resolve({ os: "linux", arch: "aarch64", abi: "gnu", linuxSysroot: "/fake" });
expect(cpuFlags(linuxGnu)).toEqual(expected);
expect(cpuFlags(withAbi(linuxGnu, "musl"))).toEqual(expected);
expect(cpuFlags(resolve({ os: "freebsd", arch: "aarch64", freebsdSysroot: "/fake" }))).toEqual(expected);
// clang-cl spells the same flags `/clang:-march=...`.
expect(cpuFlags(resolve({ os: "windows", arch: "aarch64", winsysroot: "/fake" }))).toEqual(expected);
});
test("arm64 android assumes armv8-a+crc tuned for Cortex-A78, like the C++ side", () => {
const android = withAbi(resolve({ os: "linux", arch: "aarch64", abi: "gnu", linuxSysroot: "/fake" }), "android");
expect(cargoBuildInvocation(android).triple).toBe("aarch64-linux-android");
expect(cpuFlags(android)).toEqual(["-Ctarget-cpu=generic", "-Ctarget-feature=+crc", "-Ztune-cpu=cortex-a78"]);
});
test("darwin arm64 and x64 name the C++ side's CPU model directly", () => {
// `-mcpu=apple-m1` and `-march=nehalem` (x86 -march values are CPU names)
// are LLVM CPU names, which `-Ctarget-cpu` takes as-is.
expect(cpuFlags(resolve({ os: "darwin", arch: "aarch64", mode: "rust-only" }))).toEqual(["-Ctarget-cpu=apple-m1"]);
expect(cpuFlags(resolve({ os: "darwin", arch: "x64", mode: "rust-only" }))).toEqual(["-Ctarget-cpu=nehalem"]);
const linuxX64 = resolve({ os: "linux", arch: "x64", abi: "gnu", linuxSysroot: "/fake" });
expect(cpuFlags(linuxX64)).toEqual(["-Ctarget-cpu=nehalem"]);
expect(cpuFlags(withAbi(linuxX64, "android"))).toEqual(["-Ctarget-cpu=nehalem"]);
});
});