mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-04 01:18:49 +08:00
* feat(compiler): add native LLVM object emission - ship the pinned macOS arm64 LLVM helper and platform package - add assembly/object CLI outputs, caching, diagnostics, and runtime ABI checks - validate helper artifacts through CI, packaging, docs, and differential coverage * fix(compiler): harden native LLVM emission * fix(compiler): harden LLVM helper packaging * fix(compiler): stabilize LLVM helper package size * fix(compiler): harden native helper validation * fix(compiler): harden LLVM helper verification * fix(compiler): harden native object emission
143 lines
6.0 KiB
TypeScript
143 lines
6.0 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { deserializeModule, validateModule } from "@scriptc/compiler";
|
|
import { afterEach, expect, test } from "vitest";
|
|
|
|
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 dirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
async function fixture(extension = ".ts") {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-source-output-"));
|
|
dirs.push(dir);
|
|
const entry = join(dir, `hello${extension}`);
|
|
await writeFile(entry, 'console.log("hello");\n');
|
|
return { dir, entry };
|
|
}
|
|
|
|
function cli(args: string[], env: NodeJS.ProcessEnv = process.env) {
|
|
return execFileAsync(process.execPath, ["--import", tsxLoader, cliEntry, ...args], {
|
|
env,
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
}
|
|
|
|
test("default and explicit paths identify each source primary artifact", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const expected = { ir: "hello.ir.json", c: "hello.c", llvm: "hello.ll" } as const;
|
|
for (const kind of ["ir", "c", "llvm"] as const) {
|
|
const result = await cli(["build", entry, `--emit=${kind}`]);
|
|
const path = join(dir, ".scriptc", expected[kind]);
|
|
expect(result.stdout).toBe(`${path}\n`);
|
|
expect(await readdir(join(dir, ".scriptc"))).toEqual([expected[kind]]);
|
|
}
|
|
const exact = join(dir, "artifact.with-custom-suffix");
|
|
const result = await cli(["build", entry, "--emit=llvm", "-o", exact]);
|
|
expect(result.stdout).toBe(`${exact}\n`);
|
|
expect(await readFile(exact, "utf8")).toContain("define i32 @main");
|
|
});
|
|
|
|
test("IR CLI output round-trips through the public parser and validator", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const path = join(dir, "hello.json");
|
|
await cli(["build", entry, "--emit=ir", "-o", path]);
|
|
expect(validateModule(deserializeModule(await readFile(path, "utf8")))).toEqual([]);
|
|
});
|
|
|
|
test("module-flavored TypeScript extensions retain the plain entry stem", async () => {
|
|
for (const [extension, kind, name] of [
|
|
[".mts", "ir", "hello.ir.json"],
|
|
[".cts", "llvm", "hello.ll"],
|
|
] as const) {
|
|
const { dir, entry } = await fixture(extension);
|
|
const result = await cli(["build", entry, `--emit=${kind}`]);
|
|
const path = join(dir, ".scriptc", name);
|
|
expect(result.stdout).toBe(`${path}\n`);
|
|
expect(await readdir(join(dir, ".scriptc"))).toEqual([name]);
|
|
}
|
|
});
|
|
|
|
test("an explicit default-shaped output path preserves caller-owned siblings", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const siblings = ["hello", "hello.exe", "hello.wasm", "hello.c", "hello.ll", "hello.s", "hello.o"];
|
|
await Promise.all(siblings.map((name) => writeFile(join(dir, name), `caller-owned ${name}\n`)));
|
|
const path = join(dir, "hello.ir.json");
|
|
await cli(["build", entry, "--emit=ir", "-o", path]);
|
|
for (const name of siblings) {
|
|
await expect(readFile(join(dir, name), "utf8")).resolves.toBe(`caller-owned ${name}\n`);
|
|
}
|
|
});
|
|
|
|
test("source outputs never execute compiler, archiver, or linker traps", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const traps = join(dir, "traps");
|
|
const trapLog = join(dir, "trap.log");
|
|
await mkdir(traps);
|
|
for (const tool of ["clang", "cc", "gcc", "zig", "ar", "ld", "link", "link.exe", "xcrun"]) {
|
|
const path = join(traps, tool);
|
|
await writeFile(path, `#!/bin/sh\nprintf '${tool}\\n' >> '${trapLog}'\nprintf '${tool} trap executed\\n' >&2\nexit 97\n`);
|
|
await chmod(path, 0o755);
|
|
}
|
|
const env = {
|
|
...process.env,
|
|
PATH: `${traps}${delimiter}${process.env["PATH"] ?? ""}`,
|
|
SCRIPTC_CC: "zigcc",
|
|
SCRIPTC_TARGET: "aarch64-apple-ios",
|
|
SCRIPTC_CACHE_DIR: join(dir, "cache-must-not-exist"),
|
|
};
|
|
for (const kind of ["ir", "c", "llvm"] as const) {
|
|
const path = join(dir, `hello.${kind}`);
|
|
await expect(cli(["build", entry, `--emit=${kind}`, "-o", path], env)).resolves.toMatchObject({
|
|
stderr: "",
|
|
stdout: `${path}\n`,
|
|
});
|
|
}
|
|
await expect(readdir(env.SCRIPTC_CACHE_DIR)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readFile(trapLog, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
test("source outputs reject unsupported target spellings", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const out = join(dir, "main.ll");
|
|
await expect(cli(["build", entry, "--emit=llvm", "-o", out], {
|
|
...process.env,
|
|
SCRIPTC_CC: "trap-compiler",
|
|
SCRIPTC_TARGET: "wasm64-wasi",
|
|
})).rejects.toMatchObject({
|
|
code: 1,
|
|
stderr: expect.stringContaining("unsupported WASI target 'wasm64-wasi'"),
|
|
});
|
|
await expect(readFile(out)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
test("--emit-ir remains additive but reports its deprecation", async () => {
|
|
const { dir, entry } = await fixture();
|
|
const executable = join(dir, process.platform === "win32" ? "hello.exe" : "hello-bin");
|
|
const result = await cli(["build", entry, "--emit-ir", "-o", executable]);
|
|
expect(result.stderr).toContain("--emit-ir is deprecated");
|
|
expect(await readFile(join(dir, "hello.ir.json"), "utf8")).toContain('"irVersion"');
|
|
});
|
|
|
|
test("run and incompatible compatibility flags reject non-executable outputs", async () => {
|
|
const { entry } = await fixture();
|
|
await expect(cli(["run", entry, "--emit=c"])).rejects.toMatchObject({
|
|
code: 1,
|
|
stderr: expect.stringContaining("scriptc run requires --emit=exe"),
|
|
});
|
|
await expect(cli(["build", entry, "--emit=llvm", "--emit-ir"])).rejects.toMatchObject({
|
|
code: 1,
|
|
stderr: expect.stringContaining("--emit-ir cannot be combined"),
|
|
});
|
|
});
|