mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-09 21:58:53 +08:00
* 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.
426 lines
18 KiB
TypeScript
426 lines
18 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { createRequire } from "node:module";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { compilerReleaseVersion } from "../library/sidecar.js";
|
|
import type { NativeLinkFeatures } from "./native-link-info.js";
|
|
import {
|
|
nativeArtifactDependenciesStillMatch,
|
|
type NativeArtifactDependency,
|
|
} from "./native-toolchain.js";
|
|
import { RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js";
|
|
import type { NativeTargetSpec } from "./targets.js";
|
|
|
|
export const RUNTIME_PACK_SCHEMA = "scriptc.runtime-pack.v1" as const;
|
|
export const RUNTIME_PACK_FORMAT = 1 as const;
|
|
|
|
export type RuntimePredicate =
|
|
| boolean
|
|
| string
|
|
| { all?: string[]; any?: string[]; not?: string[] };
|
|
|
|
interface RuntimePackArtifact {
|
|
path: string;
|
|
sha256: string;
|
|
size: number;
|
|
}
|
|
|
|
interface RuntimePackVariant extends RuntimePackArtifact {
|
|
id: string;
|
|
when: Record<string, boolean>;
|
|
defines: string[];
|
|
}
|
|
|
|
interface RuntimePackUnit {
|
|
source: string;
|
|
predicate: RuntimePredicate;
|
|
variants: RuntimePackVariant[];
|
|
}
|
|
|
|
interface RuntimePackArchive extends RuntimePackArtifact {
|
|
id: "quickjs" | "libregexp" | "zlib" | "mbedtls";
|
|
predicate: RuntimePredicate;
|
|
}
|
|
|
|
export interface RuntimePackManifest {
|
|
schema: typeof RUNTIME_PACK_SCHEMA;
|
|
format: typeof RUNTIME_PACK_FORMAT;
|
|
package: string;
|
|
version: string;
|
|
target: {
|
|
name: NativeTargetSpec["name"];
|
|
llvm_triple: NativeTargetSpec["llvmTriple"];
|
|
architecture: NativeTargetSpec["architecture"];
|
|
object_format: NativeTargetSpec["objectFormat"];
|
|
minimum_os: NativeTargetSpec["minimumOs"];
|
|
};
|
|
runtime_abi: { version: number; marker: string };
|
|
compiler: { command: string; identity: string; target: string };
|
|
macros: {
|
|
executable: string[];
|
|
excluded: string[];
|
|
sanitizer: "external-toolchain-required";
|
|
};
|
|
flavors: Record<"release" | "dev", {
|
|
optimization: "-O2" | "-O0";
|
|
runtime_units: RuntimePackUnit[];
|
|
}>;
|
|
archives: RuntimePackArchive[];
|
|
system_libraries: { name: string; predicate: RuntimePredicate }[];
|
|
licenses: { path: string; license: string }[];
|
|
}
|
|
|
|
export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
|
nativeFetch: boolean;
|
|
netIslandEffective: boolean;
|
|
netEffective: boolean;
|
|
httpEffective: boolean;
|
|
tlsEffective: boolean;
|
|
tlsCaEffective: boolean;
|
|
zlibEffective: boolean;
|
|
}
|
|
|
|
export interface RuntimePackSelection {
|
|
root: string;
|
|
manifestPath: string;
|
|
manifest: RuntimePackManifest;
|
|
flavor: "release" | "dev";
|
|
features: RuntimeFeatureSet;
|
|
runtimeObjects: string[];
|
|
archives: string[];
|
|
systemLibraries: string[];
|
|
dependencyPaths: string[];
|
|
/** Exact installed inputs observed while the selected artifacts were verified. */
|
|
sourceDependencies: NativeArtifactDependency[];
|
|
selectedRuntimeArtifacts: RuntimePackArtifact[];
|
|
selectedArchiveArtifacts: RuntimePackArtifact[];
|
|
}
|
|
|
|
export class RuntimePackError extends Error {
|
|
constructor(message: string, readonly code: "missing" | "invalid" | "unsupported") {
|
|
super(message);
|
|
this.name = "RuntimePackError";
|
|
}
|
|
}
|
|
|
|
function object(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function validDigest(value: unknown): value is string {
|
|
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
}
|
|
|
|
function validPredicate(value: unknown): value is RuntimePredicate {
|
|
if (typeof value === "boolean" || typeof value === "string") return true;
|
|
const item = object(value);
|
|
if (item === null) return false;
|
|
const keys = Object.keys(item);
|
|
if (keys.some((key) => key !== "all" && key !== "any" && key !== "not")) return false;
|
|
return keys.length > 0 && keys.every((key) =>
|
|
Array.isArray(item[key]) && (item[key] as unknown[]).every((feature) => typeof feature === "string")
|
|
);
|
|
}
|
|
|
|
function validArtifact(value: unknown): value is RuntimePackArtifact {
|
|
const item = object(value);
|
|
return item !== null && typeof item.path === "string" && !item.path.startsWith("/") &&
|
|
!item.path.split(/[\\/]/).includes("..") && validDigest(item.sha256) &&
|
|
typeof item.size === "number" && Number.isInteger(item.size) && item.size >= 0;
|
|
}
|
|
|
|
export function parseRuntimePackManifest(value: unknown): RuntimePackManifest {
|
|
const manifest = object(value);
|
|
const target = object(manifest?.target);
|
|
const abi = object(manifest?.runtime_abi);
|
|
const compiler = object(manifest?.compiler);
|
|
const macros = object(manifest?.macros);
|
|
const flavors = object(manifest?.flavors);
|
|
const validFlavor = (value: unknown, optimization: string): boolean => {
|
|
const flavor = object(value);
|
|
return flavor?.optimization === optimization && Array.isArray(flavor.runtime_units) &&
|
|
flavor.runtime_units.every((raw) => {
|
|
const unit = object(raw);
|
|
return typeof unit?.source === "string" && validPredicate(unit.predicate) &&
|
|
Array.isArray(unit.variants) && unit.variants.length > 0 && unit.variants.every((variantRaw) => {
|
|
const variant = object(variantRaw);
|
|
const when = object(variant?.when);
|
|
return validArtifact(variantRaw) && typeof variant?.id === "string" && when !== null &&
|
|
Object.values(when).every((entry) => typeof entry === "boolean") &&
|
|
Array.isArray(variant.defines) && variant.defines.every((entry) => typeof entry === "string");
|
|
});
|
|
});
|
|
};
|
|
if (
|
|
manifest?.schema !== RUNTIME_PACK_SCHEMA || manifest.format !== RUNTIME_PACK_FORMAT ||
|
|
typeof manifest.package !== "string" || typeof manifest.version !== "string" ||
|
|
!isRuntimePackTarget(target) ||
|
|
abi?.version !== RUNTIME_ABI_VERSION || abi.marker !== RUNTIME_ABI_MARKER ||
|
|
typeof compiler?.command !== "string" || typeof compiler.identity !== "string" ||
|
|
compiler.target !== (target?.llvm_triple) ||
|
|
!Array.isArray(macros?.executable) || !macros.executable.every((entry) => typeof entry === "string") ||
|
|
!Array.isArray(macros.excluded) || !macros.excluded.every((entry) => typeof entry === "string") ||
|
|
macros.sanitizer !== "external-toolchain-required" ||
|
|
flavors === null || !validFlavor(flavors.release, "-O2") || !validFlavor(flavors.dev, "-O0") ||
|
|
!Array.isArray(manifest.archives) || !manifest.archives.every((raw) => {
|
|
const archive = object(raw);
|
|
return validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate);
|
|
}) ||
|
|
!Array.isArray(manifest.system_libraries) || !manifest.system_libraries.every((raw) => {
|
|
const library = object(raw);
|
|
return typeof library?.name === "string" && validPredicate(library.predicate);
|
|
}) ||
|
|
!Array.isArray(manifest.licenses) || !manifest.licenses.every((raw) => {
|
|
const license = object(raw);
|
|
return typeof license?.path === "string" && typeof license.license === "string";
|
|
})
|
|
) throw new RuntimePackError("installed runtime-pack.json is malformed or incompatible", "invalid");
|
|
return manifest as unknown as RuntimePackManifest;
|
|
}
|
|
|
|
function isRuntimePackTarget(target: Record<string, unknown> | null): boolean {
|
|
return target !== null &&
|
|
typeof target.name === "string" &&
|
|
typeof target.llvm_triple === "string" &&
|
|
(target.architecture === "arm64" || target.architecture === "x64" || target.architecture === "wasm32") &&
|
|
(target.object_format === "macho" || target.object_format === "elf" || target.object_format === "coff" || target.object_format === "wasm") &&
|
|
typeof target.minimum_os === "string";
|
|
}
|
|
|
|
export function effectiveRuntimeFeatures(
|
|
features: NativeLinkFeatures,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): RuntimeFeatureSet {
|
|
const curlFetch = features.dynamic && features.fetch && env["SCRIPTC_FETCH_CURL"] === "1";
|
|
if (curlFetch) {
|
|
throw new RuntimePackError(
|
|
"SCRIPTC_FETCH_CURL=1 is an external developer-toolchain comparison mode and is not available with precompiled runtime packs",
|
|
"unsupported",
|
|
);
|
|
}
|
|
const nativeFetch = features.fetch;
|
|
const netIslandEffective = features.dynamic && (features.netIsland || nativeFetch);
|
|
const netEffective = features.net || nativeFetch || netIslandEffective;
|
|
const httpEffective = features.http || nativeFetch || netIslandEffective;
|
|
const tlsEffective = features.tls || nativeFetch || netIslandEffective;
|
|
const tlsCaEffective = features.tlsCa || tlsEffective;
|
|
return {
|
|
...features,
|
|
nativeFetch,
|
|
netIslandEffective,
|
|
netEffective,
|
|
httpEffective,
|
|
tlsEffective,
|
|
tlsCaEffective,
|
|
zlibEffective: features.zlib || nativeFetch,
|
|
};
|
|
}
|
|
|
|
export function evaluateRuntimePredicate(
|
|
predicate: RuntimePredicate,
|
|
features: object,
|
|
): boolean {
|
|
const values = features as Record<string, boolean>;
|
|
if (typeof predicate === "boolean") return predicate;
|
|
if (typeof predicate === "string") return values[predicate] === true;
|
|
return (predicate.all?.every((name) => values[name] === true) ?? true) &&
|
|
(predicate.any?.some((name) => values[name] === true) ?? true) &&
|
|
(predicate.not?.every((name) => values[name] !== true) ?? true);
|
|
}
|
|
|
|
function selectVariant(unit: RuntimePackUnit, features: RuntimeFeatureSet): RuntimePackVariant {
|
|
const matches = unit.variants.filter((variant) =>
|
|
Object.entries(variant.when).every(([name, expected]) => features[name as keyof RuntimeFeatureSet] === expected)
|
|
);
|
|
matches.sort((a, b) => Object.keys(b.when).length - Object.keys(a.when).length || a.id.localeCompare(b.id));
|
|
const selected = matches[0];
|
|
if (selected === undefined) {
|
|
throw new RuntimePackError(`runtime pack has no variant for ${unit.source}`, "invalid");
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
async function verifyArtifact(root: string, artifact: RuntimePackArtifact): Promise<string> {
|
|
const path = join(root, artifact.path);
|
|
let bytes: Buffer;
|
|
try {
|
|
bytes = await readFile(path);
|
|
} catch {
|
|
throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid");
|
|
}
|
|
if (bytes.length !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) {
|
|
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
|
}
|
|
return path;
|
|
}
|
|
|
|
async function snapshotDependencies(paths: readonly string[]): Promise<NativeArtifactDependency[]> {
|
|
const { lstat, realpath, stat } = await import("node:fs/promises");
|
|
const { resolve } = await import("node:path");
|
|
return Promise.all([...new Set(paths.map((path) => resolve(path)))].sort().map(async (path) => {
|
|
const info = await lstat(path);
|
|
const kind = info.isFile() ? "file" : info.isDirectory() ? "directory" : "symlink";
|
|
const dependency: NativeArtifactDependency = {
|
|
path,
|
|
kind,
|
|
dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size),
|
|
mtimeMs: Number(info.mtimeMs), ctimeMs: Number(info.ctimeMs),
|
|
};
|
|
if (kind === "symlink") {
|
|
const targetPath = await realpath(path);
|
|
const target = await stat(path);
|
|
const targetKind = target.isFile() ? "file" : target.isDirectory() ? "directory" : null;
|
|
if (targetKind === null) throw new Error(`unsupported runtime-pack dependency: ${path}`);
|
|
dependency.targetPath = targetPath;
|
|
dependency.targetKind = targetKind;
|
|
dependency.targetDev = Number(target.dev);
|
|
dependency.targetIno = Number(target.ino);
|
|
dependency.targetSize = Number(target.size);
|
|
dependency.targetMtimeMs = Number(target.mtimeMs);
|
|
dependency.targetCtimeMs = Number(target.ctimeMs);
|
|
}
|
|
return dependency;
|
|
}));
|
|
}
|
|
|
|
export async function stageRuntimePackArtifacts(selection: RuntimePackSelection): Promise<{
|
|
root: string;
|
|
replacements: Map<string, string>;
|
|
}> {
|
|
if (!(await nativeArtifactDependenciesStillMatch(selection.sourceDependencies).catch(() => false))) {
|
|
throw new RuntimePackError("runtime pack changed after artifact selection", "invalid");
|
|
}
|
|
const stageRoot = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-link-"));
|
|
try {
|
|
const replacements = new Map<string, string>();
|
|
await Promise.all([
|
|
...selection.selectedRuntimeArtifacts,
|
|
...selection.selectedArchiveArtifacts,
|
|
].map(async (artifact) => {
|
|
const source = join(selection.root, artifact.path);
|
|
const destination = join(stageRoot, artifact.path);
|
|
const bytes = await readFile(source).catch(() => {
|
|
throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid");
|
|
});
|
|
if (
|
|
bytes.length !== artifact.size ||
|
|
createHash("sha256").update(bytes).digest("hex") !== artifact.sha256
|
|
) {
|
|
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
|
}
|
|
await mkdir(dirname(destination), { recursive: true });
|
|
await writeFile(destination, bytes, { flag: "wx", mode: 0o400 });
|
|
replacements.set(source, destination);
|
|
}));
|
|
if (!(await nativeArtifactDependenciesStillMatch(selection.sourceDependencies).catch(() => false))) {
|
|
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
|
}
|
|
return { root: stageRoot, replacements };
|
|
} catch (error) {
|
|
await rm(stageRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function loadRuntimePack(options: {
|
|
target: NativeTargetSpec;
|
|
features: NativeLinkFeatures;
|
|
optimization: "release" | "dev";
|
|
env?: NodeJS.ProcessEnv;
|
|
resolver?: (specifier: string) => string;
|
|
}): Promise<RuntimePackSelection> {
|
|
const packageName = options.target.runtimePackPackage;
|
|
const resolvePackageJson = options.resolver ?? ((specifier: string) => createRequire(import.meta.url).resolve(specifier));
|
|
let packagePath: string;
|
|
try {
|
|
packagePath = resolvePackageJson(`${packageName}/package.json`);
|
|
} catch {
|
|
throw new RuntimePackError(
|
|
`precompiled runtime package ${packageName} is not installed; reinstall scriptc with optional dependencies enabled for ${options.target.name}`,
|
|
"missing",
|
|
);
|
|
}
|
|
const root = dirname(packagePath);
|
|
const manifestPath = join(root, "runtime-pack.json");
|
|
let identityDependencies: NativeArtifactDependency[];
|
|
try {
|
|
identityDependencies = await snapshotDependencies([packagePath, manifestPath]);
|
|
} catch {
|
|
throw new RuntimePackError(`could not read ${packageName}/runtime-pack.json`, "invalid");
|
|
}
|
|
let packageManifest: { name?: string; version?: string };
|
|
let manifest: RuntimePackManifest;
|
|
try {
|
|
[packageManifest, manifest] = await Promise.all([
|
|
readFile(packagePath, "utf8").then((text) => JSON.parse(text)),
|
|
readFile(manifestPath, "utf8").then((text) => parseRuntimePackManifest(JSON.parse(text))),
|
|
]);
|
|
} catch (error) {
|
|
if (error instanceof RuntimePackError) throw error;
|
|
throw new RuntimePackError(`could not read ${packageName}/runtime-pack.json`, "invalid");
|
|
}
|
|
if (
|
|
packageManifest.name !== packageName || manifest.package !== packageName ||
|
|
packageManifest.version !== compilerReleaseVersion() || manifest.version !== compilerReleaseVersion()
|
|
) {
|
|
throw new RuntimePackError(
|
|
`runtime pack version mismatch: expected ${packageName}@${compilerReleaseVersion()}, found ${packageManifest.name}@${packageManifest.version}`,
|
|
"invalid",
|
|
);
|
|
}
|
|
if (
|
|
manifest.target.name !== options.target.name ||
|
|
manifest.target.llvm_triple !== options.target.llvmTriple ||
|
|
manifest.target.architecture !== options.target.architecture ||
|
|
manifest.target.object_format !== options.target.objectFormat ||
|
|
manifest.target.minimum_os !== options.target.minimumOs
|
|
) throw new RuntimePackError(`runtime pack does not support target ${options.target.name}`, "invalid");
|
|
const features = effectiveRuntimeFeatures(options.features, options.env);
|
|
const flavor = options.optimization;
|
|
const selectedUnits = manifest.flavors[flavor].runtime_units
|
|
.filter((unit) => evaluateRuntimePredicate(unit.predicate, features));
|
|
const selectedVariants = selectedUnits.map((unit) => selectVariant(unit, features));
|
|
const selectedArchives = manifest.archives
|
|
.filter((archive) => evaluateRuntimePredicate(archive.predicate, features));
|
|
const selectedArtifactPaths = [
|
|
...selectedVariants,
|
|
...selectedArchives,
|
|
].map((artifact) => join(root, artifact.path));
|
|
let artifactDependencies: NativeArtifactDependency[];
|
|
try {
|
|
artifactDependencies = await snapshotDependencies(selectedArtifactPaths);
|
|
} catch {
|
|
throw new RuntimePackError("runtime pack artifact set changed during selection", "invalid");
|
|
}
|
|
const [runtimeObjects, archives] = await Promise.all([
|
|
Promise.all(selectedVariants.map((artifact) => verifyArtifact(root, artifact))),
|
|
Promise.all(selectedArchives.map((artifact) => verifyArtifact(root, artifact))),
|
|
]);
|
|
await Promise.all(manifest.licenses.map((license) => readFile(join(root, license.path)))).catch(() => {
|
|
throw new RuntimePackError("runtime pack license payload is incomplete", "invalid");
|
|
});
|
|
const sourceDependencies = [...identityDependencies, ...artifactDependencies];
|
|
if (!(await nativeArtifactDependenciesStillMatch(sourceDependencies).catch(() => false))) {
|
|
throw new RuntimePackError("runtime pack changed while verifying selected artifacts", "invalid");
|
|
}
|
|
return {
|
|
root,
|
|
manifestPath,
|
|
manifest,
|
|
flavor,
|
|
features,
|
|
runtimeObjects,
|
|
archives,
|
|
systemLibraries: manifest.system_libraries
|
|
.filter((entry) => evaluateRuntimePredicate(entry.predicate, features))
|
|
.map((entry) => entry.name),
|
|
dependencyPaths: [packagePath, manifestPath, ...runtimeObjects, ...archives],
|
|
sourceDependencies,
|
|
selectedRuntimeArtifacts: selectedVariants,
|
|
selectedArchiveArtifacts: selectedArchives,
|
|
};
|
|
}
|