mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Each call of a typed or untyped write, writev, transform, flush, final, construct or destroy option callback allocated a closure and a box holding the stream for its `callback` argument. The emitted thunks now take it from scr_stream_done_closure, which keeps the last one on the stream state and hands it out again while the stream holds its only reference and it has no own properties. A callback the user kept (stored, or passed to setImmediate or a promise) is not reused: the next call gets a new closure, and the kept one keeps its own stream reference. The cached closure holds the stream, so the stream drops it when it emits 'close' and traces it, which lets the cycle collector reclaim a stream dropped before it closes. RUNTIME_ABI_VERSION goes from 46 to 47 for scr_stream_done_closure.
171 lines
6.3 KiB
TypeScript
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: 47, marker: "scr_runtime_abi_v47" },
|
|
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_v47");
|
|
});
|
|
});
|