mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
- 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.
145 lines
6.3 KiB
TypeScript
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,
|
|
);
|