Files
scriptc/packages/cli/test/native-output.test.ts
Chris Tate 8227a5ad9c Fix Sandbox command transport and pinned native setup (#295)
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.
2026-09-12 15:59:01 -05:00

113 lines
4.5 KiB
TypeScript

import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { release as osRelease, tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, 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 supported = process.platform === "darwin" && process.arch === "arm64" &&
Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) >= 24;
const dirs: string[] = [];
afterEach(async () => {
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
async function fixture() {
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-native-output-"));
dirs.push(dir);
const entry = join(dir, "hello.ts");
await writeFile(entry, 'console.log("native output");\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("unsupported targets fail with SC3002 before creating an artifact", async () => {
const { dir, entry } = await fixture();
const output = join(dir, "hello.o");
await expect(cli(["build", entry, "--emit=obj", "-o", output], {
...process.env,
SCRIPTC_TARGET: "scriptc-unsupported-target",
})).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("SC3002"),
});
await expect(readFile(output)).rejects.toMatchObject({ code: "ENOENT" });
});
describe.runIf(supported)("macOS arm64 native outputs", () => {
test("default names and exact paths identify assembly and object artifacts", async () => {
const { dir, entry } = await fixture();
for (const [kind, name] of [["asm", "hello.s"], ["obj", "hello.o"]] as const) {
const result = await cli(["build", entry, `--emit=${kind}`]);
expect(result.stdout).toBe(`${join(dir, ".scriptc", name)}\n`);
}
const exact = join(dir, "exact.custom");
await cli(["build", entry, "--emit=obj", "-o", exact]);
expect((await readFile(exact)).subarray(0, 4)).toEqual(Buffer.from([0xcf, 0xfa, 0xed, 0xfe]));
});
test("asm and obj do not execute compiler, archiver, or linker traps", async () => {
const { dir, entry } = await fixture();
const traps = join(dir, "traps");
await execFileAsync("mkdir", [traps]);
const trapLog = join(dir, "trap.log");
for (const tool of ["clang", "cc", "gcc", "zig", "ar", "ld", "xcrun"]) {
const path = join(traps, tool);
await writeFile(path, `#!/bin/sh\nprintf '${tool}\\n' >> '${trapLog}'\nexit 97\n`);
await chmod(path, 0o755);
}
const env = {
...process.env,
PATH: `${traps}:${process.env["PATH"] ?? ""}`,
SCRIPTC_CACHE_DIR: join(dir, "cache"),
};
for (const kind of ["asm", "obj"] as const) {
await expect(cli(["build", entry, `--emit=${kind}`, "-o", join(dir, `hello.${kind}`)], env))
.resolves.toMatchObject({ stderr: "" });
}
await expect(readFile(trapLog)).rejects.toMatchObject({ code: "ENOENT" });
});
test("identical object inputs have deterministic hashes", async () => {
const { dir, entry } = await fixture();
const first = join(dir, "first.o");
const second = join(dir, "second.o");
await cli(["build", entry, "--emit=obj", "-o", first], {
...process.env,
SCRIPTC_NO_CACHE: "1",
});
await cli(["build", entry, "--emit=obj", "-o", second], {
...process.env,
SCRIPTC_NO_CACHE: "1",
});
const digest = (path: string) => readFile(path).then((bytes) =>
createHash("sha256").update(bytes).digest("hex"));
expect(await digest(first)).toBe(await digest(second));
});
test("sanitize refuses by name without publishing an object", async () => {
const { dir, entry } = await fixture();
const output = join(dir, "san.o");
await expect(cli(["build", entry, "--emit=obj", "--sanitize", "-o", output]))
.rejects.toMatchObject({
code: 1,
stderr: expect.stringMatching(/SC3002[\s\S]*AddressSanitizer/),
});
await expect(readFile(output)).rejects.toMatchObject({ code: "ENOENT" });
});
});