@scriptc/compiler 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +78 -34
  2. package/dist/backend/cc-cache.test.d.ts +1 -0
  3. package/dist/backend/cc-cache.test.js +2507 -0
  4. package/dist/backend/cc-cache.test.js.map +1 -0
  5. package/dist/backend/cc.d.ts +69 -11
  6. package/dist/backend/cc.js +1937 -222
  7. package/dist/backend/cc.js.map +1 -1
  8. package/dist/backend/emission/emit-exprs.js +355 -8
  9. package/dist/backend/emission/emit-exprs.js.map +1 -1
  10. package/dist/backend/emission/emit-walkers.js +108 -7
  11. package/dist/backend/emission/emit-walkers.js.map +1 -1
  12. package/dist/backend/emission/emitter.d.ts +16 -0
  13. package/dist/backend/emission/emitter.js +23 -2
  14. package/dist/backend/emission/emitter.js.map +1 -1
  15. package/dist/backend/llvm/dyn.d.ts +1 -0
  16. package/dist/backend/llvm/dyn.js +305 -8
  17. package/dist/backend/llvm/dyn.js.map +1 -1
  18. package/dist/backend/llvm/emitter.js +527 -2
  19. package/dist/backend/llvm/emitter.js.map +1 -1
  20. package/dist/compat/fetch-profile.d.ts +108 -0
  21. package/dist/compat/fetch-profile.js +482 -0
  22. package/dist/compat/fetch-profile.js.map +1 -0
  23. package/dist/coverage/surface-manifest.js +54 -0
  24. package/dist/coverage/surface-manifest.js.map +1 -1
  25. package/dist/frontend/lowering/lower-builtins.d.ts +21 -8
  26. package/dist/frontend/lowering/lower-builtins.js +137 -14
  27. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  28. package/dist/frontend/lowering/lower-calls.js +40 -0
  29. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  30. package/dist/frontend/lowering/lower-classes.js +4 -3
  31. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  32. package/dist/frontend/lowering/lower-exprs.d.ts +2 -2
  33. package/dist/frontend/lowering/lower-exprs.js +63 -12
  34. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  35. package/dist/frontend/lowering/lower-island.d.ts +102 -7
  36. package/dist/frontend/lowering/lower-island.js +1702 -26
  37. package/dist/frontend/lowering/lower-island.js.map +1 -1
  38. package/dist/frontend/lowering/lower-modules.js +17 -1
  39. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  40. package/dist/frontend/lowering/lower-stmts.js +25 -5
  41. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  42. package/dist/frontend/lowering/lowerer.d.ts +39 -0
  43. package/dist/frontend/lowering/lowerer.js +286 -18
  44. package/dist/frontend/lowering/lowerer.js.map +1 -1
  45. package/dist/frontend/lowering/surfaces.js +12 -0
  46. package/dist/frontend/lowering/surfaces.js.map +1 -1
  47. package/dist/frontend/program.d.ts +16 -0
  48. package/dist/frontend/program.js +130 -6
  49. package/dist/frontend/program.js.map +1 -1
  50. package/dist/frontend/types.d.ts +14 -7
  51. package/dist/frontend/types.js +95 -31
  52. package/dist/frontend/types.js.map +1 -1
  53. package/dist/index.d.ts +7 -1
  54. package/dist/index.js +70 -14
  55. package/dist/index.js.map +1 -1
  56. package/dist/ir/nodes.d.ts +32 -16
  57. package/dist/ir/nodes.js +36 -28
  58. package/dist/ir/nodes.js.map +1 -1
  59. package/dist/ir/validate.js +61 -1
  60. package/dist/ir/validate.js.map +1 -1
  61. package/dist/library/int-infer.js +7 -1
  62. package/dist/library/int-infer.js.map +1 -1
  63. package/dist/library/int-infer.test.js +80 -0
  64. package/dist/library/int-infer.test.js.map +1 -1
  65. package/dist/library/sidecar-validate.js +5 -0
  66. package/dist/library/sidecar-validate.js.map +1 -1
  67. package/dist/library/sidecar.d.ts +3 -0
  68. package/dist/library/sidecar.js.map +1 -1
  69. package/package.json +2 -2
  70. package/surface-manifest.json +859 -1
@@ -1,11 +1,36 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
+ import { constants as fsConstants } from "node:fs";
3
4
  import { createRequire } from "node:module";
4
- import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
5
- import { availableParallelism, tmpdir } from "node:os";
6
- import { basename, dirname, join } from "node:path";
5
+ import { access, chmod, copyFile, link, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
6
+ import { availableParallelism, homedir, tmpdir } from "node:os";
7
+ import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
7
8
  import { promisify } from "node:util";
8
9
  const execFileAsync = promisify(execFile);
10
+ /** Test lanes run against one immutable checkout and one immutable toolchain
11
+ * for the lifetime of each Vitest worker. Production deliberately rediscovers
12
+ * compiler/linker inputs on every invocation, but doing that thousands of
13
+ * times in the differential corpus costs far more than the compile itself.
14
+ * This test-only opt-in lets those workers reuse metadata probes for their
15
+ * session; the cache-correctness suite removes the flag and exercises the
16
+ * strict production path. */
17
+ function stableTestToolchainSession() {
18
+ return process.env["SCRIPTC_TEST_STABLE_TOOLCHAIN"] === "1";
19
+ }
20
+ function stableTestMemo(cache, key, probe) {
21
+ if (!stableTestToolchainSession())
22
+ return probe();
23
+ const existing = cache.get(key);
24
+ if (existing !== undefined)
25
+ return existing;
26
+ const pending = probe();
27
+ cache.set(key, pending);
28
+ void pending.catch(() => {
29
+ if (cache.get(key) === pending)
30
+ cache.delete(key);
31
+ });
32
+ return pending;
33
+ }
9
34
  const RUNTIME_SOURCES = ["scr_number.c", "scr_string.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_async.c", "scr_child.c", "scr_cycle.c"];
10
35
  /** The pinned quickjs-ng snapshot under packages/runtime/vendor/quickjs-ng
11
36
  * (see vendor/README.md — update both together). Keys the archive cache so
@@ -22,6 +47,113 @@ const MBEDTLS_VERSION = "3.6.7";
22
47
  * they keep the historical system `-lz` link; the vendored copy exists so
23
48
  * zlib-using programs CROSS-compile (zig has no target sysroot libz). */
24
49
  const ZLIB_VERSION = "1.3.1";
50
+ /** Environment variables consumed by clang, its linker/subtools, or the
51
+ * platform SDK selection. They are implicit command-line inputs: changing one
52
+ * must never reuse an artifact produced under the old toolchain posture. */
53
+ const TOOLCHAIN_ENV_KEYS = [
54
+ "COMPILER_PATH",
55
+ "GCC_EXEC_PREFIX",
56
+ "CPATH",
57
+ "C_INCLUDE_PATH",
58
+ "CPLUS_INCLUDE_PATH",
59
+ "OBJC_INCLUDE_PATH",
60
+ "OBJCPLUS_INCLUDE_PATH",
61
+ "LIBRARY_PATH",
62
+ "LD_LIBRARY_PATH",
63
+ "LD_RUN_PATH",
64
+ "DYLD_LIBRARY_PATH",
65
+ "DYLD_FRAMEWORK_PATH",
66
+ "DYLD_FALLBACK_LIBRARY_PATH",
67
+ "DYLD_FALLBACK_FRAMEWORK_PATH",
68
+ "SDKROOT",
69
+ "DEVELOPER_DIR",
70
+ "MACOSX_DEPLOYMENT_TARGET",
71
+ "IPHONEOS_DEPLOYMENT_TARGET",
72
+ "TVOS_DEPLOYMENT_TARGET",
73
+ "WATCHOS_DEPLOYMENT_TARGET",
74
+ "DRIVERKIT_DEPLOYMENT_TARGET",
75
+ "XROS_DEPLOYMENT_TARGET",
76
+ "CCC_OVERRIDE_OPTIONS",
77
+ "CCC_ADD_ARGS",
78
+ "CLANG_CONFIG_FILE_SYSTEM_DIR",
79
+ "CLANG_CONFIG_FILE_USER_DIR",
80
+ "CC",
81
+ "CFLAGS",
82
+ "CPPFLAGS",
83
+ "LDFLAGS",
84
+ "AR",
85
+ "RANLIB",
86
+ "CMAKE_GENERATOR",
87
+ "CMAKE_TOOLCHAIN_FILE",
88
+ "ZIG_LIB_DIR",
89
+ "ZIG_LIBC",
90
+ "SOURCE_DATE_EPOCH",
91
+ "ZERO_AR_DATE",
92
+ "LANG",
93
+ "LC_ALL",
94
+ "LC_CTYPE",
95
+ ];
96
+ /** Toolchain variables whose values name mutable files/directories consumed
97
+ * while compiling a TU (or can inject arbitrary compiler options). Hashing the
98
+ * value is insufficient: a header, SDK, config, compiler helper, or loaded
99
+ * dylib can change in place while the spelling remains stable. In that posture
100
+ * neither complete artifacts nor per-TU runtime objects are safe to reuse. */
101
+ const MUTABLE_COMPILE_ENV_KEYS = [
102
+ "COMPILER_PATH",
103
+ "GCC_EXEC_PREFIX",
104
+ "CPATH",
105
+ "C_INCLUDE_PATH",
106
+ "CPLUS_INCLUDE_PATH",
107
+ "OBJC_INCLUDE_PATH",
108
+ "OBJCPLUS_INCLUDE_PATH",
109
+ "LD_LIBRARY_PATH",
110
+ "DYLD_LIBRARY_PATH",
111
+ "DYLD_FRAMEWORK_PATH",
112
+ "DYLD_FALLBACK_LIBRARY_PATH",
113
+ "DYLD_FALLBACK_FRAMEWORK_PATH",
114
+ "SDKROOT",
115
+ "DEVELOPER_DIR",
116
+ "CCC_OVERRIDE_OPTIONS",
117
+ "CCC_ADD_ARGS",
118
+ "CLANG_CONFIG_FILE_SYSTEM_DIR",
119
+ "CLANG_CONFIG_FILE_USER_DIR",
120
+ // The direct runtime build does not consume the conventional Make/CMake
121
+ // variables, but the lazily-built QuickJS prerequisite does. Their values
122
+ // can name mutable tools, headers, libraries, and toolchain files, so a
123
+ // stable spelling is not a safe persistent-cache identity.
124
+ "CC",
125
+ "CFLAGS",
126
+ "CPPFLAGS",
127
+ "LDFLAGS",
128
+ "AR",
129
+ "RANLIB",
130
+ "CMAKE_TOOLCHAIN_FILE",
131
+ // `zig cc` resolves its bundled headers/runtime through ZIG_LIB_DIR and a
132
+ // caller-selected native libc description through ZIG_LIBC. Both values name
133
+ // mutable compiler inputs whose contents can change behind a stable path.
134
+ "ZIG_LIB_DIR",
135
+ "ZIG_LIBC",
136
+ ];
137
+ /** These variables only redirect link-time inputs. Runtime objects remain
138
+ * reusable, but a complete executable could otherwise retain a library that
139
+ * was rebuilt in place behind the same search-path spelling. */
140
+ const MUTABLE_LINK_ENV_KEYS = ["LIBRARY_PATH", "LD_RUN_PATH"];
141
+ export function toolchainEnvironmentCachePolicy(env = process.env) {
142
+ const mutableCompileInput = MUTABLE_COMPILE_ENV_KEYS.some((name) => env[name] !== undefined);
143
+ const mutableLinkInput = MUTABLE_LINK_ENV_KEYS.some((name) => env[name] !== undefined);
144
+ return {
145
+ completeArtifacts: !mutableCompileInput && !mutableLinkInput,
146
+ runtimeObjects: !mutableCompileInput,
147
+ };
148
+ }
149
+ export function toolchainEnvironmentFingerprint(env = process.env) {
150
+ const hash = createHash("sha256").update("toolchain-env-v1\0");
151
+ for (const name of TOOLCHAIN_ENV_KEYS) {
152
+ const value = env[name];
153
+ hash.update(name).update(value === undefined ? "\0unset\0" : "\0set\0").update(value ?? "").update("\0");
154
+ }
155
+ return hash.digest("hex");
156
+ }
25
157
  /** Structured compiler-driver failure. Most callers still let this surface
26
158
  * as an internal build error; the TypeScript compiler pipeline recognizes it
27
159
  * when an outbound FFI profile is active and turns user-controlled native
@@ -37,6 +169,9 @@ export class CcCompileError extends Error {
37
169
  }
38
170
  }
39
171
  export function runtimeSrcDir() {
172
+ const testRoot = process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
173
+ if (testRoot !== undefined)
174
+ return resolve(testRoot);
40
175
  const require = createRequire(import.meta.url);
41
176
  return join(dirname(require.resolve("@scriptc/runtime/package.json")), "src");
42
177
  }
@@ -85,14 +220,99 @@ export function targetPlatform(driver) {
85
220
  return "win32";
86
221
  return driver.target.includes("macos") || driver.target.includes("darwin") ? "darwin" : "other";
87
222
  }
223
+ /** Architecture identity for host-native cache entries. Explicit cross targets
224
+ * already name their complete target triple; native builds need the process
225
+ * architecture too because one per-user cache can serve both native and
226
+ * emulated processes (arm64 and Rosetta on macOS, for example). */
227
+ export function cacheTargetIdentity(driver, hostPlatform = process.platform, hostArch = process.arch) {
228
+ return driver.target === null ? `native:${hostPlatform}:${hostArch}` : `cross:${driver.target}`;
229
+ }
88
230
  function vendorEngineDir() {
89
231
  return join(runtimeSrcDir(), "..", "vendor", "quickjs-ng");
90
232
  }
233
+ /** Vendor prerequisites have historically lived beside their source trees.
234
+ * The test-only override gives cache-ordering tests a disposable root without
235
+ * renaming package-local artifacts that parallel workers may be using. */
236
+ function vendorBuildCacheRoot() {
237
+ const testRoot = process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
238
+ return testRoot === undefined
239
+ ? join(runtimeSrcDir(), "..", "vendor", ".cache")
240
+ : resolve(testRoot);
241
+ }
242
+ /** Cross targets already carry their architecture in the explicit triple.
243
+ * Host-native prerequisites need the same separation: a checkout/package can
244
+ * be used by both arm64 Node and Rosetta/x64 Node on macOS. */
245
+ export function vendorCacheTargetFlavor(driver, hostPlatform = process.platform, hostArch = process.arch) {
246
+ return driver.target === null ? `native-${hostPlatform}-${hostArch}` : driver.target;
247
+ }
248
+ /** Vendor prerequisites live outside the content-addressed build root, so
249
+ * their directory name must carry the same environment and resolved compiler
250
+ * identity as the artifacts that consume them. The short digest keeps the
251
+ * package-local path readable while preventing build-order contamination. */
252
+ export function vendorCacheBuildIdentity(environmentFingerprint, compilerIdentity) {
253
+ return createHash("sha256")
254
+ .update("vendor-toolchain-v1\0")
255
+ .update(environmentFingerprint)
256
+ .update("\0")
257
+ .update(compilerIdentity)
258
+ .digest("hex")
259
+ .slice(0, 20);
260
+ }
261
+ const vendorCompilerIdentityFallbacks = new Map();
262
+ async function currentVendorCacheBuildIdentity(driver, environmentFingerprint) {
263
+ // Native vendor recipes additionally use bare clang/ar and CMake; cross
264
+ // recipes use the zig driver for compilation and `zig ar`. Include every
265
+ // executable that can affect the package-local prerequisite, not just the
266
+ // final program's driver.
267
+ const commands = [
268
+ driver.argv[0] ?? "clang",
269
+ ...(driver.target === null ? ["clang", "cc", "ar", "ranlib", "cmake"] : []),
270
+ ].filter((command, index, all) => all.indexOf(command) === index);
271
+ const identities = await Promise.all(commands.map(async (command) => {
272
+ const spellingKey = `${environmentFingerprint}\0${command}`;
273
+ const resolved = await resolvedToolIdentity(command);
274
+ if (resolved !== null)
275
+ vendorCompilerIdentityFallbacks.set(spellingKey, resolved);
276
+ return [
277
+ command,
278
+ resolved ??
279
+ vendorCompilerIdentityFallbacks.get(spellingKey) ??
280
+ `<unresolved>\0${command}`,
281
+ ].join("\0");
282
+ }));
283
+ return vendorCacheBuildIdentity(environmentFingerprint, identities.join("\x1f"));
284
+ }
285
+ function engineArchivePath(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
286
+ const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
287
+ return join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-${flavor}`, "libqjs.a");
288
+ }
91
289
  async function fileExists(path) {
92
290
  return stat(path).then((s) => s.isFile(), () => false);
93
291
  }
292
+ function privateSiblingPath(destination, label) {
293
+ // Keep the temporary component independent of the caller's basename. A
294
+ // destination can validly consume the filesystem's entire NAME_MAX budget;
295
+ // appending or prepending that basename would make the atomic install fail.
296
+ return join(dirname(destination), `.scriptc-${label}-${process.pid}-${Math.random().toString(36).slice(2)}`);
297
+ }
298
+ /** Copy an artifact through a private sibling name, then atomically install it.
299
+ * The source can live on another filesystem (cache/temp roots commonly do), so
300
+ * a direct rename is not portable. */
301
+ async function installArtifact(source, destination, mode) {
302
+ await mkdir(dirname(destination), { recursive: true });
303
+ const tmp = privateSiblingPath(destination, "install");
304
+ try {
305
+ await copyFile(source, tmp);
306
+ if (mode !== undefined)
307
+ await chmod(tmp, mode);
308
+ await rename(tmp, destination);
309
+ }
310
+ finally {
311
+ await rm(tmp, { force: true }).catch(() => undefined);
312
+ }
313
+ }
94
314
  /** The engine archive for one flavor, built lazily on the first --dynamic
95
- * compile (~10s) and cached under vendor/.cache/<commit>-<flavor>/ — unlike
315
+ * compile (~10s) and cached under vendor/.cache/<commit>-<flavor>-<target>-<toolchain>/ — unlike
96
316
  * the runtime's own sources (recompiled every build, ~100ms), the engine is
97
317
  * far too big to rebuild per compile. The plain flavor is ALWAYS MinSizeRel
98
318
  * regardless of the program's optimization level: measured binary delta is
@@ -109,12 +329,11 @@ async function fileExists(path) {
109
329
  * (QJS_ENGINE_SOURCES) compiled with `zig cc -target <triple>` under the
110
330
  * same flags CMake's MinSizeRel/Debug+ASan configurations apply, packed
111
331
  * with `zig ar`. Host builds keep the exact historical CMake recipe. */
112
- async function ensureEngineArchive(sanitize, driver) {
113
- const flavor = `${sanitize ? "asan" : "plain"}${driver.target !== null ? `-${driver.target}` : ""}`;
332
+ async function ensureEngineArchive(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
333
+ const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
114
334
  const vendor = vendorEngineDir();
115
- const cacheRoot = join(vendor, "..", ".cache");
116
- const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-${flavor}`);
117
- const archive = join(cacheDir, "libqjs.a");
335
+ const archive = engineArchivePath(sanitize, driver, buildIdentity, cacheRoot);
336
+ const cacheDir = dirname(archive);
118
337
  if (await fileExists(archive))
119
338
  return archive;
120
339
  if (driver.target !== null)
@@ -219,9 +438,16 @@ async function buildEngineArchiveCross(sanitize, driver, cacheRoot, cacheDir) {
219
438
  * NOT the engine — a regex-using static binary links ~110KB of matcher,
220
439
  * never the ~620KB island. */
221
440
  const LRE_SOURCES = ["libregexp.c", "libunicode.c"];
441
+ function lreObjectPaths(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
442
+ const flavor = (sanitize ? "asan" : "plain") +
443
+ (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
444
+ `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
445
+ const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`);
446
+ return LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
447
+ }
222
448
  /** The libregexp objects for one flavor, compiled lazily on the first
223
449
  * regex-using static build (~1s) and cached like the engine archive —
224
- * vendor/.cache/<commit>-lre-<flavor>/*.o — with the same atomic-rename
450
+ * vendor/.cache/<commit>-lre-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
225
451
  * publish (parallel first builds race safely; losers discard their work).
226
452
  * Plain is -Os (size class matters: the regex fence pins regex-using
227
453
  * binaries well under the engine class); asan matches the final link so
@@ -230,18 +456,17 @@ const LRE_SOURCES = ["libregexp.c", "libunicode.c"];
230
456
  * args — libregexp/libunicode are plain C11, so the cross story is exactly
231
457
  * the runtime sources' (no host-built inputs); vendored SOURCES are
232
458
  * untouched, only the build wiring knows about targets. */
233
- async function ensureLreObjects(sanitize, driver) {
459
+ async function ensureLreObjects(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
234
460
  // The flavor keys the DRIVER as well as the target: a zig-cc-built object
235
461
  // set must never be handed to a clang link (or vice versa) off a shared
236
- // cache directory — the historical `lre-plain`/`lre-asan` names stay
237
- // clang's alone.
462
+ // cache directory. Native targets include host platform + architecture so
463
+ // arm64 and Rosetta processes never exchange Mach-O objects.
238
464
  const flavor = (sanitize ? "asan" : "plain") +
239
465
  (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
240
- (driver.target !== null ? `-${driver.target}` : "");
466
+ `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
241
467
  const vendor = vendorEngineDir();
242
- const cacheRoot = join(vendor, "..", ".cache");
243
- const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`);
244
- const objects = LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
468
+ const objects = lreObjectPaths(sanitize, driver, buildIdentity, cacheRoot);
469
+ const cacheDir = dirname(objects[0]);
245
470
  if ((await Promise.all(objects.map(fileExists))).every(Boolean))
246
471
  return objects;
247
472
  await mkdir(cacheRoot, { recursive: true });
@@ -279,23 +504,29 @@ function vendorZlibDir() {
279
504
  * references the gzFile API, and those TUs alone want unistd/io headers).
280
505
  * Host builds never touch this list — they link the system libz. */
281
506
  const ZLIB_SOURCES = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"];
507
+ function zlibObjectPaths(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
508
+ const flavor = (sanitize ? "asan" : "plain") +
509
+ (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
510
+ `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
511
+ const cacheDir = join(cacheRoot, `zlib-${ZLIB_VERSION}-${flavor}`);
512
+ return ZLIB_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
513
+ }
282
514
  /** The zlib objects for one flavor, compiled lazily on the first zlib-using
283
515
  * CROSS build (~1s) and cached like the lre objects —
284
- * vendor/.cache/zlib-<version>-<flavor>/*.o — with the same atomic-rename
516
+ * vendor/.cache/zlib-<version>-<flavor>-<target>-<toolchain>/*.o — with the same atomic-rename
285
517
  * publish (parallel first builds race safely; losers discard their work).
286
518
  * Plain is -Os, asan matches the final link so the sanitized lane
287
519
  * instruments the codec too. The flavor keys the driver and target exactly
288
520
  * like the lre flavor: only cross builds call this today, but the keying
289
521
  * must never hand a zig-built object set to a clang link off a shared
290
522
  * directory. */
291
- async function ensureZlibObjects(sanitize, driver) {
523
+ async function ensureZlibObjects(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
292
524
  const flavor = (sanitize ? "asan" : "plain") +
293
525
  (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
294
- (driver.target !== null ? `-${driver.target}` : "");
526
+ `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
295
527
  const vendor = vendorZlibDir();
296
- const cacheRoot = join(vendor, "..", ".cache");
297
- const cacheDir = join(cacheRoot, `zlib-${ZLIB_VERSION}-${flavor}`);
298
- const objects = ZLIB_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
528
+ const objects = zlibObjectPaths(sanitize, driver, buildIdentity, cacheRoot);
529
+ const cacheDir = dirname(objects[0]);
299
530
  if ((await Promise.all(objects.map(fileExists))).every(Boolean))
300
531
  return objects;
301
532
  await mkdir(cacheRoot, { recursive: true });
@@ -338,9 +569,12 @@ const CURL_STUB_SYMBOLS = [
338
569
  "curl_slist_append", "curl_slist_free_all",
339
570
  "curl_url", "curl_url_cleanup", "curl_url_get", "curl_url_set",
340
571
  ];
572
+ function curlStubDirPath(driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
573
+ return join(cacheRoot, `curl-stub-${driver.target}-${buildIdentity}`);
574
+ }
341
575
  /** The libcurl import stub for one CROSS target, generated lazily on the
342
576
  * first fetch-using cross build and cached like the zlib objects —
343
- * vendor/.cache/curl-stub-<target>/libcurl.so, atomic-rename publish. The
577
+ * vendor/.cache/curl-stub-<target>-<toolchain>/libcurl.so, atomic-rename publish. The
344
578
  * stub is empty definitions of CURL_STUB_SYMBOLS compiled `-shared` with
345
579
  * `-soname libcurl.so.4`: linking against it satisfies scr_fetch_curl.c's
346
580
  * references and records a plain `DT_NEEDED libcurl.so.4`, which the
@@ -349,9 +583,8 @@ const CURL_STUB_SYMBOLS = [
349
583
  * from the stub's bodies ever ships in the binary). One stub per target,
350
584
  * no sanitize flavor: the stub's code is never executed or instrumented,
351
585
  * and asan links against plain shared libraries freely. */
352
- async function ensureCurlStub(driver) {
353
- const cacheRoot = join(vendorCurlDir(), "..", ".cache");
354
- const cacheDir = join(cacheRoot, `curl-stub-${driver.target}`);
586
+ async function ensureCurlStub(driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
587
+ const cacheDir = curlStubDirPath(driver, buildIdentity, cacheRoot);
355
588
  const lib = join(cacheDir, "libcurl.so");
356
589
  if (await fileExists(lib))
357
590
  return cacheDir;
@@ -385,9 +618,13 @@ async function ensureCurlStub(driver) {
385
618
  function vendorTlsDir() {
386
619
  return join(runtimeSrcDir(), "..", "vendor", "mbedtls");
387
620
  }
621
+ function tlsArchivePath(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
622
+ const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
623
+ return join(cacheRoot, `mbedtls-${MBEDTLS_VERSION}-${flavor}`, "libmbedtls.a");
624
+ }
388
625
  /** The mbedTLS archive for one flavor, compiled lazily on the first
389
626
  * TLS-using build (~15s over ~110 TUs, parallelized) and cached like the
390
- * engine archive — vendor/.cache/mbedtls-<version>-<flavor>/libmbedtls.a —
627
+ * engine archive — vendor/.cache/mbedtls-<version>-<flavor>-<target>-<toolchain>/libmbedtls.a —
391
628
  * with the same atomic-rename publish (parallel first builds race safely;
392
629
  * losers discard their work). Plain is -Os (the TLS stack is link-gated
393
630
  * but still wants its size class small); asan matches the final link so
@@ -400,14 +637,13 @@ function vendorTlsDir() {
400
637
  * TUs compile with `zig cc -target <triple>` and the archive is packed
401
638
  * with `zig ar` (llvm-ar — the host BSD ar has no business indexing ELF
402
639
  * objects). Host builds keep the exact historical clang + ar recipe. */
403
- async function ensureTlsArchive(sanitize, driver) {
404
- const flavor = `${sanitize ? "asan" : "plain"}${driver.target !== null ? `-${driver.target}` : ""}`;
640
+ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = vendorBuildCacheRoot()) {
641
+ const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
405
642
  const compileArgv = driver.target !== null ? driver.argv : ["clang"];
406
643
  const arArgv = driver.target !== null ? [...driver.argv.slice(0, 1), "ar"] : ["ar"];
407
644
  const vendor = vendorTlsDir();
408
- const cacheRoot = join(vendor, "..", ".cache");
409
- const cacheDir = join(cacheRoot, `mbedtls-${MBEDTLS_VERSION}-${flavor}`);
410
- const archive = join(cacheDir, "libmbedtls.a");
645
+ const archive = tlsArchivePath(sanitize, driver, buildIdentity, cacheRoot);
646
+ const cacheDir = dirname(archive);
411
647
  if (await fileExists(archive))
412
648
  return archive;
413
649
  await mkdir(cacheRoot, { recursive: true });
@@ -444,8 +680,9 @@ async function ensureTlsArchive(sanitize, driver) {
444
680
  * One `scriptc build --lib` invocation produces <name>.lib.a: the program TU
445
681
  * object plus exactly the runtime objects the program's IR gates in, every
446
682
  * TU compiled with -DSCR_LIB (the per-flavor discipline that keeps library
447
- * objects apart from executable-lane objects — here trivially, because library
448
- * builds compile fresh into the archive and never touch the object cache).
683
+ * objects apart from executable-lane objects). Persistent builds cache the
684
+ * completed archive by program-TU content and cache runtime objects separately,
685
+ * so an edit recompiles only the changed program object before re-archiving.
449
686
  * The base set narrows from the executable lane's unconditional sources:
450
687
  * scr_async.c (fibers, timers, the loop) and scr_child.c drop — the
451
688
  * async_free refusal already guarantees nothing references them — and
@@ -467,8 +704,6 @@ export async function compileLibArchive(opts) {
467
704
  const driver = resolveCc();
468
705
  const sanitize = opts.sanitize ?? false;
469
706
  const regex = opts.regex ?? false;
470
- const lreObjects = regex ? await ensureLreObjects(sanitize, driver) : [];
471
- const zlibObjects = opts.zlib ? await ensureZlibObjects(sanitize, driver) : [];
472
707
  const sources = [
473
708
  ...LIB_RUNTIME_SOURCES,
474
709
  // win32 targets compile the libc-shim TU into the archive (stpcpy,
@@ -501,129 +736,1267 @@ export async function compileLibArchive(opts) {
501
736
  ...(regex ? ["-I", vendorEngineDir()] : []),
502
737
  ...(opts.zlib ? ["-I", vendorZlibDir()] : []),
503
738
  ];
739
+ const programCompilerArgs = opts.cPath.endsWith(".ll")
740
+ ? [...cflags, "-Wno-override-module"]
741
+ : cflags;
742
+ const programSourceExtension = opts.cPath.endsWith(".ll") ? ".ll" : ".c";
504
743
  const arArgv = driver.argv[0] === "zig" ? [driver.argv[0], "ar"] : ["ar"];
505
- const buildDir = await mkdtemp(join(tmpdir(), "scriptc-lib-"));
744
+ const cachePolicy = toolchainEnvironmentCachePolicy();
745
+ const configuredCacheRoot = cacheRootDir();
746
+ const toolchainEnv = toolchainEnvironmentFingerprint();
747
+ const persistentDriverCache = cachePolicy.runtimeObjects &&
748
+ configuredCacheRoot !== null &&
749
+ await compilerDriverSupportsPersistentCache(driver, toolchainEnv);
750
+ // A library archive is compile-only from clang's perspective. Link-only
751
+ // search variables cannot affect it, but any mutable compilation input or
752
+ // opaque compiler wrapper makes every persistent tier unsafe to reuse. An
753
+ // opaque archiver narrows only the completed-archive tier below.
754
+ let root = opts.cacheIdentity === undefined || !persistentDriverCache
755
+ ? null
756
+ : configuredCacheRoot;
757
+ if (root !== null) {
758
+ try {
759
+ await ensurePrivateCacheRoot(root, process.env["SCRIPTC_CACHE_DIR"] === undefined);
760
+ }
761
+ catch {
762
+ root = null;
763
+ }
764
+ }
765
+ let implicitToolchain = null;
766
+ let runtimeCompilerInvocation = null;
767
+ let programCompilerInvocation = null;
768
+ if (persistentDriverCache) {
769
+ try {
770
+ implicitToolchain = await implicitToolchainFingerprint(driver, toolchainEnv);
771
+ }
772
+ catch {
773
+ // An identity probe is cache machinery, never a reason a valid native
774
+ // compile should fail. Disable every persistent tier for this invocation.
775
+ root = null;
776
+ }
777
+ }
778
+ if (root !== null) {
779
+ try {
780
+ runtimeCompilerInvocation = await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags);
781
+ programCompilerInvocation = programSourceExtension === ".ll"
782
+ ? await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
783
+ : runtimeCompilerInvocation;
784
+ }
785
+ catch {
786
+ // A wrapper that cannot expose its effective invocation can still build,
787
+ // but its outputs cannot safely participate in a persistent cache.
788
+ root = null;
789
+ }
790
+ }
791
+ const vendorBuildIdentity = await currentVendorCacheBuildIdentity(driver, `${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`);
792
+ const cacheCompleteArchive = root !== null && await archiverSupportsPersistentCache(arArgv, driver);
793
+ let cachedArchive = null;
794
+ let compilerVersion = "";
795
+ let archiverVersion = "";
796
+ let runtimeHash = "";
797
+ let programDependencyHash = "";
798
+ let cachedProgramBytes = null;
799
+ if (root !== null) {
800
+ try {
801
+ const [cv, fingerprint, programBytes] = await Promise.all([
802
+ ccVersionOnce(driver.argv, toolchainEnv, true),
803
+ runtimeFingerprint(rtDir),
804
+ readFile(opts.cPath),
805
+ ]);
806
+ compilerVersion = cv;
807
+ runtimeHash = fingerprint;
808
+ cachedProgramBytes = programBytes;
809
+ if (cacheCompleteArchive) {
810
+ const [av, programDependencies] = await Promise.all([
811
+ toolVersionOnce(arArgv, toolchainEnv, true),
812
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, programBytes, toolchainEnv),
813
+ ]);
814
+ archiverVersion = av;
815
+ programDependencyHash = programDependencies;
816
+ const key = createHash("sha256")
817
+ // v7 adds effective compiler-wrapper invocations for the real runtime
818
+ // and program compile flavors.
819
+ .update("lib-v7\0")
820
+ .update(cacheTargetIdentity(driver)).update("\0")
821
+ .update(toolchainEnv).update("\0")
822
+ .update(implicitToolchain).update("\0")
823
+ .update(runtimeCompilerInvocation).update("\0")
824
+ .update(programCompilerInvocation).update("\0")
825
+ .update(programDependencies).update("\0")
826
+ .update(opts.cacheIdentity).update("\0")
827
+ .update(driver.argv.join("\x1f")).update("\0")
828
+ .update(cv).update("\0")
829
+ .update(fingerprint).update("\0")
830
+ .update(arArgv.join("\x1f")).update("\0")
831
+ .update(av).update("\0")
832
+ .update(cflags.join("\x1f")).update("\0")
833
+ .update(sources.join("\x1f")).update("\0")
834
+ // The compiler-visible spelling and resolved location are both inputs:
835
+ // __FILE__ observes the former, while relative includes follow the
836
+ // latter. Archive members also inherit the TU's basename.
837
+ .update(opts.cPath).update("\0")
838
+ .update(resolve(opts.cPath)).update("\0")
839
+ .update(programBytes)
840
+ .digest("hex");
841
+ cachedArchive = join(root, "lib", key);
842
+ const tmpOut = privateSiblingPath(opts.outPath, "lib-hit");
843
+ try {
844
+ await mkdir(dirname(opts.outPath), { recursive: true });
845
+ if (!(await copyValidCachedFile(cachedArchive, tmpOut))) {
846
+ throw new Error("invalid cached library archive");
847
+ }
848
+ // Match a fresh `ar` output under the caller's current umask. Cache
849
+ // entries may have been populated by a less restrictive shell.
850
+ await chmod(tmpOut, 0o666 & ~process.umask());
851
+ await rename(tmpOut, opts.outPath);
852
+ return;
853
+ }
854
+ catch {
855
+ await rm(tmpOut, { force: true }).catch(() => undefined);
856
+ // Miss (or unreadable cache): compile below and publish best-effort.
857
+ }
858
+ }
859
+ }
860
+ catch {
861
+ // Cache identity trouble is never a build failure. The fresh path below
862
+ // retains the historical compile-everything behavior.
863
+ cachedArchive = null;
864
+ }
865
+ }
866
+ const transientVendorRoot = persistentDriverCache && implicitToolchain !== null
867
+ ? null
868
+ : join(tmpdir(), `scriptc-lib-vendor-${process.pid}-${Math.random().toString(36).slice(2)}`);
869
+ const vendorCacheRoot = transientVendorRoot ?? vendorBuildCacheRoot();
506
870
  try {
507
- const objects = [];
508
- const compileOne = async (src, objName) => {
509
- const obj = join(buildDir, objName);
510
- const args = [
511
- ...driver.argv.slice(1),
512
- ...cflags,
513
- ...(src.endsWith(".ll") ? ["-Wno-override-module"] : []),
514
- "-c", src,
515
- "-o", obj,
516
- ];
517
- try {
518
- await execFileAsync(driver.argv[0] ?? "clang", args);
871
+ const lreObjects = regex
872
+ ? await ensureLreObjects(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
873
+ : [];
874
+ const zlibObjects = opts.zlib
875
+ ? await ensureZlibObjects(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
876
+ : [];
877
+ const buildDir = await mkdtemp(join(tmpdir(), "scriptc-lib-"));
878
+ try {
879
+ const compileOne = async (src, objName, compilerVisibleSource) => {
880
+ const obj = join(buildDir, objName);
881
+ const args = [
882
+ ...driver.argv.slice(1),
883
+ ...cflags,
884
+ ...(compilerVisibleSource !== undefined
885
+ ? [
886
+ `-ffile-prefix-map=${src}=${compilerVisibleSource}`,
887
+ "-iquote",
888
+ dirname(resolve(compilerVisibleSource)),
889
+ ]
890
+ : []),
891
+ ...(src.endsWith(".ll") ? ["-Wno-override-module"] : []),
892
+ "-c", src,
893
+ "-o", obj,
894
+ ];
895
+ try {
896
+ await execFileAsync(driver.argv[0] ?? "clang", args);
897
+ }
898
+ catch (err) {
899
+ const stderr = err.stderr ?? String(err);
900
+ throw new Error(`${driver.argv.join(" ")} failed compiling ${src} for the library archive.\n` +
901
+ `This is a scriptc bug (generated/runtime C should always compile) unless the compiler itself is missing/broken.\n\n${stderr}`);
902
+ }
903
+ return obj;
904
+ };
905
+ const stem = basename(opts.cPath).replace(/\.(c|ll)$/, "");
906
+ const programSource = cachedProgramBytes === null
907
+ ? opts.cPath
908
+ : join(buildDir, `program${opts.cPath.endsWith(".ll") ? ".ll" : ".c"}`);
909
+ if (cachedProgramBytes !== null)
910
+ await writeFile(programSource, cachedProgramBytes);
911
+ const programObject = await compileOne(programSource, `${stem}.program.o`, cachedProgramBytes === null ? undefined : opts.cPath);
912
+ let runtimeObjects = null;
913
+ let cacheInputsStable = true;
914
+ let objectImplicitVerification = null;
915
+ const objectImplicitToolchainStillMatches = () => {
916
+ objectImplicitVerification ??= Promise.all([
917
+ implicitToolchainFingerprint(driver, toolchainEnv),
918
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
919
+ ]).then(([currentImplicit, currentInvocation]) => currentImplicit === implicitToolchain &&
920
+ currentInvocation === runtimeCompilerInvocation, () => false);
921
+ return objectImplicitVerification;
922
+ };
923
+ if (root !== null && compilerVersion !== "" && runtimeHash !== "") {
924
+ try {
925
+ const sourcePaths = sources.map((f) => join(rtDir, f));
926
+ const cached = await ensureRuntimeObjects(root, driver.argv, cflags, sourcePaths, `lib-obj-v5\0${cacheTargetIdentity(driver)}\0${toolchainEnv}\0${implicitToolchain}\0${runtimeCompilerInvocation}\0${driver.argv.join(" ")}\0${compilerVersion}\0${runtimeHash}\0`, async () => (await runtimeFingerprint(rtDir)) === runtimeHash &&
927
+ (await objectImplicitToolchainStillMatches()));
928
+ const staged = await stageRuntimeObjects(cached, join(buildDir, "cached-runtime"));
929
+ runtimeObjects = sourcePaths.map((path) => staged.get(path));
930
+ }
931
+ catch (err) {
932
+ if (err instanceof CacheInputsChangedError)
933
+ cacheInputsStable = false;
934
+ runtimeObjects = null;
935
+ }
519
936
  }
520
- catch (err) {
521
- const stderr = err.stderr ?? String(err);
522
- throw new Error(`${driver.argv.join(" ")} failed compiling ${src} for the library archive.\n` +
523
- `This is a scriptc bug (generated/runtime C should always compile) unless the compiler itself is missing/broken.\n\n${stderr}`);
937
+ if (runtimeObjects === null) {
938
+ runtimeObjects = [];
939
+ const width = Math.min(4, availableParallelism());
940
+ for (let i = 0; i < sources.length; i += width) {
941
+ runtimeObjects.push(...(await Promise.all(sources.slice(i, i + width).map((f) => compileOne(join(rtDir, f), f.replace(/\.c$/, ".o"))))));
942
+ }
524
943
  }
525
- objects.push(obj);
526
- };
527
- const stem = basename(opts.cPath).replace(/\.(c|ll)$/, "");
528
- await compileOne(opts.cPath, `${stem}.program.o`);
529
- const width = Math.min(4, availableParallelism());
530
- for (let i = 0; i < sources.length; i += width) {
531
- await Promise.all(sources.slice(i, i + width).map((f) => compileOne(join(rtDir, f), f.replace(/\.c$/, ".o"))));
944
+ const objects = [programObject, ...runtimeObjects, ...lreObjects, ...zlibObjects];
945
+ // A cacheable build owns a private archive from `ar` through publication.
946
+ // The caller-visible output can be shared by another invocation without
947
+ // letting that invocation's bytes poison this key.
948
+ const archiveOutput = cachedArchive === null ? opts.outPath : join(buildDir, "artifact.lib.a");
949
+ await rm(archiveOutput, { force: true }); // `ar r` would append into a stale archive
950
+ await mkdir(dirname(archiveOutput), { recursive: true });
951
+ await execFileAsync(arArgv[0] ?? "ar", [
952
+ ...arArgv.slice(1),
953
+ "rcs",
954
+ archiveOutput,
955
+ ...objects,
956
+ ]);
957
+ if (archiveOutput !== opts.outPath)
958
+ await installArtifact(archiveOutput, opts.outPath);
959
+ let runtimeStillMatchesKey = false;
960
+ if (cachedArchive !== null &&
961
+ root !== null &&
962
+ runtimeHash !== "" &&
963
+ programDependencyHash !== "" &&
964
+ compilerVersion !== "" &&
965
+ archiverVersion !== "") {
966
+ const [currentRuntime, currentImplicit, currentRuntimeInvocation, currentProgramInvocation, currentProgramDependencies, currentCompiler, currentArchiver] = await Promise.all([
967
+ runtimeFingerprint(rtDir).catch(() => null),
968
+ implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
969
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags).catch(() => null),
970
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension).catch(() => null),
971
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cachedProgramBytes, toolchainEnv).catch(() => null),
972
+ ccVersionOnce(driver.argv, toolchainEnv, true).catch(() => null),
973
+ toolVersionOnce(arArgv, toolchainEnv, true).catch(() => null),
974
+ ]);
975
+ runtimeStillMatchesKey =
976
+ cacheInputsStable &&
977
+ currentRuntime === runtimeHash &&
978
+ currentImplicit === implicitToolchain &&
979
+ currentRuntimeInvocation === runtimeCompilerInvocation &&
980
+ currentProgramInvocation === programCompilerInvocation &&
981
+ currentProgramDependencies === programDependencyHash &&
982
+ currentCompiler === compilerVersion &&
983
+ currentArchiver === archiverVersion;
984
+ }
985
+ if (cachedArchive !== null && root !== null && runtimeStillMatchesKey) {
986
+ try {
987
+ await publishCachedFile(archiveOutput, cachedArchive);
988
+ }
989
+ catch {
990
+ // Publishing is best-effort; the requested archive is already valid.
991
+ }
992
+ }
993
+ }
994
+ finally {
995
+ await rm(buildDir, { recursive: true, force: true });
532
996
  }
533
- objects.push(...lreObjects, ...zlibObjects);
534
- await rm(opts.outPath, { force: true }); // `ar r` would append into a stale archive
535
- await mkdir(dirname(opts.outPath), { recursive: true });
536
- await execFileAsync(arArgv[0] ?? "ar", [...arArgv.slice(1), "rcs", opts.outPath, ...objects]);
537
997
  }
538
998
  finally {
539
- await rm(buildDir, { recursive: true, force: true });
999
+ if (transientVendorRoot !== null) {
1000
+ await rm(transientVendorRoot, { recursive: true, force: true }).catch(() => undefined);
1001
+ }
540
1002
  }
1003
+ if (root !== null)
1004
+ await pruneCache(root).catch(() => undefined);
541
1005
  }
542
- /* ------------------- build cache (opt-in via SCRIPTC_CACHE_DIR) ----------------
1006
+ /* -------------------------- persistent build cache ---------------------------
543
1007
  * Content-addressed caches that let repeat builds of unchanged programs skip
544
- * clang entirely — the test lanes' dominant cost. Two keyspaces under the
545
- * cache root:
1008
+ * payload code generation/linking — the test lanes' dominant cost. Executable
1009
+ * lookups still run lightweight dependency and link-input metadata probes.
1010
+ * Three keyspaces under the cache root:
546
1011
  *
547
- * bin/<key> — whole program binaries. key = sha256(clang version,
548
- * runtime fingerprint (every .c/.h in the runtime src dir
549
- * plus the vendor pin QJS_COMMIT), the FULL normalized
550
- * command line, the emitted C bytes). Emitted C is
1012
+ * bin/<key> — whole program binaries. key = sha256(resolved clang
1013
+ * identity/version, target + compiler/linker environment,
1014
+ * effective driver invocations for the real build flags,
1015
+ * implicit system-header and resolved linker-input bytes,
1016
+ * linker/assembler identities, runtime fingerprint
1017
+ * (every .c/.h in the runtime src dir plus the vendor pin
1018
+ * QJS_COMMIT), the caller's dependency identity, the
1019
+ * compiler-visible TU path, Darwin output basename, the
1020
+ * FULL normalized command line, and the emitted C bytes).
1021
+ * Emitted C is
551
1022
  * byte-stable by project invariant, so unchanged programs
552
1023
  * hit; any flag difference — e.g. the sanitized lane's
553
1024
  * -O1/-fsanitize=address/-DSCR_RC_AUDIT — lands in a
554
1025
  * naturally distinct key. On a hit the cached binary is
555
1026
  * copied and atomically renamed onto outPath (never an
556
- * in-place overwrite — see the hit path) and clang never
557
- * runs; the binary is still EXECUTED live by whoever
1027
+ * in-place overwrite — see the hit path) after its digest
1028
+ * is verified; the binary is still EXECUTED live by whoever
558
1029
  * asked for it, so no comparison or sanitizer coverage
559
1030
  * is ever skipped.
560
1031
  *
1032
+ * lib/<key> — whole library archives. The identity covers the resolved
1033
+ * compiler and archiver identities/versions, compiler/linker
1034
+ * environment, effective build-flavor driver invocations,
1035
+ * implicit toolchain inputs, runtime fingerprint,
1036
+ * target/flags, gated source set, caller dependency
1037
+ * identity, TU path, and program-TU bytes.
1038
+ * Checksum-verified hits skip native code generation and ar.
1039
+ *
561
1040
  * obj/<set>/<f>.o — per-flavor runtime objects for cache-miss builds. The
562
1041
  * historical single invocation recompiles every runtime
563
1042
  * TU per program (~1.3s at -O2); with cached objects a
564
1043
  * miss compiles ONLY the program's C and links (~0.15s).
1044
+ * Library-mode -DSCR_LIB objects use a distinct flavor.
565
1045
  * The clang driver hands every input the same option set,
566
1046
  * so per-TU `-c` compiles with those options plus a final
567
- * link reproduce the single invocation exactly. Object
1047
+ * link reproduce the single invocation exactly; the
1048
+ * driver's effective invocation under that flavor also
1049
+ * joins the object-set identity. Every
1050
+ * cached object carries a verified digest. Object
568
1051
  * compiles go through ccache when it is installed (silent
569
1052
  * fallback when not).
570
1053
  *
571
- * Escape hatches, honored in BOTH directions (no reads, no writes): leave
572
- * SCRIPTC_CACHE_DIR unset (the production default — the CLI never caches unless
573
- * asked; vitest.config.ts sets it for the test lanes) or set SCRIPTC_NO_CACHE=1.
574
- * Either way compileC issues the exact historical command line.
1054
+ * Frontend-generated production builds supply a dependency identity and use a
1055
+ * per-user platform cache by default. Arbitrary low-level TUs omit the identity
1056
+ * and bypass this cache because their include graph is caller-owned.
1057
+ * Compiler wrappers also bypass every persistent tier: a wrapper can branch on
1058
+ * the real source/object topology and inject inputs no synthetic probe sees.
1059
+ * Direct Clang/Zig drivers and Apple's immutable /usr/bin/clang handoff retain
1060
+ * caching; wrapper-driven builds use private, invocation-local vendor outputs.
1061
+ * Opaque archiver wrappers bypass completed library archives for the same
1062
+ * reason while retaining safely keyed runtime objects.
1063
+ * Builds with caller-supplied native link inputs (archive/object paths or
1064
+ * `-l<name>` libraries) also bypass the whole binary cache: a thin archive,
1065
+ * linker script, or ambient resolution can change transitively without
1066
+ * changing the named input's bytes or spelling. Their runtime objects remain
1067
+ * cached, and every invocation performs the final link against current inputs.
1068
+ * SCRIPTC_CACHE_DIR overrides the root (the test lanes use this to stay
1069
+ * repo-local); an explicitly empty value or SCRIPTC_NO_CACHE=1 disables reads
1070
+ * and writes. With caching disabled compileC issues the exact historical
1071
+ * command line.
575
1072
  *
576
1073
  * Eviction: size-capped LRU over the whole cache root (SCRIPTC_CACHE_MAX_MB,
577
- * default 4096) swept at most once per process after a write; reads bump
578
- * mtimes. The harness's oracle cache lives under the same root and is swept
579
- * by the same pass. Cache trouble is never a build failure — every cache
580
- * error falls back to a real compile. */
581
- function cacheRootDir() {
582
- if (process.env["SCRIPTC_NO_CACHE"] === "1")
1074
+ * default 4096). Explicit caps are checked after every successful write; the
1075
+ * large default is swept on the first and every 64th write so corpus/watch
1076
+ * loops do not repeatedly walk a growing tree. Reads bump mtimes. The harness's
1077
+ * oracle cache lives under the same root and is swept by the same pass. Cache
1078
+ * trouble is never a build failure — every cache error falls back to a real
1079
+ * compile. */
1080
+ /** Resolve the build cache without touching the filesystem. Exported from this
1081
+ * internal module so its platform and override behavior can be pinned directly. */
1082
+ export function resolveBuildCacheRoot(env = process.env, platform = process.platform, userHome = homedir()) {
1083
+ if (env["SCRIPTC_NO_CACHE"] === "1")
583
1084
  return null;
584
- const dir = process.env["SCRIPTC_CACHE_DIR"];
585
- return dir !== undefined && dir !== "" ? dir : null;
1085
+ const configured = env["SCRIPTC_CACHE_DIR"];
1086
+ if (configured !== undefined)
1087
+ return configured === "" ? null : resolve(configured);
1088
+ const xdg = env["XDG_CACHE_HOME"];
1089
+ if (xdg !== undefined && xdg !== "")
1090
+ return resolve(xdg, "scriptc", "build");
1091
+ if (platform === "win32") {
1092
+ const local = env["LOCALAPPDATA"];
1093
+ if (local !== undefined && local !== "")
1094
+ return resolve(local, "scriptc", "cache", "build");
1095
+ }
1096
+ return platform === "darwin"
1097
+ ? resolve(userHome, "Library", "Caches", "scriptc", "build")
1098
+ : resolve(userHome, ".cache", "scriptc", "build");
1099
+ }
1100
+ function cacheRootDir() {
1101
+ return resolveBuildCacheRoot();
1102
+ }
1103
+ /** The production cache can contain complete user executables/archives with
1104
+ * embedded source literals or comptime values. Its root is therefore private
1105
+ * regardless of the caller's ordinary output umask. Windows inherits the
1106
+ * per-user LOCALAPPDATA ACL. POSIX platform-default roots are hardened for
1107
+ * upgrades; an arbitrary existing SCRIPTC_CACHE_DIR override is never chmod'd
1108
+ * and participates only when its caller-provided mode is already private. */
1109
+ async function ensurePrivateCacheRoot(root, hardenExisting) {
1110
+ const existing = await stat(root).then((info) => info, (error) => {
1111
+ if (error.code === "ENOENT")
1112
+ return null;
1113
+ throw error;
1114
+ });
1115
+ if (existing !== null && !existing.isDirectory()) {
1116
+ throw new Error("native cache root is not a directory");
1117
+ }
1118
+ if (process.platform !== "win32" &&
1119
+ existing !== null &&
1120
+ !hardenExisting &&
1121
+ (existing.mode & 0o077) !== 0) {
1122
+ throw new Error("existing native cache override is not private");
1123
+ }
1124
+ await mkdir(root, { recursive: true, mode: 0o700 });
1125
+ if (process.platform !== "win32" && (existing === null || hardenExisting)) {
1126
+ await chmod(root, 0o700);
1127
+ }
586
1128
  }
587
1129
  const ccVersionMemos = new Map();
588
- function ccVersionOnce(argv) {
589
- const key = argv.join("\x1f");
590
- let memo = ccVersionMemos.get(key);
1130
+ /** Resolve the executable the OS will select for an argv[0] spelling. The
1131
+ * path and inode metadata join the version output below: two PATH postures
1132
+ * must not share cache entries merely because both drivers call themselves
1133
+ * `clang` or print the same upstream version. ctime catches an in-place tool
1134
+ * replacement even when a package manager preserves its size and mtime. */
1135
+ async function resolvedTool(command, env = process.env) {
1136
+ const hasSeparator = command.includes("/") || command.includes("\\");
1137
+ const configuredPath = (env["PATH"] ?? (process.platform === "win32" ? "" : "/usr/bin:/bin"))
1138
+ .split(delimiter);
1139
+ const pathEntries = hasSeparator
1140
+ ? [""]
1141
+ : process.platform === "win32"
1142
+ ? ["", dirname(process.execPath), ...configuredPath]
1143
+ : configuredPath;
1144
+ const windowsExtensions = process.platform === "win32" && extname(command) === ""
1145
+ ? (env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((extension) => extension !== "")
1146
+ : [""];
1147
+ for (const entry of pathEntries) {
1148
+ const directory = entry.startsWith('"') && entry.endsWith('"') ? entry.slice(1, -1) : entry;
1149
+ const base = hasSeparator
1150
+ ? isAbsolute(command) ? command : resolve(command)
1151
+ : join(directory === "" ? process.cwd() : directory, command);
1152
+ for (const extension of windowsExtensions) {
1153
+ const candidate = `${base}${extension}`;
1154
+ try {
1155
+ await access(candidate, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
1156
+ const [canonical, info] = await Promise.all([realpath(candidate), stat(candidate)]);
1157
+ if (!info.isFile())
1158
+ continue;
1159
+ const fileIdentity = [
1160
+ canonical,
1161
+ info.dev,
1162
+ info.ino,
1163
+ info.size,
1164
+ info.mtimeMs,
1165
+ info.ctimeMs,
1166
+ ].join("\0");
1167
+ return {
1168
+ canonicalPath: canonical,
1169
+ fileIdentity,
1170
+ cacheIdentity: [resolve(candidate), fileIdentity].join("\0"),
1171
+ };
1172
+ }
1173
+ catch {
1174
+ // Keep searching PATH/PATHEXT exactly as process spawning would.
1175
+ }
1176
+ }
1177
+ }
1178
+ return null;
1179
+ }
1180
+ async function resolvedToolIdentity(command, env = process.env) {
1181
+ return (await resolvedTool(command, env))?.cacheIdentity ?? null;
1182
+ }
1183
+ const directCompilerDriverMemos = new Map();
1184
+ /** `/usr/bin/clang` on Darwin is Apple's immutable driver shim: its `-###`
1185
+ * trace names the selected Xcode/CommandLineTools clang rather than the shim
1186
+ * itself. The ordinary implicit-toolchain fingerprint below captures that
1187
+ * selected executable and its dependencies, so this trusted system handoff is
1188
+ * the one intentional exception to the same-executable rule. */
1189
+ function isAppleSystemClangHandoff(driver, effectiveCompiler) {
1190
+ return process.platform === "darwin" &&
1191
+ driver.canonicalPath === "/usr/bin/clang" &&
1192
+ basename(effectiveCompiler.canonicalPath) === "clang";
1193
+ }
1194
+ /** Persistent caches require an inspectable compiler driver. A general
1195
+ * wrapper can branch on the real source/object paths or argument topology and
1196
+ * inject inputs only into the final invocation; no synthetic metadata probe
1197
+ * can safely represent that behavior. Accept direct Clang/Zig drivers (plus
1198
+ * Apple's system shim) and conservatively keep wrapper-driven builds on the
1199
+ * uncached path. */
1200
+ async function compilerDriverSupportsPersistentCache(driver, environmentFingerprint) {
1201
+ // Cache-race tests exercise publication below an intentionally instrumented
1202
+ // wrapper. This is deliberately undocumented and test-scoped.
1203
+ if (process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1")
1204
+ return true;
1205
+ const driverKey = [
1206
+ environmentFingerprint,
1207
+ driver.argv.join("\x1f"),
1208
+ driver.target ?? "<native>",
1209
+ driver.targetArgs.join("\x1f"),
1210
+ ].join("\0");
1211
+ const compiler = driver.argv[0] ?? "clang";
1212
+ const resolvedDriver = await resolvedTool(compiler);
1213
+ // A prior dependency list cannot prove that name resolution is unchanged:
1214
+ // a new header in an earlier search directory leaves every previously
1215
+ // resolved file untouched. Require the driver to be present so each cache
1216
+ // invocation can rediscover its complete dependency graph.
1217
+ if (resolvedDriver === null)
1218
+ return false;
1219
+ const probeKey = `${driverKey}\0${resolvedDriver.fileIdentity}`;
1220
+ const memoized = directCompilerDriverMemos.get(probeKey);
1221
+ if (memoized !== undefined)
1222
+ return memoized;
1223
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-driver-probe-"));
1224
+ let direct = false;
1225
+ try {
1226
+ const source = join(probeDir, "empty.c");
1227
+ const object = join(probeDir, "empty.o");
1228
+ await writeFile(source, "int scriptc_driver_probe;\n");
1229
+ const trace = await execFileAsync(compiler, [
1230
+ ...driver.argv.slice(1),
1231
+ ...driver.targetArgs,
1232
+ "-###",
1233
+ "-std=c11",
1234
+ "-c",
1235
+ source,
1236
+ "-o",
1237
+ object,
1238
+ ], { cwd: probeDir, maxBuffer: 16 * 1024 * 1024 });
1239
+ const effectiveSpellings = [];
1240
+ for (const line of `${trace.stdout}\n${trace.stderr}`.split(/\r?\n/)) {
1241
+ const tokens = driverTraceCandidates(line);
1242
+ const cc1 = tokens.indexOf("-cc1");
1243
+ if (cc1 > 0)
1244
+ effectiveSpellings.push(tokens[cc1 - 1]);
1245
+ }
1246
+ if (effectiveSpellings.length === 1) {
1247
+ const effectiveCompiler = await resolvedTool(effectiveSpellings[0]);
1248
+ direct = effectiveCompiler !== null &&
1249
+ (effectiveCompiler.fileIdentity === resolvedDriver.fileIdentity ||
1250
+ isAppleSystemClangHandoff(resolvedDriver, effectiveCompiler));
1251
+ }
1252
+ }
1253
+ catch {
1254
+ direct = false;
1255
+ }
1256
+ finally {
1257
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1258
+ }
1259
+ directCompilerDriverMemos.set(probeKey, direct);
1260
+ return direct;
1261
+ }
1262
+ /** Complete library archives may skip `ar`, so the selected archiver must be
1263
+ * as inspectable as the compiler. `zig ar` is the already-validated Zig
1264
+ * executable. For the host spelling, accept only the immutable platform
1265
+ * tool locations; a PATH wrapper can branch on the real member topology or
1266
+ * inject mutable inputs that `ar --version` cannot expose. Other archivers
1267
+ * retain runtime-object reuse but rebuild the program member and archive. */
1268
+ async function archiverSupportsPersistentCache(arArgv, driver) {
1269
+ if (arArgv.length === 2 &&
1270
+ arArgv[0] === driver.argv[0] &&
1271
+ arArgv[1] === "ar" &&
1272
+ driver.argv[0] === "zig") {
1273
+ return true;
1274
+ }
1275
+ if (arArgv.length !== 1)
1276
+ return false;
1277
+ const archiver = await resolvedTool(arArgv[0] ?? "ar");
1278
+ if (archiver === null || !/(?:^|-)ar$/.test(basename(archiver.canonicalPath))) {
1279
+ return false;
1280
+ }
1281
+ if (archiver.canonicalPath.startsWith("/usr/bin/") || archiver.canonicalPath.startsWith("/bin/")) {
1282
+ return true;
1283
+ }
1284
+ return process.platform === "darwin" &&
1285
+ /^\/Applications\/Xcode[^/]*\.app\/Contents\/Developer\/Toolchains\/[^/]+\.xctoolchain\/usr\/bin\/[^/]*ar$/.test(archiver.canonicalPath);
1286
+ }
1287
+ export async function ccVersionOnce(argv, environmentFingerprint = toolchainEnvironmentFingerprint(), fresh = false) {
1288
+ const spellingKey = `${environmentFingerprint}\0${argv.join("\x1f")}`;
1289
+ const executableIdentity = await resolvedToolIdentity(argv[0] ?? "clang");
1290
+ const key = `${spellingKey}\0${executableIdentity ?? "<unresolved>"}`;
1291
+ const probe = () =>
1292
+ // `zig cc --version` (zig 0.16) drops an empty a.o in its cwd. A private
1293
+ // probe directory avoids both caller pollution and the cross-process /
1294
+ // cross-user collision a fixed tmpdir()/a.o would create on Linux.
1295
+ (async () => {
1296
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-cc-version-"));
1297
+ try {
1298
+ const r = await execFileAsync(argv[0] ?? "clang", [...argv.slice(1), "--version"], { cwd: probeDir });
1299
+ return `${executableIdentity ?? "<unresolved>"}\0${`${r.stdout}\n${r.stderr}`.trim()}`;
1300
+ }
1301
+ finally {
1302
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1303
+ }
1304
+ })();
1305
+ const requireFreshProbe = fresh && !stableTestToolchainSession();
1306
+ let memo = requireFreshProbe && executableIdentity !== null ? probe() : ccVersionMemos.get(key);
591
1307
  if (memo === undefined) {
592
- // cwd: the version probe must not touch the caller's directory —
593
- // `zig cc --version` (zig 0.16) drops an empty a.o wherever it runs.
594
- memo = execFileAsync(argv[0] ?? "clang", [...argv.slice(1), "--version"], { cwd: tmpdir() }).then((r) => r.stdout.split("\n", 1)[0] ?? "");
1308
+ memo = probe();
1309
+ ccVersionMemos.set(key, memo);
1310
+ }
1311
+ else if (requireFreshProbe) {
595
1312
  ccVersionMemos.set(key, memo);
596
1313
  }
597
1314
  return memo;
598
1315
  }
1316
+ const toolVersionMemos = new Map();
1317
+ const toolVersionFallbacks = new Map();
1318
+ async function toolVersionOnce(argv, environmentFingerprint = toolchainEnvironmentFingerprint(), fresh = false) {
1319
+ const spellingKey = `${environmentFingerprint}\0${argv.join("\x1f")}`;
1320
+ const executableIdentity = await resolvedToolIdentity(argv[0] ?? "ar");
1321
+ if (executableIdentity === null) {
1322
+ const fallback = toolVersionFallbacks.get(spellingKey);
1323
+ if (fallback !== undefined)
1324
+ return fallback;
1325
+ }
1326
+ const key = `${spellingKey}\0${executableIdentity ?? "<unresolved>"}`;
1327
+ const probe = () => execFileAsync(argv[0] ?? "ar", [...argv.slice(1), "--version"]).then((r) => `${executableIdentity ?? "<unresolved>"}\0${`${r.stdout}\n${r.stderr}`.trim()}`, (err) => `${executableIdentity ?? "<unresolved>"}\0${`${err.stdout ?? ""}\n${err.stderr ?? ""}`.trim() || err.message || key}`);
1328
+ let memo = fresh && executableIdentity !== null ? probe() : toolVersionMemos.get(key);
1329
+ if (memo === undefined) {
1330
+ memo = probe();
1331
+ toolVersionMemos.set(key, memo);
1332
+ }
1333
+ else if (fresh) {
1334
+ toolVersionMemos.set(key, memo);
1335
+ }
1336
+ if (executableIdentity !== null)
1337
+ toolVersionFallbacks.set(spellingKey, memo);
1338
+ return memo;
1339
+ }
1340
+ function parseMakeDependencies(output, cwd = process.cwd()) {
1341
+ const flattened = output.replace(/\\\r?\n/g, " ");
1342
+ const separator = flattened.indexOf(": ");
1343
+ if (separator < 0)
1344
+ throw new Error("compiler dependency probe returned no make rule");
1345
+ const input = flattened.slice(separator + 2);
1346
+ const paths = [];
1347
+ let current = "";
1348
+ let escaped = false;
1349
+ for (const char of input) {
1350
+ if (escaped) {
1351
+ current += char;
1352
+ escaped = false;
1353
+ }
1354
+ else if (char === "\\") {
1355
+ escaped = true;
1356
+ }
1357
+ else if (/\s/.test(char)) {
1358
+ if (current !== "") {
1359
+ paths.push(current.replace(/\$\$/g, "$"));
1360
+ current = "";
1361
+ }
1362
+ }
1363
+ else {
1364
+ current += char;
1365
+ }
1366
+ }
1367
+ if (escaped)
1368
+ current += "\\";
1369
+ if (current !== "")
1370
+ paths.push(current.replace(/\$\$/g, "$"));
1371
+ return [...new Set(paths.map((path) => resolve(cwd, path)))].sort();
1372
+ }
1373
+ function normalizedProbeInvocation(result, probeDir) {
1374
+ return `${result.stdout}\n${result.stderr}`
1375
+ // Probe-local paths vary on every invocation and carry no toolchain
1376
+ // identity. Both ordinary and shell-escaped Windows spellings can appear
1377
+ // in a driver's quoted trace.
1378
+ .split(probeDir).join("<probe>")
1379
+ .split(probeDir.replace(/\\/g, "\\\\")).join("<probe>")
1380
+ .trim();
1381
+ }
1382
+ /** The effective cc1 invocation and injected dependencies for the flags used
1383
+ * by real runtime/program compiles. The broad implicit-toolchain probe below
1384
+ * intentionally uses a target-wide synthetic TU so it can discover every
1385
+ * owned system header, but that generic command is not sufficient identity for
1386
+ * a wrapper that injects flags or preincluded files only for a particular build
1387
+ * flavor (for example, only when it sees -O2 or -DSCR_DYNAMIC). */
1388
+ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFingerprint, compileArgs, sourceExtension = ".c") {
1389
+ const compiler = driver.argv[0] ?? "clang";
1390
+ const compilerIdentity = await resolvedToolIdentity(compiler);
1391
+ if (compilerIdentity === null) {
1392
+ throw new Error("compiler unavailable before effective invocation identity was established");
1393
+ }
1394
+ let probe;
1395
+ {
1396
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-effective-cc-probe-"));
1397
+ try {
1398
+ const source = join(probeDir, `program${sourceExtension}`);
1399
+ const output = join(probeDir, "program.o");
1400
+ await writeFile(source, sourceExtension === ".ll"
1401
+ ? "define i32 @scriptc_effective_probe() { ret i32 0 }\n"
1402
+ : "int scriptc_effective_probe(void) { return 0; }\n");
1403
+ const prefix = [...driver.argv.slice(1), ...compileArgs];
1404
+ const [invocation, dependencyResult] = await Promise.all([
1405
+ execFileAsync(compiler, [...prefix, "-###", "-c", source, "-o", output], { cwd: probeDir, maxBuffer: 16 * 1024 * 1024 }),
1406
+ sourceExtension === ".c"
1407
+ ? execFileAsync(compiler, [...prefix, "-M", source], {
1408
+ cwd: probeDir,
1409
+ maxBuffer: 16 * 1024 * 1024,
1410
+ })
1411
+ : Promise.resolve(null),
1412
+ ]);
1413
+ const dependencies = dependencyResult === null
1414
+ ? []
1415
+ : parseMakeDependencies(dependencyResult.stdout, probeDir).filter((path) => path !== resolve(source));
1416
+ probe = {
1417
+ compilerIdentity,
1418
+ invocation: normalizedProbeInvocation(invocation, probeDir),
1419
+ dependencies,
1420
+ dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1421
+ };
1422
+ }
1423
+ finally {
1424
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1425
+ }
1426
+ }
1427
+ return createHash("sha256")
1428
+ .update("effective-compiler-invocation-v2\0")
1429
+ .update(environmentFingerprint)
1430
+ .update("\0")
1431
+ .update(probe.compilerIdentity)
1432
+ .update("\0")
1433
+ .update(driver.argv.join("\x1f"))
1434
+ .update("\0")
1435
+ .update(compileArgs.join("\x1f"))
1436
+ .update("\0")
1437
+ .update(sourceExtension)
1438
+ .update("\0")
1439
+ .update(probe.invocation)
1440
+ .update("\0")
1441
+ .update(probe.dependencies.join("\x1f"))
1442
+ .update("\0")
1443
+ .update(probe.dependencyFingerprint)
1444
+ .digest("hex");
1445
+ }
1446
+ const stableEffectiveCompilerInvocationMemos = new Map();
1447
+ function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint, compileArgs, sourceExtension = ".c") {
1448
+ const key = [
1449
+ environmentFingerprint,
1450
+ driver.argv.join("\x1f"),
1451
+ compileArgs.join("\x1f"),
1452
+ sourceExtension,
1453
+ ].join("\0");
1454
+ return stableTestMemo(stableEffectiveCompilerInvocationMemos, key, () => effectiveCompilerInvocationFingerprintFresh(driver, environmentFingerprint, compileArgs, sourceExtension));
1455
+ }
1456
+ async function nativeSourceFiles(directory, recursive, include = (name) => name.endsWith(".c") || name.endsWith(".h")) {
1457
+ const files = [];
1458
+ const walk = async (current) => {
1459
+ const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
1460
+ for (const entry of entries) {
1461
+ const path = join(current, entry.name);
1462
+ if (entry.isDirectory() && recursive)
1463
+ await walk(path);
1464
+ else if (entry.isFile() && include(entry.name))
1465
+ files.push(path);
1466
+ }
1467
+ };
1468
+ await walk(directory);
1469
+ return files.sort();
1470
+ }
1471
+ /** Every system-header spelling consumed by a source tree scriptc owns. The
1472
+ * dependency probe preprocesses these spellings with the selected driver and
1473
+ * hashes the resolved files. Runtime-local and vendored source/header bytes
1474
+ * have their own content/version identities; scanning every owned file for
1475
+ * angle includes closes the system-header gap in separately built QuickJS,
1476
+ * libregexp, mbedTLS, zlib, curl, and Ryū translation units. */
1477
+ export async function implicitDependencyProbeIncludes(rtDir) {
1478
+ const vendor = join(rtDir, "..", "vendor");
1479
+ const quickjsSources = new Set([...QJS_ENGINE_SOURCES, ...LRE_SOURCES]);
1480
+ const zlibSources = new Set(ZLIB_SOURCES);
1481
+ const fileGroups = await Promise.all([
1482
+ nativeSourceFiles(rtDir, false),
1483
+ nativeSourceFiles(join(vendor, "ryu"), false),
1484
+ nativeSourceFiles(join(vendor, "quickjs-ng"), false, (name) => name.endsWith(".h") || quickjsSources.has(name)),
1485
+ nativeSourceFiles(join(vendor, "mbedtls", "include"), true),
1486
+ nativeSourceFiles(join(vendor, "mbedtls", "library"), true),
1487
+ nativeSourceFiles(join(vendor, "zlib"), false, (name) => name.endsWith(".h") || zlibSources.has(name)),
1488
+ nativeSourceFiles(join(vendor, "curl", "include"), true, (name) => name.endsWith(".h")),
1489
+ ]);
1490
+ const includes = new Set();
1491
+ for (const path of fileGroups.flat()) {
1492
+ const source = await readFile(path, "utf8");
1493
+ for (const match of source.matchAll(/^\s*#\s*include\s*<([^>]+)>/gm)) {
1494
+ includes.add(`<${match[1]}>`);
1495
+ }
1496
+ }
1497
+ return [...includes].sort();
1498
+ }
1499
+ function implicitDependencyIncludeDirective(include) {
1500
+ const targetCondition = include === "<cpuid.h>" || include === "<immintrin.h>"
1501
+ ? " && (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64))"
1502
+ : include === "<intrin.h>"
1503
+ ? " && defined(_WIN32)"
1504
+ : include === "<arm64_neon.h>" || include === "<arm_acle.h>" || include === "<arm_neon.h>"
1505
+ ? " && (defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64))"
1506
+ : "";
1507
+ return `#if __has_include(${include})${targetCondition}\n#include ${include}\n#endif\n`;
1508
+ }
1509
+ async function fingerprintDependencyFiles(paths) {
1510
+ const hash = createHash("sha256").update("implicit-dependencies-v1\0");
1511
+ for (let i = 0; i < paths.length; i += 128) {
1512
+ const batch = paths.slice(i, i + 128);
1513
+ const contents = await Promise.all(batch.map((path) => readFile(path)));
1514
+ for (let j = 0; j < batch.length; j++) {
1515
+ hash.update(batch[j]).update("\0").update(contents[j]).update("\0");
1516
+ }
1517
+ }
1518
+ return hash.digest("hex");
1519
+ }
1520
+ /** Exact headers selected while preprocessing the caller's translation unit.
1521
+ * The shared toolchain probe covers the runtime/vendor trees, but compileC is
1522
+ * also a public API: an opted-in caller can include a system or header-only SDK
1523
+ * surface that no runtime source names. Probe an invocation-private snapshot
1524
+ * of the keyed bytes with the real compile flags, preserving the original
1525
+ * quote-include directory and compiler-visible source spelling. */
1526
+ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
1527
+ if (sourcePath.endsWith(".ll")) {
1528
+ return createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex");
1529
+ }
1530
+ const compiler = driver.argv[0] ?? "clang";
1531
+ const compilerIdentity = await resolvedToolIdentity(compiler);
1532
+ if (compilerIdentity === null) {
1533
+ throw new Error("compiler unavailable before translation-unit dependencies were established");
1534
+ }
1535
+ let probe;
1536
+ {
1537
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-tu-probe-"));
1538
+ try {
1539
+ const snapshot = join(probeDir, "program.c");
1540
+ await writeFile(snapshot, sourceBytes);
1541
+ const result = await execFileAsync(compiler, [
1542
+ ...driver.argv.slice(1),
1543
+ // The real source directory is searched before every caller-supplied
1544
+ // -iquote/-I directory. Put its surrogate first to preserve that
1545
+ // precedence after moving the keyed bytes into the probe directory.
1546
+ "-iquote",
1547
+ dirname(resolve(sourcePath)),
1548
+ ...cflags,
1549
+ `-ffile-prefix-map=${snapshot}=${sourcePath}`,
1550
+ "-M",
1551
+ snapshot,
1552
+ ], { cwd: probeDir, maxBuffer: 16 * 1024 * 1024 });
1553
+ const ownPaths = new Set([resolve(snapshot), resolve(sourcePath)]);
1554
+ const dependencies = parseMakeDependencies(result.stdout, probeDir).filter((path) => !ownPaths.has(path));
1555
+ probe = {
1556
+ compilerIdentity,
1557
+ dependencies,
1558
+ dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1559
+ };
1560
+ }
1561
+ finally {
1562
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1563
+ }
1564
+ }
1565
+ return createHash("sha256")
1566
+ .update("translation-unit-dependencies-v1\0")
1567
+ .update(environmentFingerprint)
1568
+ .update("\0")
1569
+ .update(probe.compilerIdentity)
1570
+ .update("\0")
1571
+ .update(probe.dependencies.join("\x1f"))
1572
+ .update("\0")
1573
+ .update(probe.dependencyFingerprint)
1574
+ .digest("hex");
1575
+ }
1576
+ const stableTranslationUnitDependencyMemos = new Map();
1577
+ function translationUnitDependencyFingerprint(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
1578
+ const key = createHash("sha256")
1579
+ .update(environmentFingerprint)
1580
+ .update("\0")
1581
+ .update(driver.argv.join("\x1f"))
1582
+ .update("\0")
1583
+ .update(cflags.join("\x1f"))
1584
+ .update("\0")
1585
+ .update(sourcePath)
1586
+ .update("\0")
1587
+ .update(resolve(sourcePath))
1588
+ .update("\0")
1589
+ .update(sourceBytes)
1590
+ .digest("hex");
1591
+ return stableTestMemo(stableTranslationUnitDependencyMemos, key, () => translationUnitDependencyFingerprintFresh(driver, cflags, sourcePath, sourceBytes, environmentFingerprint));
1592
+ }
1593
+ /** Identity of implicit compiler inputs that do not appear in buildArgs:
1594
+ * default SDK/system headers and the assembler/linker selected by the driver.
1595
+ * Small preprocessor dependency probes include every header spelling used by
1596
+ * the runtime when it is available for the selected target (and therefore the
1597
+ * vendored headers those TUs consume), then hash the exact dependency bytes.
1598
+ * Vendored source snapshots remain keyed by their version pins. When the
1599
+ * compiler is available, dependency discovery runs afresh so an SDK/config
1600
+ * change that redirects includes cannot hide behind an unchanged old path
1601
+ * list. Dependency discovery must succeed on every cache-enabled invocation;
1602
+ * a prior path list cannot reveal a new higher-priority header. */
1603
+ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint) {
1604
+ let probe;
1605
+ const compiler = driver.argv[0] ?? "clang";
1606
+ const compilerIdentity = await resolvedToolIdentity(compiler);
1607
+ if (compilerIdentity === null) {
1608
+ throw new Error("compiler unavailable before implicit toolchain identity was established");
1609
+ }
1610
+ {
1611
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-toolchain-probe-"));
1612
+ try {
1613
+ const dependencyIncludes = await implicitDependencyProbeIncludes(runtimeSrcDir());
1614
+ // Clang's GNU cpuid.h and Windows intrin.h both define `__cpuid` with
1615
+ // incompatible signatures. Real TUs select only one context; preserve
1616
+ // that isolation while still discovering both targets' dependency sets.
1617
+ const sourceGroups = [
1618
+ dependencyIncludes.filter((include) => include !== "<intrin.h>"),
1619
+ dependencyIncludes.filter((include) => include === "<intrin.h>"),
1620
+ ].filter((group) => group.length > 0);
1621
+ const sources = sourceGroups.map((_, index) => join(probeDir, `empty-${index}.c`));
1622
+ const driverSource = join(probeDir, "driver-empty.c");
1623
+ const driverOutput = join(probeDir, "driver-output.o");
1624
+ await Promise.all([
1625
+ ...sources.map((source, index) => writeFile(source, sourceGroups[index].map(implicitDependencyIncludeDirective).join(""))),
1626
+ writeFile(driverSource, "int scriptc_driver_probe;\n"),
1627
+ ]);
1628
+ const prefix = [...driver.argv.slice(1), ...driver.targetArgs];
1629
+ const probeArgs = [
1630
+ ...prefix,
1631
+ "-std=c11",
1632
+ "-D_GNU_SOURCE",
1633
+ "-D_XOPEN_SOURCE=700",
1634
+ "-I", runtimeSrcDir(),
1635
+ "-I", vendorEngineDir(),
1636
+ "-I", join(vendorTlsDir(), "include"),
1637
+ "-I", join(vendorTlsDir(), "library"),
1638
+ "-I", vendorZlibDir(),
1639
+ "-I", join(vendorCurlDir(), "include"),
1640
+ "-M",
1641
+ ];
1642
+ const [dependencyResults, linker, assembler, compilerInvocation] = await Promise.all([
1643
+ Promise.all(sources.map((source) => execFileAsync(compiler, [...probeArgs, source], {
1644
+ cwd: probeDir,
1645
+ maxBuffer: 16 * 1024 * 1024,
1646
+ }))),
1647
+ execFileAsync(compiler, [...prefix, "-print-prog-name=ld"], { cwd: probeDir }),
1648
+ execFileAsync(compiler, [...prefix, "-print-prog-name=as"], { cwd: probeDir }),
1649
+ // `-###` exposes the effective cc1 invocation after compiler-driver
1650
+ // config and ordinary wrappers have injected their implicit flags. A
1651
+ // wrapper can read environment variables unknown to scriptc; hashing
1652
+ // this trace keeps those flags from hiding behind an unchanged wrapper
1653
+ // executable/version and dependency set.
1654
+ execFileAsync(compiler, [...prefix, "-std=c11", "-###", "-c", driverSource, "-o", driverOutput], { cwd: probeDir, maxBuffer: 16 * 1024 * 1024 }),
1655
+ ]);
1656
+ const toolSpellings = [linker.stdout.trim(), assembler.stdout.trim()].filter((value, index, all) => value !== "" && all.indexOf(value) === index);
1657
+ const sourceSet = new Set(sources);
1658
+ const dependencyPaths = [
1659
+ ...new Set(dependencyResults
1660
+ .flatMap((dependencies) => parseMakeDependencies(dependencies.stdout))
1661
+ .filter((path) => !sourceSet.has(path))),
1662
+ ].sort();
1663
+ probe = {
1664
+ compilerIdentity: compilerIdentity ?? `<unresolved>\0${compiler}`,
1665
+ compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir),
1666
+ dependencies: dependencyPaths,
1667
+ dependencyFingerprint: await fingerprintDependencyFiles(dependencyPaths),
1668
+ tools: await Promise.all(toolSpellings.map(async (spelling) => ({
1669
+ spelling,
1670
+ identity: await resolvedToolIdentity(spelling),
1671
+ }))),
1672
+ };
1673
+ }
1674
+ finally {
1675
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1676
+ }
1677
+ }
1678
+ const hash = createHash("sha256")
1679
+ .update("implicit-toolchain-v2\0")
1680
+ .update(environmentFingerprint)
1681
+ .update("\0")
1682
+ .update(probe.compilerIdentity)
1683
+ .update("\0")
1684
+ .update(probe.compilerInvocation)
1685
+ .update("\0")
1686
+ .update(driver.argv.join("\x1f"))
1687
+ .update("\0")
1688
+ .update(driver.targetArgs.join("\x1f"))
1689
+ .update("\0")
1690
+ .update(probe.dependencies.join("\x1f"))
1691
+ .update("\0")
1692
+ .update(probe.dependencyFingerprint)
1693
+ .update("\0");
1694
+ for (const tool of probe.tools) {
1695
+ const currentIdentity = await resolvedToolIdentity(tool.spelling);
1696
+ hash
1697
+ .update(tool.spelling)
1698
+ .update("\0")
1699
+ .update(currentIdentity ?? tool.identity ?? "<unresolved>")
1700
+ .update("\0");
1701
+ }
1702
+ return hash.digest("hex");
1703
+ }
1704
+ const stableImplicitToolchainMemos = new Map();
1705
+ function implicitToolchainFingerprint(driver, environmentFingerprint) {
1706
+ const key = [
1707
+ environmentFingerprint,
1708
+ driver.argv.join("\x1f"),
1709
+ driver.target ?? "<native>",
1710
+ driver.targetArgs.join("\x1f"),
1711
+ runtimeSrcDir(),
1712
+ ].join("\0");
1713
+ return stableTestMemo(stableImplicitToolchainMemos, key, () => implicitToolchainFingerprintFresh(driver, environmentFingerprint));
1714
+ }
1715
+ function linkTraceCandidate(line) {
1716
+ const trimmed = line.trim().replace(/^(?:LOAD|load)\s+/, "");
1717
+ if (trimmed === "")
1718
+ return [];
1719
+ const unquoted = (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
1720
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
1721
+ ? trimmed.slice(1, -1)
1722
+ : trimmed;
1723
+ const candidates = [unquoted];
1724
+ const member = unquoted.lastIndexOf("(");
1725
+ if (member > 0 && unquoted.endsWith(")"))
1726
+ candidates.push(unquoted.slice(0, member));
1727
+ return candidates;
1728
+ }
1729
+ function driverTraceCandidates(line) {
1730
+ const candidates = [];
1731
+ for (const match of line.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) {
1732
+ const token = (match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["\\])/g, "$1");
1733
+ if (token !== "")
1734
+ candidates.push(token);
1735
+ }
1736
+ return candidates;
1737
+ }
1738
+ export async function parseLinkTraceFiles(output, cwd, excludedRoot, driverDryRun = false) {
1739
+ const files = new Set();
1740
+ for (const line of output.split(/\r?\n/)) {
1741
+ const candidates = driverDryRun ? driverTraceCandidates(line) : linkTraceCandidate(line);
1742
+ for (const candidate of candidates) {
1743
+ const path = isAbsolute(candidate) ? candidate : resolve(cwd, candidate);
1744
+ if (path === excludedRoot || path.startsWith(`${excludedRoot}/`) || path.startsWith(`${excludedRoot}\\`)) {
1745
+ continue;
1746
+ }
1747
+ const info = await stat(path).catch(() => null);
1748
+ if (info?.isFile()) {
1749
+ files.add(path);
1750
+ // Traditional `-Wl,-t` output has one dependency per line, while
1751
+ // Zig's COFF `-###` fallback prints the entire lld-link argv on one
1752
+ // line. Keep scanning every dry-run token so CRT/import/runtime
1753
+ // libraries after the first path also join the fingerprint.
1754
+ if (!driverDryRun)
1755
+ break;
1756
+ }
1757
+ }
1758
+ }
1759
+ return [...files].sort();
1760
+ }
1761
+ /** Exact files selected by the compiler driver's implicit link. A tiny
1762
+ * target object plus the linker's trace mode resolves CRT objects, compiler
1763
+ * runtimes, linker scripts, SDK stubs/import libraries, and every ambient
1764
+ * default library without guessing platform search layouts. The caller adds
1765
+ * build-flavor flags such as ASan and scriptc's own fixed `-l` arguments.
1766
+ * Every cache-enabled invocation performs a fresh trace; a prior resolved path
1767
+ * list cannot reveal a newly selected higher-priority linker input. */
1768
+ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs) {
1769
+ const invocationArgs = effectiveInvocationArgs ?? [...driver.targetArgs, ...linkArgs];
1770
+ const traceArgs = traceInvocationArgs ?? [...driver.targetArgs, ...linkArgs];
1771
+ const compiler = driver.argv[0] ?? "clang";
1772
+ const compilerIdentity = await resolvedToolIdentity(compiler);
1773
+ if (compilerIdentity === null) {
1774
+ throw new Error("compiler unavailable before implicit linker identity was established");
1775
+ }
1776
+ let probe;
1777
+ {
1778
+ const probeDir = await mkdtemp(join(tmpdir(), "scriptc-linker-probe-"));
1779
+ try {
1780
+ const source = join(probeDir, "empty.c");
1781
+ const object = join(probeDir, "empty.o");
1782
+ const output = join(probeDir, process.platform === "win32" ? "empty.exe" : "empty");
1783
+ await writeFile(source, "int main(void) { return 0; }\n");
1784
+ const prefix = [...driver.argv.slice(1), ...driver.targetArgs];
1785
+ const [, linker] = await Promise.all([
1786
+ execFileAsync(compiler, [...prefix, "-std=c11", "-c", source, "-o", object], { cwd: probeDir }),
1787
+ execFileAsync(compiler, [...prefix, "-print-prog-name=ld"], { cwd: probeDir }),
1788
+ ]);
1789
+ // Unlike the compile-only toolchain trace, this exposes options a
1790
+ // compiler wrapper/config injects only for link invocations (rpaths,
1791
+ // subsystem/stack settings, --defsym, and peers). Resolved input files
1792
+ // alone cannot represent those output-affecting flags.
1793
+ const driverInvocation = await execFileAsync(compiler, [...driver.argv.slice(1), ...invocationArgs, object, "-###", "-o", output], { cwd: probeDir, maxBuffer: 32 * 1024 * 1024 });
1794
+ // The dry run exposes absolute files a wrapper injects for this exact
1795
+ // build flavor. Its unresolved -l spellings are supplemented by the
1796
+ // real trace below, which runs with the same flavor flags but omits
1797
+ // not-yet-materialized scriptc-owned vendor prerequisites.
1798
+ const driverDependencies = await parseLinkTraceFiles(`${driverInvocation.stdout}\n${driverInvocation.stderr}`, probeDir, probeDir, true);
1799
+ let tracedDependencies = [];
1800
+ try {
1801
+ const trace = await execFileAsync(compiler, [...driver.argv.slice(1), ...traceArgs, object, "-Wl,-t", "-o", output], { cwd: probeDir, maxBuffer: 32 * 1024 * 1024 });
1802
+ tracedDependencies = await parseLinkTraceFiles(`${trace.stdout}\n${trace.stderr}`, probeDir, probeDir);
1803
+ }
1804
+ catch (error) {
1805
+ // Zig's COFF linker deliberately rejects GNU ld's `-t`, but `zig cc
1806
+ // -###` prints its fully resolved lld-link input list (CRT and every
1807
+ // import/compiler-runtime library as absolute cache paths). Other
1808
+ // drivers' dry runs commonly leave `-lc`/`-lSystem` unresolved, so
1809
+ // they must not use this fallback as a complete artifact identity.
1810
+ if (driver.argv[0] !== "zig" || driver.argv[1] !== "cc")
1811
+ throw error;
1812
+ }
1813
+ const dependencies = [...new Set([...driverDependencies, ...tracedDependencies])].sort();
1814
+ if (dependencies.length === 0) {
1815
+ throw new Error("linker trace reported no resolved input files");
1816
+ }
1817
+ const linkerSpelling = linker.stdout.trim();
1818
+ probe = {
1819
+ compilerIdentity,
1820
+ linkerInvocation: normalizedProbeInvocation(driverInvocation, probeDir),
1821
+ dependencies,
1822
+ dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1823
+ linker: {
1824
+ spelling: linkerSpelling,
1825
+ identity: linkerSpelling === "" ? null : await resolvedToolIdentity(linkerSpelling),
1826
+ },
1827
+ };
1828
+ }
1829
+ finally {
1830
+ await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1831
+ }
1832
+ }
1833
+ const linkerIdentity = probe.linker.spelling === ""
1834
+ ? null
1835
+ : await resolvedToolIdentity(probe.linker.spelling);
1836
+ return createHash("sha256")
1837
+ .update("implicit-linker-v3\0")
1838
+ .update(environmentFingerprint)
1839
+ .update("\0")
1840
+ .update(probe.compilerIdentity)
1841
+ .update("\0")
1842
+ .update(probe.linkerInvocation)
1843
+ .update("\0")
1844
+ .update(driver.argv.join("\x1f"))
1845
+ .update("\0")
1846
+ .update(driver.targetArgs.join("\x1f"))
1847
+ .update("\0")
1848
+ .update(linkArgs.join("\x1f"))
1849
+ .update("\0")
1850
+ .update(invocationArgs.join("\x1f"))
1851
+ .update("\0")
1852
+ .update(traceArgs.join("\x1f"))
1853
+ .update("\0")
1854
+ .update(probe.dependencies.join("\x1f"))
1855
+ .update("\0")
1856
+ .update(probe.dependencyFingerprint)
1857
+ .update("\0")
1858
+ .update(probe.linker.spelling)
1859
+ .update("\0")
1860
+ .update(linkerIdentity ?? probe.linker.identity ?? "<unresolved>")
1861
+ .digest("hex");
1862
+ }
1863
+ const stableImplicitLinkerMemos = new Map();
1864
+ function implicitLinkerFingerprint(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs) {
1865
+ const invocationArgs = effectiveInvocationArgs ?? [...driver.targetArgs, ...linkArgs];
1866
+ const traceArgs = traceInvocationArgs ?? [...driver.targetArgs, ...linkArgs];
1867
+ const key = [
1868
+ environmentFingerprint,
1869
+ driver.argv.join("\x1f"),
1870
+ driver.target ?? "<native>",
1871
+ driver.targetArgs.join("\x1f"),
1872
+ linkArgs.join("\x1f"),
1873
+ invocationArgs.join("\x1f"),
1874
+ traceArgs.join("\x1f"),
1875
+ ].join("\0");
1876
+ return stableTestMemo(stableImplicitLinkerMemos, key, () => implicitLinkerFingerprintFresh(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs));
1877
+ }
599
1878
  let ccacheMemo = null;
600
1879
  function ccacheAvailable() {
601
1880
  ccacheMemo ??= execFileAsync("ccache", ["--version"]).then(() => true, () => false);
602
1881
  return ccacheMemo;
603
1882
  }
604
- /** Content hash of every .c/.h in the runtime src dir plus the vendor pin —
1883
+ /** Content hash of every .c/.h in the runtime src dir, the Ryū sources
1884
+ * textually included by scr_number.c, and the separately-built vendor pins —
605
1885
  * everything a binary links that the emitted C bytes don't already cover
606
- * (npm-embedded C rides inside the emitted C; the engine archive and lre
607
- * objects are pinned by QJS_COMMIT, the same trust their own caches use).
608
- * Memoized behind a stat signature so watch-mode edits re-hash. */
609
- let rtFingerprintMemo = null;
610
- async function runtimeFingerprint(rtDir) {
611
- const names = (await readdir(rtDir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
612
- const stats = await Promise.all(names.map((n) => stat(join(rtDir, n))));
613
- const sig = stats.map((s, i) => `${names[i] ?? ""}:${s.size}:${s.mtimeMs}`).join("|");
614
- if (rtFingerprintMemo !== null && rtFingerprintMemo.sig === sig)
615
- return rtFingerprintMemo.hash;
1886
+ * (npm-embedded C rides inside the emitted C; the engine and standalone
1887
+ * vendor archives are pinned by their version constants). The small source
1888
+ * tree is hashed on every identity calculation: a stat-only memo can miss a
1889
+ * same-size edit whose timestamp was preserved by a copy/sync tool. */
1890
+ async function runtimeFingerprintFresh(rtDir) {
1891
+ const groups = await Promise.all([
1892
+ { label: "runtime", dir: rtDir },
1893
+ { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
1894
+ ].map(async (group) => {
1895
+ const names = (await readdir(group.dir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
1896
+ return { ...group, names };
1897
+ }));
616
1898
  const h = createHash("sha256").update(QJS_COMMIT).update(MBEDTLS_VERSION).update(ZLIB_VERSION);
617
- for (const n of names)
618
- h.update(n).update("\0").update(await readFile(join(rtDir, n))).update("\0");
619
- const hash = h.digest("hex");
620
- rtFingerprintMemo = { sig, hash };
621
- return hash;
1899
+ for (const group of groups) {
1900
+ for (const n of group.names) {
1901
+ h.update(group.label).update("/").update(n).update("\0").update(await readFile(join(group.dir, n))).update("\0");
1902
+ }
1903
+ }
1904
+ return h.digest("hex");
1905
+ }
1906
+ const stableRuntimeFingerprintMemos = new Map();
1907
+ export function runtimeFingerprint(rtDir) {
1908
+ const key = resolve(rtDir);
1909
+ return stableTestMemo(stableRuntimeFingerprintMemos, key, () => runtimeFingerprintFresh(rtDir));
1910
+ }
1911
+ class CacheInputsChangedError extends Error {
1912
+ constructor() {
1913
+ super("runtime inputs changed while populating the native object cache");
1914
+ this.name = "CacheInputsChangedError";
1915
+ }
1916
+ }
1917
+ function cacheDigestPath(path) {
1918
+ return `${path}.sha256`;
1919
+ }
1920
+ async function fileDigest(path) {
1921
+ return createHash("sha256").update(await readFile(path)).digest("hex");
1922
+ }
1923
+ /** Cache entries are disposable data, not trusted compiler output. Atomic
1924
+ * rename prevents ordinary partial writes, while the adjacent digest catches
1925
+ * disk damage, manual truncation, and entries left by an interrupted/older
1926
+ * publisher before they reach clang, the linker, or `ar`. */
1927
+ async function validCachedFile(object) {
1928
+ try {
1929
+ const digestPath = cacheDigestPath(object);
1930
+ const expected = (await readFile(digestPath, "utf8")).trim();
1931
+ const valid = /^[0-9a-f]{64}$/.test(expected) && (await fileDigest(object)) === expected;
1932
+ if (valid) {
1933
+ const now = new Date();
1934
+ await Promise.all([
1935
+ utimes(object, now, now).catch(() => undefined),
1936
+ utimes(digestPath, now, now).catch(() => undefined),
1937
+ ]);
1938
+ }
1939
+ return valid;
1940
+ }
1941
+ catch {
1942
+ return false;
1943
+ }
1944
+ }
1945
+ /** Copy a cache entry and verify the private copy before it can become a
1946
+ * caller-visible artifact. Verifying after the copy closes the gap between a
1947
+ * source-side digest check and a concurrent replacement/truncation. */
1948
+ async function copyValidCachedFile(source, destination) {
1949
+ try {
1950
+ const digestPath = cacheDigestPath(source);
1951
+ const expected = (await readFile(digestPath, "utf8")).trim();
1952
+ if (!/^[0-9a-f]{64}$/.test(expected))
1953
+ return false;
1954
+ await copyFile(source, destination);
1955
+ if ((await fileDigest(destination)) !== expected) {
1956
+ await rm(destination, { force: true }).catch(() => undefined);
1957
+ return false;
1958
+ }
1959
+ const now = new Date();
1960
+ await Promise.all([
1961
+ utimes(source, now, now).catch(() => undefined),
1962
+ utimes(digestPath, now, now).catch(() => undefined),
1963
+ ]);
1964
+ return true;
1965
+ }
1966
+ catch {
1967
+ await rm(destination, { force: true }).catch(() => undefined);
1968
+ return false;
1969
+ }
1970
+ }
1971
+ /** Publish a complete executable/archive and its digest through private names.
1972
+ * Data is installed before its verifier, so a racing reader can only observe
1973
+ * an invalid/missing digest and take the fresh-build path. */
1974
+ async function publishCachedFile(source, destination) {
1975
+ const directory = dirname(destination);
1976
+ await mkdir(directory, { recursive: true });
1977
+ const nonce = `${process.pid}-${Math.random().toString(36).slice(2)}`;
1978
+ const tmp = join(directory, `.tmp-${basename(destination).slice(0, 8)}-${nonce}`);
1979
+ const tmpDigest = `${tmp}.sha256`;
1980
+ try {
1981
+ await copyFile(source, tmp);
1982
+ await chmod(tmp, 0o600);
1983
+ await writeFile(tmpDigest, `${await fileDigest(tmp)}\n`, { mode: 0o600 });
1984
+ await rename(tmp, destination);
1985
+ await rename(tmpDigest, cacheDigestPath(destination));
1986
+ }
1987
+ finally {
1988
+ await Promise.all([
1989
+ rm(tmp, { force: true }).catch(() => undefined),
1990
+ rm(tmpDigest, { force: true }).catch(() => undefined),
1991
+ ]);
1992
+ }
622
1993
  }
623
1994
  /** The cached .o set for one flag flavor, compiled on first need. Concurrent
624
1995
  * first builds (parallel test workers on a cold cache) may duplicate work;
625
- * per-file atomic renames make every winner equivalent. */
626
- async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix) {
1996
+ * per-file atomic renames make every winner equivalent. Publication is held
1997
+ * until verifyInputs confirms that the source/header fingerprint used by the
1998
+ * key still describes the bytes clang just read. */
1999
+ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix, verifyInputs) {
627
2000
  const setKey = createHash("sha256")
628
2001
  .update(keyPrefix)
629
2002
  .update(cflags.join("\x1f"))
@@ -631,15 +2004,19 @@ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix) {
631
2004
  .slice(0, 24);
632
2005
  const objDir = join(root, "obj", setKey);
633
2006
  const objOf = (src) => join(objDir, `${basename(src, ".c")}.o`);
634
- const present = await Promise.all(sources.map((s) => fileExists(objOf(s))));
2007
+ const present = await Promise.all(sources.map((s) => validCachedFile(objOf(s))));
635
2008
  const missing = sources.filter((_, i) => !present[i]);
636
2009
  if (missing.length > 0) {
637
2010
  // ccache wraps only the default clang driver — multi-word drivers
638
2011
  // (`zig cc`) run bare; their object sets are keyed apart anyway.
639
- const useCcache = ccArgv.length === 1 && ccArgv[0] === "clang" && (await ccacheAvailable());
2012
+ const useCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"] !== "1" &&
2013
+ ccArgv.length === 1 &&
2014
+ ccArgv[0] === "clang" &&
2015
+ (await ccacheAvailable());
640
2016
  await mkdir(objDir, { recursive: true });
641
2017
  const tmpDir = await mkdtemp(join(root, "obj", "build-"));
642
2018
  try {
2019
+ const compiled = new Map();
643
2020
  // Modest parallelism: a flavor's objects build once, but several cold
644
2021
  // workers can race here — keep each build's CPU footprint small.
645
2022
  const width = 4;
@@ -648,25 +2025,75 @@ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix) {
648
2025
  const tmpObj = join(tmpDir, `${basename(src, ".c")}.o`);
649
2026
  const argv = [...(useCcache ? ["ccache"] : []), ...ccArgv, ...cflags, "-c", src, "-o", tmpObj];
650
2027
  await execFileAsync(argv[0] ?? "clang", argv.slice(1));
651
- await rename(tmpObj, objOf(src));
2028
+ compiled.set(src, { object: tmpObj, digest: await fileDigest(tmpObj) });
652
2029
  }));
653
2030
  }
2031
+ // The fingerprint was computed before these subprocesses started. Do
2032
+ // not place their outputs under that key if a checkout/package update
2033
+ // changed any runtime source or included header while clang was reading.
2034
+ if (!(await verifyInputs()))
2035
+ throw new CacheInputsChangedError();
2036
+ for (const [src, built] of compiled) {
2037
+ const destination = objOf(src);
2038
+ const tmpDigest = `${built.object}.sha256`;
2039
+ await writeFile(tmpDigest, `${built.digest}\n`);
2040
+ // Publish data first and its verifier second. A concurrent reader in
2041
+ // the tiny gap sees a missing/mismatched digest and falls back safely.
2042
+ await rename(built.object, destination);
2043
+ await rename(tmpDigest, cacheDigestPath(destination));
2044
+ }
654
2045
  }
655
2046
  finally {
656
2047
  await rm(tmpDir, { recursive: true, force: true });
657
2048
  }
658
2049
  }
659
- return new Map(sources.map((s) => [s, objOf(s)]));
2050
+ if (!(await verifyInputs()))
2051
+ throw new CacheInputsChangedError();
2052
+ const objects = new Map(sources.map((s) => [s, objOf(s)]));
2053
+ if (!(await Promise.all([...objects.values()].map(validCachedFile))).every(Boolean)) {
2054
+ throw new Error("native object cache integrity check failed");
2055
+ }
2056
+ return objects;
660
2057
  }
661
- /** Size-capped LRU sweep of the whole cache root, at most once per process
662
- * and only after a write. Oldest-mtime files go first until the tree is back
663
- * under 75% of the cap; read hits bump mtimes so hot entries survive. */
664
- let pruneDone = false;
2058
+ /** Give one active link/archive operation private names for its cached
2059
+ * runtime objects. A hard link keeps the inode alive if another process's LRU
2060
+ * sweep unlinks the cache entry; filesystems that cannot hard-link across the
2061
+ * cache/tmp boundary fall back to a copy. If eviction wins before staging,
2062
+ * the caller catches the read failure and performs a fully fresh compile. */
2063
+ export async function stageRuntimeObjects(objects, stageDir) {
2064
+ await mkdir(stageDir, { recursive: true });
2065
+ const now = new Date();
2066
+ const staged = await Promise.all([...objects].map(async ([source, object]) => {
2067
+ const destination = join(stageDir, basename(object));
2068
+ try {
2069
+ await link(object, destination);
2070
+ }
2071
+ catch {
2072
+ await copyFile(object, destination);
2073
+ }
2074
+ // Object files participate in the same mtime-based LRU as complete
2075
+ // artifacts. A successful stage is a cache read, so promote the source
2076
+ // name best-effort (it may have raced an eviction after the hard link).
2077
+ await utimes(object, now, now).catch(() => undefined);
2078
+ return [source, destination];
2079
+ }));
2080
+ return new Map(staged);
2081
+ }
2082
+ /** Size-capped LRU sweep of the whole cache root. A caller-configured cap is
2083
+ * enforced after every successful cache write. The 4 GiB default is checked on
2084
+ * the first and every 64th write in a long-lived process: a full tree walk per
2085
+ * corpus program would otherwise become quadratic as the cache grows. Oldest-
2086
+ * mtime files go first until the tree is back under 75% of the cap; reads bump
2087
+ * mtimes. Active links use private staged names/hard links, so cache names can
2088
+ * be unlinked safely. */
2089
+ const rootWriteCounts = new Map();
665
2090
  async function pruneCache(root) {
666
- if (pruneDone)
2091
+ const configuredCap = process.env["SCRIPTC_CACHE_MAX_MB"];
2092
+ const writes = (rootWriteCounts.get(root) ?? 0) + 1;
2093
+ rootWriteCounts.set(root, writes);
2094
+ if (configuredCap === undefined && writes !== 1 && writes % 64 !== 0)
667
2095
  return;
668
- pruneDone = true;
669
- const capBytes = Number(process.env["SCRIPTC_CACHE_MAX_MB"] ?? "4096") * 1024 * 1024;
2096
+ const capBytes = Number(configuredCap ?? "4096") * 1024 * 1024;
670
2097
  if (!Number.isFinite(capBytes) || capBytes <= 0)
671
2098
  return;
672
2099
  const files = [];
@@ -688,29 +2115,35 @@ async function pruneCache(root) {
688
2115
  if (total <= capBytes)
689
2116
  return;
690
2117
  files.sort((a, b) => a.mtimeMs - b.mtimeMs);
691
- const recent = Date.now() - 60 * 60 * 1000; // never evict anything a live run may be using
692
2118
  for (const f of files) {
693
- if (total <= capBytes * 0.75 || f.mtimeMs > recent)
2119
+ if (total <= capBytes * 0.75)
694
2120
  break;
695
- await unlink(f.path).catch(() => undefined);
696
- total -= f.size;
2121
+ try {
2122
+ await unlink(f.path);
2123
+ total -= f.size;
2124
+ }
2125
+ catch {
2126
+ // A concurrent reader/publisher may already have moved the name.
2127
+ }
697
2128
  }
698
2129
  }
699
2130
  /** Compiles one C program together with the runtime sources.
700
- * Without a cache dir (the production default), the runtime (a dozen small
701
- * files) is recompiled on every build in one historical clang invocation —
702
- * no cached-archive staleness bugs. --dynamic additionally compiles
2131
+ * With caching disabled, the runtime (a dozen small files) is recompiled on
2132
+ * every build in one historical clang invocation — no cached-archive
2133
+ * staleness bugs. --dynamic additionally compiles
703
2134
  * scr_island.c under SCR_DYNAMIC and links the cached engine archive (built
704
2135
  * lazily, see above); regex-using programs additionally compile scr_regex.c
705
2136
  * and link libregexp (the cached objects, or the archive's own copy under
706
2137
  * --dynamic). Without either, the command line is exactly the historical
707
2138
  * one — regex-free static builds must stay byte-identical.
708
2139
  *
709
- * With SCRIPTC_CACHE_DIR set (the test lanes; see the cache block above),
710
- * unchanged programs skip clang via the binary cache, and misses link the
711
- * program's own TU against cached per-flavor runtime objects. */
2140
+ * With a caller-supplied dependency identity and an enabled cache root,
2141
+ * unchanged programs skip payload code generation/linking via the binary
2142
+ * cache after lightweight metadata probes, and misses link the program's own
2143
+ * TU against cached per-flavor runtime objects. */
712
2144
  export async function compileC(opts) {
713
2145
  const rtDir = runtimeSrcDir();
2146
+ const sanitize = opts.sanitize ?? false;
714
2147
  const dynamic = opts.dynamic ?? false;
715
2148
  const regex = opts.regex ?? false;
716
2149
  // fetch's implementation switch: the default is the NATIVE bridge
@@ -720,7 +2153,10 @@ export async function compileC(opts) {
720
2153
  // (scr_fetch_curl.c + system libcurl / the linux soname stub)
721
2154
  // compilable for one release as the flip's reference.
722
2155
  const fetchOn = opts.fetch ?? false;
723
- const curlFetch = fetchOn && process.env["SCRIPTC_FETCH_CURL"] === "1";
2156
+ // The retired curl bridge has only a SCR_DYNAMIC implementation.
2157
+ // Static fetch always keeps the native runtime even when a developer
2158
+ // has the comparison switch exported in their shell.
2159
+ const curlFetch = dynamic && fetchOn && process.env["SCRIPTC_FETCH_CURL"] === "1";
724
2160
  const nativeFetch = fetchOn && !curlFetch;
725
2161
  // The island's node:http/https client bridge: embedded graphs that
726
2162
  // import those builtins get working clients over the same socket units
@@ -762,28 +2198,99 @@ export async function compileC(opts) {
762
2198
  `(host-built vendor archives / system libs — see docs/linux-port.md).`);
763
2199
  }
764
2200
  }
765
- const engineArchive = dynamic ? await ensureEngineArchive(opts.sanitize ?? false, driver) : null;
766
- const tlsArchive = tls ? await ensureTlsArchive(opts.sanitize ?? false, driver) : null;
2201
+ const cachePolicy = toolchainEnvironmentCachePolicy();
2202
+ const configuredCacheRoot = cacheRootDir();
2203
+ const toolchainEnv = toolchainEnvironmentFingerprint();
2204
+ const persistentDriverCache = cachePolicy.runtimeObjects &&
2205
+ configuredCacheRoot !== null &&
2206
+ await compilerDriverSupportsPersistentCache(driver, toolchainEnv);
2207
+ // Only compiler-generated TUs opt in. Arbitrary `compileC` / `--from-c`
2208
+ // inputs may include caller-owned headers whose contents are not otherwise
2209
+ // represented in this key, so they retain the fully uncached historical
2210
+ // path unless the caller supplies its own complete dependency identity.
2211
+ let root = opts.cacheIdentity === undefined || !persistentDriverCache
2212
+ ? null
2213
+ : configuredCacheRoot;
2214
+ if (root !== null) {
2215
+ try {
2216
+ await ensurePrivateCacheRoot(root, process.env["SCRIPTC_CACHE_DIR"] === undefined);
2217
+ }
2218
+ catch {
2219
+ root = null;
2220
+ }
2221
+ }
2222
+ let implicitToolchain = null;
2223
+ if (persistentDriverCache) {
2224
+ try {
2225
+ implicitToolchain = await implicitToolchainFingerprint(driver, toolchainEnv);
2226
+ }
2227
+ catch {
2228
+ // Cache discovery is best-effort. In particular, a compiler wrapper can
2229
+ // compile successfully without implementing the metadata probes.
2230
+ root = null;
2231
+ }
2232
+ }
2233
+ const vendorBuildIdentity = await currentVendorCacheBuildIdentity(driver, `${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`);
2234
+ // Mutable include/SDK/config inputs can change behind a stable environment
2235
+ // spelling. The complete/runtime cache is disabled below; vendor objects
2236
+ // must follow the same rule instead of silently surviving in package-local
2237
+ // .cache. A private root gives this invocation the usual vendor build recipe
2238
+ // without publishing or reusing those prerequisites.
2239
+ const transientVendorRoot = persistentDriverCache && implicitToolchain !== null
2240
+ ? null
2241
+ : join(tmpdir(), `scriptc-vendor-${process.pid}-${Math.random().toString(36).slice(2)}`);
2242
+ const vendorCacheRoot = transientVendorRoot ?? vendorBuildCacheRoot();
2243
+ // Every vendor output path is deterministic from pins, flags, driver, and
2244
+ // target. Build the command/key from those paths now, but do not materialize
2245
+ // them until a complete-binary lookup has missed.
2246
+ const engineArchive = dynamic
2247
+ ? engineArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
2248
+ : null;
2249
+ const tlsArchive = tls
2250
+ ? tlsArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
2251
+ : null;
767
2252
  // --dynamic + regex shares the archive's libregexp (its host hooks and
768
2253
  // ours would collide; see scr_regex.c) — the standalone objects are for
769
2254
  // static builds only.
770
- const lreObjects = regex && !dynamic ? await ensureLreObjects(opts.sanitize ?? false, driver) : [];
2255
+ const lreObjects = regex && !dynamic
2256
+ ? lreObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
2257
+ : [];
771
2258
  // Vendored zlib is the CROSS story only — host builds keep the exact
772
2259
  // historical `-lz` system link (see CcOptions.zlib). The native fetch's
773
2260
  // gzip decoder rides the same objects/link.
774
2261
  const zlibObjects = ((opts.zlib ?? false) || nativeFetch) && driver.target !== null
775
- ? await ensureZlibObjects(opts.sanitize ?? false, driver)
2262
+ ? zlibObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
776
2263
  : [];
777
2264
  // The libcurl import stub is likewise CROSS-only — host builds keep the
778
2265
  // exact historical system `-lcurl` link (see CcOptions.fetch). Curl
779
2266
  // reference builds only.
780
- const curlStubDir = curlFetch && driver.target !== null ? await ensureCurlStub(driver) : null;
2267
+ const curlStubDir = curlFetch && driver.target !== null
2268
+ ? curlStubDirPath(driver, vendorBuildIdentity, vendorCacheRoot)
2269
+ : null;
2270
+ const materializeVendorPrerequisites = async () => {
2271
+ // Preserve the historical order to avoid multiplying first-build resource
2272
+ // pressure when several large vendor sets are cold simultaneously.
2273
+ if (dynamic) {
2274
+ await ensureEngineArchive(sanitize, driver, vendorBuildIdentity, vendorCacheRoot);
2275
+ }
2276
+ if (tls)
2277
+ await ensureTlsArchive(sanitize, driver, vendorBuildIdentity, vendorCacheRoot);
2278
+ if (regex && !dynamic) {
2279
+ await ensureLreObjects(sanitize, driver, vendorBuildIdentity, vendorCacheRoot);
2280
+ }
2281
+ if (zlibObjects.length > 0) {
2282
+ await ensureZlibObjects(sanitize, driver, vendorBuildIdentity, vendorCacheRoot);
2283
+ }
2284
+ if (curlStubDir !== null) {
2285
+ await ensureCurlStub(driver, vendorBuildIdentity, vendorCacheRoot);
2286
+ }
2287
+ };
781
2288
  // rt() maps each runtime source's path on the command line: identity for
782
2289
  // the historical single invocation, cached-.o substitution on cache misses.
783
- const buildArgs = (rt) => [
2290
+ const buildArgs = (rt, build = {}) => [
784
2291
  "-std=c11",
785
2292
  ...driver.targetArgs,
786
- ...(opts.sanitize
2293
+ ...(sanitize
787
2294
  ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
788
2295
  : ["-O2"]),
789
2296
  "-fno-math-errno",
@@ -818,20 +2325,20 @@ export async function compileC(opts) {
818
2325
  : []),
819
2326
  ...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []),
820
2327
  ...(opts.inspect ? [rt(join(rtDir, "scr_inspect.c"))] : []),
821
- ...(opts.dynInvoke ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
2328
+ ...((opts.dynInvoke || nativeFetch) ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
822
2329
  ...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
823
- ...(opts.dynAsync || opts.dynInvoke || opts.dc ? [rt(join(rtDir, "scr_async_dyn.c"))] : []),
2330
+ ...(opts.dynAsync || opts.dynInvoke || opts.dc || nativeFetch ? [rt(join(rtDir, "scr_async_dyn.c"))] : []),
824
2331
  // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
825
2332
  // libz on hosts, the vendored per-target objects on cross builds)
826
2333
  // also serves the native fetch's gzip decoder — spread exactly once.
827
2334
  ...(opts.zlib
828
2335
  ? driver.target !== null
829
2336
  ? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects]
830
- : [rt(join(rtDir, "scr_zlib.c")), "-lz"]
2337
+ : [rt(join(rtDir, "scr_zlib.c"))]
831
2338
  : nativeFetch
832
2339
  ? driver.target !== null
833
2340
  ? ["-I", vendorZlibDir(), ...zlibObjects]
834
- : ["-lz"]
2341
+ : []
835
2342
  : []),
836
2343
  // The zlib ↔ island bridge: only when BOTH halves are in the build
837
2344
  // (the scr_inspect_island.c pattern) — the emitted main calls its
@@ -881,6 +2388,10 @@ export async function compileC(opts) {
881
2388
  ...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
882
2389
  ]
883
2390
  : []),
2391
+ // Static fetch is the engine-free half of scr_fetch.c. Dynamic builds
2392
+ // compile the same source beside scr_island.c below, where its
2393
+ // SCR_DYNAMIC half installs the full web surface.
2394
+ ...(nativeFetch && !dynamic ? [rt(join(rtDir, "scr_fetch.c"))] : []),
884
2395
  ...(engineArchive
885
2396
  ? [
886
2397
  "-DSCR_DYNAMIC",
@@ -932,15 +2443,52 @@ export async function compileC(opts) {
932
2443
  // note about that. Never present for .c inputs, so the historical C
933
2444
  // command line cannot change by a byte.
934
2445
  ...(opts.cPath.endsWith(".ll") ? ["-Wno-override-module"] : []),
935
- opts.cPath,
2446
+ ...(build.compilerVisibleSource !== undefined && build.programPath !== undefined
2447
+ ? [
2448
+ `-ffile-prefix-map=${build.programPath}=${build.compilerVisibleSource}`,
2449
+ "-iquote",
2450
+ dirname(resolve(build.compilerVisibleSource)),
2451
+ ]
2452
+ : []),
2453
+ build.programPath ?? opts.cPath,
936
2454
  ...(opts.linkInputs ?? []),
937
2455
  ...(opts.systemLibraries ?? []).map((name) => `-l${name}`),
2456
+ // GNU ld resolves libraries from left to right and commonly enables
2457
+ // --as-needed: host libz must follow scr_zlib.c/scr_fetch.c and every
2458
+ // generated/native input that references inflate symbols. Cross
2459
+ // builds use vendored zlib objects in the input section above.
2460
+ ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null
2461
+ ? ["-lz"]
2462
+ : []),
938
2463
  // glibc keeps libm separate from libc. This must trail the generated
939
2464
  // program and every native FFI input because GNU ld resolves archives
940
2465
  // from left to right.
941
2466
  ...driver.linkArgs,
942
- "-o", opts.outPath,
2467
+ "-o", build.outPath ?? opts.outPath,
2468
+ ];
2469
+ // Compile-only flags shared by runtime-object population and the caller-TU
2470
+ // dependency probe. They reproduce the option set every TU sees in the
2471
+ // historical single clang invocation.
2472
+ const cflags = [
2473
+ "-std=c11",
2474
+ ...driver.targetArgs,
2475
+ ...(sanitize
2476
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
2477
+ : ["-O2"]),
2478
+ "-fno-math-errno",
2479
+ "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
2480
+ "-Wno-deprecated-declarations",
2481
+ "-I", rtDir,
2482
+ ...(regex || dynamic ? ["-I", vendorEngineDir()] : []),
2483
+ ...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
2484
+ ...(curlStubDir !== null ? ["-I", join(vendorCurlDir(), "include")] : []),
2485
+ ...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
2486
+ ...(dynamic ? ["-DSCR_DYNAMIC"] : []),
943
2487
  ];
2488
+ const programCompilerArgs = opts.cPath.endsWith(".ll")
2489
+ ? [...cflags, "-Wno-override-module"]
2490
+ : cflags;
2491
+ const programSourceExtension = opts.cPath.endsWith(".ll") ? ".ll" : ".c";
944
2492
  const ccName = driver.argv.join(" ");
945
2493
  const runClang = async (args) => {
946
2494
  try {
@@ -958,74 +2506,178 @@ export async function compileC(opts) {
958
2506
  `${guidance}\n\n${stderr}`);
959
2507
  }
960
2508
  };
961
- const root = cacheRootDir();
2509
+ let runtimeCompilerInvocation = null;
2510
+ let programCompilerInvocation = null;
2511
+ if (root !== null) {
2512
+ try {
2513
+ runtimeCompilerInvocation = await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags);
2514
+ programCompilerInvocation = programSourceExtension === ".ll"
2515
+ ? await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
2516
+ : runtimeCompilerInvocation;
2517
+ }
2518
+ catch {
2519
+ // Preserve the uncached build for wrappers that compile successfully but
2520
+ // cannot provide a dry-run trace for the real build flavor.
2521
+ root = null;
2522
+ }
2523
+ }
962
2524
  if (root === null) {
963
2525
  // The exact historical command line, byte for byte.
964
- await runClang(buildArgs((p) => p));
2526
+ try {
2527
+ await materializeVendorPrerequisites();
2528
+ await runClang(buildArgs((p) => p));
2529
+ }
2530
+ finally {
2531
+ if (transientVendorRoot !== null) {
2532
+ await rm(transientVendorRoot, { recursive: true, force: true }).catch(() => undefined);
2533
+ }
2534
+ }
965
2535
  return;
966
2536
  }
967
- const [cv, fingerprint, cBytes, linkBytes] = await Promise.all([
968
- ccVersionOnce(driver.argv),
969
- runtimeFingerprint(rtDir),
970
- readFile(opts.cPath),
971
- Promise.all((opts.linkInputs ?? []).map((path) => readFile(path))),
972
- ]);
973
- // The key sees the full command line with the two program-specific paths
974
- // normalized out (their CONTENT is what matters: the C bytes are hashed,
975
- // the out path is where the result lands). Runtime and vendor paths stay
976
- // verbatim — their contents are covered by the fingerprint and the pin.
977
- const identityArgs = buildArgs((p) => p).map((a) => a === opts.cPath ? "<program.c>" : a === opts.outPath ? "<out>" : a);
978
- const key = createHash("sha256")
979
- .update("bin-v1\0")
980
- // The driver spelling joins the version string: `zig cc --version`
981
- // reports the clang underneath and could otherwise collide with a
982
- // same-version host clang.
983
- .update(ccName).update("\0")
984
- .update(cv).update("\0")
985
- .update(fingerprint).update("\0")
986
- .update(identityArgs.join("\x1f")).update("\0")
987
- .update(cBytes);
988
- for (const bytes of linkBytes)
989
- key.update("\0ffi-link\0").update(bytes);
990
- const keyHex = key
991
- .digest("hex");
992
- const binDir = join(root, "bin");
993
- const cachedBin = join(binDir, keyHex);
2537
+ let cv;
2538
+ let fingerprint;
2539
+ let cBytes;
994
2540
  try {
995
- // NEVER copy over outPath in place: overwriting an already-executed
996
- // signed binary invalidates the kernel's per-vnode code-signature cache
997
- // on macOS and the next exec dies with SIGKILL. Copy to a fresh inode
998
- // and rename it into place instead.
999
- const tmpOut = `${opts.outPath}.hit-${process.pid}-${Math.random().toString(36).slice(2)}`;
1000
- await copyFile(cachedBin, tmpOut);
1001
- await chmod(tmpOut, 0o755);
1002
- await rename(tmpOut, opts.outPath);
1003
- const now = new Date();
1004
- await utimes(cachedBin, now, now).catch(() => undefined); // LRU bump
1005
- return; // hit: clang skipped entirely
2541
+ [cv, fingerprint, cBytes] = await Promise.all([
2542
+ ccVersionOnce(driver.argv, toolchainEnv, true),
2543
+ runtimeFingerprint(rtDir),
2544
+ readFile(opts.cPath),
2545
+ ]);
1006
2546
  }
1007
2547
  catch {
1008
- /* miss — build below, then publish */
2548
+ // A version/fingerprint probe is an optimization boundary. If the compiler
2549
+ // itself can still compile, preserve the pre-cache behavior instead of
2550
+ // surfacing a metadata command's failure as the build result.
2551
+ await materializeVendorPrerequisites();
2552
+ await runClang(buildArgs((p) => p));
2553
+ return;
1009
2554
  }
1010
- // Miss: link the program's own TU against cached per-flavor runtime
1011
- // objects. cflags reproduces exactly the option set every TU sees in the
1012
- // single invocation (the clang driver applies all options to all inputs).
1013
- const cflags = [
1014
- "-std=c11",
1015
- ...driver.targetArgs,
1016
- ...(opts.sanitize
1017
- ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
1018
- : ["-O2"]),
1019
- "-fno-math-errno",
1020
- "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
1021
- "-Wno-deprecated-declarations",
1022
- "-I", rtDir,
1023
- ...(regex || dynamic ? ["-I", vendorEngineDir()] : []),
1024
- ...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
1025
- ...(curlStubDir !== null ? ["-I", join(vendorCurlDir(), "include")] : []),
1026
- ...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
1027
- ...(dynamic ? ["-DSCR_DYNAMIC"] : []),
2555
+ // Caller-supplied native inputs can all hide mutable dependencies: `-l`
2556
+ // resolves through ambient search paths, while a thin archive or linker
2557
+ // script can retain identical top-level bytes as its referenced files are
2558
+ // rebuilt. Keep the safe runtime-object cache, but force a fresh final link
2559
+ // whenever the caller supplies either form.
2560
+ let cacheCompleteArtifact = cachePolicy.completeArtifacts &&
2561
+ (opts.linkInputs?.length ?? 0) === 0 &&
2562
+ (opts.systemLibraries?.length ?? 0) === 0;
2563
+ let programDependencies = null;
2564
+ if (cacheCompleteArtifact) {
2565
+ try {
2566
+ programDependencies = await translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv);
2567
+ }
2568
+ catch {
2569
+ // Program-header discovery is needed only for a complete hit. Runtime
2570
+ // objects remain safe because they never contain the caller's TU.
2571
+ cacheCompleteArtifact = false;
2572
+ }
2573
+ }
2574
+ const linkProbeArgs = [
2575
+ ...(sanitize ? ["-fsanitize=address"] : []),
2576
+ ...(targetPlatform(driver) === "win32"
2577
+ ? ["-ladvapi32", "-liphlpapi", "-lws2_32"]
2578
+ : []),
2579
+ ...(tls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
2580
+ ...(curlFetch && driver.target === null ? ["-lcurl"] : []),
2581
+ ...(dynamic && !driver.linkArgs.includes("-lm") ? ["-lm"] : []),
2582
+ ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null ? ["-lz"] : []),
2583
+ ...driver.linkArgs,
1028
2584
  ];
2585
+ // Both the wrapper dry run and dependency trace need the real build's
2586
+ // compile/link flag shape: wrappers commonly inject flags or native inputs
2587
+ // conditionally on optimization, sanitizer, dynamic, or platform switches.
2588
+ const effectiveLinkInvocationArgs = [
2589
+ ...programCompilerArgs,
2590
+ ...(targetPlatform(driver) === "win32"
2591
+ ? ["-ladvapi32", "-liphlpapi", "-lws2_32"]
2592
+ : []),
2593
+ ...(tls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
2594
+ ...(curlStubDir !== null ? [`-L${curlStubDir}`] : []),
2595
+ ...(curlFetch && driver.target === null ? ["-lcurl"] : []),
2596
+ ...(dynamic && !driver.linkArgs.includes("-lm") ? ["-lm"] : []),
2597
+ ...(dynamic && targetPlatform(driver) === "darwin" ? ["-Wl,-dead_strip"] : []),
2598
+ ...(dynamic && targetPlatform(driver) === "win32"
2599
+ ? ["-Wl,--stack,8388608"]
2600
+ : []),
2601
+ ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null ? ["-lz"] : []),
2602
+ ...driver.linkArgs,
2603
+ ];
2604
+ // A complete hit is checked before cross-target curl's generated import stub
2605
+ // is materialized. Its -L spelling still joins the dry-run identity, while
2606
+ // the real trace omits only that not-yet-existing scriptc-owned directory.
2607
+ const linkTraceInvocationArgs = curlStubDir === null
2608
+ ? effectiveLinkInvocationArgs
2609
+ : effectiveLinkInvocationArgs.filter((arg) => arg !== `-L${curlStubDir}`);
2610
+ let implicitLinker = null;
2611
+ if (cacheCompleteArtifact) {
2612
+ try {
2613
+ implicitLinker = await implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs);
2614
+ }
2615
+ catch {
2616
+ // Some compiler wrappers/linkers do not implement trace mode. Runtime
2617
+ // objects remain safely cacheable, but a complete executable cannot be
2618
+ // keyed without knowing every implicit link input.
2619
+ cacheCompleteArtifact = false;
2620
+ }
2621
+ }
2622
+ const binDir = join(root, "bin");
2623
+ let keyHex = null;
2624
+ let cachedBin = null;
2625
+ if (cacheCompleteArtifact) {
2626
+ // The key sees the full command line with the two program-specific paths
2627
+ // normalized out. The C bytes are hashed separately; Darwin additionally
2628
+ // keys the output basename because ld embeds it in the ad-hoc signature.
2629
+ // Runtime and vendor paths stay verbatim — their contents are covered by
2630
+ // the fingerprint and the pin.
2631
+ const identityArgs = buildArgs((p) => p).map((a) => a === opts.cPath ? "<program.c>" : a === opts.outPath ? "<out>" : a);
2632
+ const key = createHash("sha256")
2633
+ // v9 adds effective compiler-wrapper invocations for the real runtime,
2634
+ // program, and link flavors.
2635
+ .update("bin-v9\0")
2636
+ .update(cacheTargetIdentity(driver)).update("\0")
2637
+ .update(toolchainEnv).update("\0")
2638
+ .update(implicitToolchain).update("\0")
2639
+ .update(runtimeCompilerInvocation).update("\0")
2640
+ .update(programCompilerInvocation).update("\0")
2641
+ .update(implicitLinker).update("\0")
2642
+ .update(programDependencies).update("\0")
2643
+ .update(opts.cacheIdentity).update("\0")
2644
+ // Preserve both the spelling clang sees (__FILE__) and the location used
2645
+ // to resolve relative includes. The top-level bytes are not sufficient.
2646
+ .update(opts.cPath).update("\0")
2647
+ .update(resolve(opts.cPath)).update("\0")
2648
+ .update(targetPlatform(driver) === "darwin" ? basename(opts.outPath) : "<out>").update("\0")
2649
+ // The driver spelling joins the version string: `zig cc --version`
2650
+ // reports the clang underneath and could otherwise collide with a
2651
+ // same-version host clang.
2652
+ .update(ccName).update("\0")
2653
+ .update(cv).update("\0")
2654
+ .update(fingerprint).update("\0")
2655
+ .update(identityArgs.join("\x1f")).update("\0")
2656
+ .update(cBytes);
2657
+ keyHex = key.digest("hex");
2658
+ cachedBin = join(binDir, keyHex);
2659
+ const tmpOut = privateSiblingPath(opts.outPath, "bin-hit");
2660
+ try {
2661
+ // NEVER copy over outPath in place: overwriting an already-executed
2662
+ // signed binary invalidates the kernel's per-vnode code-signature cache
2663
+ // on macOS and the next exec dies with SIGKILL. Copy to a fresh inode
2664
+ // and rename it into place instead.
2665
+ if (!(await copyValidCachedFile(cachedBin, tmpOut))) {
2666
+ throw new Error("invalid cached executable");
2667
+ }
2668
+ // Match a fresh linker output under the caller's current umask. Reusing a
2669
+ // cache entry populated by a less restrictive shell must not widen access.
2670
+ await chmod(tmpOut, 0o777 & ~process.umask());
2671
+ await rename(tmpOut, opts.outPath);
2672
+ return; // hit: the program/runtime payload compile and link were skipped
2673
+ }
2674
+ catch {
2675
+ await rm(tmpOut, { force: true }).catch(() => undefined);
2676
+ /* miss — build below, then publish */
2677
+ }
2678
+ }
2679
+ await materializeVendorPrerequisites();
2680
+ // Miss: link the program's own TU against cached per-flavor runtime objects.
1029
2681
  // Collect the runtime sources this build actually compiles (the same
1030
2682
  // conditionals as the command line, by construction).
1031
2683
  const rtInputs = [];
@@ -1033,24 +2685,87 @@ export async function compileC(opts) {
1033
2685
  rtInputs.push(p);
1034
2686
  return p;
1035
2687
  });
1036
- let objects = null;
1037
- try {
1038
- objects = await ensureRuntimeObjects(root, driver.argv, cflags, rtInputs, `obj-v1\0${ccName}\0${cv}\0${fingerprint}\0`);
1039
- }
1040
- catch {
1041
- objects = null; // cache trouble is never a build failure
1042
- }
1043
- await runClang(buildArgs((p) => objects?.get(p) ?? p));
2688
+ const buildDir = await mkdtemp(join(tmpdir(), "scriptc-cache-build-"));
1044
2689
  try {
1045
- await mkdir(binDir, { recursive: true });
1046
- const tmp = join(binDir, `.tmp-${keyHex.slice(0, 8)}-${process.pid}-${Math.random().toString(36).slice(2)}`);
1047
- await copyFile(opts.outPath, tmp);
1048
- await chmod(tmp, 0o755);
1049
- await rename(tmp, cachedBin);
1050
- await pruneCache(root);
2690
+ // Freeze the exact bytes used for the key. Generated TUs live at stable
2691
+ // paths under .scriptc/, so two builds can otherwise overwrite that path
2692
+ // between hashing and clang and publish one invocation's code under the
2693
+ // other's key. The prefix map preserves the original __FILE__/debug-file
2694
+ // spelling while clang reads this invocation-private snapshot.
2695
+ const programPath = join(buildDir, `program${opts.cPath.endsWith(".ll") ? ".ll" : ".c"}`);
2696
+ // Preserve the caller-visible basename while keeping Darwin builds on a
2697
+ // private inode: ld uses this spelling as the embedded ad-hoc signing
2698
+ // identifier. Other targets retain the basename-independent cache key.
2699
+ const privateOut = join(buildDir, targetPlatform(driver) === "darwin"
2700
+ ? basename(opts.outPath)
2701
+ : process.platform === "win32"
2702
+ ? "artifact.exe"
2703
+ : "artifact");
2704
+ await writeFile(programPath, cBytes);
2705
+ let objects = null;
2706
+ let cacheInputsStable = true;
2707
+ let objectImplicitVerification = null;
2708
+ const objectImplicitToolchainStillMatches = () => {
2709
+ objectImplicitVerification ??= Promise.all([
2710
+ implicitToolchainFingerprint(driver, toolchainEnv),
2711
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
2712
+ ]).then(([currentImplicit, currentInvocation]) => currentImplicit === implicitToolchain &&
2713
+ currentInvocation === runtimeCompilerInvocation, () => false);
2714
+ return objectImplicitVerification;
2715
+ };
2716
+ try {
2717
+ const cached = await ensureRuntimeObjects(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 runtimeFingerprint(rtDir)) === fingerprint &&
2718
+ (await objectImplicitToolchainStillMatches()));
2719
+ objects = await stageRuntimeObjects(cached, join(buildDir, "runtime-objects"));
2720
+ }
2721
+ catch (err) {
2722
+ if (err instanceof CacheInputsChangedError)
2723
+ cacheInputsStable = false;
2724
+ objects = null; // cache trouble is never a build failure
2725
+ }
2726
+ await runClang(buildArgs((p) => objects?.get(p) ?? p, { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }));
2727
+ await installArtifact(privateOut, opts.outPath);
2728
+ if (cachedBin !== null && keyHex !== null) {
2729
+ // This check is deliberately fresh rather than the object-publication
2730
+ // verification above. The final program compile/link can itself race an
2731
+ // SDK header, compiler, linker, CRT, or system-library replacement; its
2732
+ // bytes must not be published under the pre-build identity in that case.
2733
+ const [currentRuntime, currentImplicit, currentRuntimeInvocation, currentProgramInvocation, currentProgramDependencies, currentLinker, currentCompiler] = await Promise.all([
2734
+ runtimeFingerprint(rtDir).catch(() => null),
2735
+ implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
2736
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags).catch(() => null),
2737
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension).catch(() => null),
2738
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv).catch(() => null),
2739
+ implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs).catch(() => null),
2740
+ ccVersionOnce(driver.argv, toolchainEnv, true).catch(() => null),
2741
+ ]);
2742
+ cacheInputsStable =
2743
+ cacheInputsStable &&
2744
+ currentRuntime === fingerprint &&
2745
+ currentImplicit === implicitToolchain &&
2746
+ currentRuntimeInvocation === runtimeCompilerInvocation &&
2747
+ currentProgramInvocation === programCompilerInvocation &&
2748
+ currentProgramDependencies === programDependencies &&
2749
+ currentLinker === implicitLinker &&
2750
+ currentCompiler === cv;
2751
+ }
2752
+ if (cachedBin !== null && keyHex !== null && cacheInputsStable) {
2753
+ try {
2754
+ // Cache artifacts are data, never execution targets. Publication keeps
2755
+ // generated code and embedded literals private; the hit path reapplies
2756
+ // the caller's current executable mode to its destination copy.
2757
+ await publishCachedFile(privateOut, cachedBin);
2758
+ }
2759
+ catch {
2760
+ /* publishing is best-effort */
2761
+ }
2762
+ }
1051
2763
  }
1052
- catch {
1053
- /* publishing is best-effort */
2764
+ finally {
2765
+ await rm(buildDir, { recursive: true, force: true }).catch(() => undefined);
1054
2766
  }
2767
+ // Runtime-object population is itself a cache write, including on builds
2768
+ // whose native link inputs disable complete-artifact publication.
2769
+ await pruneCache(root).catch(() => undefined);
1055
2770
  }
1056
2771
  //# sourceMappingURL=cc.js.map