@scriptc/compiler 0.0.32 → 0.0.34

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 (120) hide show
  1. package/dist/backend/cc-cache.test.js +430 -1
  2. package/dist/backend/cc-cache.test.js.map +1 -1
  3. package/dist/backend/cc.d.ts +25 -0
  4. package/dist/backend/cc.js +818 -88
  5. package/dist/backend/cc.js.map +1 -1
  6. package/dist/backend/emission/emit-exprs.js +79 -3
  7. package/dist/backend/emission/emit-exprs.js.map +1 -1
  8. package/dist/backend/emission/emit-shapes.js +37 -4
  9. package/dist/backend/emission/emit-shapes.js.map +1 -1
  10. package/dist/backend/emission/emitter.d.ts +23 -7
  11. package/dist/backend/emission/emitter.js +171 -27
  12. package/dist/backend/emission/emitter.js.map +1 -1
  13. package/dist/backend/emission/may-throw.js +7 -4
  14. package/dist/backend/emission/may-throw.js.map +1 -1
  15. package/dist/backend/ffi-callbacks.d.ts +38 -1
  16. package/dist/backend/ffi-callbacks.js +76 -3
  17. package/dist/backend/ffi-callbacks.js.map +1 -1
  18. package/dist/backend/library-identity.d.ts +22 -0
  19. package/dist/backend/library-identity.js +103 -0
  20. package/dist/backend/library-identity.js.map +1 -0
  21. package/dist/backend/llvm/emitter.d.ts +3 -0
  22. package/dist/backend/llvm/emitter.js +278 -17
  23. package/dist/backend/llvm/emitter.js.map +1 -1
  24. package/dist/backend/llvm/shapes.d.ts +1 -0
  25. package/dist/backend/llvm/shapes.js +25 -1
  26. package/dist/backend/llvm/shapes.js.map +1 -1
  27. package/dist/backend/mangle.d.ts +1 -0
  28. package/dist/backend/mangle.js +3 -0
  29. package/dist/backend/mangle.js.map +1 -1
  30. package/dist/backend/object-localize.d.ts +5 -5
  31. package/dist/backend/object-localize.js +19 -16
  32. package/dist/backend/object-localize.js.map +1 -1
  33. package/dist/backend/object-localize.test.js +37 -13
  34. package/dist/backend/object-localize.test.js.map +1 -1
  35. package/dist/diagnostics/diagnostic.js +2 -0
  36. package/dist/diagnostics/diagnostic.js.map +1 -1
  37. package/dist/ffi/profile.d.ts +17 -5
  38. package/dist/ffi/profile.js +169 -22
  39. package/dist/ffi/profile.js.map +1 -1
  40. package/dist/frontend/input-tracker.d.ts +82 -0
  41. package/dist/frontend/input-tracker.js +380 -0
  42. package/dist/frontend/input-tracker.js.map +1 -0
  43. package/dist/frontend/input-tracker.test.d.ts +1 -0
  44. package/dist/frontend/input-tracker.test.js +137 -0
  45. package/dist/frontend/input-tracker.test.js.map +1 -0
  46. package/dist/frontend/lowering/lower-builtins.js +2 -8
  47. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  48. package/dist/frontend/lowering/lower-calls.d.ts +1 -0
  49. package/dist/frontend/lowering/lower-calls.js +244 -163
  50. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  51. package/dist/frontend/lowering/lower-classes.js +3 -0
  52. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  53. package/dist/frontend/lowering/lower-containers.js +113 -28
  54. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  55. package/dist/frontend/lowering/lower-exprs.js +67 -1
  56. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  57. package/dist/frontend/lowering/lower-inspect.js +45 -26
  58. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  59. package/dist/frontend/lowering/lower-modules.js +1 -1
  60. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  61. package/dist/frontend/lowering/lower-stmts.js +48 -3
  62. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  63. package/dist/frontend/lowering/lowerer.d.ts +79 -29
  64. package/dist/frontend/lowering/lowerer.js +429 -87
  65. package/dist/frontend/lowering/lowerer.js.map +1 -1
  66. package/dist/frontend/npm-static-rewrite.js +25 -26
  67. package/dist/frontend/npm-static-rewrite.js.map +1 -1
  68. package/dist/frontend/npm-static.js +29 -30
  69. package/dist/frontend/npm-static.js.map +1 -1
  70. package/dist/frontend/npm-static.test.d.ts +1 -0
  71. package/dist/frontend/npm-static.test.js +15 -0
  72. package/dist/frontend/npm-static.test.js.map +1 -0
  73. package/dist/frontend/npm.js +5 -17
  74. package/dist/frontend/npm.js.map +1 -1
  75. package/dist/frontend/program.js +2 -6
  76. package/dist/frontend/program.js.map +1 -1
  77. package/dist/frontend/resolve.js +8 -29
  78. package/dist/frontend/resolve.js.map +1 -1
  79. package/dist/frontend/ts7/checker.d.ts +2 -0
  80. package/dist/frontend/ts7/checker.js +20 -3
  81. package/dist/frontend/ts7/checker.js.map +1 -1
  82. package/dist/frontend/ts7/program.js +17 -18
  83. package/dist/frontend/ts7/program.js.map +1 -1
  84. package/dist/frontend/types.d.ts +41 -0
  85. package/dist/frontend/types.js +147 -8
  86. package/dist/frontend/types.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.js +275 -41
  89. package/dist/index.js.map +1 -1
  90. package/dist/ir/nodes.d.ts +42 -12
  91. package/dist/ir/nodes.js +7 -1
  92. package/dist/ir/nodes.js.map +1 -1
  93. package/dist/ir/serialize.d.ts +1 -1
  94. package/dist/ir/serialize.js +1 -1
  95. package/dist/ir/validate.js +104 -1
  96. package/dist/ir/validate.js.map +1 -1
  97. package/dist/library/early-cache.d.ts +65 -0
  98. package/dist/library/early-cache.js +380 -0
  99. package/dist/library/early-cache.js.map +1 -0
  100. package/dist/library/early-cache.test.d.ts +1 -0
  101. package/dist/library/early-cache.test.js +528 -0
  102. package/dist/library/early-cache.test.js.map +1 -0
  103. package/dist/library/int-infer.js +14 -0
  104. package/dist/library/int-infer.js.map +1 -1
  105. package/dist/library/int-infer.test.js +21 -4
  106. package/dist/library/int-infer.test.js.map +1 -1
  107. package/dist/library/profile.d.ts +3 -0
  108. package/dist/library/profile.js +9 -1
  109. package/dist/library/profile.js.map +1 -1
  110. package/dist/library/semantic-source.d.ts +13 -0
  111. package/dist/library/semantic-source.js +257 -0
  112. package/dist/library/semantic-source.js.map +1 -0
  113. package/dist/library/semantic-source.test.d.ts +1 -0
  114. package/dist/library/semantic-source.test.js +113 -0
  115. package/dist/library/semantic-source.test.js.map +1 -0
  116. package/dist/library/sidecar.d.ts +6 -0
  117. package/dist/library/sidecar.js +11 -0
  118. package/dist/library/sidecar.js.map +1 -1
  119. package/package.json +2 -2
  120. package/surface-manifest.json +1 -1
@@ -2,9 +2,9 @@ import { execFile, spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { constants as fsConstants, existsSync, readdirSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
- import { access, chmod, copyFile, link, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
5
+ import { access, chmod, copyFile, link, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
6
6
  import { availableParallelism, homedir, tmpdir } from "node:os";
7
- import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
7
+ import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import { promisify } from "node:util";
9
9
  import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
10
10
  const execFileAsync = promisify(execFile);
@@ -32,7 +32,7 @@ function stableTestMemo(cache, key, probe) {
32
32
  });
33
33
  return pending;
34
34
  }
35
- 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"];
35
+ 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_ffi.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"];
36
36
  /** The pinned quickjs-ng snapshot under packages/runtime/vendor/quickjs-ng
37
37
  * (see vendor/README.md — update both together). Keys the archive cache so
38
38
  * a vendor bump can never reuse a stale engine build. */
@@ -974,13 +974,14 @@ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = ven
974
974
  /** The library base: the executable lane's unconditional sources minus the
975
975
  * fiber/loop and child-process units, plus the library-mode TU. */
976
976
  const LIB_RUNTIME_SOURCES = [
977
- ...RUNTIME_SOURCES.filter((f) => f !== "scr_async.c" && f !== "scr_child.c"),
977
+ ...RUNTIME_SOURCES.filter((f) => f !== "scr_async.c" && f !== "scr_child.c" && f !== "scr_ffi.c"),
978
978
  "scr_library.c",
979
979
  ];
980
980
  export async function compileLibArchive(opts) {
981
981
  const rtDir = runtimeSrcDir();
982
982
  const driver = resolveCc();
983
983
  const sanitize = opts.sanitize ?? false;
984
+ const optimization = opts.optimization ?? "release";
984
985
  const regex = opts.regex ?? false;
985
986
  const sources = [
986
987
  ...LIB_RUNTIME_SOURCES,
@@ -1008,7 +1009,9 @@ export async function compileLibArchive(opts) {
1008
1009
  const cflags = [
1009
1010
  "-std=c11",
1010
1011
  ...driver.targetArgs,
1011
- ...(sanitize ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"] : ["-O2"]),
1012
+ ...(sanitize
1013
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
1014
+ : [optimization === "dev" ? "-O0" : "-O2"]),
1012
1015
  "-fno-math-errno",
1013
1016
  "-fno-strict-aliasing", // the emitted object model type-puns — see compileC's buildArgs
1014
1017
  "-Wno-deprecated-declarations",
@@ -1084,24 +1087,26 @@ export async function compileLibArchive(opts) {
1084
1087
  let archiverVersion = "";
1085
1088
  let runtimeHash = "";
1086
1089
  let programDependencyHash = "";
1087
- let cachedProgramBytes = null;
1090
+ let cachedProgramBytes = opts.programSource === undefined
1091
+ ? null
1092
+ : Buffer.from(opts.programSource, "utf8");
1093
+ const identityBytes = opts.identityCSource === undefined
1094
+ ? null
1095
+ : Buffer.from(opts.identityCSource, "utf8");
1088
1096
  if (root !== null) {
1089
1097
  try {
1090
1098
  const [cv, fingerprint, programBytes] = await Promise.all([
1091
1099
  ccVersionOnce(driver.argv, toolchainEnv, true),
1092
1100
  runtimeFingerprint(rtDir),
1093
- readFile(opts.cPath),
1101
+ cachedProgramBytes === null ? readFile(opts.cPath) : Promise.resolve(cachedProgramBytes),
1094
1102
  ]);
1095
1103
  compilerVersion = cv;
1096
1104
  runtimeHash = fingerprint;
1097
1105
  cachedProgramBytes = programBytes;
1106
+ programDependencyHash = await translationUnitDependencyFingerprint(driver, cflags, opts.cPath, programBytes, toolchainEnv);
1098
1107
  if (cacheCompleteArchive) {
1099
- const [av, programDependencies] = await Promise.all([
1100
- toolVersionOnce(arArgv, toolchainEnv, true),
1101
- translationUnitDependencyFingerprint(driver, cflags, opts.cPath, programBytes, toolchainEnv),
1102
- ]);
1108
+ const av = await toolVersionOnce(arArgv, toolchainEnv, true);
1103
1109
  archiverVersion = av;
1104
- programDependencyHash = programDependencies;
1105
1110
  const key = createHash("sha256")
1106
1111
  // v7 adds effective compiler-wrapper invocations for the real runtime
1107
1112
  // and program compile flavors.
@@ -1111,7 +1116,7 @@ export async function compileLibArchive(opts) {
1111
1116
  .update(implicitToolchain).update("\0")
1112
1117
  .update(runtimeCompilerInvocation).update("\0")
1113
1118
  .update(programCompilerInvocation).update("\0")
1114
- .update(programDependencies).update("\0")
1119
+ .update(programDependencyHash).update("\0")
1115
1120
  .update(opts.cacheIdentity).update("\0")
1116
1121
  .update(driver.argv.join("\x1f")).update("\0")
1117
1122
  .update(cv).update("\0")
@@ -1126,6 +1131,9 @@ export async function compileLibArchive(opts) {
1126
1131
  .update(opts.cPath).update("\0")
1127
1132
  .update(resolve(opts.cPath)).update("\0")
1128
1133
  .update(programBytes)
1134
+ .update("\0identity\0")
1135
+ .update(identityBytes === null ? "<none>" : "<generated>").update("\0")
1136
+ .update(identityBytes ?? Buffer.alloc(0))
1129
1137
  .digest("hex");
1130
1138
  cachedArchive = join(root, "lib", key);
1131
1139
  const tmpOut = privateSiblingPath(opts.outPath, "lib-hit");
@@ -1197,7 +1205,64 @@ export async function compileLibArchive(opts) {
1197
1205
  : join(buildDir, `program${opts.cPath.endsWith(".ll") ? ".ll" : ".c"}`);
1198
1206
  if (cachedProgramBytes !== null)
1199
1207
  await writeFile(programSource, cachedProgramBytes);
1200
- const programObject = await compileOne(programSource, `${stem}.program.o`, cachedProgramBytes === null ? undefined : opts.cPath);
1208
+ let cachedProgramObject = null;
1209
+ if (root !== null && cachedProgramBytes !== null && compilerVersion !== "" &&
1210
+ implicitToolchain !== null && programCompilerInvocation !== null) {
1211
+ const programKey = createHash("sha256")
1212
+ .update("lib-program-obj-v1\0")
1213
+ .update(cacheTargetIdentity(driver)).update("\0")
1214
+ .update(toolchainEnv).update("\0")
1215
+ .update(implicitToolchain).update("\0")
1216
+ .update(programCompilerInvocation).update("\0")
1217
+ .update(opts.cacheIdentity).update("\0")
1218
+ .update(driver.argv.join("\x1f")).update("\0")
1219
+ .update(compilerVersion).update("\0")
1220
+ .update(runtimeHash).update("\0")
1221
+ .update(programDependencyHash).update("\0")
1222
+ .update(programCompilerArgs.join("\x1f")).update("\0")
1223
+ .update(opts.cPath).update("\0")
1224
+ .update(resolve(opts.cPath)).update("\0")
1225
+ .update(cachedProgramBytes)
1226
+ .digest("hex");
1227
+ cachedProgramObject = join(root, "program-obj", programKey);
1228
+ }
1229
+ const stagedProgramObject = join(buildDir, `${stem}.program.o`);
1230
+ let programObject;
1231
+ if (cachedProgramObject !== null &&
1232
+ await copyValidCachedFile(cachedProgramObject, stagedProgramObject)) {
1233
+ programObject = stagedProgramObject;
1234
+ }
1235
+ else {
1236
+ programObject = await compileOne(programSource, `${stem}.program.o`, cachedProgramBytes === null ? undefined : opts.cPath);
1237
+ if (cachedProgramObject !== null) {
1238
+ try {
1239
+ const [currentRuntime, currentImplicit, currentInvocation, currentDependencies, currentCompiler] = await Promise.all([
1240
+ runtimeFingerprint(rtDir),
1241
+ implicitToolchainFingerprint(driver, toolchainEnv),
1242
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension),
1243
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cachedProgramBytes, toolchainEnv),
1244
+ ccVersionOnce(driver.argv, toolchainEnv, true),
1245
+ ]);
1246
+ if (currentRuntime === runtimeHash &&
1247
+ currentImplicit === implicitToolchain &&
1248
+ currentInvocation === programCompilerInvocation &&
1249
+ currentDependencies === programDependencyHash &&
1250
+ currentCompiler === compilerVersion) {
1251
+ await publishCachedFile(programObject, cachedProgramObject);
1252
+ }
1253
+ }
1254
+ catch {
1255
+ // Best-effort: the archive build already owns a valid object.
1256
+ }
1257
+ }
1258
+ }
1259
+ const identityObject = identityBytes === null
1260
+ ? null
1261
+ : await (async () => {
1262
+ const source = join(buildDir, "identity.c");
1263
+ await writeFile(source, identityBytes);
1264
+ return compileOne(source, `${stem}.identity.o`);
1265
+ })();
1201
1266
  let runtimeObjects = null;
1202
1267
  let cacheInputsStable = true;
1203
1268
  let objectImplicitVerification = null;
@@ -1230,14 +1295,14 @@ export async function compileLibArchive(opts) {
1230
1295
  runtimeObjects.push(...(await Promise.all(sources.slice(i, i + width).map((f) => compileOne(join(rtDir, f), f.replace(/\.c$/, ".o"))))));
1231
1296
  }
1232
1297
  }
1233
- const objects = [programObject, ...runtimeObjects, ...lreObjects, ...zlibObjects];
1298
+ const objects = [programObject, ...(identityObject === null ? [] : [identityObject]), ...runtimeObjects, ...lreObjects, ...zlibObjects];
1234
1299
  // Multi-instance library mode: the archive's one member becomes the
1235
1300
  // combined, symbol-localized object (cached vendor/runtime objects
1236
1301
  // are read-only inputs here — the combine step never mutates them).
1237
1302
  const archiveMembers = opts.localizeSymbols === undefined
1238
1303
  ? objects
1239
1304
  : [
1240
- await localizeLibraryObjects(driver, arArgv, buildDir, programObject, [...runtimeObjects, ...lreObjects, ...zlibObjects], opts.localizeSymbols, stem),
1305
+ await localizeLibraryObjects(driver, arArgv, buildDir, [programObject, ...(identityObject === null ? [] : [identityObject])], [...runtimeObjects, ...lreObjects, ...zlibObjects], opts.localizeSymbols, stem),
1241
1306
  ];
1242
1307
  // A cacheable or runtime-localized build owns a private archive from
1243
1308
  // `ar` through publication. Localized archives deliberately bypass the
@@ -1317,7 +1382,8 @@ export async function compileLibArchive(opts) {
1317
1382
  * shared by design. Windows embedders additionally link advapi32, iphlpapi,
1318
1383
  * and ws2_32.
1319
1384
  *
1320
- * Member selection matters: a classic archive's unused members (and their
1385
+ * The generated program and optional identity objects are mandatory roots;
1386
+ * support-member selection still matters. A classic archive's unused members (and their
1321
1387
  * undefined references to units library mode excludes, like the
1322
1388
  * fs-promises unit's fiber symbols) never reach an embedder's link. A
1323
1389
  * blind merge of every object would carry those references into the one
@@ -1326,7 +1392,7 @@ export async function compileLibArchive(opts) {
1326
1392
  * members the program object transitively needs (the COFF arm implements
1327
1393
  * the same member semantics in process).
1328
1394
  *
1329
- * Mach-O — one host-ld64 invocation: -r merges program + needed members,
1395
+ * Mach-O — one host-ld64 invocation: -r merges roots + needed members,
1330
1396
  * -exported_symbols_list demotes every unlisted global to
1331
1397
  * private extern, and -r without -keep_private_externs writes
1332
1398
  * private externs out as non-external symbols. Apple ASan's
@@ -1357,7 +1423,7 @@ export async function compileLibArchive(opts) {
1357
1423
  * mirrors MSVC link.exe; zig's COFF driver refuses multi-object
1358
1424
  * merges) and llvm-objcopy rejects symbol-scope flags for
1359
1425
  * COFF, so no tool pairing exists to shell out to. */
1360
- async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, supportObjects, keepSymbols, stem) {
1426
+ async function localizeLibraryObjects(driver, arArgv, buildDir, rootObjects, supportObjects, keepSymbols, stem) {
1361
1427
  const platform = targetPlatform(driver);
1362
1428
  const combined = join(buildDir, `${stem}.localized.o`);
1363
1429
  const staging = join(buildDir, `${stem}.localize-staging.a`);
@@ -1375,10 +1441,13 @@ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, s
1375
1441
  if (platform === "win32") {
1376
1442
  // COFF has no relocatable-link tool to stage through; the member
1377
1443
  // selection and combine+demote happen in process over the object bytes.
1378
- const [program, ...support] = await Promise.all([programObject, ...supportObjects].map((path) => readFile(path)));
1444
+ const [roots, support] = await Promise.all([
1445
+ Promise.all(rootObjects.map((path) => readFile(path))),
1446
+ Promise.all(supportObjects.map((path) => readFile(path))),
1447
+ ]);
1379
1448
  try {
1380
- await writeFile(combined, mergeAndLocalizeCoffObjects(program, support, new Set(keepSymbols), {
1381
- program: basename(programObject),
1449
+ await writeFile(combined, mergeAndLocalizeCoffObjects(roots, support, new Set(keepSymbols), {
1450
+ roots: rootObjects.map((path) => basename(path)),
1382
1451
  support: supportObjects.map((path) => basename(path)),
1383
1452
  }));
1384
1453
  }
@@ -1390,11 +1459,11 @@ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, s
1390
1459
  await run([arArgv[0] ?? "ar", ...arArgv.slice(1), "rcs", staging, ...supportObjects]);
1391
1460
  if (platform === "darwin") {
1392
1461
  await writeFile(keepFile, keepSymbols.map((s) => `_${s}\n`).join(""));
1393
- await run(["ld", "-r", programObject, staging, "-o", combined, "-exported_symbols_list", keepFile]);
1462
+ await run(["ld", "-r", ...rootObjects, staging, "-o", combined, "-exported_symbols_list", keepFile]);
1394
1463
  }
1395
1464
  else if (platform === "linux" && driver.target === null) {
1396
1465
  await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join(""));
1397
- await run(["ld", "-r", "--force-group-allocation", programObject, staging, "-o", combined]);
1466
+ await run(["ld", "-r", "--force-group-allocation", ...rootObjects, staging, "-o", combined]);
1398
1467
  await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]);
1399
1468
  }
1400
1469
  else if (platform === "linux") {
@@ -1409,7 +1478,7 @@ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, s
1409
1478
  ...driver.argv.slice(1),
1410
1479
  "-target", driver.zigTarget ?? driver.target,
1411
1480
  "-nostdlib",
1412
- "-r", programObject, staging,
1481
+ "-r", ...rootObjects, staging,
1413
1482
  "-o", combined,
1414
1483
  ]);
1415
1484
  try {
@@ -1518,6 +1587,31 @@ export function resolveBuildCacheRoot(env = process.env, platform = process.plat
1518
1587
  ? resolve(userHome, "Library", "Caches", "scriptc", "build")
1519
1588
  : resolve(userHome, ".cache", "scriptc", "build");
1520
1589
  }
1590
+ /** Shared persistent-cache root for compiler-level tiers. The early library
1591
+ * cache deliberately follows the native cache's activation and hard-disable
1592
+ * contract, while native compilation retains ownership of toolchain safety. */
1593
+ export function buildCacheRoot() {
1594
+ return resolveBuildCacheRoot();
1595
+ }
1596
+ /** Harden/create a compiler-level cache root using the same privacy policy as
1597
+ * the artifact caches. Failure disables only the optional caller's tier. */
1598
+ export async function prepareBuildCacheRoot(root) {
1599
+ if (root === null)
1600
+ return null;
1601
+ try {
1602
+ await ensurePrivateCacheRoot(root, process.env["SCRIPTC_CACHE_DIR"] === undefined);
1603
+ return root;
1604
+ }
1605
+ catch {
1606
+ return null;
1607
+ }
1608
+ }
1609
+ /** Register a successful compiler-level cache write with the shared bounded
1610
+ * LRU policy. */
1611
+ export async function pruneBuildCache(root) {
1612
+ if (root !== null)
1613
+ await pruneCache(root).catch(() => undefined);
1614
+ }
1521
1615
  function cacheRootDir() {
1522
1616
  return resolveBuildCacheRoot();
1523
1617
  }
@@ -1602,6 +1696,15 @@ async function resolvedToolIdentity(command, env = process.env) {
1602
1696
  return (await resolvedTool(command, env))?.cacheIdentity ?? null;
1603
1697
  }
1604
1698
  const directCompilerDriverMemos = new Map();
1699
+ const directCompilerSelections = new Map();
1700
+ function compilerDriverProbeKey(driver, environmentFingerprint) {
1701
+ return [
1702
+ environmentFingerprint,
1703
+ driver.argv.join("\x1f"),
1704
+ driver.target ?? "<native>",
1705
+ driver.targetArgs.join("\x1f"),
1706
+ ].join("\0");
1707
+ }
1605
1708
  /** `/usr/bin/clang` on Darwin is Apple's immutable driver shim: its `-###`
1606
1709
  * trace names the selected Xcode/CommandLineTools clang rather than the shim
1607
1710
  * itself. The ordinary implicit-toolchain fingerprint below captures that
@@ -1623,12 +1726,7 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1623
1726
  // wrapper. This is deliberately undocumented and test-scoped.
1624
1727
  if (process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1")
1625
1728
  return true;
1626
- const driverKey = [
1627
- environmentFingerprint,
1628
- driver.argv.join("\x1f"),
1629
- driver.target ?? "<native>",
1630
- driver.targetArgs.join("\x1f"),
1631
- ].join("\0");
1729
+ const driverKey = compilerDriverProbeKey(driver, environmentFingerprint);
1632
1730
  const compiler = driver.argv[0] ?? "clang";
1633
1731
  const resolvedDriver = await resolvedTool(compiler);
1634
1732
  // A prior dependency list cannot prove that name resolution is unchanged:
@@ -1669,6 +1767,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1669
1767
  direct = effectiveCompiler !== null &&
1670
1768
  (effectiveCompiler.fileIdentity === resolvedDriver.fileIdentity ||
1671
1769
  isAppleSystemClangHandoff(resolvedDriver, effectiveCompiler));
1770
+ if (direct)
1771
+ directCompilerSelections.set(driverKey, effectiveCompiler);
1672
1772
  }
1673
1773
  }
1674
1774
  catch {
@@ -1678,6 +1778,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1678
1778
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1679
1779
  }
1680
1780
  directCompilerDriverMemos.set(probeKey, direct);
1781
+ if (!direct)
1782
+ directCompilerSelections.delete(driverKey);
1681
1783
  return direct;
1682
1784
  }
1683
1785
  /** Complete library archives may skip `ar`, so the selected archiver must be
@@ -1758,6 +1860,29 @@ async function toolVersionOnce(argv, environmentFingerprint = toolchainEnvironme
1758
1860
  toolVersionFallbacks.set(spellingKey, memo);
1759
1861
  return memo;
1760
1862
  }
1863
+ /** Dependency paths discovered while computing one strict fingerprint. The
1864
+ * output-local cache stamp snapshots these exact files/directories after a
1865
+ * validated build, then can prove a later same-output no-op without spawning
1866
+ * clang again. The content identity closes the gap between hashing and that
1867
+ * metadata snapshot: a file changed in the gap cannot make new bytes ride an
1868
+ * old key. Keep the map bounded for long-lived corpus/test processes. */
1869
+ const fingerprintDependencies = new Map();
1870
+ function rememberFingerprintDependencies(fingerprint, paths, contentPaths = [], contentFingerprint = null) {
1871
+ fingerprintDependencies.set(fingerprint, {
1872
+ paths: [...new Set(paths)].sort(),
1873
+ contentPaths: [...new Set(contentPaths)].sort(),
1874
+ contentFingerprint,
1875
+ });
1876
+ if (fingerprintDependencies.size > 256) {
1877
+ const oldest = fingerprintDependencies.keys().next().value;
1878
+ if (oldest !== undefined)
1879
+ fingerprintDependencies.delete(oldest);
1880
+ }
1881
+ return fingerprint;
1882
+ }
1883
+ function fingerprintDependencyPaths(fingerprint) {
1884
+ return fingerprintDependencies.get(fingerprint)?.paths ?? [];
1885
+ }
1761
1886
  function parseMakeDependencies(output, cwd = process.cwd()) {
1762
1887
  const flattened = output.replace(/\\\r?\n/g, " ");
1763
1888
  const separator = flattened.indexOf(": ");
@@ -1839,13 +1964,14 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1839
1964
  invocation: normalizedProbeInvocation(invocation, probeDir),
1840
1965
  dependencies,
1841
1966
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1967
+ invocationPaths: await existingDriverTracePaths(`${invocation.stdout}\n${invocation.stderr}`, probeDir, probeDir),
1842
1968
  };
1843
1969
  }
1844
1970
  finally {
1845
1971
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1846
1972
  }
1847
1973
  }
1848
- return createHash("sha256")
1974
+ const fingerprint = createHash("sha256")
1849
1975
  .update("effective-compiler-invocation-v2\0")
1850
1976
  .update(environmentFingerprint)
1851
1977
  .update("\0")
@@ -1863,6 +1989,7 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1863
1989
  .update("\0")
1864
1990
  .update(probe.dependencyFingerprint)
1865
1991
  .digest("hex");
1992
+ return rememberFingerprintDependencies(fingerprint, [...probe.dependencies, ...probe.invocationPaths], probe.dependencies, probe.dependencyFingerprint);
1866
1993
  }
1867
1994
  const stableEffectiveCompilerInvocationMemos = new Map();
1868
1995
  function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint, compileArgs, sourceExtension = ".c") {
@@ -1876,17 +2003,30 @@ function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint,
1876
2003
  }
1877
2004
  async function nativeSourceFiles(directory, recursive, include = (name) => name.endsWith(".c") || name.endsWith(".h")) {
1878
2005
  const files = [];
1879
- const walk = async (current) => {
2006
+ const walk = async (current, ancestors) => {
2007
+ const canonical = await realpath(current).catch(() => resolve(current));
2008
+ if (ancestors.has(canonical))
2009
+ return;
2010
+ const nestedAncestors = new Set(ancestors).add(canonical);
1880
2011
  const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
1881
2012
  for (const entry of entries) {
1882
2013
  const path = join(current, entry.name);
1883
- if (entry.isDirectory() && recursive)
1884
- await walk(path);
1885
- else if (entry.isFile() && include(entry.name))
2014
+ if (entry.isDirectory() && recursive) {
2015
+ await walk(path, nestedAncestors);
2016
+ }
2017
+ else if (entry.isSymbolicLink()) {
2018
+ const target = await stat(path).catch(() => null);
2019
+ if (target?.isDirectory() && recursive)
2020
+ await walk(path, nestedAncestors);
2021
+ else if (target?.isFile() && include(entry.name))
2022
+ files.push(path);
2023
+ }
2024
+ else if (entry.isFile() && include(entry.name)) {
1886
2025
  files.push(path);
2026
+ }
1887
2027
  }
1888
2028
  };
1889
- await walk(directory);
2029
+ await walk(directory, new Set());
1890
2030
  return files.sort();
1891
2031
  }
1892
2032
  /** Every system-header spelling consumed by a source tree scriptc owns. The
@@ -1938,6 +2078,18 @@ async function fingerprintDependencyFiles(paths) {
1938
2078
  }
1939
2079
  return hash.digest("hex");
1940
2080
  }
2081
+ async function fingerprintDependenciesStillMatch(fingerprints) {
2082
+ const distinct = [...new Set(fingerprints)];
2083
+ const identities = distinct
2084
+ .map((fingerprint) => fingerprintDependencies.get(fingerprint))
2085
+ // A restored native-metadata stamp carries and revalidates the dependency
2086
+ // snapshot from the process that minted this fingerprint. Only identities
2087
+ // computed in this process have an additional content hash to recheck here.
2088
+ .filter((identity) => identity !== undefined);
2089
+ return (await Promise.all(identities.map(async (identity) => identity.contentFingerprint === null ||
2090
+ await fingerprintDependencyFiles(identity.contentPaths).catch(() => null) ===
2091
+ identity.contentFingerprint))).every(Boolean);
2092
+ }
1941
2093
  /** Exact headers selected while preprocessing the caller's translation unit.
1942
2094
  * The shared toolchain probe covers the runtime/vendor trees, but compileC is
1943
2095
  * also a public API: an opted-in caller can include a system or header-only SDK
@@ -1946,7 +2098,7 @@ async function fingerprintDependencyFiles(paths) {
1946
2098
  * quote-include directory and compiler-visible source spelling. */
1947
2099
  async function translationUnitDependencyFingerprintFresh(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
1948
2100
  if (sourcePath.endsWith(".ll")) {
1949
- return createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex");
2101
+ return rememberFingerprintDependencies(createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex"), []);
1950
2102
  }
1951
2103
  const compiler = driver.argv[0] ?? "clang";
1952
2104
  const compilerIdentity = await resolvedToolIdentity(compiler);
@@ -1983,7 +2135,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1983
2135
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1984
2136
  }
1985
2137
  }
1986
- return createHash("sha256")
2138
+ const fingerprint = createHash("sha256")
1987
2139
  .update("translation-unit-dependencies-v1\0")
1988
2140
  .update(environmentFingerprint)
1989
2141
  .update("\0")
@@ -1993,6 +2145,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1993
2145
  .update("\0")
1994
2146
  .update(probe.dependencyFingerprint)
1995
2147
  .digest("hex");
2148
+ return rememberFingerprintDependencies(fingerprint, probe.dependencies, probe.dependencies, probe.dependencyFingerprint);
1996
2149
  }
1997
2150
  const stableTranslationUnitDependencyMemos = new Map();
1998
2151
  function translationUnitDependencyFingerprint(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
@@ -2086,10 +2239,15 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2086
2239
  compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir),
2087
2240
  dependencies: dependencyPaths,
2088
2241
  dependencyFingerprint: await fingerprintDependencyFiles(dependencyPaths),
2089
- tools: await Promise.all(toolSpellings.map(async (spelling) => ({
2090
- spelling,
2091
- identity: await resolvedToolIdentity(spelling),
2092
- }))),
2242
+ invocationPaths: await existingDriverTracePaths(`${compilerInvocation.stdout}\n${compilerInvocation.stderr}`, probeDir, probeDir),
2243
+ tools: await Promise.all(toolSpellings.map(async (spelling) => {
2244
+ const resolved = await resolvedTool(spelling);
2245
+ return {
2246
+ spelling,
2247
+ identity: resolved?.cacheIdentity ?? null,
2248
+ path: resolved?.canonicalPath ?? null,
2249
+ };
2250
+ })),
2093
2251
  };
2094
2252
  }
2095
2253
  finally {
@@ -2120,7 +2278,11 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2120
2278
  .update(currentIdentity ?? tool.identity ?? "<unresolved>")
2121
2279
  .update("\0");
2122
2280
  }
2123
- return hash.digest("hex");
2281
+ return rememberFingerprintDependencies(hash.digest("hex"), [
2282
+ ...probe.dependencies,
2283
+ ...probe.invocationPaths,
2284
+ ...probe.tools.flatMap((tool) => tool.path === null ? [] : [tool.path]),
2285
+ ], probe.dependencies, probe.dependencyFingerprint);
2124
2286
  }
2125
2287
  const stableImplicitToolchainMemos = new Map();
2126
2288
  function implicitToolchainFingerprint(driver, environmentFingerprint) {
@@ -2156,6 +2318,51 @@ function driverTraceCandidates(line) {
2156
2318
  }
2157
2319
  return candidates;
2158
2320
  }
2321
+ async function existingDriverTracePaths(output, cwd, excludedRoot) {
2322
+ const candidates = new Set();
2323
+ for (const line of output.split(/\r?\n/)) {
2324
+ const tokens = driverTraceCandidates(line);
2325
+ for (let index = 0; index < tokens.length; index++) {
2326
+ const token = tokens[index];
2327
+ const joinedPathOption = ["-I", "-L", "-F"].find((option) => token.startsWith(option) && token.length > option.length);
2328
+ const equalsPathOption = ["--sysroot=", "-resource-dir="].find((option) => token.startsWith(option));
2329
+ const separatePathOptions = [
2330
+ "-I",
2331
+ "-L",
2332
+ "-F",
2333
+ "--sysroot",
2334
+ "-isysroot",
2335
+ "-resource-dir",
2336
+ "-isystem",
2337
+ "-iquote",
2338
+ "-internal-isystem",
2339
+ "-internal-externc-isystem",
2340
+ "-internal-iframework",
2341
+ ];
2342
+ const optionPath = joinedPathOption !== undefined
2343
+ ? token.slice(joinedPathOption.length)
2344
+ : equalsPathOption !== undefined
2345
+ ? token.slice(equalsPathOption.length)
2346
+ : separatePathOptions.includes(token)
2347
+ ? tokens[index + 1] ?? ""
2348
+ : token;
2349
+ if (!isAbsolute(optionPath))
2350
+ continue;
2351
+ const path = resolve(cwd, optionPath);
2352
+ if (path === excludedRoot ||
2353
+ path.startsWith(`${excludedRoot}/`) ||
2354
+ path.startsWith(`${excludedRoot}\\`)) {
2355
+ continue;
2356
+ }
2357
+ candidates.add(path);
2358
+ }
2359
+ }
2360
+ const existing = await Promise.all([...candidates].map(async (path) => [path, await lstat(path).catch(() => null)]));
2361
+ return existing
2362
+ .filter((entry) => entry[1] !== null)
2363
+ .map(([path]) => path)
2364
+ .sort();
2365
+ }
2159
2366
  export async function parseLinkTraceFiles(output, cwd, excludedRoot, driverDryRun = false) {
2160
2367
  const files = new Set();
2161
2368
  for (const line of output.split(/\r?\n/)) {
@@ -2241,9 +2448,15 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2241
2448
  linkerInvocation: normalizedProbeInvocation(driverInvocation, probeDir),
2242
2449
  dependencies,
2243
2450
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
2451
+ invocationPaths: await existingDriverTracePaths(`${driverInvocation.stdout}\n${driverInvocation.stderr}`, probeDir, probeDir),
2244
2452
  linker: {
2245
2453
  spelling: linkerSpelling,
2246
- identity: linkerSpelling === "" ? null : await resolvedToolIdentity(linkerSpelling),
2454
+ identity: linkerSpelling === ""
2455
+ ? null
2456
+ : (await resolvedTool(linkerSpelling))?.cacheIdentity ?? null,
2457
+ path: linkerSpelling === ""
2458
+ ? null
2459
+ : (await resolvedTool(linkerSpelling))?.canonicalPath ?? null,
2247
2460
  },
2248
2461
  };
2249
2462
  }
@@ -2254,7 +2467,7 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2254
2467
  const linkerIdentity = probe.linker.spelling === ""
2255
2468
  ? null
2256
2469
  : await resolvedToolIdentity(probe.linker.spelling);
2257
- return createHash("sha256")
2470
+ const fingerprint = createHash("sha256")
2258
2471
  .update("implicit-linker-v3\0")
2259
2472
  .update(environmentFingerprint)
2260
2473
  .update("\0")
@@ -2280,6 +2493,11 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2280
2493
  .update("\0")
2281
2494
  .update(linkerIdentity ?? probe.linker.identity ?? "<unresolved>")
2282
2495
  .digest("hex");
2496
+ return rememberFingerprintDependencies(fingerprint, [
2497
+ ...probe.dependencies,
2498
+ ...probe.invocationPaths,
2499
+ ...(probe.linker.path === null ? [] : [probe.linker.path]),
2500
+ ], probe.dependencies, probe.dependencyFingerprint);
2283
2501
  }
2284
2502
  const stableImplicitLinkerMemos = new Map();
2285
2503
  function implicitLinkerFingerprint(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs) {
@@ -2301,21 +2519,18 @@ function ccacheAvailable() {
2301
2519
  ccacheMemo ??= execFileAsync("ccache", ["--version"]).then(() => true, () => false);
2302
2520
  return ccacheMemo;
2303
2521
  }
2304
- /** Content hash of every .c/.h in the runtime src dir, the Ryū sources
2522
+ /** Content hash of every .c/.h in the runtime src tree, the Ryū sources
2305
2523
  * textually included by scr_number.c, and the separately-built vendor pins —
2306
2524
  * everything a binary links that the emitted C bytes don't already cover
2307
2525
  * (npm-embedded C rides inside the emitted C; the engine and standalone
2308
2526
  * vendor archives are pinned by their version constants). The small source
2309
- * tree is hashed on every identity calculation: a stat-only memo can miss a
2310
- * same-size edit whose timestamp was preserved by a copy/sync tool. */
2527
+ * tree is hashed on every identity calculation: recursive enumeration is
2528
+ * required because a newly added nested header can begin shadowing a system
2529
+ * include without changing any previously selected dependency, while content
2530
+ * hashing catches same-size edits whose timestamp was preserved by a copy/sync
2531
+ * tool. */
2311
2532
  async function runtimeFingerprintFresh(rtDir) {
2312
- const groups = await Promise.all([
2313
- { label: "runtime", dir: rtDir },
2314
- { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2315
- ].map(async (group) => {
2316
- const names = (await readdir(group.dir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
2317
- return { ...group, names };
2318
- }));
2533
+ const groups = await runtimeFingerprintInputGroups(rtDir);
2319
2534
  const h = createHash("sha256").update(QJS_COMMIT).update(MBEDTLS_VERSION).update(ZLIB_VERSION);
2320
2535
  for (const group of groups) {
2321
2536
  for (const n of group.names) {
@@ -2324,6 +2539,20 @@ async function runtimeFingerprintFresh(rtDir) {
2324
2539
  }
2325
2540
  return h.digest("hex");
2326
2541
  }
2542
+ async function runtimeFingerprintInputGroups(rtDir) {
2543
+ return Promise.all([
2544
+ { label: "runtime", dir: rtDir },
2545
+ { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2546
+ ].map(async (group) => {
2547
+ const names = (await nativeSourceFiles(group.dir, true))
2548
+ .map((path) => relative(group.dir, path))
2549
+ .sort();
2550
+ return { ...group, names };
2551
+ }));
2552
+ }
2553
+ async function runtimeFingerprintInputPaths(rtDir) {
2554
+ return (await runtimeFingerprintInputGroups(rtDir)).flatMap((group) => group.names.map((name) => join(group.dir, name)));
2555
+ }
2327
2556
  const stableRuntimeFingerprintMemos = new Map();
2328
2557
  export function runtimeFingerprint(rtDir) {
2329
2558
  const key = resolve(rtDir);
@@ -2412,6 +2641,279 @@ async function publishCachedFile(source, destination) {
2412
2641
  ]);
2413
2642
  }
2414
2643
  }
2644
+ async function directoryTreeDigest(root, excludedPaths = []) {
2645
+ const hash = createHash("sha256").update("native-dependency-tree-v1\0");
2646
+ const visited = new Set();
2647
+ const excluded = excludedPaths.map((path) => resolve(path));
2648
+ const walk = async (directory, relative) => {
2649
+ const canonical = await realpath(directory);
2650
+ if (visited.has(canonical)) {
2651
+ hash.update(relative).update("\0cycle\0").update(canonical).update("\0");
2652
+ return;
2653
+ }
2654
+ visited.add(canonical);
2655
+ const entries = await readdir(directory, { withFileTypes: true });
2656
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2657
+ for (const entry of entries) {
2658
+ const path = join(directory, entry.name);
2659
+ const absolute = resolve(path);
2660
+ if (excluded.some((candidate) => absolute === candidate ||
2661
+ absolute.startsWith(`${candidate}/`) ||
2662
+ absolute.startsWith(`${candidate}\\`)))
2663
+ continue;
2664
+ const child = relative === "" ? entry.name : `${relative}/${entry.name}`;
2665
+ const info = await lstat(path);
2666
+ const kind = info.isDirectory()
2667
+ ? "directory"
2668
+ : info.isFile()
2669
+ ? "file"
2670
+ : info.isSymbolicLink()
2671
+ ? "symlink"
2672
+ : "other";
2673
+ hash.update(child).update("\0").update(kind).update("\0");
2674
+ if (kind === "symlink") {
2675
+ const target = await realpath(path).catch(() => "<missing>");
2676
+ hash.update(target).update("\0");
2677
+ const targetInfo = await stat(path).catch(() => null);
2678
+ if (targetInfo?.isDirectory())
2679
+ await walk(path, child);
2680
+ }
2681
+ if (kind === "directory")
2682
+ await walk(path, child);
2683
+ }
2684
+ visited.delete(canonical);
2685
+ };
2686
+ await walk(root, "");
2687
+ return hash.digest("hex");
2688
+ }
2689
+ function localDependencyKind(info) {
2690
+ return info.isFile()
2691
+ ? "file"
2692
+ : info.isDirectory()
2693
+ ? "directory"
2694
+ : info.isSymbolicLink()
2695
+ ? "symlink"
2696
+ : null;
2697
+ }
2698
+ async function snapshotLocalArtifactDependency(path, treeExclusions = null) {
2699
+ const info = await lstat(path);
2700
+ const kind = localDependencyKind(info);
2701
+ if (kind === null)
2702
+ throw new Error(`unsupported local artifact dependency: ${path}`);
2703
+ const dependency = {
2704
+ path,
2705
+ kind,
2706
+ dev: info.dev,
2707
+ ino: info.ino,
2708
+ size: info.size,
2709
+ mtimeMs: info.mtimeMs,
2710
+ ctimeMs: info.ctimeMs,
2711
+ };
2712
+ if (kind === "directory" && treeExclusions !== null) {
2713
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2714
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2715
+ }
2716
+ if (kind === "symlink") {
2717
+ const targetPath = await realpath(path);
2718
+ const target = await stat(path);
2719
+ const targetKind = target.isFile() ? "file" : target.isDirectory() ? "directory" : null;
2720
+ if (targetKind === null)
2721
+ throw new Error(`unsupported symlink target dependency: ${path}`);
2722
+ dependency.targetPath = targetPath;
2723
+ dependency.targetKind = targetKind;
2724
+ dependency.targetDev = target.dev;
2725
+ dependency.targetIno = target.ino;
2726
+ dependency.targetSize = target.size;
2727
+ dependency.targetMtimeMs = target.mtimeMs;
2728
+ dependency.targetCtimeMs = target.ctimeMs;
2729
+ if (targetKind === "directory" && treeExclusions !== null) {
2730
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2731
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2732
+ }
2733
+ }
2734
+ return dependency;
2735
+ }
2736
+ async function snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2737
+ const recursive = new Set(recursiveDirectories.map((path) => resolve(path)));
2738
+ return Promise.all([...new Set(dependencyPaths)].sort().map((path) => snapshotLocalArtifactDependency(path, recursive.has(resolve(path)) ? recursiveExclusions : null)));
2739
+ }
2740
+ async function localArtifactDependenciesStillMatch(dependencies) {
2741
+ return (await Promise.all(dependencies.map(async (dependency) => {
2742
+ const current = await snapshotLocalArtifactDependency(dependency.path, dependency.treeDigest === undefined ? null : dependency.treeExclusions ?? []).catch(() => null);
2743
+ return current !== null && JSON.stringify(current) === JSON.stringify(dependency);
2744
+ }))).every(Boolean);
2745
+ }
2746
+ function nativeMetadataStampPath(root, key) {
2747
+ return join(root, "meta", createHash("sha256").update(key).digest("hex"));
2748
+ }
2749
+ function nativeMetadataStampIntegrity(stamp) {
2750
+ return createHash("sha256")
2751
+ .update("native-metadata-stamp-v2\0")
2752
+ .update(JSON.stringify(stamp))
2753
+ .digest("hex");
2754
+ }
2755
+ async function readNativeMetadataStamp(root, key) {
2756
+ try {
2757
+ const stamp = JSON.parse(await readFile(nativeMetadataStampPath(root, key), "utf8"));
2758
+ if (stamp.version !== 2 ||
2759
+ stamp.key !== key ||
2760
+ stamp.values === null ||
2761
+ typeof stamp.values !== "object" ||
2762
+ !Array.isArray(stamp.dependencies) ||
2763
+ !/^[0-9a-f]{64}$/.test(stamp.integrity) ||
2764
+ nativeMetadataStampIntegrity({
2765
+ version: stamp.version,
2766
+ key: stamp.key,
2767
+ values: stamp.values,
2768
+ dependencies: stamp.dependencies,
2769
+ }) !== stamp.integrity ||
2770
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies))) {
2771
+ return null;
2772
+ }
2773
+ return stamp;
2774
+ }
2775
+ catch {
2776
+ return null;
2777
+ }
2778
+ }
2779
+ async function publishNativeMetadataStamp(root, key, values, dependencyPaths, fingerprints = []) {
2780
+ const destination = nativeMetadataStampPath(root, key);
2781
+ await mkdir(dirname(destination), { recursive: true });
2782
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths);
2783
+ if (!(await fingerprintDependenciesStillMatch(fingerprints))) {
2784
+ throw new CacheInputsChangedError();
2785
+ }
2786
+ const unsigned = { version: 2, key, values, dependencies };
2787
+ const stamp = {
2788
+ ...unsigned,
2789
+ integrity: nativeMetadataStampIntegrity(unsigned),
2790
+ };
2791
+ const tmp = `${destination}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2792
+ try {
2793
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2794
+ await rename(tmp, destination);
2795
+ }
2796
+ finally {
2797
+ await rm(tmp, { force: true }).catch(() => undefined);
2798
+ }
2799
+ return stamp;
2800
+ }
2801
+ function nativeMetadataKey(kind, parts) {
2802
+ const hash = createHash("sha256").update(`native-metadata-${kind}-v2\0`);
2803
+ for (const part of parts) {
2804
+ hash.update(typeof part === "string" ? part : part.join("\x1f")).update("\0");
2805
+ }
2806
+ return `${kind}-${hash.digest("hex")}`;
2807
+ }
2808
+ /** The caller-visible output is itself the cheapest safe cache tier. Once a
2809
+ * generated TU has produced this exact binary, an unchanged rebuild need not
2810
+ * rediscover every SDK header and linker input merely to copy equivalent bytes
2811
+ * back onto the same path. This tier is deliberately narrower than the CAS:
2812
+ * only frontend-generated programs with no caller-owned native inputs opt in.
2813
+ * The generated TU bytes, every scriptc runtime source, the selected direct
2814
+ * compiler inode, target/options/environment, and the output path all join the
2815
+ * key. A digest rejects a modified/truncated output before the no-op hit. */
2816
+ function localArtifactIdentity(opts, driver, environmentFingerprint, compilerIdentity, runtimeHash, programBytes) {
2817
+ const normalizedOptions = Object.fromEntries(Object.entries(opts)
2818
+ .filter(([, value]) => value !== undefined)
2819
+ .sort(([a], [b]) => a.localeCompare(b)));
2820
+ return createHash("sha256")
2821
+ .update("local-artifact-v1\0")
2822
+ .update(cacheTargetIdentity(driver)).update("\0")
2823
+ .update(environmentFingerprint).update("\0")
2824
+ .update(compilerIdentity).update("\0")
2825
+ .update(runtimeHash).update("\0")
2826
+ .update(driver.argv.join("\x1f")).update("\0")
2827
+ .update(driver.targetArgs.join("\x1f")).update("\0")
2828
+ .update(driver.linkArgs.join("\x1f")).update("\0")
2829
+ .update(process.env["SCRIPTC_FETCH_CURL"] === "1" ? "fetch-curl" : "fetch-native").update("\0")
2830
+ .update(JSON.stringify(normalizedOptions)).update("\0")
2831
+ .update(resolve(opts.cPath)).update("\0")
2832
+ .update(resolve(opts.outPath)).update("\0")
2833
+ .update(programBytes)
2834
+ .digest("hex");
2835
+ }
2836
+ function localArtifactStampPath(root, outPath) {
2837
+ const outputKey = createHash("sha256").update(resolve(outPath)).digest("hex");
2838
+ return join(root, "local", outputKey);
2839
+ }
2840
+ function localArtifactStampIntegrity(stamp) {
2841
+ return createHash("sha256")
2842
+ .update("local-artifact-stamp-v2\0")
2843
+ .update(JSON.stringify(stamp))
2844
+ .digest("hex");
2845
+ }
2846
+ async function localArtifactHit(stampPath, outPath, key) {
2847
+ try {
2848
+ const stamp = JSON.parse(await readFile(stampPath, "utf8"));
2849
+ const output = await lstat(outPath);
2850
+ const expectedMode = 0o777 & ~process.umask();
2851
+ if (stamp.version !== 2 ||
2852
+ stamp.key !== key ||
2853
+ !/^[0-9a-f]{64}$/.test(stamp.digest ?? "") ||
2854
+ !Array.isArray(stamp.dependencies) ||
2855
+ !/^[0-9a-f]{64}$/.test(stamp.integrity ?? "") ||
2856
+ localArtifactStampIntegrity({
2857
+ version: stamp.version,
2858
+ key: stamp.key,
2859
+ digest: stamp.digest,
2860
+ dependencies: stamp.dependencies,
2861
+ }) !== stamp.integrity ||
2862
+ !output.isFile() ||
2863
+ (output.mode & 0o777) !== expectedMode ||
2864
+ stamp.dependencies.some((dependency) => dependency === null ||
2865
+ typeof dependency !== "object" ||
2866
+ typeof dependency.path !== "string" ||
2867
+ dependency.kind !== "file" &&
2868
+ dependency.kind !== "directory" &&
2869
+ dependency.kind !== "symlink" ||
2870
+ typeof dependency.dev !== "number" ||
2871
+ typeof dependency.ino !== "number" ||
2872
+ typeof dependency.size !== "number" ||
2873
+ typeof dependency.mtimeMs !== "number" ||
2874
+ typeof dependency.ctimeMs !== "number" ||
2875
+ dependency.treeDigest !== undefined && typeof dependency.treeDigest !== "string" ||
2876
+ dependency.kind === "symlink" && (typeof dependency.targetPath !== "string" ||
2877
+ dependency.targetKind !== "file" && dependency.targetKind !== "directory" ||
2878
+ typeof dependency.targetDev !== "number" ||
2879
+ typeof dependency.targetIno !== "number" ||
2880
+ typeof dependency.targetSize !== "number" ||
2881
+ typeof dependency.targetMtimeMs !== "number" ||
2882
+ typeof dependency.targetCtimeMs !== "number")) ||
2883
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies)) ||
2884
+ await fileDigest(outPath) !== stamp.digest) {
2885
+ return false;
2886
+ }
2887
+ const now = new Date();
2888
+ await utimes(stampPath, now, now).catch(() => undefined);
2889
+ return true;
2890
+ }
2891
+ catch {
2892
+ return false;
2893
+ }
2894
+ }
2895
+ async function publishLocalArtifactStamp(stampPath, outPath, key, dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2896
+ await mkdir(dirname(stampPath), { recursive: true });
2897
+ const tmp = `${stampPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2898
+ try {
2899
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories, recursiveExclusions);
2900
+ const unsigned = {
2901
+ version: 2,
2902
+ key,
2903
+ digest: await fileDigest(outPath),
2904
+ dependencies,
2905
+ };
2906
+ const stamp = {
2907
+ ...unsigned,
2908
+ integrity: localArtifactStampIntegrity(unsigned),
2909
+ };
2910
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2911
+ await rename(tmp, stampPath);
2912
+ }
2913
+ finally {
2914
+ await rm(tmp, { force: true }).catch(() => undefined);
2915
+ }
2916
+ }
2415
2917
  /** The cached .o set for one flag flavor, compiled on first need. Concurrent
2416
2918
  * first builds (parallel test workers on a cold cache) may duplicate work;
2417
2919
  * per-file atomic renames make every winner equivalent. Publication is held
@@ -2445,7 +2947,22 @@ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix, ve
2445
2947
  await Promise.all(missing.slice(i, i + width).map(async (src) => {
2446
2948
  const tmpObj = join(tmpDir, `${basename(src, ".c")}.o`);
2447
2949
  const argv = [...(useCcache ? ["ccache"] : []), ...ccArgv, ...cflags, "-c", src, "-o", tmpObj];
2448
- await execFileAsync(argv[0] ?? "clang", argv.slice(1));
2950
+ await execFileAsync(argv[0] ?? "clang", argv.slice(1), useCcache
2951
+ ? {
2952
+ // ccache direct mode remembers only the headers selected by
2953
+ // its previous manifest and can miss a newly created,
2954
+ // higher-priority header. The scriptc object-set key already
2955
+ // includes the recursive runtime namespace fingerprint, so
2956
+ // carry it into ccache's own keyspace as well.
2957
+ env: {
2958
+ ...process.env,
2959
+ CCACHE_NAMESPACE: [
2960
+ process.env["CCACHE_NAMESPACE"],
2961
+ `scriptc-${setKey}`,
2962
+ ].filter((value) => value !== undefined && value !== "").join(":"),
2963
+ },
2964
+ }
2965
+ : undefined);
2449
2966
  compiled.set(src, { object: tmpObj, digest: await fileDigest(tmpObj) });
2450
2967
  }));
2451
2968
  }
@@ -2657,10 +3174,88 @@ export async function compileC(opts) {
2657
3174
  root = null;
2658
3175
  }
2659
3176
  }
3177
+ let localArtifact = null;
3178
+ // Generated executable TUs are closed over scriptc's own runtime tree. A
3179
+ // same-output rebuild can therefore check those bytes directly before the
3180
+ // broader cross-output CAS performs its compiler/SDK/linker rediscovery.
3181
+ // FFI/native-input builds and the public arbitrary-C cache API stay on the
3182
+ // strict path because their dependency graphs are caller-owned.
3183
+ if (root !== null &&
3184
+ cachePolicy.completeArtifacts &&
3185
+ opts.cacheIdentity === "scriptc-generated-v1" &&
3186
+ (opts.linkInputs?.length ?? 0) === 0 &&
3187
+ (opts.systemLibraries?.length ?? 0) === 0 &&
3188
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] !== "1") {
3189
+ try {
3190
+ const [compiler, runtimeHash, programBytes] = await Promise.all([
3191
+ resolvedTool(driver.argv[0] ?? "clang"),
3192
+ runtimeFingerprint(rtDir),
3193
+ readFile(opts.cPath),
3194
+ ]);
3195
+ if (compiler !== null) {
3196
+ const effectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? compiler;
3197
+ const key = localArtifactIdentity(opts, driver, toolchainEnv, `${compiler.cacheIdentity}\0${effectiveCompiler.cacheIdentity}`, runtimeHash, programBytes);
3198
+ const stampPath = localArtifactStampPath(root, opts.outPath);
3199
+ localArtifact = {
3200
+ stampPath,
3201
+ key,
3202
+ runtimeHash,
3203
+ programBytes,
3204
+ compilerPath: effectiveCompiler.canonicalPath,
3205
+ };
3206
+ if (await localArtifactHit(stampPath, opts.outPath, key))
3207
+ return;
3208
+ }
3209
+ }
3210
+ catch {
3211
+ // The output-local tier is only an optimization; the fully validated
3212
+ // CAS below remains the source of truth on any metadata trouble.
3213
+ localArtifact = null;
3214
+ }
3215
+ }
2660
3216
  let implicitToolchain = null;
3217
+ let toolchainMetadataStamp = null;
3218
+ const metadataCompiler = root === null
3219
+ ? null
3220
+ : await resolvedTool(driver.argv[0] ?? "clang");
3221
+ const metadataEffectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? metadataCompiler;
3222
+ const toolchainMetadataKey = root === null ||
3223
+ metadataCompiler === null ||
3224
+ metadataEffectiveCompiler === null ||
3225
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3226
+ ? null
3227
+ : nativeMetadataKey("toolchain", [
3228
+ cacheTargetIdentity(driver),
3229
+ toolchainEnv,
3230
+ driver.argv,
3231
+ driver.targetArgs,
3232
+ metadataCompiler.cacheIdentity,
3233
+ metadataEffectiveCompiler.cacheIdentity,
3234
+ rtDir,
3235
+ ]);
2661
3236
  if (persistentDriverCache) {
2662
3237
  try {
2663
- implicitToolchain = await implicitToolchainFingerprint(driver, toolchainEnv);
3238
+ toolchainMetadataStamp =
3239
+ root === null || toolchainMetadataKey === null
3240
+ ? null
3241
+ : await readNativeMetadataStamp(root, toolchainMetadataKey);
3242
+ implicitToolchain = toolchainMetadataStamp?.values["implicitToolchain"] ??
3243
+ await implicitToolchainFingerprint(driver, toolchainEnv);
3244
+ let compilerVersion = toolchainMetadataStamp?.values["compilerVersion"];
3245
+ if (compilerVersion === undefined) {
3246
+ compilerVersion = await ccVersionOnce(driver.argv, toolchainEnv, true);
3247
+ }
3248
+ if (toolchainMetadataStamp === null &&
3249
+ root !== null &&
3250
+ toolchainMetadataKey !== null) {
3251
+ if (metadataCompiler !== null && metadataEffectiveCompiler !== null) {
3252
+ toolchainMetadataStamp = await publishNativeMetadataStamp(root, toolchainMetadataKey, { implicitToolchain, compilerVersion }, [
3253
+ metadataCompiler.canonicalPath,
3254
+ metadataEffectiveCompiler.canonicalPath,
3255
+ ...fingerprintDependencyPaths(implicitToolchain),
3256
+ ], [implicitToolchain]);
3257
+ }
3258
+ }
2664
3259
  }
2665
3260
  catch {
2666
3261
  // Cache discovery is best-effort. In particular, a compiler wrapper can
@@ -2817,6 +3412,7 @@ export async function compileC(opts) {
2817
3412
  ...(opts.http2 ?? false ? [rt(join(rtDir, "scr_http2.c"))] : []),
2818
3413
  ...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
2819
3414
  ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
3415
+ ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
2820
3416
  ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
2821
3417
  // The CA-store unit rides its own gate OR the tls one: scr_tls.c
2822
3418
  // references its default-set override unconditionally.
@@ -2959,12 +3555,63 @@ export async function compileC(opts) {
2959
3555
  };
2960
3556
  let runtimeCompilerInvocation = null;
2961
3557
  let programCompilerInvocation = null;
3558
+ let payloadMetadata = null;
3559
+ let compileMetadataStamp = null;
3560
+ const compileMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3561
+ ? null
3562
+ : nativeMetadataKey("compile", [
3563
+ cacheTargetIdentity(driver),
3564
+ toolchainEnv,
3565
+ implicitToolchain ?? "<uncached>",
3566
+ driver.argv,
3567
+ cflags,
3568
+ programCompilerArgs,
3569
+ programSourceExtension,
3570
+ ]);
2962
3571
  if (root !== null) {
2963
3572
  try {
2964
- runtimeCompilerInvocation = await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags);
2965
- programCompilerInvocation = programSourceExtension === ".ll"
2966
- ? await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
2967
- : runtimeCompilerInvocation;
3573
+ // These probes inspect disjoint inputs. Start the payload reads here as
3574
+ // well so runtime hashing and clang's dry-run traces overlap instead of
3575
+ // forming a serial prelude before every cache lookup.
3576
+ payloadMetadata = Promise.all([
3577
+ Promise.resolve(toolchainMetadataStamp?.values["compilerVersion"] ??
3578
+ ccVersionOnce(driver.argv, toolchainEnv, true)),
3579
+ localArtifact === null
3580
+ ? runtimeFingerprint(rtDir)
3581
+ : Promise.resolve(localArtifact.runtimeHash),
3582
+ localArtifact === null
3583
+ ? readFile(opts.cPath)
3584
+ : Promise.resolve(localArtifact.programBytes),
3585
+ ]);
3586
+ compileMetadataStamp = compileMetadataKey === null
3587
+ ? null
3588
+ : await readNativeMetadataStamp(root, compileMetadataKey);
3589
+ if (compileMetadataStamp !== null) {
3590
+ runtimeCompilerInvocation = compileMetadataStamp.values["runtimeInvocation"] ?? null;
3591
+ programCompilerInvocation = compileMetadataStamp.values["programInvocation"] ?? null;
3592
+ if (runtimeCompilerInvocation === null || programCompilerInvocation === null) {
3593
+ compileMetadataStamp = null;
3594
+ }
3595
+ }
3596
+ if (compileMetadataStamp === null) {
3597
+ const [runtimeInvocation, programInvocation] = await Promise.all([
3598
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3599
+ programSourceExtension === ".ll"
3600
+ ? effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
3601
+ : Promise.resolve(null),
3602
+ ]);
3603
+ runtimeCompilerInvocation = runtimeInvocation;
3604
+ programCompilerInvocation = programInvocation ?? runtimeInvocation;
3605
+ if (compileMetadataKey !== null) {
3606
+ compileMetadataStamp = await publishNativeMetadataStamp(root, compileMetadataKey, {
3607
+ runtimeInvocation: runtimeCompilerInvocation,
3608
+ programInvocation: programCompilerInvocation,
3609
+ }, [
3610
+ ...fingerprintDependencyPaths(runtimeCompilerInvocation),
3611
+ ...fingerprintDependencyPaths(programCompilerInvocation),
3612
+ ], [runtimeCompilerInvocation, programCompilerInvocation]);
3613
+ }
3614
+ }
2968
3615
  }
2969
3616
  catch {
2970
3617
  // Preserve the uncached build for wrappers that compile successfully but
@@ -2989,11 +3636,11 @@ export async function compileC(opts) {
2989
3636
  let fingerprint;
2990
3637
  let cBytes;
2991
3638
  try {
2992
- [cv, fingerprint, cBytes] = await Promise.all([
3639
+ [cv, fingerprint, cBytes] = await (payloadMetadata ?? Promise.all([
2993
3640
  ccVersionOnce(driver.argv, toolchainEnv, true),
2994
3641
  runtimeFingerprint(rtDir),
2995
3642
  readFile(opts.cPath),
2996
- ]);
3643
+ ]));
2997
3644
  }
2998
3645
  catch {
2999
3646
  // A version/fingerprint probe is an optimization boundary. If the compiler
@@ -3012,16 +3659,6 @@ export async function compileC(opts) {
3012
3659
  (opts.linkInputs?.length ?? 0) === 0 &&
3013
3660
  (opts.systemLibraries?.length ?? 0) === 0;
3014
3661
  let programDependencies = null;
3015
- if (cacheCompleteArtifact) {
3016
- try {
3017
- programDependencies = await translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv);
3018
- }
3019
- catch {
3020
- // Program-header discovery is needed only for a complete hit. Runtime
3021
- // objects remain safe because they never contain the caller's TU.
3022
- cacheCompleteArtifact = false;
3023
- }
3024
- }
3025
3662
  const linkProbeArgs = [
3026
3663
  ...(sanitize ? ["-fsanitize=address"] : []),
3027
3664
  ...threadArgs,
@@ -3062,15 +3699,89 @@ export async function compileC(opts) {
3062
3699
  ? effectiveLinkInvocationArgs
3063
3700
  : effectiveLinkInvocationArgs.filter((arg) => arg !== `-L${curlStubDir}`);
3064
3701
  let implicitLinker = null;
3702
+ let preBuildDependencies = null;
3703
+ let localArtifactDependencyPaths = null;
3704
+ let linkMetadataStamp = null;
3705
+ const linkMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3706
+ ? null
3707
+ : nativeMetadataKey("link", [
3708
+ cacheTargetIdentity(driver),
3709
+ toolchainEnv,
3710
+ implicitToolchain ?? "<uncached>",
3711
+ runtimeCompilerInvocation ?? "<uncached>",
3712
+ programCompilerInvocation ?? "<uncached>",
3713
+ driver.argv,
3714
+ linkProbeArgs,
3715
+ effectiveLinkInvocationArgs,
3716
+ linkTraceInvocationArgs,
3717
+ ]);
3065
3718
  if (cacheCompleteArtifact) {
3066
3719
  try {
3067
- implicitLinker = await implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs);
3720
+ // Header discovery and linker tracing are independent subprocess trees.
3721
+ // Running them together removes one complete probe round-trip from both
3722
+ // cache hits and ordinary edit/build misses without changing either key.
3723
+ linkMetadataStamp = linkMetadataKey === null
3724
+ ? null
3725
+ : await readNativeMetadataStamp(root, linkMetadataKey);
3726
+ if (linkMetadataStamp !== null) {
3727
+ implicitLinker = linkMetadataStamp.values["implicitLinker"] ?? null;
3728
+ if (implicitLinker === null)
3729
+ linkMetadataStamp = null;
3730
+ }
3731
+ [programDependencies, implicitLinker] = await Promise.all([
3732
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv),
3733
+ linkMetadataStamp === null
3734
+ ? implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs)
3735
+ : Promise.resolve(implicitLinker),
3736
+ ]);
3737
+ if (linkMetadataStamp === null && linkMetadataKey !== null) {
3738
+ linkMetadataStamp = await publishNativeMetadataStamp(root, linkMetadataKey, { implicitLinker }, fingerprintDependencyPaths(implicitLinker), [implicitLinker]);
3739
+ }
3740
+ preBuildDependencies = await snapshotLocalArtifactDependencies([
3741
+ ...(await runtimeFingerprintInputPaths(rtDir)),
3742
+ ...(toolchainMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3743
+ fingerprintDependencyPaths(implicitToolchain)),
3744
+ ...(compileMetadataStamp?.dependencies.map((dependency) => dependency.path) ?? []),
3745
+ ...(linkMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3746
+ fingerprintDependencyPaths(implicitLinker)),
3747
+ ...fingerprintDependencyPaths(programDependencies),
3748
+ ]);
3749
+ // Every content-bearing fingerprint was computed before this metadata
3750
+ // snapshot. Re-read those exact files now so the snapshot cannot certify
3751
+ // bytes that changed after hashing but before the final compile starts.
3752
+ if (!(await fingerprintDependenciesStillMatch([
3753
+ implicitToolchain,
3754
+ runtimeCompilerInvocation,
3755
+ programCompilerInvocation,
3756
+ implicitLinker,
3757
+ programDependencies,
3758
+ ])) ||
3759
+ await runtimeFingerprint(rtDir).catch(() => null) !== fingerprint ||
3760
+ !(await Promise.all([toolchainMetadataStamp, compileMetadataStamp, linkMetadataStamp]
3761
+ .filter((stamp) => stamp !== null)
3762
+ .map((stamp) => localArtifactDependenciesStillMatch(stamp.dependencies)))).every(Boolean)) {
3763
+ throw new CacheInputsChangedError();
3764
+ }
3765
+ if (localArtifact !== null) {
3766
+ // Native metadata stamps persist their dependency paths across CLI
3767
+ // processes; fingerprintDependencyPaths deliberately does not. Build
3768
+ // the output-local stamp from this complete validated snapshot so a
3769
+ // source edit in a fresh process cannot replace it with only the two
3770
+ // process-local fallback paths.
3771
+ localArtifactDependencyPaths = [
3772
+ localArtifact.compilerPath,
3773
+ dirname(resolve(opts.cPath)),
3774
+ ...preBuildDependencies.map((dependency) => dependency.path),
3775
+ ];
3776
+ }
3068
3777
  }
3069
3778
  catch {
3070
- // Some compiler wrappers/linkers do not implement trace mode. Runtime
3071
- // objects remain safely cacheable, but a complete executable cannot be
3072
- // keyed without knowing every implicit link input.
3779
+ // Program-header discovery and linker tracing are both required for a
3780
+ // complete hit. Runtime objects remain safely cacheable if either probe
3781
+ // is unavailable.
3073
3782
  cacheCompleteArtifact = false;
3783
+ preBuildDependencies = null;
3784
+ localArtifactDependencyPaths = null;
3074
3785
  }
3075
3786
  }
3076
3787
  const binDir = join(root, "bin");
@@ -3123,6 +3834,9 @@ export async function compileC(opts) {
3123
3834
  // cache entry populated by a less restrictive shell must not widen access.
3124
3835
  await chmod(tmpOut, 0o777 & ~process.umask());
3125
3836
  await rename(tmpOut, opts.outPath);
3837
+ if (localArtifact !== null && localArtifactDependencyPaths !== null) {
3838
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3839
+ }
3126
3840
  return; // hit: the program/runtime payload compile and link were skipped
3127
3841
  }
3128
3842
  catch {
@@ -3158,18 +3872,25 @@ export async function compileC(opts) {
3158
3872
  await writeFile(programPath, cBytes);
3159
3873
  let objects = null;
3160
3874
  let cacheInputsStable = true;
3161
- let objectImplicitVerification = null;
3875
+ let strictObjectVerification = null;
3162
3876
  const objectImplicitToolchainStillMatches = () => {
3163
- objectImplicitVerification ??= Promise.all([
3877
+ if (preBuildDependencies !== null) {
3878
+ return localArtifactDependenciesStillMatch(preBuildDependencies);
3879
+ }
3880
+ // Complete-artifact caching can be disabled by caller-owned native
3881
+ // inputs while the safe runtime-object tier remains active. Preserve its
3882
+ // strict discovery fallback in that posture.
3883
+ strictObjectVerification ??= Promise.all([
3884
+ runtimeFingerprint(rtDir),
3164
3885
  implicitToolchainFingerprint(driver, toolchainEnv),
3165
3886
  effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3166
- ]).then(([currentImplicit, currentInvocation]) => currentImplicit === implicitToolchain &&
3887
+ ]).then(([currentRuntime, currentImplicit, currentInvocation]) => currentRuntime === fingerprint &&
3888
+ currentImplicit === implicitToolchain &&
3167
3889
  currentInvocation === runtimeCompilerInvocation, () => false);
3168
- return objectImplicitVerification;
3890
+ return strictObjectVerification;
3169
3891
  };
3170
3892
  try {
3171
- 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 &&
3172
- (await objectImplicitToolchainStillMatches()));
3893
+ 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 objectImplicitToolchainStillMatches());
3173
3894
  objects = await stageRuntimeObjects(cached, join(buildDir, "runtime-objects"));
3174
3895
  }
3175
3896
  catch (err) {
@@ -3180,10 +3901,11 @@ export async function compileC(opts) {
3180
3901
  await runClang(buildArgs((p) => objects?.get(p) ?? p, { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }));
3181
3902
  await installArtifact(privateOut, opts.outPath);
3182
3903
  if (cachedBin !== null && keyHex !== null) {
3183
- // This check is deliberately fresh rather than the object-publication
3184
- // verification above. The final program compile/link can itself race an
3185
- // SDK header, compiler, linker, CRT, or system-library replacement; its
3186
- // bytes must not be published under the pre-build identity in that case.
3904
+ // Metadata comparison catches ordinary changes cheaply, but cannot by
3905
+ // itself prove the snapshot was taken from the same bytes hashed into
3906
+ // the key. Recompute every content-bearing identity after the final link
3907
+ // so a header/SDK/compiler change in either pre-build gap cannot publish
3908
+ // new output under an old key.
3187
3909
  const [currentRuntime, currentImplicit, currentRuntimeInvocation, currentProgramInvocation, currentProgramDependencies, currentLinker, currentCompiler] = await Promise.all([
3188
3910
  runtimeFingerprint(rtDir).catch(() => null),
3189
3911
  implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
@@ -3195,6 +3917,8 @@ export async function compileC(opts) {
3195
3917
  ]);
3196
3918
  cacheInputsStable =
3197
3919
  cacheInputsStable &&
3920
+ preBuildDependencies !== null &&
3921
+ await localArtifactDependenciesStillMatch(preBuildDependencies) &&
3198
3922
  currentRuntime === fingerprint &&
3199
3923
  currentImplicit === implicitToolchain &&
3200
3924
  currentRuntimeInvocation === runtimeCompilerInvocation &&
@@ -3214,6 +3938,12 @@ export async function compileC(opts) {
3214
3938
  /* publishing is best-effort */
3215
3939
  }
3216
3940
  }
3941
+ if (localArtifact !== null &&
3942
+ localArtifactDependencyPaths !== null &&
3943
+ cacheCompleteArtifact &&
3944
+ cacheInputsStable) {
3945
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3946
+ }
3217
3947
  }
3218
3948
  finally {
3219
3949
  await rm(buildDir, { recursive: true, force: true }).catch(() => undefined);