Files
scriptc/packages/cli/test/bootstrap.test.ts
T
Chris Tate 3f66c8e20b Make LLVM the sole production backend
- Remove C emission, debugging options, and automatic backend fallback.
- Complete LLVM lowering and native bootstrap coverage across supported targets.
- Link executables and libraries with precompiled C runtime packs, including iOS and Android.
2026-09-29 20:54:09 -05:00

145 lines
6.3 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { release as osRelease, tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js";
import { platformLinkerSupportsPersistentCache } from "../../compiler/src/backend/linker.js";
const execFileAsync = promisify(execFile);
const repoRoot = join(import.meta.dirname, "../../..");
const bootstrap = join(repoRoot, "packages/cli/dist/bootstrap.js");
const runtimePackHost = process.platform === "darwin" && process.arch === "arm64" &&
Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) >= 24;
const helperTarget = precompiledRuntimePackTarget();
const persistentExecutable = helperTarget === null || platformLinkerSupportsPersistentCache(process.env, helperTarget);
test("bootstrap serves version and help without loading the compiler graph", async () => {
const preloadDir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-preload-"));
const preload = join(preloadDir, "preload.mjs");
try {
await writeFile(preload, [
"import { registerHooks } from 'node:module';",
"registerHooks({ load(url, context, nextLoad) {",
" if (url.includes('/packages/compiler/dist/index.js')) throw new Error('compiler graph loaded');",
" return nextLoad(url, context);",
"}});",
"",
].join("\n"));
const version = await execFileAsync(process.execPath, ["--import", preload, bootstrap, "--version"]);
expect(version.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
const help = await execFileAsync(process.execPath, ["--import", preload, bootstrap, "--help"]);
expect(help.stdout).toContain("scriptc build <file.ts|.js>");
} finally {
await rm(preloadDir, { recursive: true, force: true }).catch(() => undefined);
}
});
test("bootstrap exact builds use the routed cache and source edits fall through", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-cache-"));
const cacheRoot = join(dir, "cache");
const entry = join(dir, "main.ts");
const outDir = join(dir, ".scriptc");
const outPath = join(outDir, process.platform === "win32" ? "main.exe" : "main");
const preload = join(dir, "reject-full-compiler.mjs");
const env = { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1" };
const build = (rejectFullCompiler = false): Promise<{ stderr: string }> =>
execFileAsync(process.execPath, [
...(rejectFullCompiler ? ["--import", preload] : []),
bootstrap,
"build",
entry,
], {
env,
maxBuffer: 4 * 1024 * 1024,
});
try {
await mkdir(cacheRoot, { mode: 0o700 });
await Promise.all([
writeFile(entry, 'console.log("one");\n'),
writeFile(preload, [
"import { registerHooks } from 'node:module';",
"registerHooks({ load(url, context, nextLoad) {",
" if (url.includes('/packages/compiler/dist/index.js')) throw new Error('compiler graph loaded');",
" return nextLoad(url, context);",
"}});",
"",
].join("\n")),
]);
expect((await build()).stderr).toContain("scriptc lowering");
expect((await build()).stderr).not.toContain("scriptc lowering");
expect((await execFileAsync(outPath)).stdout).toBe("one\n");
// A routed cache hit preserves outputs from earlier invocations.
const savedIr = join(outDir, "main.ir.json");
const savedC = join(outDir, "main.c");
await writeFile(savedIr, "saved IR\n");
await writeFile(savedC, "saved C\n");
await expect(build()).resolves.toBeDefined();
expect(await readFile(savedIr, "utf8")).toBe("saved IR\n");
expect(await readFile(savedC, "utf8")).toBe("saved C\n");
// Route metadata can be evicted independently of the executable payload.
// One full-compiler fallback must repair it so the following invocation is
// once again able to run with the package root import forbidden.
await Promise.all([
rm(join(cacheRoot, "early-exe-route"), { recursive: true, force: true }),
rm(join(cacheRoot, "early-exe-implementation"), { recursive: true, force: true }),
]);
expect((await build()).stderr).not.toContain("scriptc lowering");
if (persistentExecutable) {
await expect(build(true)).resolves.toMatchObject({ stderr: "" });
} else {
// This target intentionally lacks a persistent native dependency proof.
// It must enter the full compiler to relink, even on a frontend hit.
await expect(build(true)).rejects.toMatchObject({ stderr: expect.stringContaining("compiler graph loaded") });
expect((await build()).stderr).not.toContain("scriptc lowering");
expect((await execFileAsync(outPath)).stdout).toBe("one\n");
}
await writeFile(entry, 'console.log("two");\n');
expect((await build()).stderr).toContain("scriptc lowering");
expect((await build()).stderr).not.toContain("scriptc lowering");
expect((await execFileAsync(outPath)).stdout).toBe("two\n");
expect(await readFile(join(outDir, "main.ll"), "utf8")).toContain("two");
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 120_000);
test.skipIf(!runtimePackHost)(
"bootstrap cache hits reuse LLVM executables",
async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-llvm-cache-"));
const cacheRoot = join(dir, "cache");
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const env = {
...process.env,
SCRIPTC_CACHE_DIR: cacheRoot,
SCRIPTC_CC: "clang",
SCRIPTC_TIMING: "1",
};
delete env.SCRIPTC_NO_CACHE;
const build = () => execFileAsync(
process.execPath,
[bootstrap, "build", entry, "-o", outPath],
{ env, maxBuffer: 4 * 1024 * 1024 },
);
try {
await writeFile(entry, 'console.log("LLVM cache");\n');
const first = await build();
expect((await execFileAsync(outPath)).stdout).toBe("LLVM cache\n");
expect(first.stderr).toContain("scriptc lowering");
const cached = await build();
expect((await execFileAsync(outPath)).stdout).toBe("LLVM cache\n");
expect(cached.stderr).not.toContain("scriptc lowering");
} finally {
await rm(dir, { recursive: true, force: true });
}
},
120_000,
);