mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
The frontend's syntax trees and program state refer to each other, so reference counting frees almost none of them when a build disposes the frontend. Whether a cycle pass reclaimed them before the build ended depended on where the collector's schedule happened to be. Passes during the frontend found its data live and widened the growth fractions, so the next pass waited until the heap had grown well past its size at disposal, and the dead frontend and the emitter's garbage stayed until the build ended. The native compiler now runs scr_collect_cycles_phase right after it disposes the frontend, through a host function. The runtime entry point sweeps, then restarts the growth fractions and the death rule's backoff from the heap the sweep leaves, as after a productive pass. Under Node the hook is unset.
175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { runCli } from "../cli/command.js";
|
|
import type { NativeCacheWarmProfile } from "../cli/host.js";
|
|
import {
|
|
selectNativeRuntimePack,
|
|
stageNativeRuntimeSelection,
|
|
} from "../backend/runtime-pack-native.js";
|
|
import { mergeNativeProfiles } from "../backend/native-tools.js";
|
|
import { setDeclarationRoot } from "../frontend/dts-paths.js";
|
|
import { setFrontendDisposedHook } from "../frontend/dispose-hook.js";
|
|
import { FrontendServices } from "../frontend/services.js";
|
|
import { createNativeTs7Api } from "../frontend/ts7/native-api.js";
|
|
import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js";
|
|
import { setCompilerReleaseVersion } from "../library/sidecar.js";
|
|
import { NativeCompiler } from "./compiler.js";
|
|
import { openNativeCache } from "./cache.js";
|
|
import { loadNativeToolchain } from "./toolchain.js";
|
|
import { buildSanitizedRuntime } from "./sanitizer.js";
|
|
import { runCompilerTask } from "./task.js";
|
|
|
|
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
|
|
declare function compilerNativeCollectCycles(): void;
|
|
|
|
async function main(): Promise<number> {
|
|
setFrontendDisposedHook(() => compilerNativeCollectCycles());
|
|
const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json";
|
|
let compiler: NativeCompiler | null = null;
|
|
let cache: ReturnType<typeof openNativeCache> = null;
|
|
const getCompiler = (): NativeCompiler => {
|
|
if (compiler !== null) return compiler;
|
|
const toolchain = loadNativeToolchain(manifestPath, process.env);
|
|
if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot);
|
|
setCompilerReleaseVersion(toolchain.compilerVersion);
|
|
cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden));
|
|
compiler = new NativeCompiler(toolchain, cache);
|
|
return compiler;
|
|
};
|
|
return runCli(process.argv.slice(2), {
|
|
version: () => loadNativeToolchain(manifestPath).compilerVersion,
|
|
sourceTargetPlatform: () => getCompiler().toolchain.target.platform,
|
|
analyze: (entry, options) => runCompilerTask(() => getCompiler().analyzeCached(entry, options)),
|
|
compile: (entry, options) => runCompilerTask(() => getCompiler().compile(entry, options)),
|
|
compileLibrary: (options) => runCompilerTask(() => getCompiler().compileLibrary(options)),
|
|
resolveProvenanceSources: async (entry) => {
|
|
const toolchain = getCompiler().toolchain;
|
|
const services = new FrontendServices((options) =>
|
|
createNativeTs7Api({ ...options, executable: toolchain.ts7Executable }),
|
|
);
|
|
try {
|
|
return await resolveProvenanceSourcesWithParser(entry, (path, source, kind) =>
|
|
services.parse(path, source, kind),
|
|
);
|
|
} finally {
|
|
services.close();
|
|
}
|
|
},
|
|
warmNativeCaches: async (options) => {
|
|
const toolchain = getCompiler().toolchain;
|
|
if (cache === null) throw new Error("the native build cache is disabled or unavailable");
|
|
if (!toolchain.target.supports.exe)
|
|
throw new Error(
|
|
`native cache warming requires an executable target; ${toolchain.target.name} supports library archives`,
|
|
);
|
|
const profiles: { profile: NativeCacheWarmProfile; elapsedMs: number }[] = [];
|
|
for (const profile of [
|
|
...new Set(options.profiles ?? (["runtime", "tls", "dynamic"] as NativeCacheWarmProfile[])),
|
|
]) {
|
|
const start = performance.now();
|
|
const stage = mkdtempSync(join(tmpdir(), "scriptc-warm-"));
|
|
try {
|
|
const pack = selectNativeRuntimePack(
|
|
toolchain.runtimePackRoot,
|
|
toolchain.target,
|
|
toolchain.compilerVersion,
|
|
{
|
|
fetch: profile === "tls",
|
|
regex: false,
|
|
copying: false,
|
|
textDecoderLegacy: false,
|
|
fileHandle: false,
|
|
zlib: false,
|
|
assert: false,
|
|
inspect: false,
|
|
dynInvoke: false,
|
|
dc: false,
|
|
dynAsync: false,
|
|
events: false,
|
|
emitter: false,
|
|
symbol: false,
|
|
bigint: false,
|
|
searchParams: false,
|
|
qs: false,
|
|
utilPromisify: false,
|
|
dynBuiltinClass: false,
|
|
dynBuiltinStream: false,
|
|
parseArgs: false,
|
|
stream: false,
|
|
net: false,
|
|
http: false,
|
|
http2: false,
|
|
dgram: false,
|
|
dnsPromises: false,
|
|
dgramDyn: false,
|
|
watch: false,
|
|
foreignFfi: false,
|
|
nodeTest: false,
|
|
tls: false,
|
|
cipher: false,
|
|
ecdh: false,
|
|
webcrypto: false,
|
|
perfLoop: false,
|
|
tlsCa: false,
|
|
},
|
|
options.optimization ?? "release",
|
|
);
|
|
if (options.sanitize) buildSanitizedRuntime(toolchain, pack, stage, "executable", cache);
|
|
else stageNativeRuntimeSelection(pack, stage);
|
|
} finally {
|
|
rmSync(stage, { recursive: true, force: true });
|
|
}
|
|
profiles.push({ profile, elapsedMs: performance.now() - start });
|
|
}
|
|
return { cacheRoot: cache.root, profiles };
|
|
},
|
|
mergeProfiles: async (outputPath, inputPaths, sparse) => {
|
|
const toolchain = getCompiler().toolchain;
|
|
mergeNativeProfiles({
|
|
executable: toolchain.helperExecutable,
|
|
packageRoot: toolchain.helperPackageRoot,
|
|
compilerVersion: toolchain.compilerVersion,
|
|
target: toolchain.target,
|
|
helper: toolchain.helper,
|
|
outputPath,
|
|
inputPaths,
|
|
sparse,
|
|
});
|
|
},
|
|
run: async (binary) => {
|
|
const toolchain = getCompiler().toolchain;
|
|
let executable = binary;
|
|
const args: string[] = [];
|
|
if (toolchain.target.platform === "wasi") {
|
|
if (toolchain.wasiNodeRunner === undefined)
|
|
throw new Error("the WASI runner is missing from this installation");
|
|
executable = "node";
|
|
args.push("--no-warnings", toolchain.wasiNodeRunner, binary);
|
|
}
|
|
const result = spawnSync(executable, args, { stdio: "inherit" });
|
|
if (result.error) {
|
|
if (toolchain.target.platform === "wasi") {
|
|
throw new Error(
|
|
`could not start the Node.js WASI host: ${result.error.message}; running WASI modules requires Node.js 24 or newer on PATH; use scriptc build to compile without Node`,
|
|
);
|
|
}
|
|
throw new Error(result.error.message);
|
|
}
|
|
if (result.signal) {
|
|
process.stderr.write(`scriptc: program killed by ${result.signal}\n`);
|
|
return 1;
|
|
}
|
|
return result.status ?? 0;
|
|
},
|
|
});
|
|
}
|
|
|
|
try {
|
|
process.exitCode = await main();
|
|
} catch (error) {
|
|
process.stderr.write(`scriptc: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
}
|