Files
bun-src/test/internal/build-post-link-ordering.test.ts
2026-08-27 21:09:14 +00:00

139 lines
5.7 KiB
TypeScript

/**
* Regression tests for ninja ordering of post-link steps (strip, smoke test,
* dsymutil) in scripts/build/bun.ts.
*
* The smoke_test and dsymutil rule commands are wrapped through
* `cfg.jsRuntime` (= process.execPath). When `bun` on PATH resolves inside the
* build directory, that path is the strip output itself (build/release/bun),
* and without an ordering edge ninja will run strip and the wrapper exec
* concurrently, failing with "Permission denied" on the half-written file.
*
* These exercise the ninja-emission logic only (no compiler or ninja needed),
* so they run on every host.
*/
import { describe, expect, test } from "bun:test";
import { isMacOS, tempDir } from "harness";
import { join, resolve } from "node:path";
import { emitPostLink } from "../../scripts/build/bun.ts";
import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../scripts/build/config.ts";
import { Ninja } from "../../scripts/build/ninja.ts";
/** A fully-populated fake toolchain; resolveConfig never spawns any of these. */
function mockToolchain(overrides: Partial<Toolchain> = {}): Toolchain {
return {
cc: "/fake/llvm/bin/clang",
cxx: "/fake/llvm/bin/clang++",
hostCc: undefined,
hostCxx: undefined,
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,
...overrides,
};
}
/**
* Resolve a host-targeted config: no os/arch override, so `canRunOnHost` is
* true and the smoke_test rule emits the real edge (not the phony short-circuit).
*/
function hostConfig(partial: PartialConfig, buildDir: string): Config {
return resolveConfig(
{ buildDir, ...partial },
// jsRuntime = the strip output: what resolveToolchain() produces when
// `bun` on PATH resolves into build/release/.
mockToolchain({ jsRuntime: join(buildDir, "bun") }),
);
}
/** Find one build-edge line in the generated ninja text (continuations unwrapped). */
function buildEdge(ninja: string, rule: string): string {
const flat = ninja.replace(/ \$\n +/g, " ");
const line = flat.split("\n").find(l => l.startsWith("build ") && l.includes(`: ${rule} `));
if (line === undefined) throw new Error(`no '${rule}' edge in ninja output:\n${ninja}`);
return line;
}
describe("emitPostLink ninja ordering", () => {
test("release smoke_test is ordered after strip", () => {
using dir = tempDir("build-post-link", {});
const buildDir = String(dir);
const cfg = hostConfig({ buildType: "Release" }, buildDir);
expect(cfg.canRunOnHost).toBe(true);
const n = new Ninja({ buildDir });
const exe = resolve(buildDir, `bun-profile${cfg.exeSuffix}`);
const { strippedExe } = emitPostLink(n, cfg, exe, "bun-profile", []);
const out = n.toString();
expect(strippedExe).toBe(resolve(buildDir, `bun${cfg.exeSuffix}`));
// strip writes `bun`; the smoke_test wrapper execs cfg.jsRuntime
// (= `bun` here). Without `|| bun` ninja schedules them concurrently
// and the wrapper sees a half-written file.
expect(buildEdge(out, "smoke_test")).toBe(
`build bun-profile.smoke-test-passed: smoke_test bun-profile${cfg.exeSuffix} || bun${cfg.exeSuffix}`,
);
expect(buildEdge(out, "strip")).toBe(`build bun${cfg.exeSuffix}: strip bun-profile${cfg.exeSuffix}`);
});
test("debug smoke_test has no strip dep (nothing to order against)", () => {
using dir = tempDir("build-post-link", {});
const buildDir = String(dir);
const cfg = hostConfig({ buildType: "Debug", assertions: true }, buildDir);
const n = new Ninja({ buildDir });
const exe = resolve(buildDir, `bun-debug${cfg.exeSuffix}`);
const { strippedExe, dsym } = emitPostLink(n, cfg, exe, "bun-debug", []);
const out = n.toString();
expect({ strippedExe, dsym }).toEqual({ strippedExe: undefined, dsym: undefined });
expect(buildEdge(out, "smoke_test")).toBe(
`build bun-debug.smoke-test-passed: smoke_test bun-debug${cfg.exeSuffix}`,
);
expect(buildEdge(out, "phony")).toBe(`build bun: phony bun-debug${cfg.exeSuffix}`);
});
// Cross-config path only: on macOS, resolveConfig({ os: "darwin" }) probes
// xcode-select for the real SDK, which belongs to the native test above.
// The ordering logic is identical to the smoke_test case.
test.skipIf(isMacOS)("darwin release dsymutil is ordered after strip", () => {
using dir = tempDir("build-post-link", {});
const buildDir = String(dir);
const cfg = resolveConfig({ os: "darwin", arch: "aarch64", buildType: "Release", buildDir }, mockToolchain());
expect(cfg.canRunOnHost).toBe(false);
const n = new Ninja({ buildDir });
const exe = resolve(buildDir, "bun-profile");
const { dsym } = emitPostLink(n, cfg, exe, "bun-profile", []);
const out = n.toString();
expect(dsym).toBe(resolve(buildDir, "bun-profile.dSYM"));
expect(buildEdge(out, "dsymutil")).toBe("build bun-profile.dSYM: dsymutil bun-profile || bun");
// Cross-compile: smoke_test short-circuits to a `check` phony (the
// binary can't run on this host), so the strip race can't happen there.
expect(buildEdge(out, "phony")).toBe("build check: phony bun-profile");
});
});