mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Builds on Linux and macOS now connect to a shared `tsc-ts --api --listen` server by default, which keeps the files it parsed and bound, so a later build in the same working directory does not parse the TypeScript libs and scriptc's declaration files again. SCRIPTC_CHECKER_SERVER=0 turns it off. When CI is set to a value other than 0 or false, builds use their own tsc-ts process unless SCRIPTC_CHECKER_SERVER=1. - The server exits after 600 seconds without a build (was 300). - Its name also digests the compiler executable's identity, so each scriptc installation and build connects only to servers it started. - The server process runs in the root directory, so it does not keep the first build's directory in use; its --cwd argument is unchanged. - When the connection to the server fails after it answered, through a failed transfer or an ended stream, the native CLI runs the command again in a new process with SCRIPTC_CHECKER_SERVER=0 and exits with its status. - SCRIPTC_CHECKER_SERVER no longer changes the frontend cache key: the server's answers are the build's own checker's. The CLI docs describe the background process, its socket directory, when it exits and how to stop it.
279 lines
11 KiB
TypeScript
279 lines
11 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { appendFileSync, 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 { runTypePatchCommand } from "../frontend/type-patches/command.js";
|
|
import { checkerServerLost, 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 { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js";
|
|
import {
|
|
checkerExecutableIdentity,
|
|
checkerPackageVersion,
|
|
distributionPrebuiltLibDiagnostics,
|
|
recordPrebuiltLibDiagnostics,
|
|
setLibDiagnosticsStore,
|
|
} from "../frontend/ts7/lib-diagnostics.js";
|
|
import { setCheckerFeatureStore } from "../frontend/ts7/checker-features.js";
|
|
import { loadNativeToolchain } from "./toolchain.js";
|
|
import { buildSanitizedRuntime } from "./sanitizer.js";
|
|
import { runCompilerTask } from "./task.js";
|
|
import { processFork } from "../frontend/process-fork.js";
|
|
|
|
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
|
|
declare function compilerNativeCollectCycles(): void;
|
|
declare function compilerNativeFork(): number;
|
|
declare function compilerNativeExit(code: number): void;
|
|
declare function compilerNativeWait(pid: number): number;
|
|
declare function compilerNativeLockOpen(path: string): boolean;
|
|
declare function compilerNativeLock(): void;
|
|
declare function compilerNativeUnlock(): void;
|
|
declare function compilerNativeBoundCycleGrowth(): void;
|
|
|
|
/** Runs a build. When this process's connection to the shared checker server
|
|
* failed during it, the command runs again in a new process that checks with
|
|
* its own tsc-ts process (SCRIPTC_CHECKER_SERVER=0), and this process exits
|
|
* with that one's status: the new process prints the build's output. */
|
|
async function withCheckerFallback<T>(work: () => Promise<T>): Promise<T> {
|
|
let result: T;
|
|
try {
|
|
result = await work();
|
|
} catch (error) {
|
|
if (!checkerServerLost()) throw error;
|
|
return runWithOwnChecker();
|
|
}
|
|
return checkerServerLost() ? runWithOwnChecker() : result;
|
|
}
|
|
|
|
function runWithOwnChecker(): never {
|
|
const run = spawnSync(process.execPath, process.argv.slice(2), {
|
|
stdio: "inherit",
|
|
env: { ...process.env, SCRIPTC_CHECKER_SERVER: "0" },
|
|
});
|
|
if (run.error) throw new Error(run.error.message);
|
|
process.exit(run.status ?? 1);
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
compilerNativeBoundCycleGrowth();
|
|
setFrontendDisposedHook(() => compilerNativeCollectCycles());
|
|
if (process.platform !== "win32")
|
|
processFork.primitives = {
|
|
fork: () => compilerNativeFork(),
|
|
exit: (code) => compilerNativeExit(code),
|
|
wait: (pid) => compilerNativeWait(pid),
|
|
openLock: (path) => compilerNativeLockOpen(path),
|
|
lock: () => compilerNativeLock(),
|
|
unlock: () => compilerNativeUnlock(),
|
|
};
|
|
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));
|
|
if (cache !== null) {
|
|
const store = cache;
|
|
setLibDiagnosticsStore(
|
|
{
|
|
read: (key) =>
|
|
store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null,
|
|
write: (key, text) => store.write("lib-diagnostics", key, text),
|
|
},
|
|
checkerExecutableIdentity(toolchain.ts7Executable),
|
|
toolchain.prebuiltLibDiagnostics === undefined
|
|
? null
|
|
: () =>
|
|
distributionPrebuiltLibDiagnostics(
|
|
toolchain.prebuiltLibDiagnostics!,
|
|
toolchain.ts7Executable,
|
|
),
|
|
);
|
|
// A distribution build records its prebuilt lib diagnostics (build-native-cli.mts).
|
|
const recordPath = process.env["SCRIPTC_LIB_DIAGNOSTICS_RECORD"];
|
|
const checkerVersion =
|
|
recordPath === undefined ? null : checkerPackageVersion(toolchain.ts7Executable);
|
|
if (recordPath !== undefined && checkerVersion !== null)
|
|
recordPrebuiltLibDiagnostics({
|
|
checker: checkerVersion,
|
|
record: (key) => appendFileSync(recordPath, `${key}\n`),
|
|
});
|
|
setCheckerFeatureStore({
|
|
read: (key) =>
|
|
store.read("checker-features", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null,
|
|
write: (key, text) => store.write("checker-features", key, text),
|
|
});
|
|
}
|
|
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) =>
|
|
withCheckerFallback(() => runCompilerTask(() => getCompiler().analyzeCached(entry, options))),
|
|
compile: (entry, options) =>
|
|
withCheckerFallback(() => runCompilerTask(() => getCompiler().compile(entry, options))),
|
|
compileLibrary: (options) =>
|
|
withCheckerFallback(() => 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,
|
|
textCodecDyn: false,
|
|
watch: false,
|
|
foreignFfi: false,
|
|
nodeTest: false,
|
|
tls: false,
|
|
cipher: false,
|
|
ecdh: false,
|
|
dh: false,
|
|
pki: false,
|
|
ed25519: false,
|
|
x509: 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 };
|
|
},
|
|
typePatches: async (request) => {
|
|
const toolchain = getCompiler().toolchain;
|
|
const services = new FrontendServices((options) =>
|
|
createNativeTs7Api({ ...options, executable: toolchain.ts7Executable }),
|
|
);
|
|
try {
|
|
return runTypePatchCommand(request, (path, source, kind) =>
|
|
services.parse(path, source, kind),
|
|
);
|
|
} finally {
|
|
services.close();
|
|
}
|
|
},
|
|
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;
|
|
}
|