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 "); } 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 retain the legacy C executable warning", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-legacy-warning-")); 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("legacy warning");\n'); const first = await build(); expect(first.stderr).toContain("deprecated legacy C executable path"); expect(first.stderr).toContain("scriptc lowering"); const cached = await build(); expect(cached.stderr).toContain("deprecated legacy C executable path"); expect(cached.stderr).not.toContain("scriptc lowering"); } finally { await rm(dir, { recursive: true, force: true }); } }, 120_000, );