Files
scriptc/packages/cli/test/native-link-info.test.ts

148 lines
6.1 KiB
TypeScript

import { execFile } from "node:child_process";
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";
import type { NativeLinkInfo } from "@scriptc/compiler";
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 })));
});
function cli(args: string[]) {
return execFileAsync(process.execPath, ["--import", tsxLoader, cliEntry, ...args], {
maxBuffer: 16 * 1024 * 1024,
});
}
async function fixture() {
const dir = await mkdtemp(join(tmpdir(), "scriptc-native-link-info-"));
dirs.push(dir);
const entry = join(dir, "main.ts");
await writeFile(entry, 'console.log("external link");\n');
return { dir, entry, object: join(dir, "app.o") };
}
test("print option validation is explicit", async () => {
const { entry } = await fixture();
await expect(cli(["build", entry, "--print=wat"]))
.rejects.toMatchObject({ stderr: expect.stringContaining("unknown print kind") });
await expect(cli(["build", entry, "--print=native-link-info", "--emit=llvm"]))
.rejects.toMatchObject({ stderr: expect.stringContaining("requires --emit=obj") });
await expect(cli(["run", entry, "--print=native-link-info"]))
.rejects.toMatchObject({ stderr: expect.stringContaining("is a build option") });
});
describe.runIf(supported)("macOS arm64 native link info", () => {
test("prints a stable cache-independent precompiled runtime recipe", async () => {
const { entry, object } = await fixture();
const { stdout, stderr } = await cli([
"build", entry, "--print=native-link-info", "-o", object,
]);
expect(stderr).toBe("");
const info = JSON.parse(stdout) as NativeLinkInfo;
expect(info).toMatchObject({
schema: "scriptc.native-link-info.v1",
format: 1,
object_abi: { stability: "experimental", compatibility: "exact-runtime-version" },
target: {
name: "macos-arm64",
llvm_triple: "arm64-apple-macosx14.0.0",
object_format: "macho",
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
path_base: "runtime_pack.root",
},
link: { system_libraries: ["System"], frameworks: [] },
});
expect(info.runtime_pack.root).not.toContain("node_modules/.cache");
expect(info.link.input_order.join("\n")).not.toContain("node_modules/.cache");
expect(info.runtime_pack.objects.map((object) => object.path)).toEqual(expect.arrayContaining([
"artifacts/release/runtime/default/scr_console.o",
"artifacts/release/runtime/default/scr_async.o",
"artifacts/release/runtime/default/scr_cycle.o",
]));
await expect(readFile(object)).resolves.toBeInstanceOf(Buffer);
});
test("printing link info emits an object without invoking external tools", async () => {
const { dir, entry, object } = 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 { stdout, stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, "build", entry, "--print=native-link-info", "-o", object],
{
env: {
...process.env,
PATH: `${traps}:${process.env["PATH"] ?? ""}`,
SCRIPTC_CACHE_DIR: join(dir, "cache"),
},
maxBuffer: 16 * 1024 * 1024,
},
);
expect(stderr).toBe("");
expect((JSON.parse(stdout) as NativeLinkInfo).program.object).toBe(object);
await expect(readFile(trapLog)).rejects.toMatchObject({ code: "ENOENT" });
});
test("an out-of-tree C-driver build consumes the reported object and runtime pack", async () => {
const { dir, entry, object } = await fixture();
const { stdout } = await cli([
"build", entry, "--print=native-link-info", "-o", object,
]);
const info = JSON.parse(stdout) as NativeLinkInfo;
const executable = join(dir, "app");
await execFileAsync("clang", [
...info.link.driver_flags,
...info.link.input_order,
...info.link.system_libraries.map((name) => `-l${name}`),
"-o", executable,
]);
await expect(execFileAsync(executable, [], { encoding: "utf8" }))
.resolves.toMatchObject({ stdout: "external link\n", stderr: "" });
}, 30_000);
test("a mismatched runtime ABI fails at link time through the versioned marker", async () => {
const { dir, entry, object } = await fixture();
await cli(["build", entry, "--emit=obj", "-o", object]);
const stub = join(dir, "wrong-runtime.c");
await writeFile(stub, [
"void scr_runtime_abi_v1(void) {} /* previous ABI */",
"void scr_console_log(void) {}",
"void scr_init(void) {}",
"void scr_lib_init(void) {}",
"void scr_str_release(void) {}",
"void scr_str_retain_v(void) {}",
"void scr_error_vts(void) {}",
"",
].join("\n"));
const error = await execFileAsync("clang", [
"-target", "arm64-apple-macosx14.0.0", object, stub, "-o", join(dir, "bad"),
]).then(() => null, (failure: { stderr?: string }) => failure);
expect(error?.stderr).toContain("scr_runtime_abi_v6");
});
});