@scriptc/compiler 0.0.31 → 0.0.33

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 (55) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +1 -1
  2. package/dist/backend/cc-cache.test.js +308 -1
  3. package/dist/backend/cc-cache.test.js.map +1 -1
  4. package/dist/backend/cc.d.ts +17 -5
  5. package/dist/backend/cc.js +745 -75
  6. package/dist/backend/cc.js.map +1 -1
  7. package/dist/backend/emission/emit-exprs.js +53 -2
  8. package/dist/backend/emission/emit-exprs.js.map +1 -1
  9. package/dist/backend/emission/emitter.d.ts +12 -5
  10. package/dist/backend/emission/emitter.js +160 -22
  11. package/dist/backend/emission/emitter.js.map +1 -1
  12. package/dist/backend/emission/may-throw.js +7 -4
  13. package/dist/backend/emission/may-throw.js.map +1 -1
  14. package/dist/backend/ffi-callbacks.d.ts +38 -1
  15. package/dist/backend/ffi-callbacks.js +76 -3
  16. package/dist/backend/ffi-callbacks.js.map +1 -1
  17. package/dist/backend/llvm/emitter.js +243 -12
  18. package/dist/backend/llvm/emitter.js.map +1 -1
  19. package/dist/diagnostics/diagnostic.js +2 -0
  20. package/dist/diagnostics/diagnostic.js.map +1 -1
  21. package/dist/ffi/profile.d.ts +17 -5
  22. package/dist/ffi/profile.js +169 -22
  23. package/dist/ffi/profile.js.map +1 -1
  24. package/dist/frontend/lowering/lower-calls.d.ts +1 -0
  25. package/dist/frontend/lowering/lower-calls.js +89 -23
  26. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  27. package/dist/frontend/lowering/lower-containers.js +65 -19
  28. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  29. package/dist/frontend/lowering/lowerer.d.ts +12 -0
  30. package/dist/frontend/lowering/lowerer.js +20 -1
  31. package/dist/frontend/lowering/lowerer.js.map +1 -1
  32. package/dist/frontend/ts7/checker.d.ts +2 -0
  33. package/dist/frontend/ts7/checker.js +20 -3
  34. package/dist/frontend/ts7/checker.js.map +1 -1
  35. package/dist/frontend/types.d.ts +8 -0
  36. package/dist/frontend/types.js +33 -5
  37. package/dist/frontend/types.js.map +1 -1
  38. package/dist/index.js +35 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir/nodes.d.ts +22 -9
  41. package/dist/ir/nodes.js +7 -1
  42. package/dist/ir/nodes.js.map +1 -1
  43. package/dist/ir/serialize.d.ts +1 -1
  44. package/dist/ir/serialize.js +1 -1
  45. package/dist/ir/validate.js +74 -1
  46. package/dist/ir/validate.js.map +1 -1
  47. package/dist/library/int-infer.js +7 -1
  48. package/dist/library/int-infer.js.map +1 -1
  49. package/dist/library/int-infer.test.js +73 -4
  50. package/dist/library/int-infer.test.js.map +1 -1
  51. package/dist/library/profile.d.ts +3 -0
  52. package/dist/library/profile.js +9 -1
  53. package/dist/library/profile.js.map +1 -1
  54. package/package.json +2 -2
  55. 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. */
@@ -169,6 +169,30 @@ export class CcCompileError extends Error {
169
169
  this.name = "CcCompileError";
170
170
  }
171
171
  }
172
+ /** Preserve the useful output from a failed compiler/tool invocation. Node's
173
+ * execFile error exposes stderr and stdout independently, but either stream
174
+ * may be present as an empty string. Prefer compiler diagnostics, retain a
175
+ * non-standard stdout diagnostic when that is all the tool emitted, and only
176
+ * then fall back to the process error itself. */
177
+ export function subprocessFailureDetail(err) {
178
+ const failure = err;
179
+ const output = (value) => {
180
+ const text = Buffer.isBuffer(value) ? value.toString("utf8") : value ?? "";
181
+ return text.trim().length > 0 ? text.trimEnd() : "";
182
+ };
183
+ const stderr = output(failure.stderr);
184
+ const stdout = output(failure.stdout);
185
+ if (stderr !== "" && stdout !== "")
186
+ return `${stderr}\n\ncompiler stdout:\n${stdout}`;
187
+ if (stderr !== "")
188
+ return stderr;
189
+ if (stdout !== "")
190
+ return `compiler stdout:\n${stdout}`;
191
+ if (typeof failure.message === "string" && failure.message.trim() !== "") {
192
+ return failure.message;
193
+ }
194
+ return String(err);
195
+ }
172
196
  export function runtimeSrcDir() {
173
197
  const testRoot = process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
174
198
  if (testRoot !== undefined)
@@ -611,7 +635,7 @@ async function ensureEngineArchive(sanitize, driver, buildIdentity, cacheRoot =
611
635
  "Install cmake (e.g. `brew install cmake`) and retry. Static builds do not need it.");
612
636
  }
613
637
  throw new Error(`cmake failed configuring the embedded engine (${vendor}).\n\n` +
614
- `${err.stderr ?? String(err)}`);
638
+ subprocessFailureDetail(err));
615
639
  }
616
640
  await execFileAsync("cmake", [
617
641
  "--build", buildDir,
@@ -950,13 +974,14 @@ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = ven
950
974
  /** The library base: the executable lane's unconditional sources minus the
951
975
  * fiber/loop and child-process units, plus the library-mode TU. */
952
976
  const LIB_RUNTIME_SOURCES = [
953
- ...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"),
954
978
  "scr_library.c",
955
979
  ];
956
980
  export async function compileLibArchive(opts) {
957
981
  const rtDir = runtimeSrcDir();
958
982
  const driver = resolveCc();
959
983
  const sanitize = opts.sanitize ?? false;
984
+ const optimization = opts.optimization ?? "release";
960
985
  const regex = opts.regex ?? false;
961
986
  const sources = [
962
987
  ...LIB_RUNTIME_SOURCES,
@@ -984,7 +1009,9 @@ export async function compileLibArchive(opts) {
984
1009
  const cflags = [
985
1010
  "-std=c11",
986
1011
  ...driver.targetArgs,
987
- ...(sanitize ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"] : ["-O2"]),
1012
+ ...(sanitize
1013
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
1014
+ : [optimization === "dev" ? "-O0" : "-O2"]),
988
1015
  "-fno-math-errno",
989
1016
  "-fno-strict-aliasing", // the emitted object model type-puns — see compileC's buildArgs
990
1017
  "-Wno-deprecated-declarations",
@@ -1161,7 +1188,7 @@ export async function compileLibArchive(opts) {
1161
1188
  await execFileAsync(driver.argv[0] ?? "clang", args);
1162
1189
  }
1163
1190
  catch (err) {
1164
- const stderr = err.stderr ?? String(err);
1191
+ const stderr = subprocessFailureDetail(err);
1165
1192
  throw new Error(`${driver.argv.join(" ")} failed compiling ${src} for the library archive.\n` +
1166
1193
  `This is a scriptc bug (generated/runtime C should always compile) unless the compiler itself is missing/broken.\n\n${stderr}`);
1167
1194
  }
@@ -1343,7 +1370,7 @@ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, s
1343
1370
  await execFileAsync(argv[0], [...argv.slice(1)]);
1344
1371
  }
1345
1372
  catch (err) {
1346
- const stderr = err.stderr ?? String(err);
1373
+ const stderr = subprocessFailureDetail(err);
1347
1374
  throw new Error(`${argv[0]} failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n` +
1348
1375
  `Runtime symbol localization needs the ${platform === "darwin" ? "host toolchain's ld" : driver.target === null ? "host toolchain's ld and objcopy" : "cross driver's relocatable link"} beside the C compiler.\n\n${stderr}`);
1349
1376
  }
@@ -1578,6 +1605,15 @@ async function resolvedToolIdentity(command, env = process.env) {
1578
1605
  return (await resolvedTool(command, env))?.cacheIdentity ?? null;
1579
1606
  }
1580
1607
  const directCompilerDriverMemos = new Map();
1608
+ const directCompilerSelections = new Map();
1609
+ function compilerDriverProbeKey(driver, environmentFingerprint) {
1610
+ return [
1611
+ environmentFingerprint,
1612
+ driver.argv.join("\x1f"),
1613
+ driver.target ?? "<native>",
1614
+ driver.targetArgs.join("\x1f"),
1615
+ ].join("\0");
1616
+ }
1581
1617
  /** `/usr/bin/clang` on Darwin is Apple's immutable driver shim: its `-###`
1582
1618
  * trace names the selected Xcode/CommandLineTools clang rather than the shim
1583
1619
  * itself. The ordinary implicit-toolchain fingerprint below captures that
@@ -1599,12 +1635,7 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1599
1635
  // wrapper. This is deliberately undocumented and test-scoped.
1600
1636
  if (process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1")
1601
1637
  return true;
1602
- const driverKey = [
1603
- environmentFingerprint,
1604
- driver.argv.join("\x1f"),
1605
- driver.target ?? "<native>",
1606
- driver.targetArgs.join("\x1f"),
1607
- ].join("\0");
1638
+ const driverKey = compilerDriverProbeKey(driver, environmentFingerprint);
1608
1639
  const compiler = driver.argv[0] ?? "clang";
1609
1640
  const resolvedDriver = await resolvedTool(compiler);
1610
1641
  // A prior dependency list cannot prove that name resolution is unchanged:
@@ -1645,6 +1676,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1645
1676
  direct = effectiveCompiler !== null &&
1646
1677
  (effectiveCompiler.fileIdentity === resolvedDriver.fileIdentity ||
1647
1678
  isAppleSystemClangHandoff(resolvedDriver, effectiveCompiler));
1679
+ if (direct)
1680
+ directCompilerSelections.set(driverKey, effectiveCompiler);
1648
1681
  }
1649
1682
  }
1650
1683
  catch {
@@ -1654,6 +1687,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1654
1687
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1655
1688
  }
1656
1689
  directCompilerDriverMemos.set(probeKey, direct);
1690
+ if (!direct)
1691
+ directCompilerSelections.delete(driverKey);
1657
1692
  return direct;
1658
1693
  }
1659
1694
  /** Complete library archives may skip `ar`, so the selected archiver must be
@@ -1734,6 +1769,29 @@ async function toolVersionOnce(argv, environmentFingerprint = toolchainEnvironme
1734
1769
  toolVersionFallbacks.set(spellingKey, memo);
1735
1770
  return memo;
1736
1771
  }
1772
+ /** Dependency paths discovered while computing one strict fingerprint. The
1773
+ * output-local cache stamp snapshots these exact files/directories after a
1774
+ * validated build, then can prove a later same-output no-op without spawning
1775
+ * clang again. The content identity closes the gap between hashing and that
1776
+ * metadata snapshot: a file changed in the gap cannot make new bytes ride an
1777
+ * old key. Keep the map bounded for long-lived corpus/test processes. */
1778
+ const fingerprintDependencies = new Map();
1779
+ function rememberFingerprintDependencies(fingerprint, paths, contentPaths = [], contentFingerprint = null) {
1780
+ fingerprintDependencies.set(fingerprint, {
1781
+ paths: [...new Set(paths)].sort(),
1782
+ contentPaths: [...new Set(contentPaths)].sort(),
1783
+ contentFingerprint,
1784
+ });
1785
+ if (fingerprintDependencies.size > 256) {
1786
+ const oldest = fingerprintDependencies.keys().next().value;
1787
+ if (oldest !== undefined)
1788
+ fingerprintDependencies.delete(oldest);
1789
+ }
1790
+ return fingerprint;
1791
+ }
1792
+ function fingerprintDependencyPaths(fingerprint) {
1793
+ return fingerprintDependencies.get(fingerprint)?.paths ?? [];
1794
+ }
1737
1795
  function parseMakeDependencies(output, cwd = process.cwd()) {
1738
1796
  const flattened = output.replace(/\\\r?\n/g, " ");
1739
1797
  const separator = flattened.indexOf(": ");
@@ -1815,13 +1873,14 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1815
1873
  invocation: normalizedProbeInvocation(invocation, probeDir),
1816
1874
  dependencies,
1817
1875
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1876
+ invocationPaths: await existingDriverTracePaths(`${invocation.stdout}\n${invocation.stderr}`, probeDir, probeDir),
1818
1877
  };
1819
1878
  }
1820
1879
  finally {
1821
1880
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1822
1881
  }
1823
1882
  }
1824
- return createHash("sha256")
1883
+ const fingerprint = createHash("sha256")
1825
1884
  .update("effective-compiler-invocation-v2\0")
1826
1885
  .update(environmentFingerprint)
1827
1886
  .update("\0")
@@ -1839,6 +1898,7 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1839
1898
  .update("\0")
1840
1899
  .update(probe.dependencyFingerprint)
1841
1900
  .digest("hex");
1901
+ return rememberFingerprintDependencies(fingerprint, [...probe.dependencies, ...probe.invocationPaths], probe.dependencies, probe.dependencyFingerprint);
1842
1902
  }
1843
1903
  const stableEffectiveCompilerInvocationMemos = new Map();
1844
1904
  function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint, compileArgs, sourceExtension = ".c") {
@@ -1852,17 +1912,30 @@ function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint,
1852
1912
  }
1853
1913
  async function nativeSourceFiles(directory, recursive, include = (name) => name.endsWith(".c") || name.endsWith(".h")) {
1854
1914
  const files = [];
1855
- const walk = async (current) => {
1915
+ const walk = async (current, ancestors) => {
1916
+ const canonical = await realpath(current).catch(() => resolve(current));
1917
+ if (ancestors.has(canonical))
1918
+ return;
1919
+ const nestedAncestors = new Set(ancestors).add(canonical);
1856
1920
  const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
1857
1921
  for (const entry of entries) {
1858
1922
  const path = join(current, entry.name);
1859
- if (entry.isDirectory() && recursive)
1860
- await walk(path);
1861
- else if (entry.isFile() && include(entry.name))
1923
+ if (entry.isDirectory() && recursive) {
1924
+ await walk(path, nestedAncestors);
1925
+ }
1926
+ else if (entry.isSymbolicLink()) {
1927
+ const target = await stat(path).catch(() => null);
1928
+ if (target?.isDirectory() && recursive)
1929
+ await walk(path, nestedAncestors);
1930
+ else if (target?.isFile() && include(entry.name))
1931
+ files.push(path);
1932
+ }
1933
+ else if (entry.isFile() && include(entry.name)) {
1862
1934
  files.push(path);
1935
+ }
1863
1936
  }
1864
1937
  };
1865
- await walk(directory);
1938
+ await walk(directory, new Set());
1866
1939
  return files.sort();
1867
1940
  }
1868
1941
  /** Every system-header spelling consumed by a source tree scriptc owns. The
@@ -1914,6 +1987,18 @@ async function fingerprintDependencyFiles(paths) {
1914
1987
  }
1915
1988
  return hash.digest("hex");
1916
1989
  }
1990
+ async function fingerprintDependenciesStillMatch(fingerprints) {
1991
+ const distinct = [...new Set(fingerprints)];
1992
+ const identities = distinct
1993
+ .map((fingerprint) => fingerprintDependencies.get(fingerprint))
1994
+ // A restored native-metadata stamp carries and revalidates the dependency
1995
+ // snapshot from the process that minted this fingerprint. Only identities
1996
+ // computed in this process have an additional content hash to recheck here.
1997
+ .filter((identity) => identity !== undefined);
1998
+ return (await Promise.all(identities.map(async (identity) => identity.contentFingerprint === null ||
1999
+ await fingerprintDependencyFiles(identity.contentPaths).catch(() => null) ===
2000
+ identity.contentFingerprint))).every(Boolean);
2001
+ }
1917
2002
  /** Exact headers selected while preprocessing the caller's translation unit.
1918
2003
  * The shared toolchain probe covers the runtime/vendor trees, but compileC is
1919
2004
  * also a public API: an opted-in caller can include a system or header-only SDK
@@ -1922,7 +2007,7 @@ async function fingerprintDependencyFiles(paths) {
1922
2007
  * quote-include directory and compiler-visible source spelling. */
1923
2008
  async function translationUnitDependencyFingerprintFresh(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
1924
2009
  if (sourcePath.endsWith(".ll")) {
1925
- return createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex");
2010
+ return rememberFingerprintDependencies(createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex"), []);
1926
2011
  }
1927
2012
  const compiler = driver.argv[0] ?? "clang";
1928
2013
  const compilerIdentity = await resolvedToolIdentity(compiler);
@@ -1959,7 +2044,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1959
2044
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1960
2045
  }
1961
2046
  }
1962
- return createHash("sha256")
2047
+ const fingerprint = createHash("sha256")
1963
2048
  .update("translation-unit-dependencies-v1\0")
1964
2049
  .update(environmentFingerprint)
1965
2050
  .update("\0")
@@ -1969,6 +2054,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1969
2054
  .update("\0")
1970
2055
  .update(probe.dependencyFingerprint)
1971
2056
  .digest("hex");
2057
+ return rememberFingerprintDependencies(fingerprint, probe.dependencies, probe.dependencies, probe.dependencyFingerprint);
1972
2058
  }
1973
2059
  const stableTranslationUnitDependencyMemos = new Map();
1974
2060
  function translationUnitDependencyFingerprint(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
@@ -2062,10 +2148,15 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2062
2148
  compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir),
2063
2149
  dependencies: dependencyPaths,
2064
2150
  dependencyFingerprint: await fingerprintDependencyFiles(dependencyPaths),
2065
- tools: await Promise.all(toolSpellings.map(async (spelling) => ({
2066
- spelling,
2067
- identity: await resolvedToolIdentity(spelling),
2068
- }))),
2151
+ invocationPaths: await existingDriverTracePaths(`${compilerInvocation.stdout}\n${compilerInvocation.stderr}`, probeDir, probeDir),
2152
+ tools: await Promise.all(toolSpellings.map(async (spelling) => {
2153
+ const resolved = await resolvedTool(spelling);
2154
+ return {
2155
+ spelling,
2156
+ identity: resolved?.cacheIdentity ?? null,
2157
+ path: resolved?.canonicalPath ?? null,
2158
+ };
2159
+ })),
2069
2160
  };
2070
2161
  }
2071
2162
  finally {
@@ -2096,7 +2187,11 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2096
2187
  .update(currentIdentity ?? tool.identity ?? "<unresolved>")
2097
2188
  .update("\0");
2098
2189
  }
2099
- return hash.digest("hex");
2190
+ return rememberFingerprintDependencies(hash.digest("hex"), [
2191
+ ...probe.dependencies,
2192
+ ...probe.invocationPaths,
2193
+ ...probe.tools.flatMap((tool) => tool.path === null ? [] : [tool.path]),
2194
+ ], probe.dependencies, probe.dependencyFingerprint);
2100
2195
  }
2101
2196
  const stableImplicitToolchainMemos = new Map();
2102
2197
  function implicitToolchainFingerprint(driver, environmentFingerprint) {
@@ -2132,6 +2227,51 @@ function driverTraceCandidates(line) {
2132
2227
  }
2133
2228
  return candidates;
2134
2229
  }
2230
+ async function existingDriverTracePaths(output, cwd, excludedRoot) {
2231
+ const candidates = new Set();
2232
+ for (const line of output.split(/\r?\n/)) {
2233
+ const tokens = driverTraceCandidates(line);
2234
+ for (let index = 0; index < tokens.length; index++) {
2235
+ const token = tokens[index];
2236
+ const joinedPathOption = ["-I", "-L", "-F"].find((option) => token.startsWith(option) && token.length > option.length);
2237
+ const equalsPathOption = ["--sysroot=", "-resource-dir="].find((option) => token.startsWith(option));
2238
+ const separatePathOptions = [
2239
+ "-I",
2240
+ "-L",
2241
+ "-F",
2242
+ "--sysroot",
2243
+ "-isysroot",
2244
+ "-resource-dir",
2245
+ "-isystem",
2246
+ "-iquote",
2247
+ "-internal-isystem",
2248
+ "-internal-externc-isystem",
2249
+ "-internal-iframework",
2250
+ ];
2251
+ const optionPath = joinedPathOption !== undefined
2252
+ ? token.slice(joinedPathOption.length)
2253
+ : equalsPathOption !== undefined
2254
+ ? token.slice(equalsPathOption.length)
2255
+ : separatePathOptions.includes(token)
2256
+ ? tokens[index + 1] ?? ""
2257
+ : token;
2258
+ if (!isAbsolute(optionPath))
2259
+ continue;
2260
+ const path = resolve(cwd, optionPath);
2261
+ if (path === excludedRoot ||
2262
+ path.startsWith(`${excludedRoot}/`) ||
2263
+ path.startsWith(`${excludedRoot}\\`)) {
2264
+ continue;
2265
+ }
2266
+ candidates.add(path);
2267
+ }
2268
+ }
2269
+ const existing = await Promise.all([...candidates].map(async (path) => [path, await lstat(path).catch(() => null)]));
2270
+ return existing
2271
+ .filter((entry) => entry[1] !== null)
2272
+ .map(([path]) => path)
2273
+ .sort();
2274
+ }
2135
2275
  export async function parseLinkTraceFiles(output, cwd, excludedRoot, driverDryRun = false) {
2136
2276
  const files = new Set();
2137
2277
  for (const line of output.split(/\r?\n/)) {
@@ -2217,9 +2357,15 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2217
2357
  linkerInvocation: normalizedProbeInvocation(driverInvocation, probeDir),
2218
2358
  dependencies,
2219
2359
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
2360
+ invocationPaths: await existingDriverTracePaths(`${driverInvocation.stdout}\n${driverInvocation.stderr}`, probeDir, probeDir),
2220
2361
  linker: {
2221
2362
  spelling: linkerSpelling,
2222
- identity: linkerSpelling === "" ? null : await resolvedToolIdentity(linkerSpelling),
2363
+ identity: linkerSpelling === ""
2364
+ ? null
2365
+ : (await resolvedTool(linkerSpelling))?.cacheIdentity ?? null,
2366
+ path: linkerSpelling === ""
2367
+ ? null
2368
+ : (await resolvedTool(linkerSpelling))?.canonicalPath ?? null,
2223
2369
  },
2224
2370
  };
2225
2371
  }
@@ -2230,7 +2376,7 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2230
2376
  const linkerIdentity = probe.linker.spelling === ""
2231
2377
  ? null
2232
2378
  : await resolvedToolIdentity(probe.linker.spelling);
2233
- return createHash("sha256")
2379
+ const fingerprint = createHash("sha256")
2234
2380
  .update("implicit-linker-v3\0")
2235
2381
  .update(environmentFingerprint)
2236
2382
  .update("\0")
@@ -2256,6 +2402,11 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2256
2402
  .update("\0")
2257
2403
  .update(linkerIdentity ?? probe.linker.identity ?? "<unresolved>")
2258
2404
  .digest("hex");
2405
+ return rememberFingerprintDependencies(fingerprint, [
2406
+ ...probe.dependencies,
2407
+ ...probe.invocationPaths,
2408
+ ...(probe.linker.path === null ? [] : [probe.linker.path]),
2409
+ ], probe.dependencies, probe.dependencyFingerprint);
2259
2410
  }
2260
2411
  const stableImplicitLinkerMemos = new Map();
2261
2412
  function implicitLinkerFingerprint(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs) {
@@ -2277,21 +2428,18 @@ function ccacheAvailable() {
2277
2428
  ccacheMemo ??= execFileAsync("ccache", ["--version"]).then(() => true, () => false);
2278
2429
  return ccacheMemo;
2279
2430
  }
2280
- /** Content hash of every .c/.h in the runtime src dir, the Ryū sources
2431
+ /** Content hash of every .c/.h in the runtime src tree, the Ryū sources
2281
2432
  * textually included by scr_number.c, and the separately-built vendor pins —
2282
2433
  * everything a binary links that the emitted C bytes don't already cover
2283
2434
  * (npm-embedded C rides inside the emitted C; the engine and standalone
2284
2435
  * vendor archives are pinned by their version constants). The small source
2285
- * tree is hashed on every identity calculation: a stat-only memo can miss a
2286
- * same-size edit whose timestamp was preserved by a copy/sync tool. */
2436
+ * tree is hashed on every identity calculation: recursive enumeration is
2437
+ * required because a newly added nested header can begin shadowing a system
2438
+ * include without changing any previously selected dependency, while content
2439
+ * hashing catches same-size edits whose timestamp was preserved by a copy/sync
2440
+ * tool. */
2287
2441
  async function runtimeFingerprintFresh(rtDir) {
2288
- const groups = await Promise.all([
2289
- { label: "runtime", dir: rtDir },
2290
- { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2291
- ].map(async (group) => {
2292
- const names = (await readdir(group.dir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
2293
- return { ...group, names };
2294
- }));
2442
+ const groups = await runtimeFingerprintInputGroups(rtDir);
2295
2443
  const h = createHash("sha256").update(QJS_COMMIT).update(MBEDTLS_VERSION).update(ZLIB_VERSION);
2296
2444
  for (const group of groups) {
2297
2445
  for (const n of group.names) {
@@ -2300,6 +2448,20 @@ async function runtimeFingerprintFresh(rtDir) {
2300
2448
  }
2301
2449
  return h.digest("hex");
2302
2450
  }
2451
+ async function runtimeFingerprintInputGroups(rtDir) {
2452
+ return Promise.all([
2453
+ { label: "runtime", dir: rtDir },
2454
+ { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2455
+ ].map(async (group) => {
2456
+ const names = (await nativeSourceFiles(group.dir, true))
2457
+ .map((path) => relative(group.dir, path))
2458
+ .sort();
2459
+ return { ...group, names };
2460
+ }));
2461
+ }
2462
+ async function runtimeFingerprintInputPaths(rtDir) {
2463
+ return (await runtimeFingerprintInputGroups(rtDir)).flatMap((group) => group.names.map((name) => join(group.dir, name)));
2464
+ }
2303
2465
  const stableRuntimeFingerprintMemos = new Map();
2304
2466
  export function runtimeFingerprint(rtDir) {
2305
2467
  const key = resolve(rtDir);
@@ -2388,6 +2550,279 @@ async function publishCachedFile(source, destination) {
2388
2550
  ]);
2389
2551
  }
2390
2552
  }
2553
+ async function directoryTreeDigest(root, excludedPaths = []) {
2554
+ const hash = createHash("sha256").update("native-dependency-tree-v1\0");
2555
+ const visited = new Set();
2556
+ const excluded = excludedPaths.map((path) => resolve(path));
2557
+ const walk = async (directory, relative) => {
2558
+ const canonical = await realpath(directory);
2559
+ if (visited.has(canonical)) {
2560
+ hash.update(relative).update("\0cycle\0").update(canonical).update("\0");
2561
+ return;
2562
+ }
2563
+ visited.add(canonical);
2564
+ const entries = await readdir(directory, { withFileTypes: true });
2565
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2566
+ for (const entry of entries) {
2567
+ const path = join(directory, entry.name);
2568
+ const absolute = resolve(path);
2569
+ if (excluded.some((candidate) => absolute === candidate ||
2570
+ absolute.startsWith(`${candidate}/`) ||
2571
+ absolute.startsWith(`${candidate}\\`)))
2572
+ continue;
2573
+ const child = relative === "" ? entry.name : `${relative}/${entry.name}`;
2574
+ const info = await lstat(path);
2575
+ const kind = info.isDirectory()
2576
+ ? "directory"
2577
+ : info.isFile()
2578
+ ? "file"
2579
+ : info.isSymbolicLink()
2580
+ ? "symlink"
2581
+ : "other";
2582
+ hash.update(child).update("\0").update(kind).update("\0");
2583
+ if (kind === "symlink") {
2584
+ const target = await realpath(path).catch(() => "<missing>");
2585
+ hash.update(target).update("\0");
2586
+ const targetInfo = await stat(path).catch(() => null);
2587
+ if (targetInfo?.isDirectory())
2588
+ await walk(path, child);
2589
+ }
2590
+ if (kind === "directory")
2591
+ await walk(path, child);
2592
+ }
2593
+ visited.delete(canonical);
2594
+ };
2595
+ await walk(root, "");
2596
+ return hash.digest("hex");
2597
+ }
2598
+ function localDependencyKind(info) {
2599
+ return info.isFile()
2600
+ ? "file"
2601
+ : info.isDirectory()
2602
+ ? "directory"
2603
+ : info.isSymbolicLink()
2604
+ ? "symlink"
2605
+ : null;
2606
+ }
2607
+ async function snapshotLocalArtifactDependency(path, treeExclusions = null) {
2608
+ const info = await lstat(path);
2609
+ const kind = localDependencyKind(info);
2610
+ if (kind === null)
2611
+ throw new Error(`unsupported local artifact dependency: ${path}`);
2612
+ const dependency = {
2613
+ path,
2614
+ kind,
2615
+ dev: info.dev,
2616
+ ino: info.ino,
2617
+ size: info.size,
2618
+ mtimeMs: info.mtimeMs,
2619
+ ctimeMs: info.ctimeMs,
2620
+ };
2621
+ if (kind === "directory" && treeExclusions !== null) {
2622
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2623
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2624
+ }
2625
+ if (kind === "symlink") {
2626
+ const targetPath = await realpath(path);
2627
+ const target = await stat(path);
2628
+ const targetKind = target.isFile() ? "file" : target.isDirectory() ? "directory" : null;
2629
+ if (targetKind === null)
2630
+ throw new Error(`unsupported symlink target dependency: ${path}`);
2631
+ dependency.targetPath = targetPath;
2632
+ dependency.targetKind = targetKind;
2633
+ dependency.targetDev = target.dev;
2634
+ dependency.targetIno = target.ino;
2635
+ dependency.targetSize = target.size;
2636
+ dependency.targetMtimeMs = target.mtimeMs;
2637
+ dependency.targetCtimeMs = target.ctimeMs;
2638
+ if (targetKind === "directory" && treeExclusions !== null) {
2639
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2640
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2641
+ }
2642
+ }
2643
+ return dependency;
2644
+ }
2645
+ async function snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2646
+ const recursive = new Set(recursiveDirectories.map((path) => resolve(path)));
2647
+ return Promise.all([...new Set(dependencyPaths)].sort().map((path) => snapshotLocalArtifactDependency(path, recursive.has(resolve(path)) ? recursiveExclusions : null)));
2648
+ }
2649
+ async function localArtifactDependenciesStillMatch(dependencies) {
2650
+ return (await Promise.all(dependencies.map(async (dependency) => {
2651
+ const current = await snapshotLocalArtifactDependency(dependency.path, dependency.treeDigest === undefined ? null : dependency.treeExclusions ?? []).catch(() => null);
2652
+ return current !== null && JSON.stringify(current) === JSON.stringify(dependency);
2653
+ }))).every(Boolean);
2654
+ }
2655
+ function nativeMetadataStampPath(root, key) {
2656
+ return join(root, "meta", createHash("sha256").update(key).digest("hex"));
2657
+ }
2658
+ function nativeMetadataStampIntegrity(stamp) {
2659
+ return createHash("sha256")
2660
+ .update("native-metadata-stamp-v2\0")
2661
+ .update(JSON.stringify(stamp))
2662
+ .digest("hex");
2663
+ }
2664
+ async function readNativeMetadataStamp(root, key) {
2665
+ try {
2666
+ const stamp = JSON.parse(await readFile(nativeMetadataStampPath(root, key), "utf8"));
2667
+ if (stamp.version !== 2 ||
2668
+ stamp.key !== key ||
2669
+ stamp.values === null ||
2670
+ typeof stamp.values !== "object" ||
2671
+ !Array.isArray(stamp.dependencies) ||
2672
+ !/^[0-9a-f]{64}$/.test(stamp.integrity) ||
2673
+ nativeMetadataStampIntegrity({
2674
+ version: stamp.version,
2675
+ key: stamp.key,
2676
+ values: stamp.values,
2677
+ dependencies: stamp.dependencies,
2678
+ }) !== stamp.integrity ||
2679
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies))) {
2680
+ return null;
2681
+ }
2682
+ return stamp;
2683
+ }
2684
+ catch {
2685
+ return null;
2686
+ }
2687
+ }
2688
+ async function publishNativeMetadataStamp(root, key, values, dependencyPaths, fingerprints = []) {
2689
+ const destination = nativeMetadataStampPath(root, key);
2690
+ await mkdir(dirname(destination), { recursive: true });
2691
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths);
2692
+ if (!(await fingerprintDependenciesStillMatch(fingerprints))) {
2693
+ throw new CacheInputsChangedError();
2694
+ }
2695
+ const unsigned = { version: 2, key, values, dependencies };
2696
+ const stamp = {
2697
+ ...unsigned,
2698
+ integrity: nativeMetadataStampIntegrity(unsigned),
2699
+ };
2700
+ const tmp = `${destination}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2701
+ try {
2702
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2703
+ await rename(tmp, destination);
2704
+ }
2705
+ finally {
2706
+ await rm(tmp, { force: true }).catch(() => undefined);
2707
+ }
2708
+ return stamp;
2709
+ }
2710
+ function nativeMetadataKey(kind, parts) {
2711
+ const hash = createHash("sha256").update(`native-metadata-${kind}-v2\0`);
2712
+ for (const part of parts) {
2713
+ hash.update(typeof part === "string" ? part : part.join("\x1f")).update("\0");
2714
+ }
2715
+ return `${kind}-${hash.digest("hex")}`;
2716
+ }
2717
+ /** The caller-visible output is itself the cheapest safe cache tier. Once a
2718
+ * generated TU has produced this exact binary, an unchanged rebuild need not
2719
+ * rediscover every SDK header and linker input merely to copy equivalent bytes
2720
+ * back onto the same path. This tier is deliberately narrower than the CAS:
2721
+ * only frontend-generated programs with no caller-owned native inputs opt in.
2722
+ * The generated TU bytes, every scriptc runtime source, the selected direct
2723
+ * compiler inode, target/options/environment, and the output path all join the
2724
+ * key. A digest rejects a modified/truncated output before the no-op hit. */
2725
+ function localArtifactIdentity(opts, driver, environmentFingerprint, compilerIdentity, runtimeHash, programBytes) {
2726
+ const normalizedOptions = Object.fromEntries(Object.entries(opts)
2727
+ .filter(([, value]) => value !== undefined)
2728
+ .sort(([a], [b]) => a.localeCompare(b)));
2729
+ return createHash("sha256")
2730
+ .update("local-artifact-v1\0")
2731
+ .update(cacheTargetIdentity(driver)).update("\0")
2732
+ .update(environmentFingerprint).update("\0")
2733
+ .update(compilerIdentity).update("\0")
2734
+ .update(runtimeHash).update("\0")
2735
+ .update(driver.argv.join("\x1f")).update("\0")
2736
+ .update(driver.targetArgs.join("\x1f")).update("\0")
2737
+ .update(driver.linkArgs.join("\x1f")).update("\0")
2738
+ .update(process.env["SCRIPTC_FETCH_CURL"] === "1" ? "fetch-curl" : "fetch-native").update("\0")
2739
+ .update(JSON.stringify(normalizedOptions)).update("\0")
2740
+ .update(resolve(opts.cPath)).update("\0")
2741
+ .update(resolve(opts.outPath)).update("\0")
2742
+ .update(programBytes)
2743
+ .digest("hex");
2744
+ }
2745
+ function localArtifactStampPath(root, outPath) {
2746
+ const outputKey = createHash("sha256").update(resolve(outPath)).digest("hex");
2747
+ return join(root, "local", outputKey);
2748
+ }
2749
+ function localArtifactStampIntegrity(stamp) {
2750
+ return createHash("sha256")
2751
+ .update("local-artifact-stamp-v2\0")
2752
+ .update(JSON.stringify(stamp))
2753
+ .digest("hex");
2754
+ }
2755
+ async function localArtifactHit(stampPath, outPath, key) {
2756
+ try {
2757
+ const stamp = JSON.parse(await readFile(stampPath, "utf8"));
2758
+ const output = await lstat(outPath);
2759
+ const expectedMode = 0o777 & ~process.umask();
2760
+ if (stamp.version !== 2 ||
2761
+ stamp.key !== key ||
2762
+ !/^[0-9a-f]{64}$/.test(stamp.digest ?? "") ||
2763
+ !Array.isArray(stamp.dependencies) ||
2764
+ !/^[0-9a-f]{64}$/.test(stamp.integrity ?? "") ||
2765
+ localArtifactStampIntegrity({
2766
+ version: stamp.version,
2767
+ key: stamp.key,
2768
+ digest: stamp.digest,
2769
+ dependencies: stamp.dependencies,
2770
+ }) !== stamp.integrity ||
2771
+ !output.isFile() ||
2772
+ (output.mode & 0o777) !== expectedMode ||
2773
+ stamp.dependencies.some((dependency) => dependency === null ||
2774
+ typeof dependency !== "object" ||
2775
+ typeof dependency.path !== "string" ||
2776
+ dependency.kind !== "file" &&
2777
+ dependency.kind !== "directory" &&
2778
+ dependency.kind !== "symlink" ||
2779
+ typeof dependency.dev !== "number" ||
2780
+ typeof dependency.ino !== "number" ||
2781
+ typeof dependency.size !== "number" ||
2782
+ typeof dependency.mtimeMs !== "number" ||
2783
+ typeof dependency.ctimeMs !== "number" ||
2784
+ dependency.treeDigest !== undefined && typeof dependency.treeDigest !== "string" ||
2785
+ dependency.kind === "symlink" && (typeof dependency.targetPath !== "string" ||
2786
+ dependency.targetKind !== "file" && dependency.targetKind !== "directory" ||
2787
+ typeof dependency.targetDev !== "number" ||
2788
+ typeof dependency.targetIno !== "number" ||
2789
+ typeof dependency.targetSize !== "number" ||
2790
+ typeof dependency.targetMtimeMs !== "number" ||
2791
+ typeof dependency.targetCtimeMs !== "number")) ||
2792
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies)) ||
2793
+ await fileDigest(outPath) !== stamp.digest) {
2794
+ return false;
2795
+ }
2796
+ const now = new Date();
2797
+ await utimes(stampPath, now, now).catch(() => undefined);
2798
+ return true;
2799
+ }
2800
+ catch {
2801
+ return false;
2802
+ }
2803
+ }
2804
+ async function publishLocalArtifactStamp(stampPath, outPath, key, dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2805
+ await mkdir(dirname(stampPath), { recursive: true });
2806
+ const tmp = `${stampPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2807
+ try {
2808
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories, recursiveExclusions);
2809
+ const unsigned = {
2810
+ version: 2,
2811
+ key,
2812
+ digest: await fileDigest(outPath),
2813
+ dependencies,
2814
+ };
2815
+ const stamp = {
2816
+ ...unsigned,
2817
+ integrity: localArtifactStampIntegrity(unsigned),
2818
+ };
2819
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2820
+ await rename(tmp, stampPath);
2821
+ }
2822
+ finally {
2823
+ await rm(tmp, { force: true }).catch(() => undefined);
2824
+ }
2825
+ }
2391
2826
  /** The cached .o set for one flag flavor, compiled on first need. Concurrent
2392
2827
  * first builds (parallel test workers on a cold cache) may duplicate work;
2393
2828
  * per-file atomic renames make every winner equivalent. Publication is held
@@ -2421,7 +2856,22 @@ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix, ve
2421
2856
  await Promise.all(missing.slice(i, i + width).map(async (src) => {
2422
2857
  const tmpObj = join(tmpDir, `${basename(src, ".c")}.o`);
2423
2858
  const argv = [...(useCcache ? ["ccache"] : []), ...ccArgv, ...cflags, "-c", src, "-o", tmpObj];
2424
- await execFileAsync(argv[0] ?? "clang", argv.slice(1));
2859
+ await execFileAsync(argv[0] ?? "clang", argv.slice(1), useCcache
2860
+ ? {
2861
+ // ccache direct mode remembers only the headers selected by
2862
+ // its previous manifest and can miss a newly created,
2863
+ // higher-priority header. The scriptc object-set key already
2864
+ // includes the recursive runtime namespace fingerprint, so
2865
+ // carry it into ccache's own keyspace as well.
2866
+ env: {
2867
+ ...process.env,
2868
+ CCACHE_NAMESPACE: [
2869
+ process.env["CCACHE_NAMESPACE"],
2870
+ `scriptc-${setKey}`,
2871
+ ].filter((value) => value !== undefined && value !== "").join(":"),
2872
+ },
2873
+ }
2874
+ : undefined);
2425
2875
  compiled.set(src, { object: tmpObj, digest: await fileDigest(tmpObj) });
2426
2876
  }));
2427
2877
  }
@@ -2562,6 +3012,7 @@ export async function compileC(opts) {
2562
3012
  const net = (opts.net ?? false) || nativeFetch || netIsland;
2563
3013
  const http = (opts.http ?? false) || nativeFetch || netIsland;
2564
3014
  const tls = (opts.tls ?? false) || nativeFetch || netIsland;
3015
+ const tlsCa = (opts.tlsCa ?? false) || tls;
2565
3016
  const driver = resolveCc();
2566
3017
  // Mobile triples produce library archives, never standalone executables:
2567
3018
  // the executable-lane runtime (event loop, sockets, child processes) is
@@ -2632,10 +3083,88 @@ export async function compileC(opts) {
2632
3083
  root = null;
2633
3084
  }
2634
3085
  }
3086
+ let localArtifact = null;
3087
+ // Generated executable TUs are closed over scriptc's own runtime tree. A
3088
+ // same-output rebuild can therefore check those bytes directly before the
3089
+ // broader cross-output CAS performs its compiler/SDK/linker rediscovery.
3090
+ // FFI/native-input builds and the public arbitrary-C cache API stay on the
3091
+ // strict path because their dependency graphs are caller-owned.
3092
+ if (root !== null &&
3093
+ cachePolicy.completeArtifacts &&
3094
+ opts.cacheIdentity === "scriptc-generated-v1" &&
3095
+ (opts.linkInputs?.length ?? 0) === 0 &&
3096
+ (opts.systemLibraries?.length ?? 0) === 0 &&
3097
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] !== "1") {
3098
+ try {
3099
+ const [compiler, runtimeHash, programBytes] = await Promise.all([
3100
+ resolvedTool(driver.argv[0] ?? "clang"),
3101
+ runtimeFingerprint(rtDir),
3102
+ readFile(opts.cPath),
3103
+ ]);
3104
+ if (compiler !== null) {
3105
+ const effectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? compiler;
3106
+ const key = localArtifactIdentity(opts, driver, toolchainEnv, `${compiler.cacheIdentity}\0${effectiveCompiler.cacheIdentity}`, runtimeHash, programBytes);
3107
+ const stampPath = localArtifactStampPath(root, opts.outPath);
3108
+ localArtifact = {
3109
+ stampPath,
3110
+ key,
3111
+ runtimeHash,
3112
+ programBytes,
3113
+ compilerPath: effectiveCompiler.canonicalPath,
3114
+ };
3115
+ if (await localArtifactHit(stampPath, opts.outPath, key))
3116
+ return;
3117
+ }
3118
+ }
3119
+ catch {
3120
+ // The output-local tier is only an optimization; the fully validated
3121
+ // CAS below remains the source of truth on any metadata trouble.
3122
+ localArtifact = null;
3123
+ }
3124
+ }
2635
3125
  let implicitToolchain = null;
3126
+ let toolchainMetadataStamp = null;
3127
+ const metadataCompiler = root === null
3128
+ ? null
3129
+ : await resolvedTool(driver.argv[0] ?? "clang");
3130
+ const metadataEffectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? metadataCompiler;
3131
+ const toolchainMetadataKey = root === null ||
3132
+ metadataCompiler === null ||
3133
+ metadataEffectiveCompiler === null ||
3134
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3135
+ ? null
3136
+ : nativeMetadataKey("toolchain", [
3137
+ cacheTargetIdentity(driver),
3138
+ toolchainEnv,
3139
+ driver.argv,
3140
+ driver.targetArgs,
3141
+ metadataCompiler.cacheIdentity,
3142
+ metadataEffectiveCompiler.cacheIdentity,
3143
+ rtDir,
3144
+ ]);
2636
3145
  if (persistentDriverCache) {
2637
3146
  try {
2638
- implicitToolchain = await implicitToolchainFingerprint(driver, toolchainEnv);
3147
+ toolchainMetadataStamp =
3148
+ root === null || toolchainMetadataKey === null
3149
+ ? null
3150
+ : await readNativeMetadataStamp(root, toolchainMetadataKey);
3151
+ implicitToolchain = toolchainMetadataStamp?.values["implicitToolchain"] ??
3152
+ await implicitToolchainFingerprint(driver, toolchainEnv);
3153
+ let compilerVersion = toolchainMetadataStamp?.values["compilerVersion"];
3154
+ if (compilerVersion === undefined) {
3155
+ compilerVersion = await ccVersionOnce(driver.argv, toolchainEnv, true);
3156
+ }
3157
+ if (toolchainMetadataStamp === null &&
3158
+ root !== null &&
3159
+ toolchainMetadataKey !== null) {
3160
+ if (metadataCompiler !== null && metadataEffectiveCompiler !== null) {
3161
+ toolchainMetadataStamp = await publishNativeMetadataStamp(root, toolchainMetadataKey, { implicitToolchain, compilerVersion }, [
3162
+ metadataCompiler.canonicalPath,
3163
+ metadataEffectiveCompiler.canonicalPath,
3164
+ ...fingerprintDependencyPaths(implicitToolchain),
3165
+ ], [implicitToolchain]);
3166
+ }
3167
+ }
2639
3168
  }
2640
3169
  catch {
2641
3170
  // Cache discovery is best-effort. In particular, a compiler wrapper can
@@ -2792,22 +3321,27 @@ export async function compileC(opts) {
2792
3321
  ...(opts.http2 ?? false ? [rt(join(rtDir, "scr_http2.c"))] : []),
2793
3322
  ...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
2794
3323
  ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
3324
+ ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
2795
3325
  ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
2796
3326
  // The CA-store unit rides its own gate OR the tls one: scr_tls.c
2797
3327
  // references its default-set override unconditionally.
2798
- ...(opts.tlsCa || tls ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
3328
+ ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
2799
3329
  ...(tlsArchive
2800
3330
  ? [
2801
3331
  "-I", join(vendorTlsDir(), "include"),
2802
3332
  rt(join(rtDir, "scr_tls.c")),
2803
3333
  tlsArchive,
2804
- // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h) —
2805
- // an import the unconditional win32 libs above don't carry.
3334
+ // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
3335
+ // The unconditional win32 libs above do not carry it.
2806
3336
  // Never present on the default path, so the historical TLS
2807
3337
  // link line cannot change.
2808
3338
  ...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
2809
3339
  ]
2810
3340
  : []),
3341
+ // scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
3342
+ // This is independent of the mbedTLS archive: getCACertificates-only
3343
+ // programs need crypt32 too, while TLS programs imply the CA unit.
3344
+ ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
2811
3345
  // Static fetch is the engine-free half of scr_fetch.c. Dynamic builds
2812
3346
  // compile the same source beside scr_island.c below, where its
2813
3347
  // SCR_DYNAMIC half installs the full web surface.
@@ -2917,7 +3451,7 @@ export async function compileC(opts) {
2917
3451
  await execFileAsync(driver.argv[0] ?? "clang", [...driver.argv.slice(1), ...args]);
2918
3452
  }
2919
3453
  catch (err) {
2920
- const stderr = err.stderr ?? String(err);
3454
+ const stderr = subprocessFailureDetail(err);
2921
3455
  const guidance = (opts.linkInputs?.length ?? 0) > 0 ||
2922
3456
  (opts.systemLibraries?.length ?? 0) > 0
2923
3457
  ? "This build includes native FFI link inputs. Check that every symbol and system library exists, " +
@@ -2930,12 +3464,63 @@ export async function compileC(opts) {
2930
3464
  };
2931
3465
  let runtimeCompilerInvocation = null;
2932
3466
  let programCompilerInvocation = null;
3467
+ let payloadMetadata = null;
3468
+ let compileMetadataStamp = null;
3469
+ const compileMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3470
+ ? null
3471
+ : nativeMetadataKey("compile", [
3472
+ cacheTargetIdentity(driver),
3473
+ toolchainEnv,
3474
+ implicitToolchain ?? "<uncached>",
3475
+ driver.argv,
3476
+ cflags,
3477
+ programCompilerArgs,
3478
+ programSourceExtension,
3479
+ ]);
2933
3480
  if (root !== null) {
2934
3481
  try {
2935
- runtimeCompilerInvocation = await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags);
2936
- programCompilerInvocation = programSourceExtension === ".ll"
2937
- ? await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
2938
- : runtimeCompilerInvocation;
3482
+ // These probes inspect disjoint inputs. Start the payload reads here as
3483
+ // well so runtime hashing and clang's dry-run traces overlap instead of
3484
+ // forming a serial prelude before every cache lookup.
3485
+ payloadMetadata = Promise.all([
3486
+ Promise.resolve(toolchainMetadataStamp?.values["compilerVersion"] ??
3487
+ ccVersionOnce(driver.argv, toolchainEnv, true)),
3488
+ localArtifact === null
3489
+ ? runtimeFingerprint(rtDir)
3490
+ : Promise.resolve(localArtifact.runtimeHash),
3491
+ localArtifact === null
3492
+ ? readFile(opts.cPath)
3493
+ : Promise.resolve(localArtifact.programBytes),
3494
+ ]);
3495
+ compileMetadataStamp = compileMetadataKey === null
3496
+ ? null
3497
+ : await readNativeMetadataStamp(root, compileMetadataKey);
3498
+ if (compileMetadataStamp !== null) {
3499
+ runtimeCompilerInvocation = compileMetadataStamp.values["runtimeInvocation"] ?? null;
3500
+ programCompilerInvocation = compileMetadataStamp.values["programInvocation"] ?? null;
3501
+ if (runtimeCompilerInvocation === null || programCompilerInvocation === null) {
3502
+ compileMetadataStamp = null;
3503
+ }
3504
+ }
3505
+ if (compileMetadataStamp === null) {
3506
+ const [runtimeInvocation, programInvocation] = await Promise.all([
3507
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3508
+ programSourceExtension === ".ll"
3509
+ ? effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
3510
+ : Promise.resolve(null),
3511
+ ]);
3512
+ runtimeCompilerInvocation = runtimeInvocation;
3513
+ programCompilerInvocation = programInvocation ?? runtimeInvocation;
3514
+ if (compileMetadataKey !== null) {
3515
+ compileMetadataStamp = await publishNativeMetadataStamp(root, compileMetadataKey, {
3516
+ runtimeInvocation: runtimeCompilerInvocation,
3517
+ programInvocation: programCompilerInvocation,
3518
+ }, [
3519
+ ...fingerprintDependencyPaths(runtimeCompilerInvocation),
3520
+ ...fingerprintDependencyPaths(programCompilerInvocation),
3521
+ ], [runtimeCompilerInvocation, programCompilerInvocation]);
3522
+ }
3523
+ }
2939
3524
  }
2940
3525
  catch {
2941
3526
  // Preserve the uncached build for wrappers that compile successfully but
@@ -2960,11 +3545,11 @@ export async function compileC(opts) {
2960
3545
  let fingerprint;
2961
3546
  let cBytes;
2962
3547
  try {
2963
- [cv, fingerprint, cBytes] = await Promise.all([
3548
+ [cv, fingerprint, cBytes] = await (payloadMetadata ?? Promise.all([
2964
3549
  ccVersionOnce(driver.argv, toolchainEnv, true),
2965
3550
  runtimeFingerprint(rtDir),
2966
3551
  readFile(opts.cPath),
2967
- ]);
3552
+ ]));
2968
3553
  }
2969
3554
  catch {
2970
3555
  // A version/fingerprint probe is an optimization boundary. If the compiler
@@ -2983,16 +3568,6 @@ export async function compileC(opts) {
2983
3568
  (opts.linkInputs?.length ?? 0) === 0 &&
2984
3569
  (opts.systemLibraries?.length ?? 0) === 0;
2985
3570
  let programDependencies = null;
2986
- if (cacheCompleteArtifact) {
2987
- try {
2988
- programDependencies = await translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv);
2989
- }
2990
- catch {
2991
- // Program-header discovery is needed only for a complete hit. Runtime
2992
- // objects remain safe because they never contain the caller's TU.
2993
- cacheCompleteArtifact = false;
2994
- }
2995
- }
2996
3571
  const linkProbeArgs = [
2997
3572
  ...(sanitize ? ["-fsanitize=address"] : []),
2998
3573
  ...threadArgs,
@@ -3000,6 +3575,7 @@ export async function compileC(opts) {
3000
3575
  ? ["-ladvapi32", "-liphlpapi", "-lws2_32"]
3001
3576
  : []),
3002
3577
  ...(tls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
3578
+ ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
3003
3579
  ...(curlFetch && driver.target === null ? ["-lcurl"] : []),
3004
3580
  ...(dynamic && !driver.linkArgs.includes("-lm") ? ["-lm"] : []),
3005
3581
  ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null ? ["-lz"] : []),
@@ -3014,6 +3590,7 @@ export async function compileC(opts) {
3014
3590
  ? ["-ladvapi32", "-liphlpapi", "-lws2_32"]
3015
3591
  : []),
3016
3592
  ...(tls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
3593
+ ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
3017
3594
  ...(curlStubDir !== null ? [`-L${curlStubDir}`] : []),
3018
3595
  ...(curlFetch && driver.target === null ? ["-lcurl"] : []),
3019
3596
  ...(dynamic && !driver.linkArgs.includes("-lm") ? ["-lm"] : []),
@@ -3031,15 +3608,89 @@ export async function compileC(opts) {
3031
3608
  ? effectiveLinkInvocationArgs
3032
3609
  : effectiveLinkInvocationArgs.filter((arg) => arg !== `-L${curlStubDir}`);
3033
3610
  let implicitLinker = null;
3611
+ let preBuildDependencies = null;
3612
+ let localArtifactDependencyPaths = null;
3613
+ let linkMetadataStamp = null;
3614
+ const linkMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3615
+ ? null
3616
+ : nativeMetadataKey("link", [
3617
+ cacheTargetIdentity(driver),
3618
+ toolchainEnv,
3619
+ implicitToolchain ?? "<uncached>",
3620
+ runtimeCompilerInvocation ?? "<uncached>",
3621
+ programCompilerInvocation ?? "<uncached>",
3622
+ driver.argv,
3623
+ linkProbeArgs,
3624
+ effectiveLinkInvocationArgs,
3625
+ linkTraceInvocationArgs,
3626
+ ]);
3034
3627
  if (cacheCompleteArtifact) {
3035
3628
  try {
3036
- implicitLinker = await implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs);
3629
+ // Header discovery and linker tracing are independent subprocess trees.
3630
+ // Running them together removes one complete probe round-trip from both
3631
+ // cache hits and ordinary edit/build misses without changing either key.
3632
+ linkMetadataStamp = linkMetadataKey === null
3633
+ ? null
3634
+ : await readNativeMetadataStamp(root, linkMetadataKey);
3635
+ if (linkMetadataStamp !== null) {
3636
+ implicitLinker = linkMetadataStamp.values["implicitLinker"] ?? null;
3637
+ if (implicitLinker === null)
3638
+ linkMetadataStamp = null;
3639
+ }
3640
+ [programDependencies, implicitLinker] = await Promise.all([
3641
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv),
3642
+ linkMetadataStamp === null
3643
+ ? implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs)
3644
+ : Promise.resolve(implicitLinker),
3645
+ ]);
3646
+ if (linkMetadataStamp === null && linkMetadataKey !== null) {
3647
+ linkMetadataStamp = await publishNativeMetadataStamp(root, linkMetadataKey, { implicitLinker }, fingerprintDependencyPaths(implicitLinker), [implicitLinker]);
3648
+ }
3649
+ preBuildDependencies = await snapshotLocalArtifactDependencies([
3650
+ ...(await runtimeFingerprintInputPaths(rtDir)),
3651
+ ...(toolchainMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3652
+ fingerprintDependencyPaths(implicitToolchain)),
3653
+ ...(compileMetadataStamp?.dependencies.map((dependency) => dependency.path) ?? []),
3654
+ ...(linkMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3655
+ fingerprintDependencyPaths(implicitLinker)),
3656
+ ...fingerprintDependencyPaths(programDependencies),
3657
+ ]);
3658
+ // Every content-bearing fingerprint was computed before this metadata
3659
+ // snapshot. Re-read those exact files now so the snapshot cannot certify
3660
+ // bytes that changed after hashing but before the final compile starts.
3661
+ if (!(await fingerprintDependenciesStillMatch([
3662
+ implicitToolchain,
3663
+ runtimeCompilerInvocation,
3664
+ programCompilerInvocation,
3665
+ implicitLinker,
3666
+ programDependencies,
3667
+ ])) ||
3668
+ await runtimeFingerprint(rtDir).catch(() => null) !== fingerprint ||
3669
+ !(await Promise.all([toolchainMetadataStamp, compileMetadataStamp, linkMetadataStamp]
3670
+ .filter((stamp) => stamp !== null)
3671
+ .map((stamp) => localArtifactDependenciesStillMatch(stamp.dependencies)))).every(Boolean)) {
3672
+ throw new CacheInputsChangedError();
3673
+ }
3674
+ if (localArtifact !== null) {
3675
+ // Native metadata stamps persist their dependency paths across CLI
3676
+ // processes; fingerprintDependencyPaths deliberately does not. Build
3677
+ // the output-local stamp from this complete validated snapshot so a
3678
+ // source edit in a fresh process cannot replace it with only the two
3679
+ // process-local fallback paths.
3680
+ localArtifactDependencyPaths = [
3681
+ localArtifact.compilerPath,
3682
+ dirname(resolve(opts.cPath)),
3683
+ ...preBuildDependencies.map((dependency) => dependency.path),
3684
+ ];
3685
+ }
3037
3686
  }
3038
3687
  catch {
3039
- // Some compiler wrappers/linkers do not implement trace mode. Runtime
3040
- // objects remain safely cacheable, but a complete executable cannot be
3041
- // keyed without knowing every implicit link input.
3688
+ // Program-header discovery and linker tracing are both required for a
3689
+ // complete hit. Runtime objects remain safely cacheable if either probe
3690
+ // is unavailable.
3042
3691
  cacheCompleteArtifact = false;
3692
+ preBuildDependencies = null;
3693
+ localArtifactDependencyPaths = null;
3043
3694
  }
3044
3695
  }
3045
3696
  const binDir = join(root, "bin");
@@ -3092,6 +3743,9 @@ export async function compileC(opts) {
3092
3743
  // cache entry populated by a less restrictive shell must not widen access.
3093
3744
  await chmod(tmpOut, 0o777 & ~process.umask());
3094
3745
  await rename(tmpOut, opts.outPath);
3746
+ if (localArtifact !== null && localArtifactDependencyPaths !== null) {
3747
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3748
+ }
3095
3749
  return; // hit: the program/runtime payload compile and link were skipped
3096
3750
  }
3097
3751
  catch {
@@ -3127,18 +3781,25 @@ export async function compileC(opts) {
3127
3781
  await writeFile(programPath, cBytes);
3128
3782
  let objects = null;
3129
3783
  let cacheInputsStable = true;
3130
- let objectImplicitVerification = null;
3784
+ let strictObjectVerification = null;
3131
3785
  const objectImplicitToolchainStillMatches = () => {
3132
- objectImplicitVerification ??= Promise.all([
3786
+ if (preBuildDependencies !== null) {
3787
+ return localArtifactDependenciesStillMatch(preBuildDependencies);
3788
+ }
3789
+ // Complete-artifact caching can be disabled by caller-owned native
3790
+ // inputs while the safe runtime-object tier remains active. Preserve its
3791
+ // strict discovery fallback in that posture.
3792
+ strictObjectVerification ??= Promise.all([
3793
+ runtimeFingerprint(rtDir),
3133
3794
  implicitToolchainFingerprint(driver, toolchainEnv),
3134
3795
  effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3135
- ]).then(([currentImplicit, currentInvocation]) => currentImplicit === implicitToolchain &&
3796
+ ]).then(([currentRuntime, currentImplicit, currentInvocation]) => currentRuntime === fingerprint &&
3797
+ currentImplicit === implicitToolchain &&
3136
3798
  currentInvocation === runtimeCompilerInvocation, () => false);
3137
- return objectImplicitVerification;
3799
+ return strictObjectVerification;
3138
3800
  };
3139
3801
  try {
3140
- 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 &&
3141
- (await objectImplicitToolchainStillMatches()));
3802
+ 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());
3142
3803
  objects = await stageRuntimeObjects(cached, join(buildDir, "runtime-objects"));
3143
3804
  }
3144
3805
  catch (err) {
@@ -3149,10 +3810,11 @@ export async function compileC(opts) {
3149
3810
  await runClang(buildArgs((p) => objects?.get(p) ?? p, { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }));
3150
3811
  await installArtifact(privateOut, opts.outPath);
3151
3812
  if (cachedBin !== null && keyHex !== null) {
3152
- // This check is deliberately fresh rather than the object-publication
3153
- // verification above. The final program compile/link can itself race an
3154
- // SDK header, compiler, linker, CRT, or system-library replacement; its
3155
- // bytes must not be published under the pre-build identity in that case.
3813
+ // Metadata comparison catches ordinary changes cheaply, but cannot by
3814
+ // itself prove the snapshot was taken from the same bytes hashed into
3815
+ // the key. Recompute every content-bearing identity after the final link
3816
+ // so a header/SDK/compiler change in either pre-build gap cannot publish
3817
+ // new output under an old key.
3156
3818
  const [currentRuntime, currentImplicit, currentRuntimeInvocation, currentProgramInvocation, currentProgramDependencies, currentLinker, currentCompiler] = await Promise.all([
3157
3819
  runtimeFingerprint(rtDir).catch(() => null),
3158
3820
  implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
@@ -3164,6 +3826,8 @@ export async function compileC(opts) {
3164
3826
  ]);
3165
3827
  cacheInputsStable =
3166
3828
  cacheInputsStable &&
3829
+ preBuildDependencies !== null &&
3830
+ await localArtifactDependenciesStillMatch(preBuildDependencies) &&
3167
3831
  currentRuntime === fingerprint &&
3168
3832
  currentImplicit === implicitToolchain &&
3169
3833
  currentRuntimeInvocation === runtimeCompilerInvocation &&
@@ -3183,6 +3847,12 @@ export async function compileC(opts) {
3183
3847
  /* publishing is best-effort */
3184
3848
  }
3185
3849
  }
3850
+ if (localArtifact !== null &&
3851
+ localArtifactDependencyPaths !== null &&
3852
+ cacheCompleteArtifact &&
3853
+ cacheInputsStable) {
3854
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3855
+ }
3186
3856
  }
3187
3857
  finally {
3188
3858
  await rm(buildDir, { recursive: true, force: true }).catch(() => undefined);