mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
With --exceptions=rare, recursive functions and functions up to three calls above a throw also propagate exceptions by unwinding on Linux, so calls to them need no pending-flag test. Functions with a throw of their own keep the flag. The default, common, keeps the current rules, and builds without the flag emit the same code as before. The option is part of the executable cache key; the bootstrap's cached route leaves builds with it to the full compiler, and coverage and library builds refuse it.
293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { readFile, rm } from "node:fs/promises";
|
|
import { enableCompileCache } from "node:module";
|
|
import { basename, dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
|
|
|
// Node 24 can persist V8's compiled module bytecode. scriptc's CLI imports
|
|
// the compiler and its lowering/backend graph before handling any command, so
|
|
// enabling this in the tiny bootstrap avoids reparsing that graph on every
|
|
// edit/build invocation.
|
|
try {
|
|
enableCompileCache();
|
|
} catch {
|
|
// Bytecode caching is an optimization boundary. A read-only temp directory
|
|
// must never prevent the compiler from running.
|
|
}
|
|
|
|
function packageVersion(): string {
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
try {
|
|
return (
|
|
(JSON.parse(requireText(join(here, "..", "package.json"))) as { version?: string }).version ??
|
|
"unknown"
|
|
);
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
function requireText(path: string): string {
|
|
// This tiny synchronous read keeps --version free of the compiler graph and
|
|
// preserves the package manifest as the one release-version authority.
|
|
const { readFileSync } = process.getBuiltinModule("node:fs") as typeof import("node:fs");
|
|
return readFileSync(path, "utf8");
|
|
}
|
|
|
|
async function tryFastPath(): Promise<number | null> {
|
|
let parsed: ReturnType<
|
|
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
|
>;
|
|
try {
|
|
parsed = parseArgs({ options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
|
|
} catch {
|
|
return null; // main owns exact user-error wording
|
|
}
|
|
const { values, positionals } = parsed;
|
|
if (values.version) {
|
|
process.stdout.write(`${packageVersion()}\n`);
|
|
return 0;
|
|
}
|
|
if (values.help || positionals.length === 0) {
|
|
process.stdout.write(USAGE);
|
|
return values.help ? 0 : 1;
|
|
}
|
|
const [command, inputArg] = positionals;
|
|
if (command === "coverage") return coverageFastPath(values, inputArg, positionals.length);
|
|
if (
|
|
(command !== "build" && command !== "run") ||
|
|
inputArg === undefined ||
|
|
(values.emit !== undefined && values.emit !== "exe") ||
|
|
values.print !== undefined ||
|
|
values["emit-ir"] ||
|
|
values.lib ||
|
|
values["provenance-sources"] ||
|
|
// The full compiler keys these builds by the profile's bytes.
|
|
values["profile-generate"] !== undefined ||
|
|
values["profile-use"] !== undefined ||
|
|
values.sparse ||
|
|
// A --libnode=beside build places the pack and the island modules next
|
|
// to the executable, which a routed hit does not do.
|
|
values.libnode === "beside" ||
|
|
// The full compiler resolves `native` against the build host.
|
|
values["target-cpu"] !== undefined ||
|
|
// The full CLI validates the value.
|
|
values.exceptions !== undefined ||
|
|
(values["external-types"] ?? []).length > 0 ||
|
|
// The full CLI validates the Node island policy flags.
|
|
hasNodeIslandFlags(values) ||
|
|
// The full compiler writes the representation report and keys the heap
|
|
// census build (backend/llvm/repr-report.ts, heap-census.ts).
|
|
(process.env["SCRIPTC_REPR_REPORT"] ?? "") !== "" ||
|
|
(process.env["SCRIPTC_HEAP_CENSUS"] ?? "") !== ""
|
|
)
|
|
return null;
|
|
const backend = values.backend;
|
|
if (backend !== undefined && backend !== "llvm") return null;
|
|
const optimization = values.optimization;
|
|
if (
|
|
optimization !== undefined &&
|
|
optimization !== "release" &&
|
|
optimization !== "dev" &&
|
|
optimization !== "speed"
|
|
)
|
|
return null;
|
|
const npmRaw = (values["npm-static"] ?? [])
|
|
.flatMap((value) => value.split(","))
|
|
.map((value) => value.trim())
|
|
.filter((value) => value !== "");
|
|
let npmStatic: string[] | "auto" | null = null;
|
|
if (npmRaw.includes("auto")) {
|
|
if (npmRaw.length !== 1) return null;
|
|
npmStatic = "auto";
|
|
} else if (npmRaw.length > 0) {
|
|
npmStatic = npmRaw;
|
|
}
|
|
|
|
let startup: typeof import("@scriptc/compiler/startup-cache");
|
|
let buildPlatform: string;
|
|
try {
|
|
startup = await import("@scriptc/compiler/startup-cache");
|
|
buildPlatform = startup.configuredTargetPlatform();
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (command === "run" && buildPlatform === "wasi") return null;
|
|
const input = resolve(inputArg);
|
|
const outDir = values.out ? dirname(resolve(values.out)) : join(dirname(input), ".scriptc");
|
|
const stem = basename(input).replace(/\.(ts|mts|cts|js|mjs|cjs|c|ll)$/, "");
|
|
const defaultName =
|
|
buildPlatform === "win32" ? `${stem}.exe` : buildPlatform === "wasi" ? `${stem}.wasm` : stem;
|
|
const outPath = values.out ? resolve(values.out) : join(outDir, defaultName);
|
|
const ffiPath = values.ffi === undefined ? null : resolve(values.ffi);
|
|
const ffiBytes = ffiPath === null ? null : await readFile(ffiPath).catch(() => null);
|
|
if (ffiPath !== null && ffiBytes === null) return null;
|
|
const root = await startup.prepareBuildCacheRoot(startup.resolveBuildCacheRoot());
|
|
// This must exactly mirror the ordinary LLVM executable's route in the
|
|
// full compiler. Otherwise a valid helper/runtime-pack cache entry has a
|
|
// different target/compiler identity and bootstrap must unnecessarily load
|
|
// the whole compiler graph to rediscover it.
|
|
const helperRuntimePackTarget = !values.sanitize ? startup.precompiledRuntimePackTarget() : null;
|
|
const helperObjectRoute = helperRuntimePackTarget !== null;
|
|
// The full compiler reports a checker it cannot find.
|
|
let checker: string | undefined;
|
|
try {
|
|
checker = startup.checkerCacheIdentity();
|
|
} catch {
|
|
return null;
|
|
}
|
|
const hit = await startup.readRoutedExecutableCache(root, {
|
|
entryPath: input,
|
|
outDir,
|
|
outPath,
|
|
emitIr: values["emit-ir"],
|
|
sanitize: values.sanitize,
|
|
backend: "llvm",
|
|
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
|
|
...(values.strip ? { strip: true as const } : {}),
|
|
...(values.static ? { staticLink: true as const } : {}),
|
|
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
|
|
...(values["large-pages"] ? { largePages: true as const } : {}),
|
|
...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}),
|
|
...(checker === undefined ? {} : { checker }),
|
|
npmStatic,
|
|
// Mirrors the full compiler's key: shape inference's overlay is opt-in
|
|
// (SCRIPTC_SHAPE_OVERLAY=1) and --no-shape-overlay turns it off.
|
|
...(process.env["SCRIPTC_SHAPE_OVERLAY"] === "1" && values["shape-overlay"] !== false
|
|
? { shapeOverlay: true as const }
|
|
: {}),
|
|
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
|
|
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
|
|
helperObjectRoute ? "runtime-pack" : "driver-tu"
|
|
}`,
|
|
compiler: [
|
|
helperObjectRoute
|
|
? startup.resolvePlatformLinker(process.env, helperRuntimePackTarget.defaultLinker)
|
|
: (process.env["SCRIPTC_CC"] ?? "clang"),
|
|
],
|
|
nativeEnvironment: () =>
|
|
helperObjectRoute
|
|
? startup.executableLinkerEnvironmentFingerprint(
|
|
process.env,
|
|
helperRuntimePackTarget.defaultLinker,
|
|
)
|
|
: startup.executableNativeEnvironmentFingerprint(),
|
|
nodeVersion: process.version,
|
|
// An entry of a program that calls into Node.js serves only the same pack.
|
|
libnodeIdentity: () => startup.libnodeCacheKey(values.libnode, process.env, dirname(input)),
|
|
});
|
|
if (hit === null) return null;
|
|
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
|
|
if (command === "build") {
|
|
// A cached build repeats its performance warning (SC6101).
|
|
const performance = hit.performance ?? [];
|
|
if (performance.length > 0 && values["dynamic-tier-warning"] !== false)
|
|
process.stderr.write(startup.renderPerformanceWarnings(performance) + "\n");
|
|
process.stdout.write(`${outPath}\n`);
|
|
return 0;
|
|
}
|
|
return new Promise<number>((resolveExit) => {
|
|
const child = spawn(outPath, [], { stdio: "inherit" });
|
|
child.on("exit", (code, signal) => {
|
|
if (signal) {
|
|
process.stderr.write(`scriptc: program killed by ${signal}\n`);
|
|
resolveExit(1);
|
|
} else {
|
|
resolveExit(code ?? 0);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function hasNodeIslandFlags(
|
|
values: ReturnType<
|
|
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
|
>["values"],
|
|
): boolean {
|
|
return (
|
|
values["node-islands"] !== undefined ||
|
|
(values["node-island"] ?? []).length > 0 ||
|
|
(values.native ?? []).length > 0 ||
|
|
values.node !== undefined ||
|
|
values["explain-island"] !== undefined ||
|
|
values["strict-user-js"]
|
|
);
|
|
}
|
|
|
|
/** An unchanged program's coverage verdict replays from the verdict cache
|
|
* without loading the compiler graph. Anything else (a miss, a comment-only
|
|
* edit, options the replay does not cover) defers to the full CLI. */
|
|
async function coverageFastPath(
|
|
values: ReturnType<
|
|
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
|
>["values"],
|
|
inputArg: string | undefined,
|
|
positionalCount: number,
|
|
): Promise<number | null> {
|
|
const print = values.print;
|
|
const failOn = values["fail-on"];
|
|
if (
|
|
inputArg === undefined ||
|
|
positionalCount !== 2 ||
|
|
(print !== undefined && print !== "diagnostics") ||
|
|
(failOn !== undefined && failOn !== "blockers" && failOn !== "divergences") ||
|
|
values.emit !== undefined ||
|
|
values.strip ||
|
|
values.static ||
|
|
values["detached-exit"] ||
|
|
values["large-pages"] ||
|
|
values.exceptions !== undefined ||
|
|
values["target-cpu"] !== undefined ||
|
|
values["cpu-dispatch"] !== undefined ||
|
|
values.lib ||
|
|
values.profile !== undefined ||
|
|
values.ffi !== undefined ||
|
|
values.backend !== undefined ||
|
|
values.optimization !== undefined ||
|
|
values["windows-subsystem"] !== undefined ||
|
|
values["provenance-sources"] ||
|
|
values["emit-ir"] ||
|
|
(values["npm-static"] ?? []).length > 0 ||
|
|
values["shape-overlay"] === false ||
|
|
(values["external-types"] ?? []).length > 0 ||
|
|
// The replay covers the default Node island policy only.
|
|
hasNodeIslandFlags(values)
|
|
)
|
|
return null;
|
|
let startup: typeof import("@scriptc/compiler/startup-cache");
|
|
try {
|
|
startup = await import("@scriptc/compiler/startup-cache");
|
|
} catch {
|
|
return null;
|
|
}
|
|
const hit = await startup.readCoverageVerdict(resolve(inputArg), {}).catch(() => null);
|
|
if (hit === null) return null;
|
|
const { coverage, sourceTexts } = hit;
|
|
if (values["dynamic-tier-warning"] === false) delete coverage.performance;
|
|
if (print === "diagnostics") {
|
|
const envelope = startup.coverageEnvelope(coverage, {
|
|
compilerVersion: packageVersion(),
|
|
sourceTexts,
|
|
...(failOn === undefined ? {} : { failOn }),
|
|
});
|
|
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
} else {
|
|
const color = process.stdout.isTTY ?? false;
|
|
process.stdout.write(
|
|
startup.renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
|
);
|
|
}
|
|
return startup.coveragePasses(coverage, failOn) ? 0 : 1;
|
|
}
|
|
|
|
const fastExit = await tryFastPath();
|
|
if (fastExit === null) {
|
|
await import("./main.js");
|
|
} else {
|
|
process.exitCode = fastExit;
|
|
}
|