Files
scriptc/scripts/verify-llvm-package.mjs
Chris Tate c41f1a8be9 Expand LLVM native output targets (#280)
* Expand LLVM native output targets

- Add owned helper and runtime-pack contracts for macOS x64, Linux glibc/musl, Windows x64, and WASI.
- Generalize LLVM target selection, package validation, linker plans, and release/CI matrices.
- Document linker boundaries and add focused native-output and runtime-pack coverage.

* Pin Windows LLVM helper CI to VS 2022

The official LLVM archive needs the Visual Studio 2022 CMake generator, which is not guaranteed by windows-latest.

* Align bootstrap cache with LLVM runtime-pack builds

Use the same target-specific helper and linker identity before and after compiler loading so routed executable cache hits remain lightweight.

* Install Windows LLVM CI tools with Chocolatey

windows-2022 includes Visual Studio but not winget; use its available Chocolatey bootstrap for CMake and Ninja.

* Prevent Zig version probes from leaving runtime-pack objects

Run runtime-pack compiler version probes in a private temporary directory and remove the accidental tracked WASI a.o file.

* Use 7-Zip for Windows LLVM setup

Extract the official LLVM development archive with two-stage 7-Zip instead of Windows tar.exe, which timed out while materializing the toolchain tree.

* Define ssize_t for the MSVC runtime pack

Clang's MSVC target does not expose POSIX ssize_t through sys/types.h; define the pointer-sized runtime type without affecting MinGW.

* Build the Windows runtime pack with Zig

Use the MinGW-compatible Windows sysroot required by the runtime while retaining COFF helper output and installing Zig in the Windows native CI lane.

* Give macOS LLVM differential shard time to finish

Shard 1 passed its setup and focused contracts but was cancelled during its cold LLVM differential slice at the 20-minute job limit.
2026-09-01 13:41:34 -05:00

104 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { accessSync, constants, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { validateLlvmHelperExports } from "./llvm-package-symbols.mjs";
const tarball = process.argv[2];
if (tarball === undefined) throw new Error("usage: verify-llvm-package.mjs <tarball>");
// Homebrew's arm64 LLVM 22 bottles are not byte-identical across supported
// macOS runner images: clean builds have produced stripped helpers ranging
// from roughly 26 MiB to 53 MiB despite identical protocol, LLVM version,
// target set, and dynamic dependencies. Keep a hard package-size regression
// fence with enough room for both observed bottle layouts.
const installedBudget = 64 * 1024 * 1024;
const packedBudget = 32 * 1024 * 1024;
const work = mkdtempSync(join(tmpdir(), "scriptc-llvm-pack-"));
try {
execFileSync("tar", ["-xzf", tarball, "-C", work]);
const root = join(work, "package");
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
for (const notice of ["LICENSE", "SCRIPTC_LICENSE", "THIRD_PARTY_NOTICES"]) {
if (statSync(join(root, notice)).size === 0) throw new Error(`${notice} is missing or empty`);
}
if (manifest.os?.join(",") !== "darwin" || manifest.cpu?.join(",") !== "arm64") {
throw new Error("helper manifest must constrain os=darwin and cpu=arm64");
}
if (manifest.bin?.["scriptc-llvm-codegen"] !== "bin/scriptc-llvm-codegen") {
throw new Error("helper manifest must expose the executable as an npm bin");
}
const binary = join(root, "bin", "scriptc-llvm-codegen");
accessSync(binary, constants.X_OK);
const installedSize = statSync(binary).size;
const packedSize = statSync(tarball).size;
const sizeFailures = [
...(installedSize > installedBudget
? [`stripped helper is ${installedSize} bytes, exceeding the ${installedBudget}-byte installed-size budget`]
: []),
...(packedSize > packedBudget
? [`helper tarball is ${packedSize} bytes, exceeding the ${packedBudget}-byte compressed-size budget`]
: []),
];
if (sizeFailures.length > 0) throw new Error(sizeFailures.join("; "));
const version = JSON.parse(execFileSync(binary, ["version", "--format=json"], {
encoding: "utf8",
}));
if (
version.protocol_version !== "1" ||
version.scriptc_package_version !== manifest.version ||
version.llvm_version !== "22.1.8" ||
version.default_target !== "arm64-apple-macosx14.0.0" ||
!Array.isArray(version.supported_targets) ||
!version.supported_targets.includes(version.default_target)
) throw new Error(`packed helper identity mismatch: ${JSON.stringify(version)}`);
const dependencies = execFileSync("otool", ["-L", binary], { encoding: "utf8" });
const nonSystemDependencies = dependencies.trim().split("\n").slice(1)
.map((line) => line.trim().split(" (compatibility version", 1)[0])
.filter((path) => path !== undefined &&
!path.startsWith("/usr/lib/") && !path.startsWith("/System/Library/"));
if (nonSystemDependencies.length > 0) {
throw new Error(
`packed helper has non-system runtime dependencies: ${nonSystemDependencies.join(", ")}\n` +
dependencies,
);
}
const exportedSymbols = execFileSync("nm", ["-gU", binary], { encoding: "utf8" })
.trim().split("\n").filter(Boolean)
.map((line) => line.trim().split(/\s+/).at(-1));
const exportValidation = validateLlvmHelperExports(exportedSymbols);
if (!exportValidation.hasMain || exportValidation.unexpected.length > 0) {
throw new Error(
`packed helper must export _main without LLVM globals, found: ${exportedSymbols.join(", ")}`,
);
}
const loadCommands = execFileSync("otool", ["-l", binary], { encoding: "utf8" });
if (!/LC_BUILD_VERSION[\s\S]*?platform 1[\s\S]*?minos 15\.0(?:\s|$)/.test(loadCommands)) {
throw new Error("packed helper must declare its macOS 15.0 minimum host version");
}
const probeInput = join(work, "probe.ll");
const probeObject = join(work, "probe.o");
writeFileSync(probeInput, "define i32 @answer() { ret i32 42 }\n");
execFileSync(binary, [
"emit", "--input", probeInput, "--output", probeObject,
"--filetype", "obj", "--target", version.default_target,
"--opt-level", "2", "--relocation-model", "pic",
"--diagnostic-format", "json", "--source-path", "/src/probe.ts",
]);
const objectLoadCommands = execFileSync("otool", ["-l", probeObject], { encoding: "utf8" });
if (!/LC_BUILD_VERSION[\s\S]*?platform 1[\s\S]*?minos 14\.0(?:\s|$)/.test(objectLoadCommands)) {
throw new Error("packed helper must emit objects with the macOS 14.0 deployment target");
}
const attributes = execFileSync("xattr", [binary], { encoding: "utf8" });
if (attributes.split("\n").includes("com.apple.quarantine")) {
throw new Error("packed helper carries a quarantine attribute");
}
execFileSync("codesign", ["--verify", "--strict", binary], { stdio: "inherit" });
process.stdout.write(
`verified ${manifest.name}@${manifest.version}: ${installedSize} bytes installed, ` +
`${packedSize} bytes packed\n`,
);
} finally {
rmSync(work, { recursive: true, force: true });
}