mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-03 17:09:28 +08:00
Build the Linux LLVM helper and runtime pack before running both test lanes, and align native cache contracts with the current target policy. Full managed Sandbox gate passed.
194 lines
7.9 KiB
TypeScript
194 lines
7.9 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
import { release as osRelease, tmpdir } from "node:os";
|
|
import { delimiter, dirname, 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 require = createRequire(import.meta.url);
|
|
const repoRoot = join(import.meta.dirname, "../../..");
|
|
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
|
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
|
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("exact executable repeats skip lowering while edits and damaged outputs stay correct", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-cache-"));
|
|
const cacheRoot = join(dir, "cache");
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
|
|
const tuPath = join(dir, "main.ll");
|
|
try {
|
|
await mkdir(cacheRoot, { mode: 0o700 });
|
|
await writeFile(entry, 'console.log("one");\n');
|
|
const build = async (
|
|
extra: string[] = [],
|
|
env: NodeJS.ProcessEnv = {},
|
|
): Promise<{ stderr: string }> => {
|
|
const { stderr } = await execFileAsync(
|
|
process.execPath,
|
|
["--import", tsxLoader, cliEntry, "build", entry, "-o", outPath, ...extra],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
SCRIPTC_CACHE_DIR: cacheRoot,
|
|
SCRIPTC_TIMING: "1",
|
|
...env,
|
|
},
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
},
|
|
);
|
|
return { stderr };
|
|
};
|
|
const run = async (): Promise<string> =>
|
|
(await execFileAsync(outPath, [], { maxBuffer: 1024 * 1024 })).stdout;
|
|
|
|
expect((await build()).stderr).toContain("scriptc lowering");
|
|
expect(await run()).toBe("one\n");
|
|
const originalBinaryTime = (await stat(outPath)).mtimeMs;
|
|
const originalTuTime = (await stat(tuPath)).mtimeMs;
|
|
|
|
// Every route reuses the frontend. Only routes with a complete native
|
|
// dependency proof can also retain the executable inode; other targets
|
|
// deliberately relink their cached translation unit.
|
|
expect((await build()).stderr).not.toContain("scriptc lowering");
|
|
if (persistentExecutable) expect((await stat(outPath)).mtimeMs).toBe(originalBinaryTime);
|
|
expect(await run()).toBe("one\n");
|
|
expect((await stat(tuPath)).mtimeMs).toBe(originalTuTime);
|
|
|
|
// Output-kind cleanup is honest even on an exact early hit: an IR file
|
|
// left by a prior additive --emit-ir invocation is not part of a later
|
|
// executable-only result.
|
|
const irPath = join(dir, "main.ir.json");
|
|
await writeFile(irPath, "stale IR\n");
|
|
expect((await build()).stderr).not.toContain("scriptc lowering");
|
|
await expect(stat(irPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
|
|
// Caller damage does not poison the cache: the verified cached binary is
|
|
// atomically restored without rebuilding the frontend.
|
|
await writeFile(outPath, "damaged\n");
|
|
await chmod(outPath, 0o777 & ~process.umask());
|
|
expect((await build()).stderr).not.toContain("scriptc lowering");
|
|
expect(await run()).toBe("one\n");
|
|
|
|
// Source bytes are frontend inputs. A semantic edit misses, emits a new
|
|
// TU/binary, and the next exact repeat returns early again.
|
|
await writeFile(entry, 'console.log("two");\n');
|
|
expect((await build()).stderr).toContain("scriptc lowering");
|
|
expect(await run()).toBe("two\n");
|
|
expect((await build()).stderr).not.toContain("scriptc lowering");
|
|
|
|
// The explicit backend is a cache-key input; it cannot consume auto's
|
|
// LLVM result even when source and output paths are identical.
|
|
expect((await build(["--backend", "c"], {
|
|
SCRIPTC_LINKER: join(dir, "first-unused-linker"),
|
|
})).stderr).toContain("scriptc lowering");
|
|
expect(await readFile(join(dir, "main.c"), "utf8")).toContain("Generated by scriptc");
|
|
if (runtimePackHost) {
|
|
// C builds are keyed by their compiler environment. Changing the
|
|
// object-only linker must not turn an exact C repeat into a miss.
|
|
expect((await build(["--backend", "c"], {
|
|
SCRIPTC_LINKER: join(dir, "second-unused-linker"),
|
|
})).stderr).not.toContain("scriptc lowering");
|
|
}
|
|
|
|
// Native optimization posture is independently keyed too: dev cannot
|
|
// consume a release executable or TU, and then exact dev repeats hit.
|
|
expect((await build(["--optimization", "dev"])).stderr).toContain("scriptc lowering");
|
|
expect(await run()).toBe("two\n");
|
|
expect((await build(["--optimization", "dev"])).stderr).not.toContain("scriptc lowering");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}, 120_000);
|
|
|
|
test("--from-c forwards the dev optimization posture", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-from-c-optimization-"));
|
|
const entry = join(dir, "main.c");
|
|
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
|
|
try {
|
|
await writeFile(entry, [
|
|
"int main(void) {",
|
|
"#ifdef __OPTIMIZE__",
|
|
" return 19;",
|
|
"#else",
|
|
" return 0;",
|
|
"#endif",
|
|
"}",
|
|
"",
|
|
].join("\n"));
|
|
const result = await execFileAsync(
|
|
process.execPath,
|
|
[
|
|
"--import",
|
|
tsxLoader,
|
|
cliEntry,
|
|
"build",
|
|
entry,
|
|
"--from-c",
|
|
"--optimization",
|
|
"dev",
|
|
"-o",
|
|
outPath,
|
|
],
|
|
{
|
|
env: { ...process.env, SCRIPTC_CC: "clang" },
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
},
|
|
);
|
|
expect(result.stderr).not.toContain("deprecated legacy C executable path");
|
|
await expect(execFileAsync(outPath)).resolves.toMatchObject({ stdout: "" });
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}, 120_000);
|
|
|
|
test.skipIf(process.platform === "win32")(
|
|
"an executable appearing earlier on unchanged PATH invalidates the early binary",
|
|
async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-path-"));
|
|
// Keep the mutable tool directory outside the frontend/native output tree:
|
|
// this pins driver resolution itself rather than incidental directory
|
|
// dependency tracking under outDir.
|
|
const toolDir = await mkdtemp(join(tmpdir(), "scriptc-cli-tool-path-"));
|
|
const cacheRoot = join(dir, "cache");
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, "program");
|
|
const path = `${toolDir}${delimiter}${process.env["PATH"] ?? ""}`;
|
|
const build = (): Promise<void> =>
|
|
execFileAsync(
|
|
process.execPath,
|
|
["--import", tsxLoader, cliEntry, "build", entry, "-o", outPath],
|
|
{
|
|
env: { ...process.env, PATH: path, SCRIPTC_CACHE_DIR: cacheRoot },
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
},
|
|
).then(() => undefined);
|
|
try {
|
|
await mkdir(cacheRoot, { mode: 0o700 });
|
|
await writeFile(entry, 'console.log("cached");\n');
|
|
await build();
|
|
|
|
const clang = join(toolDir, "clang");
|
|
await writeFile(clang, "#!/bin/sh\nprintf 'new clang selected\\n' >&2\nexit 97\n");
|
|
await chmod(clang, 0o755);
|
|
|
|
await expect(build()).rejects.toMatchObject({
|
|
stderr: expect.stringContaining("new clang selected"),
|
|
});
|
|
} finally {
|
|
await Promise.all([
|
|
rm(dir, { recursive: true, force: true }),
|
|
rm(toolDir, { recursive: true, force: true }),
|
|
]);
|
|
}
|
|
},
|
|
120_000,
|
|
);
|