mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
A value that contradicts a type an npm package declares used to throw
"expected number at $, got undefined" and nothing else. Such a check now
names the declaration and the type patch that admits the value:
profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
if that declaration is wrong, patch it in package.json: ...
Origins are found for values a package function returns under a written
return type, for properties of records typed by a package-declared type
(record projections into such a type are keyed by it), and for
declaration-boundary casts, whose origin the projection records from the
declaration-world checker. Generic declarations get none: their types
come from the program's own instantiation.
Checked conversions carry dynCheck.origin, an index into
IrModule.declOrigins; the site's existing cold exception block calls
scr_type_origin_note (new runtime unit behind the typeOrigins feature),
which appends the note to the pending TypeError and derives the selector
suffix and the admitted kind from the failure. Union extractions get the
note per arm at compile time. The hot path is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
879 lines
31 KiB
TypeScript
879 lines
31 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
chmod,
|
|
copyFile,
|
|
mkdir,
|
|
readFile,
|
|
rename,
|
|
rm,
|
|
stat,
|
|
utimes,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { basename, dirname, join, resolve } from "node:path";
|
|
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
|
import type { FrontendInputSnapshot } from "../frontend/input-tracker.js";
|
|
import { frontendInputsStillMatch, validFrontendInputSnapshot } from "../frontend/input-tracker.js";
|
|
import { compilerReleaseVersion } from "../library/sidecar.js";
|
|
import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js";
|
|
import { type NativeArtifactDependency } from "../backend/native/contracts.js";
|
|
import { optimizationKeyParts } from "../backend/optimization.js";
|
|
import { type TargetCpu, targetCpuKeyParts } from "../backend/target-cpu.js";
|
|
import { privateSiblingPath } from "../backend/build-cache.js";
|
|
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
|
|
import {
|
|
installDarwinDebugSymbols,
|
|
needsDarwinDebugSymbols,
|
|
readDarwinDebugSymbols,
|
|
} from "../backend/debug-symbols.js";
|
|
import {
|
|
compilerImplementationDependenciesStillMatch,
|
|
compilerImplementationRoot,
|
|
} from "../library/compiler-self-identity.js";
|
|
import {
|
|
cacheKey as sharedCacheKey,
|
|
digest,
|
|
frontendOutputExclusions,
|
|
installBytes,
|
|
outputPaths,
|
|
readCachedFile,
|
|
stampIntegrity as sharedStampIntegrity,
|
|
stampPath as sharedStampPath,
|
|
validNativeFeatures as validSharedNativeFeatures,
|
|
} from "../library/cache-primitives.js";
|
|
|
|
interface CachedExecutableFile {
|
|
name: string;
|
|
digest: string;
|
|
}
|
|
|
|
export interface EarlyExecutableNativeFeatures {
|
|
backend: "llvm";
|
|
/** Omitted is the historical release posture. */
|
|
optimization?: "dev" | "speed";
|
|
workers?: boolean;
|
|
youngNursery?: boolean;
|
|
regex: boolean;
|
|
copying: boolean;
|
|
textDecoderLegacy: boolean;
|
|
fileHandle: boolean;
|
|
fetch: boolean;
|
|
eventTarget?: boolean;
|
|
wasm?: boolean;
|
|
warningReplace?: boolean;
|
|
stdioExtended?: boolean;
|
|
moduleNamespace?: boolean;
|
|
zlib: boolean;
|
|
brotli?: boolean;
|
|
assert: boolean;
|
|
inspect: boolean;
|
|
dynInvoke: boolean;
|
|
dc: boolean;
|
|
dynAsync: boolean;
|
|
dynTier?: boolean;
|
|
streamDyn?: boolean;
|
|
handleProps?: boolean;
|
|
declAdopt?: boolean;
|
|
typeOrigins?: boolean;
|
|
dynForAwait?: boolean;
|
|
eventsOnceSignal?: boolean;
|
|
dynBuffer?: boolean;
|
|
builtinValues?: boolean;
|
|
events: boolean;
|
|
emitter: boolean;
|
|
symbol: boolean;
|
|
bigint: boolean;
|
|
searchParams: boolean;
|
|
qs: boolean;
|
|
utilPromisify: boolean;
|
|
dynBuiltinClass: boolean;
|
|
dynBuiltinStream: boolean;
|
|
parseArgs: boolean;
|
|
stream: boolean;
|
|
net: boolean;
|
|
http: boolean;
|
|
http2: boolean;
|
|
dgram: boolean;
|
|
dnsPromises: boolean;
|
|
dgramDyn: boolean;
|
|
textCodecDyn: boolean;
|
|
watch: boolean;
|
|
foreignFfi: boolean;
|
|
nodeTest: boolean;
|
|
tls: boolean;
|
|
cipher: boolean;
|
|
ecdh: boolean;
|
|
dh: boolean;
|
|
pki: boolean;
|
|
ed25519: boolean;
|
|
x509: boolean;
|
|
perfLoop: boolean;
|
|
tlsCa: boolean;
|
|
webcrypto: boolean;
|
|
}
|
|
|
|
interface EarlyExecutableCacheStamp {
|
|
version: 1;
|
|
key: string;
|
|
frontend: FrontendInputSnapshot;
|
|
files: {
|
|
translationUnit: CachedExecutableFile;
|
|
ir: CachedExecutableFile | null;
|
|
executable: CachedExecutableFile | null;
|
|
debugSymbols?: CachedExecutableFile;
|
|
};
|
|
nativeDependencies: NativeArtifactDependency[] | null;
|
|
native: EarlyExecutableNativeFeatures;
|
|
/** The build's performance warnings (SC61xx), which a cache hit repeats. */
|
|
performance?: ScrDiagnostic[];
|
|
integrity: string;
|
|
}
|
|
|
|
export interface EarlyExecutableCacheOptions {
|
|
entryPath: string;
|
|
outDir: string;
|
|
outPath: string;
|
|
emitIr: boolean;
|
|
sanitize: boolean;
|
|
backend: "llvm";
|
|
/** Omitted is the historical release posture and preserves v1 keys. */
|
|
optimization?: "dev" | "speed";
|
|
/** Omitted retains the unstripped executable's historical cache key. */
|
|
strip?: true;
|
|
/** Omitted for the default Windows console subsystem. */
|
|
windowsSubsystem?: "gui";
|
|
/** Omitted unless --detached-exit is set. */
|
|
detachedExit?: true;
|
|
/** Omitted for the x86-64 baseline (`--target-cpu`). */
|
|
targetCpu?: TargetCpu;
|
|
/** Omitted without --profile-generate/--profile-use; otherwise the
|
|
* pattern, or the digest of the profile's bytes. */
|
|
profile?: string;
|
|
/** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the
|
|
* tsc-ts executable's file identity. */
|
|
checker?: string;
|
|
/** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */
|
|
youngNursery?: true;
|
|
npmStatic: readonly string[] | "auto" | null;
|
|
/** Raw manifest identity: path and bytes. Native archives remain under the
|
|
* stricter native cache's independent dependency validation. */
|
|
ffiProfile: { path: string; bytes: Uint8Array } | null;
|
|
target: string;
|
|
compiler: string[];
|
|
nativeEnvironment: string;
|
|
nodeVersion: string;
|
|
implementation: string;
|
|
implementationDependencies: CompilerImplementationDependency[];
|
|
}
|
|
|
|
export type EarlyExecutableRouteOptions = Omit<
|
|
EarlyExecutableCacheOptions,
|
|
"implementation" | "implementationDependencies" | "nativeEnvironment"
|
|
> & {
|
|
/** Discovery waits until a complete payload and its frontend proof match. */
|
|
nativeEnvironment: string | (() => Promise<string>);
|
|
};
|
|
|
|
interface EarlyExecutableRouteStamp {
|
|
version: 2;
|
|
key: string;
|
|
implementation: string;
|
|
nativeEnvironment: string;
|
|
integrity: string;
|
|
}
|
|
|
|
interface EarlyExecutableImplementationProof {
|
|
version: 1;
|
|
implementation: string;
|
|
dependencies: CompilerImplementationDependency[];
|
|
integrity: string;
|
|
}
|
|
|
|
export interface EarlyExecutableCacheHit {
|
|
llvmPath: string;
|
|
irPath?: string;
|
|
/** When the executable was not restored: private copies of the cached LLVM text and IR beside
|
|
* llvmPath and irPath, which the build compiles from and installs at those names when it is done
|
|
* (index.ts BuildSources). A restored executable needs no compile, and its files are installed at
|
|
* llvmPath and irPath. */
|
|
llvmSourcePath?: string;
|
|
irSourcePath?: string;
|
|
/** llvmPath (irPath) already holds the cached bytes: the build leaves it as it is. */
|
|
llvmPathCurrent?: true;
|
|
irPathCurrent?: true;
|
|
/** True when the final executable was restored after native dependencies
|
|
* validated. False restores only frontend artifacts and must call compileC. */
|
|
executableRestored: boolean;
|
|
native: EarlyExecutableNativeFeatures;
|
|
frontend: FrontendInputSnapshot;
|
|
/** The performance warnings the build reported (SC61xx), repeated on a hit. */
|
|
performance?: ScrDiagnostic[];
|
|
}
|
|
|
|
export interface EarlyExecutableCachePublish extends EarlyExecutableCacheHit {
|
|
nativeDependencies?: NativeArtifactDependency[];
|
|
}
|
|
|
|
const BOOLEAN_NATIVE_KEYS = [
|
|
"regex",
|
|
"copying",
|
|
"textDecoderLegacy",
|
|
"fileHandle",
|
|
"fetch",
|
|
"zlib",
|
|
"assert",
|
|
"inspect",
|
|
"dynInvoke",
|
|
"dc",
|
|
"dynAsync",
|
|
"events",
|
|
"emitter",
|
|
"symbol",
|
|
"bigint",
|
|
"searchParams",
|
|
"qs",
|
|
"utilPromisify",
|
|
"dynBuiltinClass",
|
|
"dynBuiltinStream",
|
|
"parseArgs",
|
|
"stream",
|
|
"net",
|
|
"http",
|
|
"http2",
|
|
"dgram",
|
|
"dnsPromises",
|
|
"dgramDyn",
|
|
"textCodecDyn",
|
|
"watch",
|
|
"foreignFfi",
|
|
"nodeTest",
|
|
"tls",
|
|
"tlsCa",
|
|
"cipher",
|
|
"ecdh",
|
|
"dh",
|
|
"pki",
|
|
"ed25519",
|
|
"x509",
|
|
"webcrypto",
|
|
"perfLoop",
|
|
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
|
|
|
|
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
|
|
return validSharedNativeFeatures<EarlyExecutableNativeFeatures>(
|
|
value,
|
|
BOOLEAN_NATIVE_KEYS,
|
|
(native) =>
|
|
(native.optimization === undefined ||
|
|
native.optimization === "dev" ||
|
|
native.optimization === "speed") &&
|
|
(native.workers === undefined || typeof native.workers === "boolean") &&
|
|
(native.youngNursery === undefined || typeof native.youngNursery === "boolean"),
|
|
);
|
|
}
|
|
|
|
function cacheKey(options: EarlyExecutableCacheOptions): string {
|
|
const ffiParts: (string | Uint8Array)[] =
|
|
options.ffiProfile === null
|
|
? ["<ffi-off>"]
|
|
: [resolve(options.ffiProfile.path), options.ffiProfile.bytes];
|
|
return sharedCacheKey("early-executable-v1", [
|
|
resolve(options.entryPath),
|
|
resolve(options.outDir),
|
|
resolve(options.outPath),
|
|
options.emitIr ? "emit-ir" : "no-ir",
|
|
options.sanitize ? "sanitize" : "plain",
|
|
"static",
|
|
options.backend,
|
|
...optimizationKeyParts(options.optimization),
|
|
...(options.strip ? ["strip"] : []),
|
|
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
|
|
...(options.detachedExit ? ["detached-exit"] : []),
|
|
...targetCpuKeyParts(options.targetCpu),
|
|
...(options.profile !== undefined ? [options.profile] : []),
|
|
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
|
|
...(options.youngNursery ? ["young-nursery"] : []),
|
|
options.npmStatic === null
|
|
? "<npm-static-off>"
|
|
: options.npmStatic === "auto"
|
|
? "<npm-static-auto>"
|
|
: JSON.stringify(options.npmStatic),
|
|
options.target,
|
|
options.compiler.join("\x1f"),
|
|
options.nativeEnvironment,
|
|
options.nodeVersion,
|
|
options.implementation,
|
|
...ffiParts,
|
|
]);
|
|
}
|
|
|
|
function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">): string {
|
|
const hash = createHash("sha256")
|
|
.update("early-executable-route-v2\0")
|
|
.update(compilerReleaseVersion())
|
|
.update("\0")
|
|
.update(compilerImplementationRoot())
|
|
.update("\0")
|
|
.update(resolve(options.entryPath))
|
|
.update("\0")
|
|
.update(resolve(options.outDir))
|
|
.update("\0")
|
|
.update(resolve(options.outPath))
|
|
.update("\0")
|
|
.update(options.emitIr ? "emit-ir" : "no-ir")
|
|
.update("\0")
|
|
.update(options.sanitize ? "sanitize" : "plain")
|
|
.update("\0")
|
|
.update("static")
|
|
.update("\0")
|
|
.update(options.backend)
|
|
.update("\0");
|
|
for (const part of optimizationKeyParts(options.optimization)) hash.update(`${part}\0`);
|
|
if (options.strip) hash.update("strip\0");
|
|
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
|
|
if (options.detachedExit) hash.update("detached-exit\0");
|
|
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
|
|
if (options.profile !== undefined) hash.update(`${options.profile}\0`);
|
|
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
|
|
if (options.youngNursery) hash.update("young-nursery\0");
|
|
hash
|
|
.update(
|
|
options.npmStatic === null
|
|
? "<npm-static-off>"
|
|
: options.npmStatic === "auto"
|
|
? "<npm-static-auto>"
|
|
: JSON.stringify(options.npmStatic),
|
|
)
|
|
.update("\0")
|
|
.update(options.target)
|
|
.update("\0")
|
|
.update(options.compiler.join("\x1f"))
|
|
.update("\0")
|
|
.update(options.nodeVersion)
|
|
.update("\0");
|
|
if (options.ffiProfile === null) {
|
|
hash.update("<ffi-off>");
|
|
} else {
|
|
hash.update(resolve(options.ffiProfile.path)).update("\0").update(options.ffiProfile.bytes);
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
function routePath(
|
|
root: string,
|
|
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
|
): string {
|
|
return join(root, "early-exe-route", routeKey(options));
|
|
}
|
|
|
|
function nativeBuildMarkerPath(
|
|
root: string,
|
|
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
|
): string {
|
|
return join(root, "early-exe-native-build", routeKey(options));
|
|
}
|
|
|
|
/** Records that a build of the invocation `options` emitted its native program object, so the
|
|
* native helper's partition cache holds its partitions. Unlike the route, the record does not
|
|
* depend on the build publishing its executable. */
|
|
export async function recordEarlyExecutableNativeBuild(
|
|
root: string | null,
|
|
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
|
): Promise<void> {
|
|
if (root === null) return;
|
|
const marker = nativeBuildMarkerPath(root, options);
|
|
await mkdir(dirname(marker), { recursive: true, mode: 0o700 });
|
|
await writeFile(marker, "", { mode: 0o600 });
|
|
}
|
|
|
|
/** Whether an earlier build of the invocation `options` recorded its native program object
|
|
* (`recordEarlyExecutableNativeBuild`). False when there is no cache. */
|
|
export async function hasEarlyExecutableNativeBuild(
|
|
root: string | null,
|
|
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
|
): Promise<boolean> {
|
|
if (root === null) return false;
|
|
return stat(nativeBuildMarkerPath(root, options)).then(
|
|
(info) => info.isFile(),
|
|
() => false,
|
|
);
|
|
}
|
|
|
|
function routeIntegrity(stamp: Omit<EarlyExecutableRouteStamp, "integrity">): string {
|
|
return createHash("sha256")
|
|
.update("early-executable-route-stamp-v2\0")
|
|
.update(JSON.stringify(stamp))
|
|
.digest("hex");
|
|
}
|
|
|
|
function implementationProofPath(root: string, implementation: string): string {
|
|
const install = createHash("sha256")
|
|
.update("early-executable-install-v1\0")
|
|
.update(compilerImplementationRoot())
|
|
.digest("hex");
|
|
return join(root, "early-exe-implementation", install, implementation);
|
|
}
|
|
|
|
function implementationProofIntegrity(
|
|
proof: Omit<EarlyExecutableImplementationProof, "integrity">,
|
|
): string {
|
|
return createHash("sha256")
|
|
.update("early-executable-implementation-proof-v1\0")
|
|
.update(JSON.stringify(proof))
|
|
.digest("hex");
|
|
}
|
|
|
|
async function installCacheMetadata(source: string, destination: string): Promise<void> {
|
|
await rename(source, destination).catch(async () => {
|
|
// Windows does not replace an existing destination with rename(). Route
|
|
// and implementation-proof files are republished on every full compile,
|
|
// so use the same replacement fallback as the payload cache below.
|
|
await rm(destination, { force: true });
|
|
await rename(source, destination);
|
|
});
|
|
}
|
|
|
|
function stampPath(root: string, options: EarlyExecutableCacheOptions): string {
|
|
return sharedStampPath(root, "early-exe", cacheKey(options));
|
|
}
|
|
|
|
function stampIntegrity(stamp: Omit<EarlyExecutableCacheStamp, "integrity">): string {
|
|
return sharedStampIntegrity("early-executable-stamp-v1", stamp);
|
|
}
|
|
|
|
function executableFrontendOutputExclusions(
|
|
options: EarlyExecutableCacheOptions,
|
|
backend: "llvm",
|
|
): ReturnType<typeof frontendOutputExclusions> {
|
|
return frontendOutputExclusions(options, backend, "", [
|
|
options.outPath,
|
|
`${options.outPath}.dSYM`,
|
|
]);
|
|
}
|
|
|
|
/** Ordinary source/configuration edits are cheap misses before driver
|
|
* discovery. Replay declaration-file bytes and resolution metadata in the
|
|
* final complete proof instead of twice. */
|
|
function routedFrontendCandidate(frontend: FrontendInputSnapshot): FrontendInputSnapshot {
|
|
return {
|
|
...frontend,
|
|
probes: frontend.probes.filter(
|
|
(probe) => probe.op === "file" && !/\.d\.(?:ts|mts|cts)$/.test(probe.path),
|
|
),
|
|
};
|
|
}
|
|
|
|
async function fileMatches(
|
|
destination: string,
|
|
expectedDigest: string,
|
|
expectedMode?: number,
|
|
): Promise<boolean> {
|
|
try {
|
|
if (expectedMode !== undefined) {
|
|
const info = await stat(destination);
|
|
if (!info.isFile() || (info.mode & 0o777) !== expectedMode) return false;
|
|
}
|
|
return digest(await readFile(destination)) === expectedDigest;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function installExecutable(bytes: Uint8Array, destination: string): Promise<void> {
|
|
await mkdir(dirname(destination), { recursive: true });
|
|
const tmp = join(
|
|
dirname(destination),
|
|
`.scriptc-exe-bin-${process.pid}-${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
try {
|
|
await writeFile(tmp, bytes, { mode: 0o600 });
|
|
await chmod(tmp, 0o777 & ~process.umask());
|
|
await rename(tmp, destination);
|
|
} finally {
|
|
await rm(tmp, { force: true }).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
export async function readEarlyExecutableCache(
|
|
root: string | null,
|
|
options: EarlyExecutableCacheOptions,
|
|
): Promise<EarlyExecutableCacheHit | null> {
|
|
return readExecutableCache(root, options);
|
|
}
|
|
|
|
async function readExecutableCache(
|
|
root: string | null,
|
|
options: EarlyExecutableCacheOptions,
|
|
validateRoute?: () => Promise<boolean>,
|
|
): Promise<EarlyExecutableCacheHit | null> {
|
|
if (root === null) return null;
|
|
const path = stampPath(root, options);
|
|
try {
|
|
const stamp = JSON.parse(await readFile(path, "utf8")) as EarlyExecutableCacheStamp;
|
|
const { integrity, ...unsigned } = stamp;
|
|
if (
|
|
stamp.version !== 1 ||
|
|
stamp.key !== cacheKey(options) ||
|
|
!validFrontendInputSnapshot(stamp.frontend) ||
|
|
!validNativeFeatures(stamp.native) ||
|
|
stamp.files?.translationUnit?.name !== "program.tu" ||
|
|
!/^[0-9a-f]{64}$/.test(stamp.files.translationUnit.digest) ||
|
|
(stamp.files.ir !== null &&
|
|
(stamp.files.ir?.name !== "program.ir.json" ||
|
|
!/^[0-9a-f]{64}$/.test(stamp.files.ir.digest))) ||
|
|
(stamp.files.executable !== null &&
|
|
(stamp.files.executable?.name !== "program.bin" ||
|
|
!/^[0-9a-f]{64}$/.test(stamp.files.executable.digest))) ||
|
|
(stamp.files.debugSymbols !== undefined &&
|
|
(stamp.files.executable === null ||
|
|
stamp.files.debugSymbols.name !== "program.dsym" ||
|
|
!/^[0-9a-f]{64}$/.test(stamp.files.debugSymbols.digest))) ||
|
|
(stamp.files.executable !== null &&
|
|
needsDarwinDebugSymbols(
|
|
options.target.split(":")[1] ?? "",
|
|
options.optimization,
|
|
options.strip,
|
|
) !==
|
|
(stamp.files.debugSymbols !== undefined)) ||
|
|
(stamp.files.executable === null) !== (stamp.nativeDependencies === null) ||
|
|
(stamp.nativeDependencies !== null && !Array.isArray(stamp.nativeDependencies)) ||
|
|
(stamp.files.ir !== null) !== options.emitIr ||
|
|
stampIntegrity(unsigned) !== integrity ||
|
|
!frontendInputsStillMatch(
|
|
validateRoute === undefined ? stamp.frontend : routedFrontendCandidate(stamp.frontend),
|
|
executableFrontendOutputExclusions(options, stamp.native.backend),
|
|
)
|
|
)
|
|
return null;
|
|
|
|
// Startup may defer driver discovery until the source/configuration proof
|
|
// matches. A frontend-only entry cannot serve that route, so it should
|
|
// fall through without probing a driver the full compiler will probe too.
|
|
if (validateRoute !== undefined && stamp.files.executable === null) return null;
|
|
const directory = dirname(path);
|
|
const [translationUnit, ir, executable, debugSymbols] = await Promise.all([
|
|
readCachedFile(
|
|
join(directory, stamp.files.translationUnit.name),
|
|
stamp.files.translationUnit.digest,
|
|
),
|
|
stamp.files.ir === null
|
|
? Promise.resolve(null)
|
|
: readCachedFile(join(directory, stamp.files.ir.name), stamp.files.ir.digest),
|
|
stamp.files.executable === null
|
|
? Promise.resolve(null)
|
|
: readCachedFile(
|
|
join(directory, stamp.files.executable.name),
|
|
stamp.files.executable.digest,
|
|
),
|
|
stamp.files.debugSymbols === undefined
|
|
? Promise.resolve(null)
|
|
: readCachedFile(
|
|
join(directory, stamp.files.debugSymbols.name),
|
|
stamp.files.debugSymbols.digest,
|
|
),
|
|
]);
|
|
if (
|
|
translationUnit === null ||
|
|
(stamp.files.ir !== null && ir === null) ||
|
|
(stamp.files.executable !== null && executable === null) ||
|
|
(stamp.files.debugSymbols !== undefined && debugSymbols === null)
|
|
)
|
|
return null;
|
|
|
|
if (validateRoute !== undefined) {
|
|
if (!(await validateRoute())) return null;
|
|
// The compiler proof is checked AFTER deferred discovery by the routed
|
|
// reader. Recheck source/configuration bytes here too before restoring.
|
|
if (
|
|
!frontendInputsStillMatch(
|
|
stamp.frontend,
|
|
executableFrontendOutputExclusions(options, stamp.native.backend),
|
|
)
|
|
)
|
|
return null;
|
|
}
|
|
// Validate the native proof before restoring frontend artifacts: replacing
|
|
// a TU can update its output directory metadata, which is itself part of
|
|
// compileC's same-output dependency snapshot.
|
|
let executableRestored =
|
|
executable !== null &&
|
|
stamp.nativeDependencies !== null &&
|
|
(await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies)) &&
|
|
// Recheck after hashing every dependency: a concurrent tool/runtime
|
|
// update must not win the window immediately before installation.
|
|
(await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies));
|
|
if (validateRoute !== undefined && !executableRestored) return null;
|
|
const paths = outputPaths(options, stamp.native.backend);
|
|
if (executableRestored) {
|
|
try {
|
|
if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath);
|
|
const expectedMode = 0o777 & ~process.umask();
|
|
if (!(await fileMatches(options.outPath, stamp.files.executable!.digest, expectedMode))) {
|
|
await installExecutable(executable!, options.outPath);
|
|
}
|
|
if (debugSymbols === null && options.target.split(":")[1] === "darwin") {
|
|
await rm(`${options.outPath}.dSYM`, { recursive: true, force: true });
|
|
}
|
|
} catch {
|
|
executableRestored = false;
|
|
}
|
|
}
|
|
// A build that compiles from the cached translation unit compiles its own copy: a concurrent
|
|
// build of another program with the same entry name may replace the shared names meanwhile.
|
|
let llvmSourcePath: string | undefined;
|
|
let irSourcePath: string | undefined;
|
|
let llvmPathCurrent = false;
|
|
let irPathCurrent = false;
|
|
if (executableRestored) {
|
|
if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest)))
|
|
await installBytes(translationUnit, paths.llvmPath);
|
|
if (
|
|
ir !== null &&
|
|
stamp.files.ir !== null &&
|
|
!(await fileMatches(paths.irPath, stamp.files.ir.digest))
|
|
)
|
|
await installBytes(ir, paths.irPath);
|
|
} else {
|
|
// The extension keeps clang reading the copy as LLVM IR rather than as a linker input.
|
|
llvmSourcePath = `${privateSiblingPath(paths.llvmPath, "llvm")}.ll`;
|
|
await installBytes(translationUnit, llvmSourcePath);
|
|
llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest);
|
|
if (ir !== null && stamp.files.ir !== null) {
|
|
irSourcePath = privateSiblingPath(paths.irPath, "ir");
|
|
await installBytes(ir, irSourcePath);
|
|
irPathCurrent = await fileMatches(paths.irPath, stamp.files.ir.digest);
|
|
}
|
|
}
|
|
const now = new Date();
|
|
await Promise.all(
|
|
[
|
|
path,
|
|
join(directory, stamp.files.translationUnit.name),
|
|
...(stamp.files.ir === null ? [] : [join(directory, stamp.files.ir.name)]),
|
|
...(stamp.files.executable === null ? [] : [join(directory, stamp.files.executable.name)]),
|
|
...(stamp.files.debugSymbols === undefined
|
|
? []
|
|
: [join(directory, stamp.files.debugSymbols.name)]),
|
|
].map((cachePath) => utimes(cachePath, now, now).catch(() => undefined)),
|
|
);
|
|
return {
|
|
llvmPath: paths.llvmPath,
|
|
native: stamp.native,
|
|
executableRestored,
|
|
frontend: stamp.frontend,
|
|
...(Array.isArray(stamp.performance) && stamp.performance.length > 0
|
|
? { performance: stamp.performance }
|
|
: {}),
|
|
...(ir === null ? {} : { irPath: paths.irPath }),
|
|
...(llvmSourcePath === undefined ? {} : { llvmSourcePath }),
|
|
...(irSourcePath === undefined ? {} : { irSourcePath }),
|
|
...(llvmPathCurrent ? { llvmPathCurrent: true as const } : {}),
|
|
...(irPathCurrent ? { irPathCurrent: true as const } : {}),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Follow the exact-invocation route without hashing/importing the complete
|
|
* compiler package. Source/configuration misses skip deferred driver discovery;
|
|
* a hit still proves the current native environment before restoring output.
|
|
* The route is only an index: the full payload key retains that environment. */
|
|
export async function readRoutedExecutableCache(
|
|
root: string | null,
|
|
options: EarlyExecutableRouteOptions,
|
|
): Promise<EarlyExecutableCacheHit | null> {
|
|
if (root === null) return null;
|
|
try {
|
|
const routeFile = routePath(root, options);
|
|
const route = JSON.parse(await readFile(routeFile, "utf8")) as EarlyExecutableRouteStamp;
|
|
const { integrity, ...unsigned } = route;
|
|
if (
|
|
route.version !== 2 ||
|
|
!/^[0-9a-f]{64}$/.test(route.key) ||
|
|
!/^[0-9a-f]{64}$/.test(route.implementation) ||
|
|
typeof route.nativeEnvironment !== "string" ||
|
|
(typeof options.nativeEnvironment === "string" &&
|
|
options.nativeEnvironment !== route.nativeEnvironment) ||
|
|
routeIntegrity(unsigned) !== integrity
|
|
)
|
|
return null;
|
|
const proofFile = implementationProofPath(root, route.implementation);
|
|
const proof = JSON.parse(
|
|
await readFile(proofFile, "utf8"),
|
|
) as EarlyExecutableImplementationProof;
|
|
const { integrity: proofIntegrity, ...proofUnsigned } = proof;
|
|
if (
|
|
proof.version !== 1 ||
|
|
proof.implementation !== route.implementation ||
|
|
!Array.isArray(proof.dependencies) ||
|
|
implementationProofIntegrity(proofUnsigned) !== proofIntegrity
|
|
)
|
|
return null;
|
|
const complete: EarlyExecutableCacheOptions = {
|
|
...options,
|
|
nativeEnvironment: route.nativeEnvironment,
|
|
implementation: route.implementation,
|
|
implementationDependencies: proof.dependencies,
|
|
};
|
|
if (cacheKey(complete) !== route.key) return null;
|
|
const hit = await readExecutableCache(root, complete, async () => {
|
|
const environment =
|
|
typeof options.nativeEnvironment === "function"
|
|
? await options.nativeEnvironment()
|
|
: options.nativeEnvironment;
|
|
return (
|
|
environment === route.nativeEnvironment &&
|
|
(await compilerImplementationDependenciesStillMatch(proof.dependencies))
|
|
);
|
|
});
|
|
if (hit?.executableRestored !== true) return null;
|
|
const now = new Date();
|
|
await Promise.all(
|
|
[routeFile, proofFile].map((cachePath) => utimes(cachePath, now, now).catch(() => undefined)),
|
|
);
|
|
return hit;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Publish the lightweight lookup metadata for an already-validated payload.
|
|
* Full-compiler cache hits call this as a repair path: route/proof files can be
|
|
* evicted independently of the larger executable entry, and one fallback
|
|
* lookup should make later CLI invocations lightweight again. */
|
|
export async function publishEarlyExecutableRoute(
|
|
root: string | null,
|
|
options: EarlyExecutableCacheOptions,
|
|
): Promise<void> {
|
|
if (root === null) return;
|
|
const proofDestination = implementationProofPath(root, options.implementation);
|
|
const proofUnsigned: Omit<EarlyExecutableImplementationProof, "integrity"> = {
|
|
version: 1,
|
|
implementation: options.implementation,
|
|
dependencies: options.implementationDependencies,
|
|
};
|
|
const proof: EarlyExecutableImplementationProof = {
|
|
...proofUnsigned,
|
|
integrity: implementationProofIntegrity(proofUnsigned),
|
|
};
|
|
await mkdir(dirname(proofDestination), { recursive: true, mode: 0o700 });
|
|
const proofTmp = `${proofDestination}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
try {
|
|
await writeFile(proofTmp, `${JSON.stringify(proof)}\n`, { mode: 0o600 });
|
|
await installCacheMetadata(proofTmp, proofDestination);
|
|
} finally {
|
|
await rm(proofTmp, { force: true }).catch(() => undefined);
|
|
}
|
|
|
|
const routeOptions: EarlyExecutableRouteOptions = options;
|
|
const routeDestination = routePath(root, routeOptions);
|
|
const routeUnsigned: Omit<EarlyExecutableRouteStamp, "integrity"> = {
|
|
version: 2,
|
|
key: cacheKey(options),
|
|
implementation: options.implementation,
|
|
nativeEnvironment: options.nativeEnvironment,
|
|
};
|
|
const route: EarlyExecutableRouteStamp = {
|
|
...routeUnsigned,
|
|
integrity: routeIntegrity(routeUnsigned),
|
|
};
|
|
await mkdir(dirname(routeDestination), { recursive: true, mode: 0o700 });
|
|
const routeTmp = `${routeDestination}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
try {
|
|
await writeFile(routeTmp, `${JSON.stringify(route)}\n`, { mode: 0o600 });
|
|
await installCacheMetadata(routeTmp, routeDestination);
|
|
} finally {
|
|
await rm(routeTmp, { force: true }).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
export async function publishEarlyExecutableCache(
|
|
root: string | null,
|
|
options: EarlyExecutableCacheOptions,
|
|
result: EarlyExecutableCachePublish,
|
|
): Promise<void> {
|
|
if (root === null || !result.frontend.stable) return;
|
|
const destination = dirname(stampPath(root, options));
|
|
const parent = dirname(destination);
|
|
const stage = join(
|
|
parent,
|
|
`.tmp-${basename(destination).slice(0, 12)}-${process.pid}-${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
try {
|
|
await mkdir(stage, { recursive: true, mode: 0o700 });
|
|
const publishFile = async (source: string, name: string): Promise<CachedExecutableFile> => {
|
|
const target = join(stage, name);
|
|
await copyFile(source, target);
|
|
await chmod(target, 0o600);
|
|
return { name, digest: digest(await readFile(target)) };
|
|
};
|
|
const publishExecutable =
|
|
result.nativeDependencies === undefined || !result.executableRestored
|
|
? Promise.resolve(null)
|
|
: publishFile(options.outPath, "program.bin");
|
|
const [translationUnit, ir, executable] = await Promise.all([
|
|
publishFile(result.llvmPath, "program.tu"),
|
|
result.irPath === undefined
|
|
? Promise.resolve(null)
|
|
: publishFile(result.irPath, "program.ir.json"),
|
|
publishExecutable,
|
|
]);
|
|
let debugSymbols: CachedExecutableFile | undefined;
|
|
if (
|
|
executable !== null &&
|
|
needsDarwinDebugSymbols(
|
|
options.target.split(":")[1] ?? "",
|
|
options.optimization,
|
|
options.strip,
|
|
)
|
|
) {
|
|
const bytes = await readDarwinDebugSymbols(options.outPath);
|
|
await writeFile(join(stage, "program.dsym"), bytes, { mode: 0o600 });
|
|
debugSymbols = { name: "program.dsym", digest: digest(bytes) };
|
|
}
|
|
if (
|
|
!frontendInputsStillMatch(
|
|
result.frontend,
|
|
executableFrontendOutputExclusions(options, result.native.backend),
|
|
)
|
|
)
|
|
return;
|
|
const unsigned: Omit<EarlyExecutableCacheStamp, "integrity"> = {
|
|
version: 1,
|
|
key: cacheKey(options),
|
|
frontend: result.frontend,
|
|
files: {
|
|
translationUnit,
|
|
ir,
|
|
executable,
|
|
...(debugSymbols === undefined ? {} : { debugSymbols }),
|
|
},
|
|
nativeDependencies: executable === null ? null : result.nativeDependencies!,
|
|
native: result.native,
|
|
...(result.performance === undefined || result.performance.length === 0
|
|
? {}
|
|
: { performance: result.performance }),
|
|
};
|
|
const stamp: EarlyExecutableCacheStamp = { ...unsigned, integrity: stampIntegrity(unsigned) };
|
|
await writeFile(join(stage, "stamp.json"), `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
|
|
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
const install = async (name: string): Promise<void> => {
|
|
const source = join(stage, name);
|
|
const target = join(destination, name);
|
|
await rename(source, target).catch(async () => {
|
|
await rm(target, { force: true });
|
|
await rename(source, target);
|
|
});
|
|
};
|
|
await install(translationUnit.name);
|
|
if (ir !== null) await install(ir.name);
|
|
if (executable !== null) await install(executable.name);
|
|
if (debugSymbols !== undefined) await install(debugSymbols.name);
|
|
await install("stamp.json");
|
|
await publishEarlyExecutableRoute(root, options);
|
|
} finally {
|
|
await rm(stage, { recursive: true, force: true }).catch(() => undefined);
|
|
}
|
|
}
|