260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
/**
|
|
* The two bindgen steps in scripts/build/codegen.ts have to declare every file
|
|
* their script writes, the Generated*.h headers included, not only the .cpp
|
|
* that gets compiled.
|
|
*
|
|
* Compiles only order-depend on codegen; the compiler's depfile is what rebuilds
|
|
* a TU when a generated header changes, and within one build ninja re-checks a
|
|
* depfile entry after the step that writes it only if the entry is a declared
|
|
* output of that step. A header no edge declares is stat'd once at startup, so
|
|
* a TU including one (BunObject.cpp includes GeneratedBunObject.h; the generated
|
|
* GeneratedSSLConfig.cpp includes the header-only union types next to it) was
|
|
* recompiled by the build AFTER the one that regenerated the header, and the
|
|
* first build linked the stale object against the fresh generated code.
|
|
*
|
|
* Each step is checked twice: what its emitter declares, and what its script
|
|
* writes when run for real into a scratch dir. The bindgenv2 cases use a probe
|
|
* .bindv2.ts with one header-only type (a union) and one type with a header and
|
|
* a .cpp (an enumeration); bindgen.ts finds the repo's .bind.ts files on its own,
|
|
* so its step is checked against those.
|
|
*/
|
|
import { describe, expect, test } from "bun:test";
|
|
import { bunExe, bunRun, tempDir } from "harness";
|
|
import { readdirSync, writeFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { emitBindgen, emitBindgenV2, registerCodegenRules, type CodegenOutputs } from "../../scripts/build/codegen.ts";
|
|
import { registerDirStamps } from "../../scripts/build/compile.ts";
|
|
import { resolveConfig, type Config, type Toolchain } from "../../scripts/build/config.ts";
|
|
import { Ninja } from "../../scripts/build/ninja.ts";
|
|
import type { Sources } from "../../scripts/glob-sources.ts";
|
|
|
|
/** A fully-populated fake toolchain; `bun` is the one entry that gets spawned (bindgenv2 list-outputs). */
|
|
function mockToolchain(): 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: bunExe(),
|
|
jsRuntime: bunExe(),
|
|
esbuild: "/fake/bin/esbuild",
|
|
ccache: undefined,
|
|
cmake: "/fake/bin/cmake",
|
|
cargo: undefined,
|
|
cargoHome: undefined,
|
|
rustupHome: undefined,
|
|
msvcLinker: undefined,
|
|
rc: undefined,
|
|
mt: undefined,
|
|
nasm: undefined,
|
|
};
|
|
}
|
|
|
|
interface Configured {
|
|
cfg: Config;
|
|
n: Ninja;
|
|
o: CodegenOutputs;
|
|
dirStamp: string;
|
|
}
|
|
|
|
/**
|
|
* A linux-x64 debug target in `buildDir` (resolves on every host once told where
|
|
* its sysroot is; the path is only recorded), with the rules the codegen
|
|
* emitters reference registered and the empty output groups emitCodegen hands
|
|
* them. Nothing is written to `buildDir`.
|
|
*/
|
|
function configure(buildDir: string): Configured {
|
|
const cfg = resolveConfig(
|
|
{ os: "linux", arch: "x64", abi: "gnu", buildType: "Debug", buildDir, linuxSysroot: buildDir },
|
|
mockToolchain(),
|
|
);
|
|
const n = new Ninja({ buildDir });
|
|
registerDirStamps(n, cfg);
|
|
registerCodegenRules(n, cfg);
|
|
const o: CodegenOutputs = {
|
|
all: [],
|
|
rustInputs: [],
|
|
rustOrderOnly: [],
|
|
cppSources: [],
|
|
cppHeaders: [],
|
|
cppAll: [],
|
|
bindgenV2Cpp: [],
|
|
internalModulesAsm: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.S"),
|
|
internalModulesBin: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.bin"),
|
|
rootInstall: resolve(buildDir, "stamps", "install.stamp"),
|
|
};
|
|
return { cfg, n, o, dirStamp: resolve(cfg.codegenDir, ".dir") };
|
|
}
|
|
|
|
/** Just the lists the emitter under test reads; it never looks at the rest of `Sources`. */
|
|
function sourceLists(lists: Partial<Sources>): Sources {
|
|
return lists as Sources;
|
|
}
|
|
|
|
/** The outputs of the `build` line in `n` that produces `output`, spelled as in build.ninja, sorted. */
|
|
function edgeOutputs(n: Ninja, output: string): string[] {
|
|
const lines = n
|
|
.toString()
|
|
.replace(/ \$\n +/g, " ")
|
|
.split("\n");
|
|
for (const line of lines) {
|
|
if (!line.startsWith("build ")) continue;
|
|
const outputs = line
|
|
.slice("build ".length, line.search(/(?<!\$): /))
|
|
.split(/(?<!\$) /)
|
|
.filter(token => token !== "|");
|
|
if (outputs.includes(n.rel(output))) return outputs.sort();
|
|
}
|
|
throw new Error(`no build edge produces ${output}`);
|
|
}
|
|
|
|
/** `names` in the codegen dir, spelled as in build.ninja, sorted. */
|
|
function inCodegenDir({ cfg, n }: Configured, names: string[]): string[] {
|
|
return names.map(name => n.rel(resolve(cfg.codegenDir, name))).sort();
|
|
}
|
|
|
|
/** `paths` normalized (bindgenv2 reports its outputs as `<codegen dir>/<name>`), sorted. */
|
|
function normalized(paths: string[]): string[] {
|
|
return paths.map(p => resolve(p)).sort();
|
|
}
|
|
|
|
/**
|
|
* Every .bind.ts under src/: the pattern glob-sources.ts expands for
|
|
* emitBindgen (through node:fs globSync, which is too slow under a debug
|
|
* build to run its whole pattern set here).
|
|
*/
|
|
function repoBindFiles(cfg: Config): string[] {
|
|
return Array.from(new Bun.Glob("src/**/*.bind.ts").scanSync({ cwd: cfg.cwd }), p => resolve(cfg.cwd, p)).sort();
|
|
}
|
|
|
|
/** Writes the probe .bindv2.ts into `dir` and returns its path. */
|
|
function writeProbe(dir: string, cfg: Config): string {
|
|
// The "bindgenv2" specifier the real files import is a path mapping in
|
|
// src/tsconfig.json, which a file outside src/ does not get.
|
|
const lib = resolve(cfg.cwd, "src", "codegen", "bindgenv2", "lib.ts");
|
|
const probe = resolve(dir, "probe.bindv2.ts");
|
|
writeFileSync(
|
|
probe,
|
|
`import * as b from ${JSON.stringify(lib)};\n` +
|
|
`export const ProbeUnion = b.union("ProbeUnion", { string: b.String, buffer: b.ArrayBuffer });\n` +
|
|
`export const ProbeEnum = b.enumeration("ProbeEnum", ["one", "two"]);\n`,
|
|
);
|
|
return probe;
|
|
}
|
|
|
|
const probeFiles = ["GeneratedProbeEnum.cpp", "GeneratedProbeEnum.h", "GeneratedProbeUnion.h"];
|
|
|
|
// The tests below that run a generator script run it under the build being
|
|
// tested; a debug build takes a few seconds to load one, more than the 5s
|
|
// default allows for.
|
|
const generatorTimeout = 60_000;
|
|
|
|
describe("emitBindgenV2", () => {
|
|
test(
|
|
"declares the header of every type and the .cpp of the types that have one, on one edge",
|
|
() => {
|
|
using dir = tempDir("build-codegen-bindgenv2", {});
|
|
const c = configure(String(dir));
|
|
const { cfg, n, o, dirStamp } = c;
|
|
const probe = writeProbe(String(dir), cfg);
|
|
|
|
emitBindgenV2({ n, cfg, sources: sourceLists({ bindgenV2: [probe], bindgenV2Internal: [] }), o, dirStamp });
|
|
|
|
const cpp = resolve(cfg.codegenDir, "GeneratedProbeEnum.cpp");
|
|
const headers = [
|
|
resolve(cfg.codegenDir, "GeneratedProbeEnum.h"),
|
|
resolve(cfg.codegenDir, "GeneratedProbeUnion.h"),
|
|
];
|
|
expect(edgeOutputs(n, cpp)).toEqual(inCodegenDir(c, probeFiles));
|
|
// Headers go in the group the PCH and every cxx edge order-depend on; the
|
|
// .cpp is compiled like the other bindgenv2 sources.
|
|
expect(normalized(o.cppHeaders)).toEqual(headers);
|
|
expect(normalized(o.bindgenV2Cpp)).toEqual([cpp]);
|
|
expect(normalized(o.all)).toEqual([cpp, ...headers]);
|
|
},
|
|
generatorTimeout,
|
|
);
|
|
|
|
test.concurrent(
|
|
"list-outputs names exactly the files generate writes",
|
|
async () => {
|
|
using dir = tempDir("build-codegen-bindgenv2-generate", {});
|
|
const { cfg } = configure(String(dir));
|
|
const probe = writeProbe(String(dir), cfg);
|
|
const script = resolve(cfg.cwd, "src", "codegen", "bindgenv2", "script.ts");
|
|
const args = [`--sources=${probe}`, `--codegen-path=${cfg.codegenDir}`];
|
|
|
|
const [listed, generated] = await Promise.all([
|
|
bunRun(["run", script, "--command=list-outputs", ...args]),
|
|
bunRun(["run", script, "--command=generate", ...args]),
|
|
]);
|
|
expect(listed).toSpawn();
|
|
expect(generated).toSpawn("");
|
|
|
|
const expected = probeFiles.map(name => resolve(cfg.codegenDir, name));
|
|
expect(normalized(listed.stdout.split(";"))).toEqual(expected);
|
|
expect(readdirSync(cfg.codegenDir).sort()).toEqual(probeFiles);
|
|
},
|
|
generatorTimeout,
|
|
);
|
|
});
|
|
|
|
describe("emitBindgen", () => {
|
|
test("declares Generated<Stem>.h for every .bind.ts on the GeneratedBindings.cpp edge", () => {
|
|
using dir = tempDir("build-codegen-bindgen", {});
|
|
const c = configure(String(dir));
|
|
const { cfg, n, o, dirStamp } = c;
|
|
const bindgen = [
|
|
resolve(cfg.cwd, "src", "runtime", "node", "node_os.bind.ts"),
|
|
resolve(cfg.cwd, "src", "runtime", "api", "BunObject.bind.ts"),
|
|
];
|
|
|
|
emitBindgen({ n, cfg, sources: sourceLists({ bindgen }), o, dirStamp });
|
|
|
|
const cpp = resolve(cfg.codegenDir, "GeneratedBindings.cpp");
|
|
const headers = [resolve(cfg.codegenDir, "GeneratedBunObject.h"), resolve(cfg.codegenDir, "GeneratedNodeOs.h")];
|
|
expect(edgeOutputs(n, cpp)).toEqual(
|
|
inCodegenDir(c, ["GeneratedBindings.cpp", "GeneratedBunObject.h", "GeneratedNodeOs.h"]),
|
|
);
|
|
expect(normalized(o.cppHeaders)).toEqual(headers);
|
|
expect(o.cppSources).toEqual([cpp]);
|
|
expect(normalized(o.all)).toEqual([cpp, ...headers]);
|
|
});
|
|
|
|
test.concurrent(
|
|
"declares exactly the files bindgen.ts writes for the repo's .bind.ts files",
|
|
async () => {
|
|
using dir = tempDir("build-codegen-bindgen-generate", {});
|
|
const c = configure(String(dir));
|
|
const { cfg, n, o, dirStamp } = c;
|
|
const bindgen = repoBindFiles(cfg);
|
|
expect(bindgen).not.toBeEmpty();
|
|
|
|
emitBindgen({ n, cfg, sources: sourceLists({ bindgen }), o, dirStamp });
|
|
|
|
const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts");
|
|
expect(await bunRun(["run", script, "--debug=ON", `--codegen-root=${cfg.codegenDir}`])).toSpawn("");
|
|
|
|
// GeneratedBindings.cpp plus one header per .bind.ts, and nothing else.
|
|
const written = readdirSync(cfg.codegenDir).sort();
|
|
expect(written).toHaveLength(bindgen.length + 1);
|
|
expect(edgeOutputs(n, resolve(cfg.codegenDir, "GeneratedBindings.cpp"))).toEqual(inCodegenDir(c, written));
|
|
},
|
|
generatorTimeout,
|
|
);
|
|
});
|