Files
scriptc/packages/cli/test/native-link-info.test.ts
T
Chris Tate 30cf205e90 Run untyped pipeline function stages and later iterable stages
A pipeline stage that arrives as an untyped value now follows Node's
stage ladder for more shapes. As the first stage, a function is a source
function: the pipeline calls it with { signal } while it is set up and
pumps its result, which must be iterable (ERR_INVALID_RETURN_VALUE
otherwise), and a { readable, writable } pair pumps its readable side.

After the first stage, the pipeline call is lowered once per combination
of its first three untyped stages being functions or not, and a typeof
test picks the branch at run time. A function stage is called with the
previous stage and { signal }; before the last stage its result must be
async iterable, and as the last stage a promise resolves the pipeline's
value and an async iterable feeds the PassThrough pipeline() returns
(SCR_PIPE_DEST_DYN). Any other value that is not a stream becomes
Duplex.from of it, which the next stage reads from without the previous
stage being piped into it, and Duplex.from's ERR_INVALID_ARG_TYPE for
the values it rejects.

An untyped stage followed by an untyped function stage hands its value to
that stage through a cell created per pipeline call, so the function
receives the previous stage's own iterator, as Node's makeAsyncIterable
answers it, instead of an async generator over it.

Untyped web streams, pairs after the first stage, and function stages
that return a stream throw the not-supported error.

RUNTIME_ABI_VERSION goes from 45 to 46 for the new stream exports.
2026-10-25 18:10:32 -05:00

171 lines
6.3 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: 46, marker: "scr_runtime_abi_v46" },
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_v46");
});
});