Files
scriptc/packages/cli/test/native-install.test.ts
T
Chris Tate ee9f0d64eb feat: ship the self-hosted native scriptc CLI (#585)
* feat: ship the self-hosted native scriptc CLI

- Run the shared compiler and CLI natively, including library builds and compile-time evaluation.
- Install native platform packages and ship relocatable standalone distributions.
- Preserve program semantics and reuse validated build artifacts for fast rebuilds.

* fix: complete native CLI integration and validation

- Preserve library diagnostics and run recursive compiler work on the main stack.
- Restore packaged helper permissions and exercise native npm and Wasm library builds.
- Recover interrupted Sandbox status probes within the original command deadline.
2026-09-30 02:17:56 -05:00

132 lines
7.6 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { copyFileSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { afterEach, expect, test } from "vitest";
import { installNativeCli, nativeCliPackage } from "../scripts/install-native.mjs";
import { prepareNativeCommand } from "../scripts/prepare-native.mjs";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
function fixture() {
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-"));
roots.push(root);
const directory = join(root, "installation");
const packageName = "@scriptc/cli-darwin-arm64";
const platform = join(directory, "node_modules", packageName);
const bin = join(platform, "dist/bin");
mkdirSync(bin, { recursive: true });
writeFileSync(join(directory, "package.json"), JSON.stringify({ name: "scriptc", version: "1.2.3" }));
writeFileSync(join(platform, "package.json"), JSON.stringify({ name: packageName, version: "1.2.3" }));
const binary = join(bin, process.platform === "win32" ? "scriptc.exe" : "scriptc");
writeFileSync(binary, "native compiler payload");
const toolchain = {
schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "macos-arm64",
ts7: "../lib/typescript/lib/tsc", llvm_package: "../lib/llvm", runtime_pack: "../lib/runtime",
runtime_packs: [{ target: "macos-arm64", path: "../lib/runtime" }],
linker: "clang", linker_args: [], dsymutil: "dsymutil", comptime: "../lib/comptime",
wasi_node_runner: "../lib/wasi/cli/wasi-runner.js",
};
writeFileSync(binary + ".json", JSON.stringify(toolchain));
const helpers = [toolchain.ts7, toolchain.comptime, join(toolchain.llvm_package, "bin/scriptc-llvm-codegen")]
.map((path) => resolve(bin, path));
for (const path of helpers) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, "native helper payload", { mode: 0o644 });
}
return { root, directory, packageName, platform, binary, toolchain, helpers };
}
test("selects only supported native host packages, including Linux libc", () => {
expect(nativeCliPackage("darwin", "arm64", null)).toBe("@scriptc/cli-darwin-arm64");
expect(nativeCliPackage("win32", "x64", null)).toBe("@scriptc/cli-win32-x64-msvc");
expect(nativeCliPackage("linux", "x64", "glibc")).toBe("@scriptc/cli-linux-x64-gnu");
expect(nativeCliPackage("linux", "arm64", "musl")).toBe("@scriptc/cli-linux-arm64-musl");
expect(() => nativeCliPackage("linux", "x64", null)).toThrow("identify");
expect(() => nativeCliPackage("win32", "arm64", null)).toThrow("no native command");
});
test("installs a direct executable and relocatable references to platform assets", () => {
const f = fixture();
const command = installNativeCli(f.directory, f.packageName);
expect(readFileSync(command, "utf8")).toBe("native compiler payload");
if (process.platform !== "win32") expect(statSync(command).mode & 0o111).toBe(0o111);
if (process.platform !== "win32") {
for (const helper of f.helpers) expect(statSync(helper).mode & 0o111).toBe(0o111);
}
const moved = join(f.root, "relocated");
renameSync(f.directory, moved);
const installed = join(moved, "bin/scriptc.exe");
const manifest = JSON.parse(readFileSync(installed + ".json", "utf8"));
const platform = join(moved, "node_modules", f.packageName, "dist/lib");
expect(resolve(dirname(installed), manifest.ts7)).toBe(join(platform, "typescript/lib/tsc"));
expect(resolve(dirname(installed), manifest.wasi_node_runner)).toBe(join(platform, "wasi/cli/wasi-runner.js"));
expect(resolve(dirname(installed), manifest.runtime_packs[0].path)).toBe(join(platform, "runtime"));
expect(manifest.linker).toBe("clang");
expect(JSON.stringify(manifest)).not.toContain(f.directory);
});
test("missing and mismatched packages fail without replacing an installed command", () => {
const f = fixture();
const command = installNativeCli(f.directory, f.packageName);
writeFileSync(join(f.platform, "package.json"), JSON.stringify({ name: f.packageName, version: "1.2.4" }));
expect(() => installNativeCli(f.directory, f.packageName)).toThrow("found 1.2.4");
expect(() => installNativeCli(f.directory, "@scriptc/cli-missing")).toThrow("optional dependencies");
expect(readFileSync(command, "utf8")).toBe("native compiler payload");
});
test("npm links the installed native executable without an interpreter", () => {
const f = fixture();
// Keep the package payload small while exercising npm's native shim.
// Production command coverage builds the real compiler in the bootstrap gate.
const windows = process.platform === "win32";
if (windows) copyFileSync(process.execPath, f.binary);
else {
const source = join(f.root, "payload.c");
writeFileSync(source, '#include <stdio.h>\nint main(void) { puts("native compiler"); return 0; }\n');
execFileSync("clang", [source, "-o", f.binary]);
}
for (const helper of f.helpers) writeFileSync(helper, "#!/bin/sh\nprintf 'native helper\\n'\n");
const packageName = "scriptc-native-install-test";
const scripts = join(f.directory, "scripts");
mkdirSync(scripts);
copyFileSync(join(import.meta.dirname, "../scripts/install-native.mjs"), join(scripts, "install-native.mjs"));
writeFileSync(join(scripts, "fixture-install.mjs"),
`import { installNativeCli } from './install-native.mjs'; installNativeCli(process.cwd(), ${JSON.stringify(f.packageName)});\n`);
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const npmEnv = { ...process.env, NODE_PATH: "", npm_config_cache: join(f.root, "npm-cache"), npm_config_update_notifier: "false" };
const pack = (directory: string): string => {
const result = execFileSync(npm, ["pack", "--json", "--ignore-scripts"], {
cwd: directory, env: npmEnv, encoding: "utf8", shell: process.platform === "win32",
});
return join(directory, (JSON.parse(result) as { filename: string }[])[0]!.filename);
};
const platformTarball = pack(f.platform);
writeFileSync(join(f.directory, "package.json"), JSON.stringify({
name: packageName, version: "1.2.3", type: "module",
bin: { [packageName]: "bin/scriptc.exe" }, files: ["bin", "scripts"],
scripts: { postinstall: "node scripts/fixture-install.mjs" },
optionalDependencies: { [f.packageName]: "file:" + platformTarball },
}));
prepareNativeCommand(f.directory);
const tarball = pack(f.directory);
const installed = join(f.root, "npm-install");
mkdirSync(installed);
execFileSync(npm, ["install", "--offline", "--no-audit", "--no-fund", tarball], {
cwd: installed, env: npmEnv, shell: process.platform === "win32", stdio: "pipe",
});
const command = join(installed, "node_modules/.bin", packageName + (process.platform === "win32" ? ".cmd" : ""));
const result = execFileSync(command, windows ? ["--version"] : ["native compiler"], {
env: { ...process.env, PATH: "" }, encoding: "utf8", shell: process.platform === "win32",
});
expect(result.trim()).toBe(windows ? process.version : "native compiler");
if (process.platform !== "win32") {
const bin = join(installed, "node_modules", packageName, "bin");
const manifest = JSON.parse(readFileSync(join(bin, "scriptc.exe.json"), "utf8"));
for (const helper of [manifest.ts7, manifest.comptime, join(manifest.llvm_package, "bin/scriptc-llvm-codegen")]) {
expect(execFileSync(resolve(bin, helper), [], { env: { ...process.env, PATH: "" }, encoding: "utf8" }).trim()).toBe("native helper");
}
}
}, 60_000);