mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-17 05:09:24 +08:00
* fix(windows): support GUI executables - Add Windows subsystem selection across executable link paths and cache keys. - Verify PE subsystem output and add Windows CI coverage. Fixes #259 Co-authored-by: Mateus Mamede Lage <23098414+mmamedel@users.noreply.github.com> * fix(ci): restore cross-platform test parity - Make the Windows subsystem test use a portable loader URL. - Sync TS7 order-parity baselines with current corpus fixtures. --------- Co-authored-by: Mateus Mamede Lage <23098414+mmamedel@users.noreply.github.com>
210 lines
8.2 KiB
TypeScript
210 lines
8.2 KiB
TypeScript
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, expect, test } from "vitest";
|
|
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
|
|
|
const dirs: string[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
async function fixture(source = 'console.log("source output");\n') {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-source-output-"));
|
|
dirs.push(dir);
|
|
const entry = join(dir, "main.ts");
|
|
const outDir = join(dir, ".scriptc");
|
|
await writeFile(entry, source);
|
|
return { dir, entry, outDir };
|
|
}
|
|
|
|
test("serialized IR is the primary artifact and round-trips through validation", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
const outPath = join(outDir, "custom.ir-data");
|
|
const result = await compile(entry, { outDir, outPath, outputKind: "ir" });
|
|
if (!result.ok) throw new Error("IR emission failed");
|
|
expect(result.artifact).toEqual({ kind: "ir", path: outPath });
|
|
const module = deserializeModule(await readFile(outPath, "utf8"));
|
|
expect(validateModule(module)).toEqual([]);
|
|
});
|
|
|
|
test("Windows subsystem selection refuses non-executable compiler output", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
const outPath = join(outDir, "main.ir.json");
|
|
const result = await compile(entry, { outDir, outPath, outputKind: "ir", windowsSubsystem: "gui" });
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
diagnostics: [{ code: "SC3002", message: expect.stringContaining("only supported for executable output") }],
|
|
});
|
|
await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
test("C and LLVM are exact primary artifacts and never create an executable", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
const cPath = join(outDir, "exact.output");
|
|
const c = await compile(entry, { outDir, outPath: cPath, outputKind: "c" });
|
|
if (!c.ok) throw new Error("C emission failed");
|
|
expect(c.artifact).toEqual({ kind: "c", path: cPath });
|
|
expect(await readFile(cPath, "utf8")).toContain("Generated by scriptc");
|
|
|
|
const llvmPath = join(outDir, "exact.llvm-output");
|
|
const llvm = await compile(entry, { outDir, outPath: llvmPath, outputKind: "llvm" });
|
|
if (!llvm.ok) throw new Error("LLVM emission failed");
|
|
expect(llvm.artifact).toEqual({ kind: "llvm", path: llvmPath });
|
|
expect(await readFile(llvmPath, "utf8")).toContain("define i32 @main");
|
|
expect((await readdir(outDir)).sort()).toEqual(["exact.llvm-output", "exact.output"]);
|
|
});
|
|
|
|
test("switching default source output kinds removes stale generated siblings", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
await mkdir(outDir, { recursive: true });
|
|
await writeFile(join(outDir, process.platform === "win32" ? "main.exe" : "main"), "stale executable");
|
|
for (const [kind, name] of [
|
|
["c", "main.c"],
|
|
["llvm", "main.ll"],
|
|
["ir", "main.ir.json"],
|
|
] as const) {
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, name),
|
|
outputKind: kind,
|
|
defaultOutputPath: true,
|
|
});
|
|
if (!result.ok) throw new Error(`${kind} emission failed`);
|
|
expect(await readdir(outDir)).toEqual([name]);
|
|
}
|
|
});
|
|
|
|
test("an explicit source path never deletes same-stem sibling files", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
await mkdir(outDir, { recursive: true });
|
|
const siblings = [
|
|
join(outDir, "main"),
|
|
join(outDir, "main.exe"),
|
|
join(outDir, "main.wasm"),
|
|
join(outDir, "main.c"),
|
|
join(outDir, "main.ll"),
|
|
join(outDir, "main.s"),
|
|
join(outDir, "main.o"),
|
|
];
|
|
await Promise.all(siblings.map((path) => writeFile(path, `caller-owned ${path}\n`)));
|
|
const outPath = join(outDir, "main.ir.json");
|
|
const result = await compile(entry, { outDir, outPath, outputKind: "ir" });
|
|
expect(result.ok).toBe(true);
|
|
for (const path of siblings) {
|
|
await expect(readFile(path, "utf8")).resolves.toBe(`caller-owned ${path}\n`);
|
|
}
|
|
});
|
|
|
|
test("an executable build never deletes same-stem assembly or object artifacts", async () => {
|
|
const { entry, outDir } = await fixture();
|
|
await mkdir(outDir, { recursive: true });
|
|
const siblings = [join(outDir, "main.s"), join(outDir, "main.o")];
|
|
await Promise.all(siblings.map((path) => writeFile(path, `caller-owned ${path}\n`)));
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, "custom-executable"),
|
|
backend: "c",
|
|
});
|
|
if (!result.ok) throw new Error("executable build failed");
|
|
for (const path of siblings) {
|
|
await expect(readFile(path, "utf8")).resolves.toBe(`caller-owned ${path}\n`);
|
|
}
|
|
});
|
|
|
|
test.each([undefined, "c"] as const)(
|
|
"native program-object validation rejects backend %s as a diagnostic",
|
|
async (backend) => {
|
|
const { entry, outDir } = await fixture();
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, "program"),
|
|
...(backend === undefined ? {} : { backend }),
|
|
nativeProgramObject: true,
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
diagnostics: [{ code: "SC3002", message: expect.stringContaining("backend explicitly set to llvm") }],
|
|
});
|
|
await expect(readdir(outDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
test("explicit LLVM output refuses unsupported programs with SC3001", async () => {
|
|
const dir = join(import.meta.dirname, "../../../tests/fixtures/server/cases/tls-connect-basic");
|
|
const entry = join(dir, "main.ts");
|
|
const outDir = await mkdtemp(join(tmpdir(), "scriptc-source-refusal-"));
|
|
dirs.push(outDir);
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, "main.ll"),
|
|
outputKind: "llvm",
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.diagnostics.map(({ code, message }) => ({ code, message }))).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"code": "SC3001",
|
|
"message": "the LLVM backend does not support this construct yet (libCall:tls.connectCb); build with the debugging C backend (--backend c, or the native default's transparent fallback)",
|
|
},
|
|
]
|
|
`);
|
|
expect(await readdir(outDir)).toEqual([]);
|
|
});
|
|
|
|
test("source outputs ignore invalid external compiler selection and create no executable cache", async () => {
|
|
const { dir, entry, outDir } = await fixture();
|
|
const oldCc = process.env["SCRIPTC_CC"];
|
|
const oldCache = process.env["SCRIPTC_CACHE_DIR"];
|
|
const cache = join(dir, "must-not-exist");
|
|
process.env["SCRIPTC_CC"] = "trap-compiler";
|
|
process.env["SCRIPTC_CACHE_DIR"] = cache;
|
|
try {
|
|
for (const [kind, name] of [
|
|
["ir", "main.ir.json"],
|
|
["c", "main.c"],
|
|
["llvm", "main.ll"],
|
|
] as const) {
|
|
const result = await compile(entry, {
|
|
outDir,
|
|
outPath: join(outDir, name),
|
|
outputKind: kind,
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
}
|
|
await expect(readdir(cache)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
if (oldCc === undefined) delete process.env["SCRIPTC_CC"];
|
|
else process.env["SCRIPTC_CC"] = oldCc;
|
|
if (oldCache === undefined) delete process.env["SCRIPTC_CACHE_DIR"];
|
|
else process.env["SCRIPTC_CACHE_DIR"] = oldCache;
|
|
}
|
|
});
|
|
|
|
test.each(["wasm64-wasi", "totally-invalid"])(
|
|
"source output rejects unsupported target %s without writing an artifact",
|
|
async (target) => {
|
|
const { entry, outDir } = await fixture();
|
|
const outPath = join(outDir, "main.ll");
|
|
const oldTarget = process.env["SCRIPTC_TARGET"];
|
|
const oldCc = process.env["SCRIPTC_CC"];
|
|
process.env["SCRIPTC_TARGET"] = target;
|
|
process.env["SCRIPTC_CC"] = "trap-compiler";
|
|
try {
|
|
const result = await compile(entry, { outDir, outPath, outputKind: "llvm" });
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.diagnostics).toEqual([
|
|
expect.objectContaining({ code: "SC3002", message: expect.stringContaining(target) }),
|
|
]);
|
|
await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
if (oldTarget === undefined) delete process.env["SCRIPTC_TARGET"];
|
|
else process.env["SCRIPTC_TARGET"] = oldTarget;
|
|
if (oldCc === undefined) delete process.env["SCRIPTC_CC"];
|
|
else process.env["SCRIPTC_CC"] = oldCc;
|
|
}
|
|
},
|
|
);
|