Files
scriptc/packages/compiler/src/backend/native/executable.ts
T
2026-10-21 00:08:02 -05:00

1623 lines
64 KiB
TypeScript

import {
toolchainEnvironmentCachePolicy,
toolchainEnvironmentFingerprint,
} from "../toolchain-environment.js";
import { isMobileTarget } from "../target-platform.js";
import { createHash } from "node:crypto";
import { chmod, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
import { availableParallelism, tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import {
executableOptimizationLinkerArgs,
executableStripLinkerArgs,
windowsSubsystemLinkerArgs,
} from "../targets.js";
import {
createDarwinDebugSymbols,
installDarwinDebugSymbols,
needsDarwinDebugSymbols,
readDarwinDebugSymbols,
} from "../debug-symbols.js";
import {
cacheRootDir,
copyValidCachedFile,
ensurePrivateCacheRoot,
fileDigest,
installArtifact,
privateSiblingPath,
protectCachedArtifact,
pruneCache,
publishCachedFile,
} from "../build-cache.js";
import { type CcOptions, type NativeArtifactDependency } from "./contracts.js";
import {
runtimeSrcDir,
EXECUTABLE_RUNTIME_SOURCES,
runtimeFingerprint,
runtimeFingerprintInputPaths,
} from "./runtime-inputs.js";
import {
resolveCc,
targetPlatform,
executableSectionEliminationFlags,
cacheTargetIdentity,
isZigDriver,
isMuslTarget,
} from "./driver.js";
import {
resolveProgramShardMergeIdentity,
selectProgramShards,
updateProgramShardCacheIdentity,
} from "./program-shards.js";
import {
compilerDriverSupportsPersistentCache,
resolvedTool,
directCompilerSelections,
compilerDriverProbeKey,
ccVersion,
} from "./tool-identity.js";
import {
localArtifactIdentity,
localArtifactStampPath,
localArtifactHit,
type NativeMetadataStamp,
nativeMetadataKey,
readNativeMetadataStamp,
publishNativeMetadataStamp,
snapshotLocalArtifactDependencies,
nativeArtifactDependenciesStillMatch,
publishLocalArtifactStamp,
} from "./artifact-stamps.js";
import {
implicitToolchainFingerprints,
effectiveCompilerInvocationFingerprint,
translationUnitDependencyFingerprint,
implicitToolchainFingerprint,
} from "./compiler-fingerprint.js";
import {
fingerprintDependencyPaths,
fingerprintDependenciesStillMatch,
} from "./dependency-files.js";
import {
currentVendorCacheBuildIdentity,
vendorBuildCacheRoot,
tlsArchivePath,
lreObjectPaths,
zlibObjectPaths,
stageVendorInputs,
ensureTlsArchive,
ensureLreObjects,
ensureZlibObjects,
vendorEngineDir,
vendorZlibDir,
vendorTlsDir,
} from "./vendor.js";
import { execFileAsync, subprocessFailureDetail, CcCompileError } from "./process.js";
import { implicitLinkerFingerprint } from "./linker-fingerprint.js";
import { CacheInputsChangedError } from "./session.js";
import { ensureRuntimeObjects, stageRuntimeObjects, clearCcCaches } from "./runtime-objects.js";
import { localizeLibraryObjects } from "./object-merge.js";
/** What the runtime-unit table reads: the build options, the target driver,
* the units other options imply, and the vendor inputs resolved for this
* build. */
interface RuntimeUnitContext {
opts: CcOptions;
driver: ReturnType<typeof resolveCc>;
rt: (path: string) => string;
rtDir: string;
regex: boolean;
nativeFetch: boolean;
net: boolean;
http: boolean;
tls: boolean;
tlsCa: boolean;
cipher: boolean;
ecdh: boolean;
lreObjects: readonly string[];
zlibObjects: readonly string[];
tlsArchive: string | null;
}
/** An optional runtime input: runtime sources compiled when `when` holds,
* or command-line arguments computed from the build. */
interface RuntimeUnit {
when: (u: RuntimeUnitContext) => boolean | undefined;
sources: readonly string[];
args?: (u: RuntimeUnitContext) => readonly string[];
}
const unit = (when: RuntimeUnit["when"], ...sources: string[]): RuntimeUnit => ({ when, sources });
const unitArgs = (args: (u: RuntimeUnitContext) => readonly string[]): RuntimeUnit => ({
when: () => true,
sources: [],
args,
});
/** The optional runtime inputs, in link order. The order is part of the
* executable: a new unit goes next to the units it references. */
const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit(
(u) => u.opts.workers,
"scr_worker.c",
"scr_worker_events.c",
"scr_mailbox.c",
"scr_message.c",
),
unit((u) => u.opts.copying, "scr_copying.c"),
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
// GetUserNameA), iphlpapi (GetAdaptersAddresses behind
// os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
// units ride the same import). Never present on the default path, so
// the historical line cannot change.
unitArgs((u) =>
targetPlatform(u.driver) === "win32"
? [u.rt(join(u.rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
: [],
),
// musl deliberately has no libc-identification predefine; resolveCc's
// SCR_MUSL flag and this target-selected TU travel together.
unit((u) => isMuslTarget(u.driver), "scr_musl.c"),
// scr_glob.c (path.matchesGlob) compiles its segments through the
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
// so both ride the same switch; section GC drops them from binaries
// that never use them.
unitArgs((u) =>
u.regex
? [
"-I",
vendorEngineDir(),
u.rt(join(u.rtDir, "scr_regex.c")),
u.rt(join(u.rtDir, "scr_glob.c")),
u.rt(join(u.rtDir, "scr_idna.c")),
...u.lreObjects,
]
: [],
),
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
// Fixed-width Buffer methods called on checked-dynamic values.
unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
// Builtin functions read as values in JavaScript dependencies.
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
unit((u) => u.opts.wasm, "scr_wasm.c"),
unit(
(u) =>
u.opts.dynAsync ||
u.opts.dynInvoke ||
u.opts.workers ||
u.opts.dc ||
u.opts.fileHandle ||
u.nativeFetch,
"scr_async_dyn.c",
),
// The performance timeline (every perf_hooks user-timing call selects
// dynAsync, whose unit carries the timeline's buffer warning).
unit((u) => u.opts.dynAsync, "scr_perf.c"),
// perf_hooks event-loop metrics, only for programs that read them.
unit((u) => u.opts.perfLoop, "scr_perf_loop.c"),
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
// their reads as dyn promises).
unit((u) => u.opts.dynAsync, "scr_blob.c"),
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
// libz on the default host-clang build, vendored objects on Zig builds)
// also serves the native fetch's gzip decoder — spread exactly once.
unitArgs((u) =>
u.opts.zlib
? isZigDriver(u.driver)
? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects]
: [u.rt(join(u.rtDir, "scr_zlib.c"))]
: u.nativeFetch
? isZigDriver(u.driver)
? ["-I", vendorZlibDir(), ...u.zlibObjects]
: []
: [],
),
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
// readline's terminal mode: rl.terminal sets the regex unit it
// measures with.
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"),
// events.once with options registers through the emitter and
// event-target units.
unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"),
// The checked-dynamic HANDLE support unit (listener gate + runtime
// adapter closures): every referencing unit is one of the emitter or
// net families (http implies net), so handle-free binaries keep
// their exact size class.
unit((u) => u.opts.emitter || u.opts.workers || u.net, "scr_dyn_handle.c"),
unit((u) => u.opts.symbol, "scr_symbol.c"),
unit((u) => u.opts.assert && u.opts.bigint, "scr_bigint_assert.c"),
unit((u) => u.opts.qs, "scr_qs.c"),
// The legacy url unit references the regex, querystring and dyn-async
// units; the url.parse family's IR sets all three.
unit((u) => u.regex && u.opts.qs && u.opts.dynAsync, "scr_url_legacy.c"),
unit((u) => u.opts.parseArgs, "scr_util.c"),
unit((u) => u.opts.parseArgs && u.opts.symbol, "scr_util_compare.c"),
unit((u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync, "scr_util_style.c"),
unit(
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
// util.promisify over an untyped function: dyn promises and calls.
unit((u) => u.opts.utilPromisify && u.opts.dynAsync && u.opts.dynInvoke, "scr_util_promisify.c"),
unit(
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit(
(u) =>
u.opts.dynBuiltinStream &&
u.opts.dynBuiltinClass &&
u.opts.stream &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol,
"scr_dyn_builtin_stream.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
// Checked-dynamic stream slots test values and call JavaScript stream
// members through the dyn-invoke unit.
unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
// Own properties JavaScript adds to children.
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
// for await over checked-dynamic values.
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
// FileHandle streams reference both the stream unit and FileHandle.
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
// epoll on Linux, WSAPoll on Windows — each TU is empty off its
// platform, so all three link whenever a poller-using unit does and
// the others cost nothing (ws2_32 rides the unconditional win32 libs
// above).
unit((u) => u.net || u.opts.dgram, "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c"),
unit((u) => u.net, "scr_net.c"),
unit((u) => u.http, "scr_http.c"),
unit((u) => u.opts.http2, "scr_http2.c"),
unit((u) => u.opts.dgram, "scr_dgram.c"),
unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"),
unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"),
unit((u) => u.opts.watch, "scr_watch.c"),
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
unit((u) => u.opts.nodeTest, "scr_test.c"),
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
// references its default-set override unconditionally.
unit((u) => u.tlsCa, "scr_tls_ca.c"),
unitArgs((u) =>
u.tlsArchive
? [
"-I",
join(vendorTlsDir(), "include"),
...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []),
...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []),
...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []),
u.tlsArchive,
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
// The unconditional win32 libs above do not carry it.
// Never present on the default path, so the historical TLS
// link line cannot change.
...(targetPlatform(u.driver) === "win32" ? ["-lbcrypt"] : []),
]
: [],
),
// scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
// This is independent of the mbedTLS archive: getCACertificates-only
// programs need crypt32 too, while TLS programs imply the CA unit.
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
// Fetch uses the native network, TLS, and HTTP runtime.
unit((u) => u.nativeFetch, "scr_fetch.c"),
];
/** The arguments of the optional runtime inputs, in link order. */
function runtimeUnitArgs(u: RuntimeUnitContext): string[] {
return RUNTIME_UNITS.flatMap((entry) =>
entry.args
? [...entry.args(u)]
: entry.when(u)
? entry.sources.map((source) => u.rt(join(u.rtDir, source)))
: [],
);
}
/** The runtime's optional compile-time definitions, in command-line order.
* Both the program build and the runtime-object flags use this list. */
const RUNTIME_DEFINES: readonly {
define: string;
when: (opts: CcOptions) => boolean | undefined;
}[] = [
{ define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy },
// Checked objects carry the dynamic tier's shapes (scr_json.c's SCR_JS_TIER build); only
// programs with dynamic-tier code select it.
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
];
function runtimeDefineArgs(opts: CcOptions): string[] {
return RUNTIME_DEFINES.filter((d) => d.when(opts)).map((d) => `-D${d.define}`);
}
export async function compileCInternal(
opts: CcOptions,
cacheWarmOnly: boolean,
cacheWarmPaths?: Set<string>,
): Promise<void> {
const rtDir = runtimeSrcDir();
const sanitize = opts.sanitize ?? false;
const optimization = opts.optimization ?? "release";
if (opts.workers && targetPlatform(resolveCc()) === "wasi")
throw new Error("worker threads require a native static executable");
const regex = opts.regex ?? false;
// Fetch shares the native network, TLS, HTTP, and zlib runtime.
const nativeFetch = opts.fetch ?? false;
const net = (opts.net ?? false) || nativeFetch;
const http = (opts.http ?? false) || nativeFetch;
const tls = (opts.tls ?? false) || nativeFetch;
const tlsCa = (opts.tlsCa ?? false) || tls;
// Symmetric ciphers and ECDH link the same mbedTLS archive as TLS.
const cipher = opts.cipher ?? false;
const ecdh = opts.ecdh ?? false;
const mbedtls = tls || cipher || ecdh;
const driver = resolveCc();
const darwinDebugSymbols = needsDarwinDebugSymbols(
targetPlatform(driver),
optimization,
opts.strip,
);
if (opts.frameworks?.length && targetPlatform(driver) !== "darwin")
throw new Error("FFI frameworks require a Darwin target");
if (opts.frameworks?.some((name) => !/^[A-Za-z][A-Za-z0-9_]*$/.test(name)))
throw new Error("Invalid FFI framework name");
const debugFlags =
optimization === "dev" && !opts.strip
? ["-gline-tables-only", ...(opts.cPath.endsWith(".ll") ? [] : ["-gno-column-info"])]
: [];
const { programShards, programPublicSymbols, programShardMergeIdentity } =
await selectProgramShards(driver, opts, optimization === "dev" && !sanitize);
// Mobile triples produce library archives, never standalone executables:
// the executable-lane runtime (event loop, sockets, child processes) is
// not verified on those device classes. compile() reports the SC3002
// diagnostic first; this is the backstop for direct compileC callers.
if (isMobileTarget(driver.target)) {
throw new Error(
`SCRIPTC_TARGET=${driver.target} builds library-mode static archives only — ` +
`compile with a library profile (SCRIPTC_CC=zigcc scriptc build --lib --profile <profile.json>) and link the archive from the app project.`,
);
}
const runtimeSources =
targetPlatform(driver) === "wasi"
? EXECUTABLE_RUNTIME_SOURCES.filter((source) => source !== "scr_child.c")
: EXECUTABLE_RUNTIME_SOURCES;
const executableSectionFlags = executableSectionEliminationFlags(targetPlatform(driver));
const windowsSubsystemArgs = windowsSubsystemLinkerArgs(
targetPlatform(driver),
opts.windowsSubsystem,
);
const executableLinkFlags = [
...executableSectionFlags.link,
...executableOptimizationLinkerArgs(targetPlatform(driver), optimization),
...executableStripLinkerArgs(targetPlatform(driver), opts.strip ?? false),
...windowsSubsystemArgs,
];
// scr_async.c submits callback-style filesystem work to a native worker.
// POSIX drivers need the thread compile/link mode; win32 uses CreateThread.
const threadArgs =
targetPlatform(driver) === "win32" || targetPlatform(driver) === "wasi" ? [] : ["-pthread"];
const cachePolicy = toolchainEnvironmentCachePolicy();
const configuredCacheRoot = cacheRootDir();
const toolchainEnv = toolchainEnvironmentFingerprint();
const persistentDriverCache =
cachePolicy.runtimeObjects &&
configuredCacheRoot !== null &&
(await compilerDriverSupportsPersistentCache(driver, toolchainEnv));
// Only compiler-generated TUs opt in. Arbitrary `compileC` inputs
// inputs may include caller-owned headers whose contents are not otherwise
// represented in this key, so they retain the fully uncached historical
// path unless the caller supplies its own complete dependency identity.
const cacheIdentity = opts.cacheIdentity;
let persistentCache: { root: string; identity: string } | null =
cacheIdentity === undefined || configuredCacheRoot === null || !persistentDriverCache
? null
: { root: configuredCacheRoot, identity: cacheIdentity };
if (cacheWarmOnly && persistentCache === null) {
throw new Error("native cache warming requires a persistently cacheable compiler environment");
}
if (persistentCache !== null) {
try {
await ensurePrivateCacheRoot(
persistentCache.root,
process.env["SCRIPTC_CACHE_DIR"] === undefined,
);
} catch (error) {
if (cacheWarmOnly) {
throw new Error("native cache warming could not prepare the persistent cache root", {
cause: error,
});
}
persistentCache = null;
}
}
let localArtifact: {
stampPath: string;
key: string;
runtimeHash: string;
programBytes: Buffer;
compilerPath: string;
} | null = null;
// Generated executable TUs are closed over scriptc's own runtime tree. A
// same-output rebuild can therefore check those bytes directly before the
// broader cross-output CAS performs its compiler/SDK/linker rediscovery.
// FFI/native-input builds and the public arbitrary-C cache API stay on the
// strict path because their dependency graphs are caller-owned.
if (
!cacheWarmOnly &&
persistentCache !== null &&
cachePolicy.completeArtifacts &&
persistentCache.identity === "scriptc-generated-v1" &&
(opts.linkInputs?.length ?? 0) === 0 &&
(opts.frameworks?.length ?? 0) === 0 &&
(opts.systemLibraries?.length ?? 0) === 0 &&
process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] !== "1"
) {
try {
const [compiler, runtimeHash, programBytes] = await Promise.all([
resolvedTool(driver.argv[0] ?? "clang"),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
]);
if (compiler !== null) {
const effectiveCompiler =
directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? compiler;
const key = localArtifactIdentity(
opts,
driver,
toolchainEnv,
`${compiler.cacheIdentity}\0${effectiveCompiler.cacheIdentity}`,
runtimeHash,
programBytes,
programShardMergeIdentity,
);
const stampPath = localArtifactStampPath(persistentCache.root, opts.outPath);
localArtifact = {
stampPath,
key,
runtimeHash,
programBytes,
compilerPath: effectiveCompiler.canonicalPath,
};
const hit = await localArtifactHit(stampPath, opts.outPath, key, darwinDebugSymbols);
if (hit !== null) {
await opts.onArtifactReady?.({ dependencies: hit.dependencies }).catch(() => undefined);
return;
}
}
} catch {
// The output-local tier is only an optimization; the fully validated
// CAS below remains the source of truth on any metadata trouble.
localArtifact = null;
}
}
let implicitToolchain: string | null = null;
let implicitCompileToolchain: string | null = null;
let toolchainMetadataStamp: NativeMetadataStamp | null = null;
const metadataCompiler =
persistentCache === null ? null : await resolvedTool(driver.argv[0] ?? "clang");
const metadataEffectiveCompiler =
directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? metadataCompiler;
const toolchainMetadataKey =
persistentCache === null ||
metadataCompiler === null ||
metadataEffectiveCompiler === null ||
process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
? null
: nativeMetadataKey("toolchain", [
cacheTargetIdentity(driver),
toolchainEnv,
driver.argv,
driver.targetArgs,
metadataCompiler.cacheIdentity,
metadataEffectiveCompiler.cacheIdentity,
rtDir,
]);
if (persistentDriverCache) {
try {
toolchainMetadataStamp =
persistentCache === null || toolchainMetadataKey === null
? null
: await readNativeMetadataStamp(persistentCache.root, toolchainMetadataKey);
implicitToolchain = toolchainMetadataStamp?.values["implicitToolchain"] ?? null;
implicitCompileToolchain = toolchainMetadataStamp?.values["implicitCompileToolchain"] ?? null;
if (
implicitToolchain === null ||
(programShards !== null && implicitCompileToolchain === null)
) {
const fingerprints = await implicitToolchainFingerprints(driver, toolchainEnv);
implicitToolchain = fingerprints.complete;
implicitCompileToolchain = fingerprints.compile;
toolchainMetadataStamp = null;
}
let compilerVersion = toolchainMetadataStamp?.values["compilerVersion"];
if (compilerVersion === undefined) {
compilerVersion = await ccVersion(driver.argv, toolchainEnv, true);
}
if (
toolchainMetadataStamp === null &&
persistentCache !== null &&
toolchainMetadataKey !== null
) {
if (metadataCompiler !== null && metadataEffectiveCompiler !== null) {
toolchainMetadataStamp = await publishNativeMetadataStamp(
persistentCache.root,
toolchainMetadataKey,
{
implicitToolchain,
...(implicitCompileToolchain === null ? {} : { implicitCompileToolchain }),
compilerVersion,
},
[
metadataCompiler.canonicalPath,
metadataEffectiveCompiler.canonicalPath,
...fingerprintDependencyPaths(implicitToolchain),
...(implicitCompileToolchain === null
? []
: fingerprintDependencyPaths(implicitCompileToolchain)),
],
[
implicitToolchain,
...(implicitCompileToolchain === null ? [] : [implicitCompileToolchain]),
],
);
}
}
} catch (error) {
// Cache discovery is best-effort. In particular, a compiler wrapper can
// compile successfully without implementing the metadata probes.
if (cacheWarmOnly) {
throw new Error("native cache warming could not validate the compiler toolchain", {
cause: error,
});
}
persistentCache = null;
}
}
const vendorBuildIdentity = await currentVendorCacheBuildIdentity(
driver,
`${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`,
);
// Mutable include/SDK/config inputs can change behind a stable environment
// spelling. The complete/runtime cache is disabled below; vendor objects
// must follow the same rule instead of persisting in the user cache.
// A private root gives this invocation the usual vendor build recipe
// without publishing or reusing those prerequisites.
const transientVendorRoot =
persistentCache !== null && implicitToolchain !== null
? null
: join(tmpdir(), `scriptc-vendor-${process.pid}-${Math.random().toString(36).slice(2)}`);
const vendorCacheRoot = transientVendorRoot ?? vendorBuildCacheRoot(persistentCache?.root);
let tlsArchive = mbedtls
? tlsArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
: null;
// ours would collide; see scr_regex.c) — the standalone objects are for
// static builds only.
let lreObjects = regex
? lreObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
: [];
// Vendored zlib is the Zig story — default host-clang builds keep the exact
// historical `-lz` system link (see CcOptions.zlib). The native fetch's
// gzip decoder rides the same objects/link.
let zlibObjects =
((opts.zlib ?? false) || nativeFetch) && isZigDriver(driver)
? zlibObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
: [];
const materializeVendorPrerequisites = async (
stageRoot?: string,
materializeCacheRoot: string = vendorCacheRoot,
): Promise<void> => {
// Preserve source order to avoid multiplying first-build resource
// pressure when several large vendor sets are cold simultaneously.
if (mbedtls) {
const cachedArchive = tlsArchivePath(
sanitize,
driver,
vendorBuildIdentity,
materializeCacheRoot,
);
protectCachedArtifact(cacheWarmPaths, cachedArchive);
const materialize = async (): Promise<string[]> => [
await ensureTlsArchive(sanitize, driver, vendorBuildIdentity, materializeCacheRoot),
];
const paths =
stageRoot === undefined
? await materialize()
: await stageVendorInputs(materialize, join(stageRoot, "tls"));
tlsArchive = paths[0]!;
}
if (regex) {
for (const object of lreObjectPaths(
sanitize,
driver,
vendorBuildIdentity,
materializeCacheRoot,
))
protectCachedArtifact(cacheWarmPaths, object);
const materialize = async (): Promise<string[]> =>
await ensureLreObjects(sanitize, driver, vendorBuildIdentity, materializeCacheRoot);
lreObjects =
stageRoot === undefined
? await materialize()
: await stageVendorInputs(materialize, join(stageRoot, "lre"));
}
if (zlibObjects.length > 0) {
for (const object of zlibObjectPaths(
sanitize,
driver,
vendorBuildIdentity,
materializeCacheRoot,
))
protectCachedArtifact(cacheWarmPaths, object);
const materialize = async (): Promise<string[]> =>
await ensureZlibObjects(sanitize, driver, vendorBuildIdentity, materializeCacheRoot);
zlibObjects =
stageRoot === undefined
? await materialize()
: await stageVendorInputs(materialize, join(stageRoot, "zlib"));
}
};
// rt() maps each runtime source's path on the command line: identity for
// the historical single invocation, cached-.o substitution on cache misses.
const buildArgs = (
rt: (path: string) => string,
build: {
programPath?: string;
outPath?: string;
compilerVisibleSource?: string;
} = {},
): string[] => [
"-std=c11",
...debugFlags,
...driver.targetArgs,
...threadArgs,
...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(sanitize
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...runtimeDefineArgs(opts),
"-fno-math-errno",
// The runtime object model uses type-punned C: a hierarchy
// upcast is a raw pointer cast, so one object's header (rc, vt) and
// fields are read and written through BOTH the base and derived struct
// types (sc_retain_Derived vs sc_release_Base on the same object).
// C's effective-type rule calls that UB, and clang's TBAA at -O2
// reorders/elides the rc updates once everything inlines — an upcast
// identity compare frees the object while a global still owns it.
// The LLVM backend emits no TBAA metadata; this flag preserves
// matching memory semantics in the runtime. Mirrored in the
// cache-miss cflags below and compileLibArchive — the three option
// sets must stay in lockstep.
"-fno-strict-aliasing",
"-Wno-deprecated-declarations", // ucontext fibers (scr_async.c)
"-I",
rtDir,
...runtimeSources.map((f) => rt(join(rtDir, f))),
...runtimeUnitArgs({
opts,
driver,
rt,
rtDir,
regex,
nativeFetch,
net,
http,
tls,
tlsCa,
cipher,
ecdh,
lreObjects,
zlibObjects,
tlsArchive,
}),
// A .ll program TU (the LLVM backend) deliberately carries no target
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
// triple exactly as it does for .c) — silence the -Woverride-module
// note about that. Never present for .c inputs, so the historical C
// command line cannot change by a byte.
...(opts.cPath.endsWith(".ll") ? ["-Wno-override-module"] : []),
...(build.compilerVisibleSource !== undefined && build.programPath !== undefined
? [
`-ffile-prefix-map=${build.programPath}=${build.compilerVisibleSource}`,
"-iquote",
dirname(resolve(build.compilerVisibleSource)),
]
: []),
build.programPath ?? opts.cPath,
...(opts.linkInputs ?? []),
...(opts.systemLibraries ?? []).map((name) => `-l${name}`),
...(opts.frameworks ?? []).flatMap((name) => ["-framework", name]),
// GNU ld resolves libraries from left to right and commonly enables
// --as-needed: host-clang libz must follow scr_zlib.c/scr_fetch.c and every
// generated/native input that references inflate symbols. Cross
// and targetless Zig builds use vendored zlib objects in the input section above.
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
// glibc keeps libm separate from libc. This must trail the generated
// program and every native FFI input because GNU ld resolves archives
// from left to right.
...driver.linkArgs,
...executableLinkFlags,
"-o",
build.outPath ?? opts.outPath,
];
// Runtime-object flags. Program compilation and dependency discovery add
// the dev sanitizer override below so both cache identities match reality.
const cflags = [
"-std=c11",
...debugFlags,
...driver.targetArgs,
...threadArgs,
...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(sanitize
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...runtimeDefineArgs(opts),
"-fno-math-errno",
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
"-Wno-deprecated-declarations",
"-I",
rtDir,
...(regex ? ["-I", vendorEngineDir()] : []),
...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
];
// Keep sanitizer runtime objects optimized in both postures. Large dev
// programs still compile at O0, avoiding an expensive LLVM optimization
// pass while retaining ASan and reference-count checks in every runtime unit.
const separateSanitizedProgram = sanitize && optimization === "dev";
const programCompilerArgs = [
...cflags,
...(separateSanitizedProgram ? ["-O0"] : []),
...(opts.cPath.endsWith(".ll") ? ["-Wno-override-module"] : []),
];
const programSourceExtension = opts.cPath.endsWith(".ll") ? ".ll" : ".c";
const ccName = driver.argv.join(" ");
const runClang = async (args: string[]): Promise<void> => {
try {
await execFileAsync(driver.argv[0] ?? "clang", [...driver.argv.slice(1), ...args]);
} catch (err) {
const stderr = subprocessFailureDetail(err);
const guidance =
(opts.linkInputs?.length ?? 0) > 0 ||
(opts.frameworks?.length ?? 0) > 0 ||
(opts.systemLibraries?.length ?? 0) > 0
? "This build includes native FFI link inputs. Check that every symbol and system library exists, " +
"that archive/object ordering is correct, and that each input matches the selected target."
: `Check the supplied native source and the selected ${ccName} toolchain.`;
throw new CcCompileError(
ccName,
stderr,
`${ccName} failed compiling ${opts.cPath}.\n` + `${guidance}\n\n${stderr}`,
);
}
};
const buildExecutable = async (
runtimeInput: (path: string) => string,
build: { programPath?: string; outPath?: string; compilerVisibleSource?: string } = {},
): Promise<void> => {
if (!separateSanitizedProgram) {
await runClang(buildArgs(runtimeInput, build));
return;
}
const stage = await mkdtemp(join(tmpdir(), "scriptc-sanitized-program-"));
try {
const object = join(stage, "program.o");
await runClang([
...programCompilerArgs,
...(build.compilerVisibleSource === undefined
? []
: [
`-ffile-prefix-map=${build.programPath}=${build.compilerVisibleSource}`,
"-iquote",
dirname(resolve(build.compilerVisibleSource)),
]),
"-c",
build.programPath ?? opts.cPath,
"-o",
object,
]);
await runClang(
buildArgs(runtimeInput, { programPath: object, outPath: build.outPath ?? opts.outPath }),
);
if (darwinDebugSymbols) await createDarwinDebugSymbols(build.outPath ?? opts.outPath);
} finally {
await rm(stage, { recursive: true, force: true });
}
};
const runUncachedBuild = async (): Promise<void> => {
const privateVendorRoot =
transientVendorRoot ??
join(
tmpdir(),
`scriptc-vendor-fallback-${process.pid}-${Math.random().toString(36).slice(2)}`,
);
try {
await materializeVendorPrerequisites(undefined, privateVendorRoot);
await buildExecutable((p) => p);
} finally {
await rm(privateVendorRoot, { recursive: true, force: true }).catch(() => undefined);
}
};
let runtimeCompilerInvocation: string | null = null;
let programCompilerInvocation: string | null = null;
let payloadMetadata: Promise<[string, string, Buffer]> | null = null;
let compileMetadataStamp: NativeMetadataStamp | null = null;
const compileMetadataKey =
persistentCache === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
? null
: nativeMetadataKey("compile", [
cacheTargetIdentity(driver),
toolchainEnv,
implicitToolchain ?? "<uncached>",
driver.argv,
cflags,
programCompilerArgs,
programSourceExtension,
]);
if (persistentCache !== null) {
try {
// These probes inspect disjoint inputs. Start the payload reads here as
// well so runtime hashing and clang's dry-run traces overlap instead of
// forming a serial prelude before every cache lookup.
payloadMetadata = Promise.all([
Promise.resolve(
toolchainMetadataStamp?.values["compilerVersion"] ??
ccVersion(driver.argv, toolchainEnv, true),
),
localArtifact === null
? runtimeFingerprint(rtDir)
: Promise.resolve(localArtifact.runtimeHash),
localArtifact === null ? readFile(opts.cPath) : Promise.resolve(localArtifact.programBytes),
]);
compileMetadataStamp =
compileMetadataKey === null
? null
: await readNativeMetadataStamp(persistentCache.root, compileMetadataKey);
if (compileMetadataStamp !== null) {
runtimeCompilerInvocation = compileMetadataStamp.values["runtimeInvocation"] ?? null;
programCompilerInvocation = compileMetadataStamp.values["programInvocation"] ?? null;
if (runtimeCompilerInvocation === null || programCompilerInvocation === null) {
compileMetadataStamp = null;
}
}
if (compileMetadataStamp === null) {
const [runtimeInvocation, programInvocation] = await Promise.all([
effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
programSourceExtension === ".ll" || separateSanitizedProgram
? effectiveCompilerInvocationFingerprint(
driver,
toolchainEnv,
programCompilerArgs,
programSourceExtension,
)
: Promise.resolve(null),
]);
runtimeCompilerInvocation = runtimeInvocation;
programCompilerInvocation = programInvocation ?? runtimeInvocation;
if (compileMetadataKey !== null) {
compileMetadataStamp = await publishNativeMetadataStamp(
persistentCache.root,
compileMetadataKey,
{
runtimeInvocation: runtimeCompilerInvocation,
programInvocation: programCompilerInvocation,
},
[
...fingerprintDependencyPaths(runtimeCompilerInvocation),
...fingerprintDependencyPaths(programCompilerInvocation),
],
[runtimeCompilerInvocation, programCompilerInvocation],
);
}
}
} catch (error) {
// Preserve the uncached build for wrappers that compile successfully but
// cannot provide a dry-run trace for the real build flavor.
if (cacheWarmOnly) {
throw new Error("native cache warming could not validate the compiler invocation", {
cause: error,
});
}
persistentCache = null;
}
}
if (persistentCache === null) {
// The direct uncached command is the source-of-truth executable recipe.
await runUncachedBuild();
return;
}
let cv: string;
let fingerprint: string;
let cBytes: Buffer;
try {
[cv, fingerprint, cBytes] = await (payloadMetadata ??
Promise.all([
ccVersion(driver.argv, toolchainEnv, true),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
]));
} catch (error) {
// A version/fingerprint probe is an optimization boundary. If the compiler
// itself can still compile, preserve the pre-cache behavior instead of
// surfacing a metadata command's failure as the build result.
if (cacheWarmOnly) {
throw new Error("native cache warming could not validate cache inputs", {
cause: error,
});
}
await runUncachedBuild();
return;
}
// Caller-supplied native inputs can all hide mutable dependencies: `-l`
// resolves through ambient search paths, while a thin archive or linker
// script can retain identical top-level bytes as its referenced files are
// rebuilt. Keep the safe runtime-object cache, but force a fresh final link
// whenever the caller supplies either form.
let cacheCompleteArtifact =
!cacheWarmOnly &&
cachePolicy.completeArtifacts &&
(opts.linkInputs?.length ?? 0) === 0 &&
(opts.frameworks?.length ?? 0) === 0 &&
(opts.systemLibraries?.length ?? 0) === 0;
let programDependencies: string | null = null;
const linkProbeArgs = [
...(sanitize ? ["-fsanitize=address"] : []),
...threadArgs,
...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(targetPlatform(driver) === "win32" ? ["-ladvapi32", "-liphlpapi", "-lws2_32"] : []),
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
...driver.linkArgs,
...executableLinkFlags,
];
// Both the wrapper dry run and dependency trace need the real build's
// compile/link flag shape: wrappers commonly inject flags or native inputs
// conditionally on optimization, sanitizer, or platform switches.
const effectiveLinkInvocationArgs = [
...(separateSanitizedProgram ? cflags : programCompilerArgs),
...(targetPlatform(driver) === "win32" ? ["-ladvapi32", "-liphlpapi", "-lws2_32"] : []),
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
...driver.linkArgs,
...executableLinkFlags,
];
// A complete hit is checked before cross-target curl's generated import stub
// is materialized. Its -L spelling still joins the dry-run identity, while
// the real trace omits only that not-yet-existing scriptc-owned directory.
const linkTraceInvocationArgs = effectiveLinkInvocationArgs;
let implicitLinker: string | null = null;
let preBuildDependencies: NativeArtifactDependency[] | null = null;
let localArtifactDependencyPaths: string[] | null = null;
let linkMetadataStamp: NativeMetadataStamp | null = null;
const linkMetadataKey =
process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
? null
: nativeMetadataKey("link", [
cacheTargetIdentity(driver),
toolchainEnv,
implicitToolchain ?? "<uncached>",
runtimeCompilerInvocation ?? "<uncached>",
programCompilerInvocation ?? "<uncached>",
driver.argv,
linkProbeArgs,
effectiveLinkInvocationArgs,
linkTraceInvocationArgs,
...(programShardMergeIdentity === null ? [] : [programShardMergeIdentity]),
]);
if (cacheCompleteArtifact) {
try {
// Header discovery and linker tracing are independent subprocess trees.
// Running them together removes one complete probe round-trip from both
// cache hits and ordinary edit/build misses without changing either key.
linkMetadataStamp =
linkMetadataKey === null
? null
: await readNativeMetadataStamp(persistentCache.root, linkMetadataKey);
if (linkMetadataStamp !== null) {
implicitLinker = linkMetadataStamp.values["implicitLinker"] ?? null;
if (implicitLinker === null) linkMetadataStamp = null;
}
[programDependencies, implicitLinker] = await Promise.all([
translationUnitDependencyFingerprint(
driver,
programCompilerArgs,
opts.cPath,
cBytes,
toolchainEnv,
),
linkMetadataStamp === null
? implicitLinkerFingerprint(
driver,
toolchainEnv,
linkProbeArgs,
effectiveLinkInvocationArgs,
linkTraceInvocationArgs,
)
: Promise.resolve(implicitLinker!),
]);
if (linkMetadataStamp === null && linkMetadataKey !== null) {
linkMetadataStamp = await publishNativeMetadataStamp(
persistentCache.root,
linkMetadataKey,
{ implicitLinker },
fingerprintDependencyPaths(implicitLinker),
[implicitLinker],
);
}
preBuildDependencies = await snapshotLocalArtifactDependencies([
...(await runtimeFingerprintInputPaths(rtDir)),
...(toolchainMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
fingerprintDependencyPaths(implicitToolchain!)),
...(compileMetadataStamp?.dependencies.map((dependency) => dependency.path) ?? []),
...(linkMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
fingerprintDependencyPaths(implicitLinker!)),
...fingerprintDependencyPaths(programDependencies),
...(programShardMergeIdentity === null
? []
: fingerprintDependencyPaths(programShardMergeIdentity)),
]);
// Every content-bearing fingerprint was computed before this metadata
// snapshot. Re-read those exact files now so the snapshot cannot certify
// bytes that changed after hashing but before the final compile starts.
if (
!(await fingerprintDependenciesStillMatch([
implicitToolchain!,
runtimeCompilerInvocation!,
programCompilerInvocation!,
implicitLinker!,
programDependencies,
...(programShardMergeIdentity === null ? [] : [programShardMergeIdentity]),
])) ||
(await runtimeFingerprint(rtDir).catch(() => null)) !== fingerprint ||
!(
await Promise.all(
[toolchainMetadataStamp, compileMetadataStamp, linkMetadataStamp]
.filter((stamp): stamp is NativeMetadataStamp => stamp !== null)
.map((stamp) => nativeArtifactDependenciesStillMatch(stamp.dependencies)),
)
).every(Boolean)
) {
throw new CacheInputsChangedError();
}
if (localArtifact !== null) {
// Native metadata stamps persist their dependency paths across CLI
// processes; fingerprintDependencyPaths deliberately does not. Build
// the output-local stamp from this complete validated snapshot so a
// source edit in a fresh process cannot replace it with only the two
// process-local fallback paths.
localArtifactDependencyPaths = [
localArtifact.compilerPath,
dirname(resolve(opts.cPath)),
...preBuildDependencies.map((dependency) => dependency.path),
];
}
} catch {
// Program-header discovery and linker tracing are both required for a
// complete hit. Runtime objects remain safely cacheable if either probe
// is unavailable.
cacheCompleteArtifact = false;
preBuildDependencies = null;
localArtifactDependencyPaths = null;
}
}
// Warm-only builds deliberately do not need linker/header identity for a
// complete executable, but they still need a stable runtime-object key.
// The ordinary complete-artifact path computes these values above.
if (cacheWarmOnly && preBuildDependencies === null) {
try {
const [dependencies, invocation] = await Promise.all([
translationUnitDependencyFingerprint(
driver,
programCompilerArgs,
opts.cPath,
cBytes,
toolchainEnv,
),
effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
]);
programDependencies = dependencies;
if (runtimeCompilerInvocation === null) runtimeCompilerInvocation = invocation;
} catch (error) {
throw new Error("native cache warming could not validate runtime-object inputs", {
cause: error,
});
}
}
const binDir = join(persistentCache.root, "bin");
let keyHex: string | null = null;
let cachedBin: string | null = null;
if (cacheCompleteArtifact) {
// The key sees the full command line with the two program-specific paths
// normalized out. The C bytes are hashed separately; Darwin additionally
// keys the output basename because ld embeds it in the ad-hoc signature.
// Runtime and vendor paths stay verbatim — their contents are covered by
// the fingerprint and the pin.
const identityArgs = buildArgs((p) => p).map((a) =>
a === opts.cPath ? "<program.c>" : a === opts.outPath ? "<out>" : a,
);
const key = createHash("sha256")
// Sharded dev builds need the v10 identity below. Canonical single-TU
// builds retain v9 so adding the opt-in mode does not evict release
// binaries compiled by an earlier scriptc.
.update(programShards === null ? "bin-v9\0" : "bin-v10\0")
.update(cacheTargetIdentity(driver))
.update("\0")
.update(toolchainEnv)
.update("\0")
.update(implicitToolchain!)
.update("\0")
.update(runtimeCompilerInvocation!)
.update("\0")
.update(programCompilerInvocation!)
.update("\0")
.update(implicitLinker!)
.update("\0")
.update(programDependencies!)
.update("\0")
.update(persistentCache.identity)
.update("\0")
// Preserve both the spelling clang sees (__FILE__) and the location used
// to resolve relative includes. The top-level bytes are not sufficient.
.update(opts.cPath)
.update("\0")
.update(resolve(opts.cPath))
.update("\0")
.update(targetPlatform(driver) === "darwin" ? basename(opts.outPath) : "<out>")
.update("\0")
// The driver spelling joins the version string: `zig cc --version`
// reports the clang underneath and could otherwise collide with a
// same-version host clang.
.update(ccName)
.update("\0")
.update(cv)
.update("\0")
.update(fingerprint)
.update("\0")
.update(identityArgs.join("\x1f"))
.update("\0")
.update(cBytes);
if (programShards !== null) {
updateProgramShardCacheIdentity(
key,
programShards,
programPublicSymbols,
programShardMergeIdentity ?? undefined,
);
}
keyHex = key.digest("hex");
cachedBin = join(binDir, keyHex);
const tmpOut = privateSiblingPath(opts.outPath, "bin-hit");
try {
// NEVER copy over outPath in place: overwriting an already-executed
// signed binary invalidates the kernel's per-vnode code-signature cache
// on macOS and the next exec dies with SIGKILL. Copy to a fresh inode
// and rename it into place instead.
if (!(await copyValidCachedFile(cachedBin, tmpOut))) {
throw new Error("invalid cached executable");
}
if (darwinDebugSymbols) {
const cachedSymbols = `${cachedBin}.dsym`;
const symbolsCopy = privateSiblingPath(opts.outPath, "dsym-hit");
try {
if (!(await copyValidCachedFile(cachedSymbols, symbolsCopy)))
throw new Error("invalid cached dSYM");
const bytes = await readFile(symbolsCopy);
// A concurrent publisher may replace either payload. The sidecar
// also names its binary digest so a mixed pair always misses.
if (bytes.subarray(0, 32).toString("hex") !== (await fileDigest(tmpOut)))
throw new Error("mismatched cached dSYM");
await installDarwinDebugSymbols(bytes.subarray(32), opts.outPath);
} finally {
await rm(symbolsCopy, { force: true });
}
}
// Match a fresh linker output under the caller's current umask. Reusing a
// cache entry populated by a less restrictive shell must not widen access.
await chmod(tmpOut, 0o777 & ~process.umask());
await rename(tmpOut, opts.outPath);
if (localArtifact !== null && localArtifactDependencyPaths !== null) {
const stamp = await publishLocalArtifactStamp(
localArtifact.stampPath,
opts.outPath,
localArtifact.key,
localArtifactDependencyPaths,
[dirname(resolve(opts.cPath))],
[persistentCache.root],
darwinDebugSymbols,
).catch(() => null);
if (stamp !== null) {
await opts.onArtifactReady?.({ dependencies: stamp.dependencies }).catch(() => undefined);
}
}
return; // hit: the program/runtime payload compile and link were skipped
} catch {
await rm(tmpOut, { force: true }).catch(() => undefined);
/* miss — build below, then publish */
}
}
// Miss: link the program's own TU against cached per-flavor runtime objects.
// Collect the runtime sources this build actually compiles (the same
// conditionals as the command line, by construction).
const rtInputs: string[] = [];
buildArgs((p) => {
rtInputs.push(p);
return p;
});
const buildDir = await mkdtemp(join(tmpdir(), "scriptc-cache-build-"));
try {
await materializeVendorPrerequisites(join(buildDir, "vendor-inputs"));
// Freeze the exact bytes used for the key. Generated TUs live at stable
// paths under .scriptc/, so two builds can otherwise overwrite that path
// between hashing and clang and publish one invocation's code under the
// other's key. The prefix map preserves the original __FILE__/debug-file
// spelling while clang reads this invocation-private snapshot.
const programPath = join(buildDir, `program${programSourceExtension}`);
// Preserve the caller-visible basename while keeping Darwin builds on a
// private inode: ld uses this spelling as the embedded ad-hoc signing
// identifier. Other targets retain the basename-independent cache key.
const privateOut = join(
buildDir,
targetPlatform(driver) === "darwin"
? basename(opts.outPath)
: process.platform === "win32"
? "artifact.exe"
: "artifact",
);
await writeFile(programPath, cBytes);
let objects: Map<string, string> | null = null;
let cacheInputsStable = true;
let strictObjectVerification: Promise<boolean> | null = null;
const objectImplicitToolchainStillMatches = (): Promise<boolean> => {
if (preBuildDependencies !== null) {
return nativeArtifactDependenciesStillMatch(preBuildDependencies);
}
// Complete-artifact caching can be disabled by caller-owned native
// inputs while the safe runtime-object tier remains active. Preserve its
// strict discovery fallback in that posture.
strictObjectVerification ??= Promise.all([
runtimeFingerprint(rtDir),
implicitToolchainFingerprint(driver, toolchainEnv),
effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
]).then(
([currentRuntime, currentImplicit, currentInvocation]) =>
currentRuntime === fingerprint &&
currentImplicit === implicitToolchain &&
currentInvocation === runtimeCompilerInvocation,
() => false,
);
return strictObjectVerification;
};
try {
const cached = await ensureRuntimeObjects(
persistentCache.root,
driver.argv,
cflags,
rtInputs,
`obj-v5\0${cacheTargetIdentity(driver)}\0${toolchainEnv}\0${implicitToolchain}\0${runtimeCompilerInvocation}\0${ccName}\0${cv}\0${fingerprint}\0`,
async () => await objectImplicitToolchainStillMatches(),
cacheWarmPaths,
);
objects = await stageRuntimeObjects(cached, join(buildDir, "runtime-objects"));
} catch (err) {
if (err instanceof CacheInputsChangedError) cacheInputsStable = false;
if (cacheWarmOnly) {
throw new Error("native cache warming could not persist runtime objects", {
cause: err,
});
}
objects = null; // cache trouble is never a build failure
}
const compileShardedProgram = async (): Promise<string | null> => {
if (
programShards === null ||
programPublicSymbols === undefined ||
programCompilerInvocation === null ||
implicitCompileToolchain === null
)
return null;
try {
const programDependencyHash =
programDependencies ??
(await translationUnitDependencyFingerprint(
driver,
programCompilerArgs,
opts.cPath,
cBytes,
toolchainEnv,
));
const stem = basename(opts.cPath, ".ll");
const entries = programShards.map((shard, index) => {
const sourcePath = join(buildDir, shard.name);
const staged = join(buildDir, `${stem}.program-${index.toString().padStart(3, "0")}.o`);
const key = createHash("sha256")
.update("exe-program-shard-v1\0")
.update(cacheTargetIdentity(driver))
.update("\0")
.update(toolchainEnv)
.update("\0")
.update(implicitCompileToolchain)
.update("\0")
.update(programCompilerInvocation)
.update("\0")
.update(persistentCache.identity)
.update("\0")
.update(driver.argv.join("\x1f"))
.update("\0")
.update(cv)
.update("\0")
.update(fingerprint)
.update("\0")
.update(programDependencyHash)
.update("\0")
.update(programCompilerArgs.join("\x1f"))
.update("\0")
.update(opts.cPath)
.update("\0")
.update(resolve(opts.cPath))
.update("\0")
.update(shard.name)
.update("\0")
.update(shard.source)
.digest("hex");
return {
...shard,
sourcePath,
staged,
cachePath: join(persistentCache.root, "program-shard", key),
missed: false,
};
});
const shardWidth = Math.min(8, availableParallelism());
for (let i = 0; i < entries.length; i += shardWidth) {
await Promise.all(
entries.slice(i, i + shardWidth).map(async (entry) => {
await writeFile(entry.sourcePath, entry.source);
if (await copyValidCachedFile(entry.cachePath, entry.staged)) return;
entry.missed = true;
await runClang([
...programCompilerArgs,
`-ffile-prefix-map=${entry.sourcePath}=${opts.cPath}`,
"-c",
entry.sourcePath,
"-o",
entry.staged,
]);
}),
);
}
const arArgv = isZigDriver(driver) ? [driver.argv[0]!, "ar"] : ["ar"];
const merged = await localizeLibraryObjects(
driver,
arArgv,
buildDir,
entries.map((entry) => entry.staged),
[],
programPublicSymbols,
`${stem}.program`,
);
const publishable = entries.filter((entry) => entry.missed);
if (publishable.length > 0) {
try {
const [
currentRuntime,
currentFingerprints,
currentInvocation,
currentDependencies,
currentCompiler,
currentMerge,
] = await Promise.all([
runtimeFingerprint(rtDir),
implicitToolchainFingerprints(driver, toolchainEnv),
effectiveCompilerInvocationFingerprint(
driver,
toolchainEnv,
programCompilerArgs,
".ll",
),
translationUnitDependencyFingerprint(
driver,
programCompilerArgs,
opts.cPath,
cBytes,
toolchainEnv,
),
ccVersion(driver.argv, toolchainEnv, true),
resolveProgramShardMergeIdentity(driver),
]);
if (
currentRuntime === fingerprint &&
currentFingerprints.compile === implicitCompileToolchain &&
currentInvocation === programCompilerInvocation &&
currentDependencies === programDependencyHash &&
currentCompiler === cv &&
currentMerge === programShardMergeIdentity
) {
await Promise.all(
publishable.map((entry) => publishCachedFile(entry.staged, entry.cachePath)),
);
}
} catch {
// The merged object is valid for this invocation; publication is
// only an optimization for later edits.
}
}
return merged;
} catch {
// Sharding is an optimization. Compile/link the canonical TU below if
// a target tool, one shard, or relocatable merge is unavailable.
return null;
}
};
const shardedProgramObject = await compileShardedProgram();
if (programShards !== null && shardedProgramObject === null) {
// The canonical TU fallback is valid output, but it must not populate a
// cache key describing a successful shard projection/merge.
cacheInputsStable = false;
}
await buildExecutable(
(p) => objects?.get(p) ?? p,
shardedProgramObject === null
? { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }
: { programPath: shardedProgramObject, outPath: privateOut },
);
if (darwinDebugSymbols) {
// clang invokes dsymutil when compiling a source TU while linking.
// A sharded program is already an object, so run it explicitly while
// the merged object and staged runtime inputs still exist.
if (shardedProgramObject !== null) await createDarwinDebugSymbols(privateOut);
await installDarwinDebugSymbols(await readDarwinDebugSymbols(privateOut), opts.outPath);
}
await installArtifact(privateOut, opts.outPath);
if (cachedBin !== null && keyHex !== null) {
// Metadata comparison catches ordinary changes cheaply, but cannot by
// itself prove the snapshot was taken from the same bytes hashed into
// the key. Recompute every content-bearing identity after the final link
// so a header/SDK/compiler change in either pre-build gap cannot publish
// new output under an old key.
const [
currentRuntime,
currentImplicit,
currentRuntimeInvocation,
currentProgramInvocation,
currentProgramDependencies,
currentLinker,
currentCompiler,
currentProgramShardMerge,
] = await Promise.all([
runtimeFingerprint(rtDir).catch(() => null),
implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags).catch(() => null),
effectiveCompilerInvocationFingerprint(
driver,
toolchainEnv,
programCompilerArgs,
programSourceExtension,
).catch(() => null),
translationUnitDependencyFingerprint(
driver,
programCompilerArgs,
opts.cPath,
cBytes,
toolchainEnv,
).catch(() => null),
implicitLinkerFingerprint(
driver,
toolchainEnv,
linkProbeArgs,
effectiveLinkInvocationArgs,
linkTraceInvocationArgs,
).catch(() => null),
ccVersion(driver.argv, toolchainEnv, true).catch(() => null),
programShards === null
? Promise.resolve(null)
: resolveProgramShardMergeIdentity(driver).catch(() => null),
]);
cacheInputsStable =
cacheInputsStable &&
preBuildDependencies !== null &&
(await nativeArtifactDependenciesStillMatch(preBuildDependencies)) &&
currentRuntime === fingerprint &&
currentImplicit === implicitToolchain &&
currentRuntimeInvocation === runtimeCompilerInvocation &&
currentProgramInvocation === programCompilerInvocation &&
currentProgramDependencies === programDependencies &&
currentLinker === implicitLinker &&
currentCompiler === cv &&
currentProgramShardMerge === programShardMergeIdentity;
}
if (cachedBin !== null && keyHex !== null && cacheInputsStable) {
try {
// Cache artifacts are data, never execution targets. Publication keeps
// generated code and embedded literals private; the hit path reapplies
// the caller's current executable mode to its destination copy.
await publishCachedFile(privateOut, cachedBin);
if (darwinDebugSymbols) {
const symbols = privateSiblingPath(privateOut, "debug-symbols");
await writeFile(
symbols,
Buffer.concat([
Buffer.from(await fileDigest(privateOut), "hex"),
await readDarwinDebugSymbols(privateOut),
]),
);
await publishCachedFile(symbols, `${cachedBin}.dsym`);
}
} catch {
/* publishing is best-effort */
}
}
if (
localArtifact !== null &&
localArtifactDependencyPaths !== null &&
cacheCompleteArtifact &&
cacheInputsStable
) {
const stamp = await publishLocalArtifactStamp(
localArtifact.stampPath,
opts.outPath,
localArtifact.key,
localArtifactDependencyPaths,
[dirname(resolve(opts.cPath))],
[persistentCache.root],
darwinDebugSymbols,
).catch(() => null);
if (stamp !== null) {
await opts.onArtifactReady?.({ dependencies: stamp.dependencies }).catch(() => undefined);
}
}
} finally {
await rm(buildDir, { recursive: true, force: true }).catch(() => undefined);
}
// Runtime-object population is itself a cache write, including on builds
// whose native link inputs disable complete-artifact publication.
await pruneCache(persistentCache.root, cacheWarmPaths).catch(() => undefined);
}
export async function compileC(opts: CcOptions): Promise<void> {
clearCcCaches();
await compileCInternal(opts, false);
if (
targetPlatform(resolveCc()) === "darwin" &&
!needsDarwinDebugSymbols("darwin", opts.optimization, opts.strip)
) {
await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true });
}
}