Files
scriptc/scripts/verify-native-cli.mjs
T
Chris Tate d1a0bea84e Have the native CLI packages depend on their LLVM helper and runtime pack
Each @scriptc/cli-<platform> package bundled its platform's LLVM helper
and runtime pack, which are also published as @scriptc/llvm-<platform> and
@scriptc/runtime-<platform>. The CLI packages now depend on those packages
at the same version (workspace:*, which pnpm pack writes as the exact
version) and leave them out: cli-linux-x64-gnu goes from 115 MB to 60 MB
packed.

- build-native-cli.mts --platform-packages, which each CLI package's
  build:native passes, writes a manifest without llvm_package, runtime_pack
  and runtime_packs.
- loadNativeToolchain resolves a missing helper or host runtime pack as
  Node resolves the package from the compiler's directory: nested in the
  platform package, beside it (hoisted, or pnpm's virtual store) or higher
  up. It requires the compiler's version, and an absent or mismatched
  package fails with an error that names it.
- installNativeCli records where the package manager put both packages in
  the installed manifest, with the same checks.
- verify-native-cli.mjs requires the exact dependencies of a platform
  package and checks the resolved packages' versions.
- package-native-cli.mjs bundles the helper and runtime pack into the
  standalone archives, which stay self-contained.
- smoke-native-install.mjs installs the helper and runtime pack tarballs
  with the platform package; the bootstrap's exported package drops its
  bundled copies first.
- release.yml publishes the CLI packages only once every helper and
  runtime pack package resolves on npm, and scriptc only once the CLI
  packages resolve, each wait at most 25 minutes. A rerun skips the
  uploads npm already accepted and waits again.
2026-10-26 07:48:11 -05:00

138 lines
6.7 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
import { verifyLinuxAbi } from "./verify-linux-abi.mjs";
const root = realpathSync(resolve(process.argv[2] ?? "."));
const identity = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const target = identity.name.replace("@scriptc/cli-", "");
const windows = target.startsWith("win32-");
const hostPath = (value) => (windows ? value.replaceAll("\\", "/") : value);
const absolutePath = (value) => isAbsolute(value) || (windows && win32.parse(value).root !== "");
const distribution = join(root, "dist");
const binary = join(distribution, "bin", target.startsWith("win32-") ? "scriptc.exe" : "scriptc");
const toolchain = JSON.parse(readFileSync(binary + ".json", "utf8"));
if (
toolchain.schema !== "scriptc.native-toolchain.v1" ||
toolchain.compiler_version !== identity.version
) {
throw new Error("native CLI manifest version does not match the package");
}
if (statSync(binary).size < 1024) throw new Error("native compiler payload is missing");
if (!target.startsWith("win32-") && !(statSync(binary).mode & 0o111))
throw new Error("native compiler is not executable");
const within = (path) => {
const child = relative(distribution, realpathSync(path));
if (child === ".." || child.startsWith(".." + sep) || isAbsolute(child))
throw new Error(`distribution references external asset: ${path}`);
};
// An npm platform package depends on its LLVM helper and runtime pack
// packages at its own version instead of bundling them.
const platformPackages = toolchain.llvm_package === undefined;
const dependencies = identity.dependencies ?? {};
const dependency = (prefix) => {
const name = Object.keys(dependencies).find((key) => key.startsWith(prefix));
if (name === undefined || ![identity.version, "workspace:*"].includes(dependencies[name]))
throw new Error(
`${identity.name} must depend on its ${prefix}* package at ${identity.version}`,
);
return dirname(createRequire(join(root, "package.json")).resolve(`${name}/package.json`));
};
if (platformPackages && (toolchain.runtime_pack !== undefined || toolchain.runtime_packs))
throw new Error("a platform package's runtime pack is its dependency");
const helperRoot = platformPackages
? dependency("@scriptc/llvm-")
: resolve(dirname(binary), hostPath(toolchain.llvm_package));
const runtimeRoot = platformPackages
? dependency("@scriptc/runtime-")
: resolve(dirname(binary), hostPath(toolchain.runtime_pack));
for (const name of [
"ts7",
"tsc_ts",
...(platformPackages ? [] : ["llvm_package", "runtime_pack"]),
"runtime_sources",
"declarations",
"comptime",
"wasi_node_runner",
]) {
const value = toolchain[name];
if (typeof value !== "string" || absolutePath(value))
throw new Error(`native toolchain needs a relative ${name}`);
within(resolve(dirname(binary), hostPath(value)));
}
// Optional: a compiler this host could not run while building ships without it.
if (toolchain.lib_diagnostics !== undefined) {
const value = toolchain.lib_diagnostics;
if (typeof value !== "string" || absolutePath(value))
throw new Error("native toolchain needs a relative lib_diagnostics");
within(resolve(dirname(binary), hostPath(value)));
}
const pack = JSON.parse(readFileSync(join(runtimeRoot, "runtime-pack.json"), "utf8"));
if (pack.version !== identity.version || pack.target.name !== toolchain.target)
throw new Error("native compiler runtime identity mismatch");
if (JSON.parse(readFileSync(join(helperRoot, "package.json"), "utf8")).version !== identity.version)
throw new Error("native compiler LLVM helper version mismatch");
const seen = new Set();
for (const runtime of toolchain.runtime_packs ?? []) {
if (seen.has(runtime.target)) throw new Error(`duplicate runtime target: ${runtime.target}`);
seen.add(runtime.target);
if (typeof runtime.path !== "string" || absolutePath(runtime.path))
throw new Error("native runtime pack paths must be relative");
const directory = resolve(dirname(binary), hostPath(runtime.path));
within(directory);
const manifest = JSON.parse(readFileSync(join(directory, "runtime-pack.json"), "utf8"));
const packageIdentity = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
if (
manifest.schema !== "scriptc.runtime-pack.v1" ||
manifest.target.name !== runtime.target ||
manifest.version !== identity.version ||
packageIdentity.version !== identity.version ||
packageIdentity.name !== manifest.package
)
throw new Error(`native runtime pack identity mismatch: ${runtime.target}`);
}
for (const name of ["LICENSE", "THIRD_PARTY_NOTICES"]) {
if (!existsSync(join(distribution, name))) throw new Error(`native compiler is missing ${name}`);
}
for (const directory of [distribution, join(distribution, "lib/runtime-sources/vendor")]) {
if (readdirSync(directory).some((name) => ["node_modules", ".cache", "seed"].includes(name)))
throw new Error(`development files present in ${directory}`);
}
if (process.argv.includes("--run")) {
if (target.startsWith("linux-")) {
for (const executable of [
binary,
resolve(dirname(binary), toolchain.ts7),
resolve(dirname(binary), toolchain.comptime),
]) {
verifyLinuxAbi(executable, { staticOnly: target.endsWith("-musl") });
}
verifyLinuxAbi(join(helperRoot, "bin/scriptc-llvm-codegen"), { staticOnly: true });
}
const version = execFileSync(binary, ["--version"], {
encoding: "utf8",
env: { ...process.env, PATH: "" },
}).trim();
if (version !== identity.version)
throw new Error(`native command reported ${version}, expected ${identity.version}`);
// tsc-ts, the default checker, runs on the distribution's own platform.
const tscTs = resolve(dirname(binary), hostPath(toolchain.tsc_ts));
if (windows && !tscTs.endsWith("tsc-ts.exe"))
throw new Error(`tsc_ts is not tsc-ts.exe: ${tscTs}`);
const tscTsVersion = execFileSync(tscTs, ["--version"], {
encoding: "utf8",
env: { ...process.env, PATH: "" },
}).trim();
if (!tscTsVersion.startsWith("Version "))
throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`);
// The release strips tsc-ts and the comptime evaluator; arm64 macOS runs
// only executables whose linker-generated code signature matches their
// contents.
if (process.platform === "darwin" && process.arch === "arm64") {
for (const executable of [binary, tscTs, resolve(dirname(binary), toolchain.comptime)])
execFileSync("codesign", ["--verify", "--strict", executable], { stdio: "inherit" });
}
}
console.log(`${identity.name}@${identity.version}: native distribution verified`);