@scriptc/compiler 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +78 -34
  2. package/dist/backend/cc-cache.test.d.ts +1 -0
  3. package/dist/backend/cc-cache.test.js +2507 -0
  4. package/dist/backend/cc-cache.test.js.map +1 -0
  5. package/dist/backend/cc.d.ts +69 -11
  6. package/dist/backend/cc.js +1937 -222
  7. package/dist/backend/cc.js.map +1 -1
  8. package/dist/backend/emission/emit-exprs.js +355 -8
  9. package/dist/backend/emission/emit-exprs.js.map +1 -1
  10. package/dist/backend/emission/emit-walkers.js +108 -7
  11. package/dist/backend/emission/emit-walkers.js.map +1 -1
  12. package/dist/backend/emission/emitter.d.ts +16 -0
  13. package/dist/backend/emission/emitter.js +23 -2
  14. package/dist/backend/emission/emitter.js.map +1 -1
  15. package/dist/backend/llvm/dyn.d.ts +1 -0
  16. package/dist/backend/llvm/dyn.js +305 -8
  17. package/dist/backend/llvm/dyn.js.map +1 -1
  18. package/dist/backend/llvm/emitter.js +527 -2
  19. package/dist/backend/llvm/emitter.js.map +1 -1
  20. package/dist/compat/fetch-profile.d.ts +108 -0
  21. package/dist/compat/fetch-profile.js +482 -0
  22. package/dist/compat/fetch-profile.js.map +1 -0
  23. package/dist/coverage/surface-manifest.js +54 -0
  24. package/dist/coverage/surface-manifest.js.map +1 -1
  25. package/dist/frontend/lowering/lower-builtins.d.ts +21 -8
  26. package/dist/frontend/lowering/lower-builtins.js +137 -14
  27. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  28. package/dist/frontend/lowering/lower-calls.js +40 -0
  29. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  30. package/dist/frontend/lowering/lower-classes.js +4 -3
  31. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  32. package/dist/frontend/lowering/lower-exprs.d.ts +2 -2
  33. package/dist/frontend/lowering/lower-exprs.js +63 -12
  34. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  35. package/dist/frontend/lowering/lower-island.d.ts +102 -7
  36. package/dist/frontend/lowering/lower-island.js +1702 -26
  37. package/dist/frontend/lowering/lower-island.js.map +1 -1
  38. package/dist/frontend/lowering/lower-modules.js +17 -1
  39. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  40. package/dist/frontend/lowering/lower-stmts.js +25 -5
  41. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  42. package/dist/frontend/lowering/lowerer.d.ts +39 -0
  43. package/dist/frontend/lowering/lowerer.js +286 -18
  44. package/dist/frontend/lowering/lowerer.js.map +1 -1
  45. package/dist/frontend/lowering/surfaces.js +12 -0
  46. package/dist/frontend/lowering/surfaces.js.map +1 -1
  47. package/dist/frontend/program.d.ts +16 -0
  48. package/dist/frontend/program.js +130 -6
  49. package/dist/frontend/program.js.map +1 -1
  50. package/dist/frontend/types.d.ts +14 -7
  51. package/dist/frontend/types.js +95 -31
  52. package/dist/frontend/types.js.map +1 -1
  53. package/dist/index.d.ts +7 -1
  54. package/dist/index.js +70 -14
  55. package/dist/index.js.map +1 -1
  56. package/dist/ir/nodes.d.ts +32 -16
  57. package/dist/ir/nodes.js +36 -28
  58. package/dist/ir/nodes.js.map +1 -1
  59. package/dist/ir/validate.js +61 -1
  60. package/dist/ir/validate.js.map +1 -1
  61. package/dist/library/int-infer.js +7 -1
  62. package/dist/library/int-infer.js.map +1 -1
  63. package/dist/library/int-infer.test.js +80 -0
  64. package/dist/library/int-infer.test.js.map +1 -1
  65. package/dist/library/sidecar-validate.js +5 -0
  66. package/dist/library/sidecar-validate.js.map +1 -1
  67. package/dist/library/sidecar.d.ts +3 -0
  68. package/dist/library/sidecar.js.map +1 -1
  69. package/package.json +2 -2
  70. package/surface-manifest.json +859 -1
@@ -0,0 +1,2507 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { chmod, cp, mkdir, mkdtemp, readdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { delimiter, join } from "node:path";
7
+ import { afterAll, afterEach, expect, test as vitestTest } from "vitest";
8
+ import { cacheTargetIdentity, ccVersionOnce, compileC, compileLibArchive, implicitDependencyProbeIncludes, parseLinkTraceFiles, resolveBuildCacheRoot, runtimeFingerprint, runtimeSrcDir, stageRuntimeObjects, toolchainEnvironmentCachePolicy, toolchainEnvironmentFingerprint, vendorCacheBuildIdentity, vendorCacheTargetFlavor, } from "./cc.js";
9
+ const scratch = [];
10
+ const TEST_CACHE_IDENTITY = "cc-cache-tests-v1";
11
+ const originalStableToolchain = process.env["SCRIPTC_TEST_STABLE_TOOLCHAIN"];
12
+ // This file is the strict-path contract: its fixtures replace wrappers,
13
+ // headers, SDK selectors, and native inputs in place between invocations.
14
+ // The rest of the immutable differential harness may memoize those probes.
15
+ delete process.env["SCRIPTC_TEST_STABLE_TOOLCHAIN"];
16
+ const originalTrustedCompilerWrapper = process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"];
17
+ /** The cache suite mutates process-wide compiler state and must stay serial
18
+ * within one worker, but CI/Sandbox workers can safely divide its independent
19
+ * tests across processes. Unset runs the complete file for ordinary focused
20
+ * use; i/n uses the same stable hash partition as the corpus harness. */
21
+ function cacheTestSelected(name) {
22
+ const spec = process.env["SCRIPTC_CACHE_TEST_SHARD"];
23
+ if (spec === undefined || spec === "")
24
+ return true;
25
+ const match = /^(\d+)\/(\d+)$/.exec(spec);
26
+ if (match === null)
27
+ throw new Error(`invalid SCRIPTC_CACHE_TEST_SHARD '${spec}' (expected i/n)`);
28
+ const index = Number(match[1]);
29
+ const total = Number(match[2]);
30
+ if (index < 1 || total < 1 || index > total) {
31
+ throw new Error(`invalid SCRIPTC_CACHE_TEST_SHARD '${spec}' (expected 1 <= i <= n)`);
32
+ }
33
+ return createHash("sha1").update(name).digest().readUInt32BE(0) % total === index - 1;
34
+ }
35
+ const test = Object.assign((name, ...args) => Reflect.apply(cacheTestSelected(name) ? vitestTest : vitestTest.skip, undefined, [name, ...args]), {
36
+ skipIf: (condition) => (name, ...args) => Reflect.apply(condition || !cacheTestSelected(name) ? vitestTest.skip : vitestTest, undefined, [
37
+ name,
38
+ ...args,
39
+ ]),
40
+ });
41
+ const trustInstrumentedCompilerWrapper = () => {
42
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] = "1";
43
+ };
44
+ const zigExecutable = (process.env["PATH"] ?? "")
45
+ .split(delimiter)
46
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "zig"))
47
+ .find((candidate) => existsSync(candidate));
48
+ const completeArtifacts = async (root, kind) => (await readdir(join(root, kind))).filter((name) => !name.endsWith(".sha256"));
49
+ const cacheTreeBytes = async (root) => {
50
+ let total = 0;
51
+ const walk = async (directory) => {
52
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
53
+ const path = join(directory, entry.name);
54
+ if (entry.isDirectory())
55
+ await walk(path);
56
+ else if (entry.isFile())
57
+ total += (await stat(path)).size;
58
+ }
59
+ };
60
+ await walk(root);
61
+ return total;
62
+ };
63
+ afterAll(async () => {
64
+ await Promise.all(scratch.map((dir) => rm(dir, { recursive: true, force: true })));
65
+ if (originalStableToolchain === undefined) {
66
+ delete process.env["SCRIPTC_TEST_STABLE_TOOLCHAIN"];
67
+ }
68
+ else {
69
+ process.env["SCRIPTC_TEST_STABLE_TOOLCHAIN"] = originalStableToolchain;
70
+ }
71
+ });
72
+ afterEach(() => {
73
+ if (originalTrustedCompilerWrapper === undefined) {
74
+ delete process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"];
75
+ }
76
+ else {
77
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] = originalTrustedCompilerWrapper;
78
+ }
79
+ });
80
+ test("the production cache root follows overrides, platform defaults, and the hard disable", () => {
81
+ expect(resolveBuildCacheRoot({ SCRIPTC_NO_CACHE: "1" }, "linux", "/home/tester")).toBeNull();
82
+ expect(resolveBuildCacheRoot({ SCRIPTC_CACHE_DIR: "" }, "linux", "/home/tester")).toBeNull();
83
+ expect(resolveBuildCacheRoot({ SCRIPTC_CACHE_DIR: "/var/tmp/custom" }, "linux", "/home/tester")).toBe("/var/tmp/custom");
84
+ expect(resolveBuildCacheRoot({ XDG_CACHE_HOME: "/var/tmp/xdg" }, "linux", "/home/tester")).toBe("/var/tmp/xdg/scriptc/build");
85
+ expect(resolveBuildCacheRoot({}, "darwin", "/Users/tester")).toBe("/Users/tester/Library/Caches/scriptc/build");
86
+ expect(resolveBuildCacheRoot({ LOCALAPPDATA: "/Users/tester/AppData/Local" }, "win32", "/Users/tester")).toBe("/Users/tester/AppData/Local/scriptc/cache/build");
87
+ });
88
+ test("native cache identities separate host architectures while cross targets remain explicit", () => {
89
+ expect(cacheTargetIdentity({ target: null }, "darwin", "arm64")).toBe("native:darwin:arm64");
90
+ expect(cacheTargetIdentity({ target: null }, "darwin", "x64")).toBe("native:darwin:x64");
91
+ expect(cacheTargetIdentity({ target: "x86_64-linux-gnu.2.36" }, "darwin", "arm64")).toBe("cross:x86_64-linux-gnu.2.36");
92
+ expect(vendorCacheTargetFlavor({ target: null }, "darwin", "arm64")).toBe("native-darwin-arm64");
93
+ expect(vendorCacheTargetFlavor({ target: null }, "darwin", "x64")).toBe("native-darwin-x64");
94
+ expect(vendorCacheTargetFlavor({ target: "x86_64-linux-gnu.2.36" }, "darwin", "arm64")).toBe("x86_64-linux-gnu.2.36");
95
+ });
96
+ test("Zig COFF dry-run parsing retains every linker input on its single command line", async () => {
97
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-link-trace-"));
98
+ scratch.push(dir);
99
+ const probeDir = join(dir, "probe");
100
+ const first = join(dir, "crt2.obj");
101
+ const second = join(dir, "compiler_rt.lib");
102
+ const third = join(dir, "kernel32.lib");
103
+ await mkdir(probeDir);
104
+ await Promise.all([
105
+ writeFile(join(probeDir, "empty.o"), "probe"),
106
+ writeFile(first, "first"),
107
+ writeFile(second, "second"),
108
+ writeFile(third, "third"),
109
+ ]);
110
+ const output = [
111
+ "lld-link",
112
+ `\"${join(probeDir, "empty.o")}\"`,
113
+ `\"${first}\"`,
114
+ `\"${second}\"`,
115
+ `\"${third}\"`,
116
+ ].join(" ");
117
+ expect(await parseLinkTraceFiles(output, probeDir, probeDir, true)).toEqual([first, second, third].sort());
118
+ });
119
+ test("the toolchain environment joins cache identities", () => {
120
+ const base = toolchainEnvironmentFingerprint({ PATH: "/usr/bin", CPATH: "/headers/one" });
121
+ expect(toolchainEnvironmentFingerprint({ PATH: "/usr/bin", CPATH: "/headers/two" })).not.toBe(base);
122
+ expect(toolchainEnvironmentFingerprint({ ZIG_LIB_DIR: "/zig/one" })).not.toBe(toolchainEnvironmentFingerprint({ ZIG_LIB_DIR: "/zig/two" }));
123
+ expect(toolchainEnvironmentFingerprint({ ZIG_LIBC: "/libc/one.conf" })).not.toBe(toolchainEnvironmentFingerprint({ ZIG_LIBC: "/libc/two.conf" }));
124
+ // PATH is deliberately absent from this generic environment hash: the
125
+ // resolved executable identity is keyed separately, while the compiler
126
+ // must still resolve on every cache-enabled call.
127
+ expect(toolchainEnvironmentFingerprint({ PATH: "", CPATH: "/headers/one" })).toBe(base);
128
+ expect(toolchainEnvironmentFingerprint({
129
+ PATH: "/usr/bin",
130
+ CPATH: "/headers/one",
131
+ SCRIPTC_CACHE_MAX_MB: "1",
132
+ })).toBe(base);
133
+ expect(toolchainEnvironmentCachePolicy({ MACOSX_DEPLOYMENT_TARGET: "14.0" })).toEqual({
134
+ completeArtifacts: true,
135
+ runtimeObjects: true,
136
+ });
137
+ expect(toolchainEnvironmentCachePolicy({ LIBRARY_PATH: "/libraries" })).toEqual({
138
+ completeArtifacts: false,
139
+ runtimeObjects: true,
140
+ });
141
+ expect(toolchainEnvironmentCachePolicy({ CPATH: "/headers" })).toEqual({
142
+ completeArtifacts: false,
143
+ runtimeObjects: false,
144
+ });
145
+ expect(toolchainEnvironmentCachePolicy({ CFLAGS: "-I/headers" })).toEqual({
146
+ completeArtifacts: false,
147
+ runtimeObjects: false,
148
+ });
149
+ expect(toolchainEnvironmentCachePolicy({ ZIG_LIB_DIR: "/zig/lib" })).toEqual({
150
+ completeArtifacts: false,
151
+ runtimeObjects: false,
152
+ });
153
+ expect(toolchainEnvironmentCachePolicy({ ZIG_LIBC: "/zig/libc.conf" })).toEqual({
154
+ completeArtifacts: false,
155
+ runtimeObjects: false,
156
+ });
157
+ expect(vendorCacheBuildIdentity(base, "compiler-one")).not.toBe(vendorCacheBuildIdentity(base, "compiler-two"));
158
+ expect(vendorCacheBuildIdentity(base, "compiler-one")).not.toBe(vendorCacheBuildIdentity(toolchainEnvironmentFingerprint({ MACOSX_DEPLOYMENT_TARGET: "11.0" }), "compiler-one"));
159
+ });
160
+ test.skipIf(process.platform === "win32")("compiler version probes use and remove a private working directory", async () => {
161
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-version-probe-"));
162
+ scratch.push(dir);
163
+ const probeTmp = join(dir, "tmp");
164
+ const fakeCompiler = join(dir, "fake-zig");
165
+ await mkdir(probeTmp);
166
+ await writeFile(fakeCompiler, `#!/bin/sh
167
+ : > a.o
168
+ printf 'fake zig cc version probe\n'
169
+ `);
170
+ await chmod(fakeCompiler, 0o755);
171
+ const oldTmpdir = process.env["TMPDIR"];
172
+ try {
173
+ process.env["TMPDIR"] = probeTmp;
174
+ // Zig 0.16 creates cwd/a.o for `zig cc --version`. Model that side
175
+ // effect directly so this regression does not depend on Zig being
176
+ // installed in every generic test shard.
177
+ await ccVersionOnce([fakeCompiler, "cc"], `version-probe-${dir}`);
178
+ expect(await readdir(probeTmp)).toEqual([]);
179
+ }
180
+ finally {
181
+ if (oldTmpdir === undefined)
182
+ delete process.env["TMPDIR"];
183
+ else
184
+ process.env["TMPDIR"] = oldTmpdir;
185
+ }
186
+ });
187
+ test("the runtime fingerprint includes the textually included Ryū sources", async () => {
188
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-runtime-fingerprint-"));
189
+ scratch.push(dir);
190
+ const rtDir = join(dir, "src");
191
+ const ryuDir = join(dir, "vendor", "ryu");
192
+ await Promise.all([mkdir(rtDir, { recursive: true }), mkdir(ryuDir, { recursive: true })]);
193
+ const pinnedTime = new Date("2000-01-01T00:00:00.000Z");
194
+ await Promise.all([
195
+ writeFile(join(rtDir, "scr_number.c"), '#include "../vendor/ryu/d2s.c"\n'),
196
+ writeFile(join(ryuDir, "d2s.c"), "int ryu_probe = 1;\n"),
197
+ ]);
198
+ await utimes(join(ryuDir, "d2s.c"), pinnedTime, pinnedTime);
199
+ const first = await runtimeFingerprint(rtDir);
200
+ // Timestamp-preserving sync/copy tools can replace bytes without changing
201
+ // the old stat-memo signature. Content, not metadata, owns this identity.
202
+ await writeFile(join(ryuDir, "d2s.c"), "int ryu_probe = 2;\n");
203
+ await utimes(join(ryuDir, "d2s.c"), pinnedTime, pinnedTime);
204
+ expect(await runtimeFingerprint(rtDir)).not.toBe(first);
205
+ });
206
+ test("implicit dependency seeds include separately compiled vendor system headers", async () => {
207
+ const includes = await implicitDependencyProbeIncludes(runtimeSrcDir());
208
+ // zlib's crc32.c is compiled independently and is not textually reachable
209
+ // from packages/runtime/src. Its ambient C-library dependency still belongs
210
+ // to the native object/cache identity.
211
+ expect(includes).toContain("<stdatomic.h>");
212
+ });
213
+ test.skipIf(process.platform === "win32")("opaque compiler wrappers bypass persistent caches", async () => {
214
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-compiler-wrapper-"));
215
+ scratch.push(dir);
216
+ const cacheRoot = join(dir, "cache");
217
+ const binDir = join(dir, "bin");
218
+ const helperSource = join(dir, "helper.c");
219
+ const helperObject = join(dir, "helper.o");
220
+ const cPath = join(dir, "program.c");
221
+ const outPath = join(dir, "program");
222
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
223
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
224
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
225
+ const oldPath = process.env["PATH"];
226
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
227
+ const oldProgramSource = process.env["SCRIPTC_TEST_PROGRAM_SOURCE"];
228
+ const oldWrapperObject = process.env["SCRIPTC_TEST_WRAPPER_OBJECT"];
229
+ const originalClang = (oldPath ?? "")
230
+ .split(delimiter)
231
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
232
+ .find((candidate) => existsSync(candidate));
233
+ expect(originalClang).toBeDefined();
234
+ const buildHelper = async (value) => {
235
+ await writeFile(helperSource, `int scriptc_wrapper_helper(void) { return ${value}; }\n`);
236
+ execFileSync(originalClang, ["-c", helperSource, "-o", helperObject]);
237
+ };
238
+ try {
239
+ await mkdir(binDir);
240
+ const wrapper = join(binDir, "clang");
241
+ await writeFile(wrapper, `#!/bin/sh
242
+ link=1
243
+ real_build=0
244
+ for arg in "$@"; do
245
+ case "$arg" in
246
+ -c|-M|-MM|-E|-S) link=0 ;;
247
+ "$SCRIPTC_TEST_PROGRAM_SOURCE"|*/scriptc-cache-build-*/program.c) real_build=1 ;;
248
+ esac
249
+ done
250
+ if [ "$link" = 1 ] && [ "$real_build" = 1 ]; then
251
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$SCRIPTC_TEST_WRAPPER_OBJECT" "$@"
252
+ fi
253
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
254
+ `);
255
+ await chmod(wrapper, 0o755);
256
+ await writeFile(cPath, '#include <stdio.h>\nint scriptc_wrapper_helper(void);\nint main(void) { printf("%d\\n", scriptc_wrapper_helper()); return 0; }\n');
257
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
258
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
259
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = originalClang;
260
+ process.env["SCRIPTC_TEST_PROGRAM_SOURCE"] = cPath;
261
+ process.env["SCRIPTC_TEST_WRAPPER_OBJECT"] = helperObject;
262
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
263
+ delete process.env["SCRIPTC_NO_CACHE"];
264
+ await buildHelper(1);
265
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
266
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("1");
267
+ // The wrapper injects this same-path object only for the actual program
268
+ // link, not for scriptc's synthetic metadata probes. Reusing the first
269
+ // complete artifact here would return the stale value.
270
+ await buildHelper(2);
271
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
272
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("2");
273
+ await expect(stat(cacheRoot)).rejects.toMatchObject({ code: "ENOENT" });
274
+ }
275
+ finally {
276
+ if (oldCacheDir === undefined)
277
+ delete process.env["SCRIPTC_CACHE_DIR"];
278
+ else
279
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
280
+ if (oldNoCache === undefined)
281
+ delete process.env["SCRIPTC_NO_CACHE"];
282
+ else
283
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
284
+ if (oldDisableCcache === undefined)
285
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
286
+ else
287
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
288
+ if (oldPath === undefined)
289
+ delete process.env["PATH"];
290
+ else
291
+ process.env["PATH"] = oldPath;
292
+ if (oldRealClang === undefined)
293
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
294
+ else
295
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
296
+ if (oldProgramSource === undefined)
297
+ delete process.env["SCRIPTC_TEST_PROGRAM_SOURCE"];
298
+ else
299
+ process.env["SCRIPTC_TEST_PROGRAM_SOURCE"] = oldProgramSource;
300
+ if (oldWrapperObject === undefined)
301
+ delete process.env["SCRIPTC_TEST_WRAPPER_OBJECT"];
302
+ else
303
+ process.env["SCRIPTC_TEST_WRAPPER_OBJECT"] = oldWrapperObject;
304
+ }
305
+ });
306
+ test.skipIf(process.platform === "win32")("opaque archiver wrappers rebuild library archives while retaining runtime objects", async () => {
307
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-archiver-wrapper-"));
308
+ scratch.push(dir);
309
+ const cacheRoot = join(dir, "cache");
310
+ const binDir = join(dir, "bin");
311
+ const helperSource = join(dir, "helper.c");
312
+ const helperObject = join(dir, "helper.o");
313
+ const cPath = join(dir, "program.c");
314
+ const archivePath = join(dir, "program.lib.a");
315
+ const probeSource = join(dir, "probe.c");
316
+ const probePath = join(dir, "probe");
317
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
318
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
319
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
320
+ const oldPath = process.env["PATH"];
321
+ const oldRealAr = process.env["SCRIPTC_TEST_REAL_AR"];
322
+ const oldArExtra = process.env["SCRIPTC_TEST_AR_EXTRA"];
323
+ const originalClang = (oldPath ?? "")
324
+ .split(delimiter)
325
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
326
+ .find((candidate) => existsSync(candidate));
327
+ const originalAr = (oldPath ?? "")
328
+ .split(delimiter)
329
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "ar"))
330
+ .find((candidate) => existsSync(candidate));
331
+ expect(originalClang).toBeDefined();
332
+ expect(originalAr).toBeDefined();
333
+ const buildHelper = async (value) => {
334
+ await writeFile(helperSource, `int scriptc_archiver_helper(void) { return ${value}; }\n`);
335
+ execFileSync(originalClang, ["-c", helperSource, "-o", helperObject]);
336
+ };
337
+ const runProbe = () => {
338
+ execFileSync(originalClang, [probeSource, archivePath, "-o", probePath]);
339
+ return execFileSync(probePath, { encoding: "utf8" }).trim();
340
+ };
341
+ try {
342
+ await mkdir(binDir);
343
+ await writeFile(join(binDir, "ar"), `#!/bin/sh
344
+ if [ "$1" = "--version" ]; then
345
+ exec "$SCRIPTC_TEST_REAL_AR" "$@"
346
+ fi
347
+ exec "$SCRIPTC_TEST_REAL_AR" "$@" "$SCRIPTC_TEST_AR_EXTRA"
348
+ `);
349
+ await chmod(join(binDir, "ar"), 0o755);
350
+ await writeFile(cPath, "int scriptc_program_member = 1;\n");
351
+ await writeFile(probeSource, '#include <stdio.h>\nint scriptc_archiver_helper(void);\nint main(void) { printf("%d\\n", scriptc_archiver_helper()); return 0; }\n');
352
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
353
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
354
+ process.env["SCRIPTC_TEST_REAL_AR"] = originalAr;
355
+ process.env["SCRIPTC_TEST_AR_EXTRA"] = helperObject;
356
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
357
+ delete process.env["SCRIPTC_NO_CACHE"];
358
+ await buildHelper(1);
359
+ await compileLibArchive({
360
+ cPath,
361
+ outPath: archivePath,
362
+ cacheIdentity: TEST_CACHE_IDENTITY,
363
+ });
364
+ expect(runProbe()).toBe("1");
365
+ const objectSets = (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
366
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
367
+ expect(objectSets).toHaveLength(1);
368
+ await expect(stat(join(cacheRoot, "lib"))).rejects.toMatchObject({ code: "ENOENT" });
369
+ // The wrapper executable/version and every scriptc-owned input remain
370
+ // unchanged. Only the mutable object it injects into `ar rcs` changes.
371
+ // A complete archive hit would silently retain the first definition.
372
+ await buildHelper(2);
373
+ await compileLibArchive({
374
+ cPath,
375
+ outPath: archivePath,
376
+ cacheIdentity: TEST_CACHE_IDENTITY,
377
+ });
378
+ expect(runProbe()).toBe("2");
379
+ expect((await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
380
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"))).toHaveLength(1);
381
+ await expect(stat(join(cacheRoot, "lib"))).rejects.toMatchObject({ code: "ENOENT" });
382
+ }
383
+ finally {
384
+ if (oldCacheDir === undefined)
385
+ delete process.env["SCRIPTC_CACHE_DIR"];
386
+ else
387
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
388
+ if (oldNoCache === undefined)
389
+ delete process.env["SCRIPTC_NO_CACHE"];
390
+ else
391
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
392
+ if (oldDisableCcache === undefined)
393
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
394
+ else
395
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
396
+ if (oldPath === undefined)
397
+ delete process.env["PATH"];
398
+ else
399
+ process.env["PATH"] = oldPath;
400
+ if (oldRealAr === undefined)
401
+ delete process.env["SCRIPTC_TEST_REAL_AR"];
402
+ else
403
+ process.env["SCRIPTC_TEST_REAL_AR"] = oldRealAr;
404
+ if (oldArExtra === undefined)
405
+ delete process.env["SCRIPTC_TEST_AR_EXTRA"];
406
+ else
407
+ process.env["SCRIPTC_TEST_AR_EXTRA"] = oldArExtra;
408
+ }
409
+ });
410
+ test.skipIf(process.platform === "win32")("build-flavor-specific compiler-wrapper flags join the cache identity", async () => {
411
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-wrapper-env-"));
412
+ scratch.push(dir);
413
+ const cacheRoot = join(dir, "cache");
414
+ const binDir = join(dir, "bin");
415
+ const cPath = join(dir, "program.c");
416
+ const outPath = join(dir, "program");
417
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
418
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
419
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
420
+ const oldPath = process.env["PATH"];
421
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
422
+ const oldWrapperValue = process.env["SCRIPTC_TEST_WRAPPER_VALUE"];
423
+ const realClang = (oldPath ?? "")
424
+ .split(delimiter)
425
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
426
+ .find((candidate) => existsSync(candidate));
427
+ expect(realClang).toBeDefined();
428
+ try {
429
+ await mkdir(binDir);
430
+ const wrapper = join(binDir, "clang");
431
+ await writeFile(wrapper, `#!/bin/sh
432
+ if [ "$1" = "--version" ]; then
433
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
434
+ fi
435
+ inject=0
436
+ for arg in "$@"; do
437
+ if [ "$arg" = "-O2" ]; then inject=1; fi
438
+ done
439
+ if [ "$inject" = 1 ]; then
440
+ exec "$SCRIPTC_TEST_REAL_CLANG" "-DSCRIPTC_WRAPPER_VALUE=$SCRIPTC_TEST_WRAPPER_VALUE" "$@"
441
+ fi
442
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
443
+ `);
444
+ await chmod(wrapper, 0o755);
445
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { printf("%d\\n", SCRIPTC_WRAPPER_VALUE); return 0; }\n');
446
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
447
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
448
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
449
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
450
+ trustInstrumentedCompilerWrapper();
451
+ delete process.env["SCRIPTC_NO_CACHE"];
452
+ process.env["SCRIPTC_TEST_WRAPPER_VALUE"] = "1";
453
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
454
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("1");
455
+ // This variable is intentionally absent from scriptc's fixed environment
456
+ // allowlist, and the wrapper injects it only for the real -O2 build
457
+ // flavor. A generic metadata-probe trace must not hide the change.
458
+ process.env["SCRIPTC_TEST_WRAPPER_VALUE"] = "2";
459
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
460
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("2");
461
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
462
+ const objectSets = (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
463
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
464
+ expect(objectSets).toHaveLength(2);
465
+ }
466
+ finally {
467
+ if (oldCacheDir === undefined)
468
+ delete process.env["SCRIPTC_CACHE_DIR"];
469
+ else
470
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
471
+ if (oldNoCache === undefined)
472
+ delete process.env["SCRIPTC_NO_CACHE"];
473
+ else
474
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
475
+ if (oldDisableCcache === undefined)
476
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
477
+ else
478
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
479
+ if (oldPath === undefined)
480
+ delete process.env["PATH"];
481
+ else
482
+ process.env["PATH"] = oldPath;
483
+ if (oldRealClang === undefined)
484
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
485
+ else
486
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
487
+ if (oldWrapperValue === undefined)
488
+ delete process.env["SCRIPTC_TEST_WRAPPER_VALUE"];
489
+ else
490
+ process.env["SCRIPTC_TEST_WRAPPER_VALUE"] = oldWrapperValue;
491
+ }
492
+ });
493
+ test.skipIf(process.platform === "win32")("build-flavor-specific compiler-wrapper headers key runtime objects by content", async () => {
494
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-wrapper-header-"));
495
+ scratch.push(dir);
496
+ const cacheRoot = join(dir, "cache");
497
+ const binDir = join(dir, "bin");
498
+ const fakeRuntime = join(dir, "runtime", "src");
499
+ const header = join(dir, "wrapper-input.h");
500
+ const cPath = join(dir, "program.c");
501
+ const outPath = join(dir, "program");
502
+ const originalRuntime = runtimeSrcDir();
503
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
504
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
505
+ const oldRuntimeDir = process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
506
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
507
+ const oldPath = process.env["PATH"];
508
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
509
+ const oldWrapperHeader = process.env["SCRIPTC_TEST_WRAPPER_HEADER"];
510
+ const realClang = (oldPath ?? "")
511
+ .split(delimiter)
512
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
513
+ .find((candidate) => existsSync(candidate));
514
+ expect(realClang).toBeDefined();
515
+ try {
516
+ await Promise.all([
517
+ mkdir(binDir),
518
+ cp(originalRuntime, fakeRuntime, { recursive: true }),
519
+ mkdir(join(dir, "runtime", "vendor"), { recursive: true }).then(() => cp(join(originalRuntime, "..", "vendor", "ryu"), join(dir, "runtime", "vendor", "ryu"), {
520
+ recursive: true,
521
+ })),
522
+ ]);
523
+ const numberSource = join(fakeRuntime, "scr_number.c");
524
+ await writeFile(numberSource, `${await readFile(numberSource, "utf8")}\nint scriptc_wrapper_runtime_value(void) { return SCRIPTC_WRAPPER_HEADER_VALUE; }\n`);
525
+ const wrapper = join(binDir, "clang");
526
+ await writeFile(wrapper, `#!/bin/sh
527
+ case "$1" in
528
+ --version|-print-prog-name=*) exec "$SCRIPTC_TEST_REAL_CLANG" "$@" ;;
529
+ esac
530
+ flavor=0
531
+ for arg in "$@"; do
532
+ if [ "$arg" = "-O2" ]; then flavor=1; fi
533
+ done
534
+ if [ "$flavor" = 1 ]; then
535
+ exec "$SCRIPTC_TEST_REAL_CLANG" -include "$SCRIPTC_TEST_WRAPPER_HEADER" "$@"
536
+ fi
537
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
538
+ `);
539
+ await chmod(wrapper, 0o755);
540
+ await writeFile(cPath, '#include <stdio.h>\nint scriptc_wrapper_runtime_value(void);\nint main(void) { printf("%d\\n", scriptc_wrapper_runtime_value()); return 0; }\n');
541
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
542
+ process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"] = fakeRuntime;
543
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
544
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
545
+ process.env["SCRIPTC_TEST_WRAPPER_HEADER"] = header;
546
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
547
+ trustInstrumentedCompilerWrapper();
548
+ delete process.env["SCRIPTC_NO_CACHE"];
549
+ await writeFile(header, "#define SCRIPTC_WRAPPER_HEADER_VALUE 1\n");
550
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
551
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("1");
552
+ // The wrapper's -### text still names the same -include path. Its bytes,
553
+ // not merely that spelling, must re-key every runtime object it affects.
554
+ await writeFile(header, "#define SCRIPTC_WRAPPER_HEADER_VALUE 2\n");
555
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
556
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("2");
557
+ const objectSets = (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
558
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
559
+ expect(objectSets).toHaveLength(2);
560
+ }
561
+ finally {
562
+ if (oldCacheDir === undefined)
563
+ delete process.env["SCRIPTC_CACHE_DIR"];
564
+ else
565
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
566
+ if (oldNoCache === undefined)
567
+ delete process.env["SCRIPTC_NO_CACHE"];
568
+ else
569
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
570
+ if (oldRuntimeDir === undefined)
571
+ delete process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
572
+ else
573
+ process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"] = oldRuntimeDir;
574
+ if (oldDisableCcache === undefined)
575
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
576
+ else
577
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
578
+ if (oldPath === undefined)
579
+ delete process.env["PATH"];
580
+ else
581
+ process.env["PATH"] = oldPath;
582
+ if (oldRealClang === undefined)
583
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
584
+ else
585
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
586
+ if (oldWrapperHeader === undefined)
587
+ delete process.env["SCRIPTC_TEST_WRAPPER_HEADER"];
588
+ else
589
+ process.env["SCRIPTC_TEST_WRAPPER_HEADER"] = oldWrapperHeader;
590
+ }
591
+ });
592
+ test.skipIf(process.platform === "win32")("build-flavor-specific compiler-wrapper link flags join the artifact identity", async () => {
593
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-wrapper-link-env-"));
594
+ scratch.push(dir);
595
+ const cacheRoot = join(dir, "cache");
596
+ const binDir = join(dir, "bin");
597
+ const cPath = join(dir, "program.c");
598
+ const outPath = join(dir, "program");
599
+ const firstRpath = join(dir, "rpath-one");
600
+ const secondRpath = join(dir, "rpath-two");
601
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
602
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
603
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
604
+ const oldPath = process.env["PATH"];
605
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
606
+ const oldWrapperRpath = process.env["SCRIPTC_TEST_WRAPPER_RPATH"];
607
+ const realClang = (oldPath ?? "")
608
+ .split(delimiter)
609
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
610
+ .find((candidate) => existsSync(candidate));
611
+ expect(realClang).toBeDefined();
612
+ try {
613
+ await mkdir(binDir);
614
+ const wrapper = join(binDir, "clang");
615
+ await writeFile(wrapper, `#!/bin/sh
616
+ case "$1" in
617
+ --version|-print-prog-name=*) exec "$SCRIPTC_TEST_REAL_CLANG" "$@" ;;
618
+ esac
619
+ link=1
620
+ flavor=0
621
+ for arg in "$@"; do
622
+ case "$arg" in
623
+ -c|-M|-MM|-E|-S) link=0 ;;
624
+ -O2) flavor=1 ;;
625
+ esac
626
+ done
627
+ if [ "$link" = 1 ] && [ "$flavor" = 1 ]; then
628
+ exec "$SCRIPTC_TEST_REAL_CLANG" "-Wl,-rpath,$SCRIPTC_TEST_WRAPPER_RPATH" "$@"
629
+ fi
630
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
631
+ `);
632
+ await chmod(wrapper, 0o755);
633
+ await writeFile(cPath, "int main(void) { return 0; }\n");
634
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
635
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
636
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
637
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
638
+ trustInstrumentedCompilerWrapper();
639
+ delete process.env["SCRIPTC_NO_CACHE"];
640
+ process.env["SCRIPTC_TEST_WRAPPER_RPATH"] = firstRpath;
641
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
642
+ expect((await readFile(outPath)).includes(Buffer.from(firstRpath))).toBe(true);
643
+ // The wrapper's generic link trace and resolved linker inputs are
644
+ // unchanged; only the real -O2 link flavor exposes this value.
645
+ process.env["SCRIPTC_TEST_WRAPPER_RPATH"] = secondRpath;
646
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
647
+ const secondBinary = await readFile(outPath);
648
+ expect(secondBinary.includes(Buffer.from(secondRpath))).toBe(true);
649
+ expect(secondBinary.includes(Buffer.from(firstRpath))).toBe(false);
650
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
651
+ }
652
+ finally {
653
+ if (oldCacheDir === undefined)
654
+ delete process.env["SCRIPTC_CACHE_DIR"];
655
+ else
656
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
657
+ if (oldNoCache === undefined)
658
+ delete process.env["SCRIPTC_NO_CACHE"];
659
+ else
660
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
661
+ if (oldDisableCcache === undefined)
662
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
663
+ else
664
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
665
+ if (oldPath === undefined)
666
+ delete process.env["PATH"];
667
+ else
668
+ process.env["PATH"] = oldPath;
669
+ if (oldRealClang === undefined)
670
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
671
+ else
672
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
673
+ if (oldWrapperRpath === undefined)
674
+ delete process.env["SCRIPTC_TEST_WRAPPER_RPATH"];
675
+ else
676
+ process.env["SCRIPTC_TEST_WRAPPER_RPATH"] = oldWrapperRpath;
677
+ }
678
+ });
679
+ test.skipIf(process.platform === "win32")("build-flavor-specific compiler-wrapper link inputs join by content", async () => {
680
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-wrapper-link-input-"));
681
+ scratch.push(dir);
682
+ const cacheRoot = join(dir, "cache");
683
+ const binDir = join(dir, "bin");
684
+ const helperSource = join(dir, "helper.c");
685
+ const helperObject = join(dir, "helper.o");
686
+ const cPath = join(dir, "program.c");
687
+ const outPath = join(dir, "program");
688
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
689
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
690
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
691
+ const oldPath = process.env["PATH"];
692
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
693
+ const oldWrapperObject = process.env["SCRIPTC_TEST_WRAPPER_OBJECT"];
694
+ const realClang = (oldPath ?? "")
695
+ .split(delimiter)
696
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
697
+ .find((candidate) => existsSync(candidate));
698
+ expect(realClang).toBeDefined();
699
+ const buildHelper = async (value) => {
700
+ await writeFile(helperSource, `int scriptc_wrapper_helper(void) { return ${value}; }\n`);
701
+ execFileSync(realClang, ["-c", helperSource, "-o", helperObject]);
702
+ };
703
+ try {
704
+ await mkdir(binDir);
705
+ const wrapper = join(binDir, "clang");
706
+ await writeFile(wrapper, `#!/bin/sh
707
+ case "$1" in
708
+ --version|-print-prog-name=*) exec "$SCRIPTC_TEST_REAL_CLANG" "$@" ;;
709
+ esac
710
+ link=1
711
+ flavor=0
712
+ for arg in "$@"; do
713
+ case "$arg" in
714
+ -c|-M|-MM|-E|-S) link=0 ;;
715
+ -O2) flavor=1 ;;
716
+ esac
717
+ done
718
+ if [ "$link" = 1 ] && [ "$flavor" = 1 ]; then
719
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$SCRIPTC_TEST_WRAPPER_OBJECT" "$@"
720
+ fi
721
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
722
+ `);
723
+ await chmod(wrapper, 0o755);
724
+ await writeFile(cPath, '#include <stdio.h>\nint scriptc_wrapper_helper(void);\nint main(void) { printf("%d\\n", scriptc_wrapper_helper()); return 0; }\n');
725
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
726
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
727
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
728
+ process.env["SCRIPTC_TEST_WRAPPER_OBJECT"] = helperObject;
729
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
730
+ trustInstrumentedCompilerWrapper();
731
+ delete process.env["SCRIPTC_NO_CACHE"];
732
+ await buildHelper(1);
733
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
734
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("1");
735
+ // The injected path remains stable while the object is rebuilt in place.
736
+ // A flavor-mismatched trace sees neither edit and would return a stale hit.
737
+ await buildHelper(2);
738
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
739
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("2");
740
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
741
+ }
742
+ finally {
743
+ if (oldCacheDir === undefined)
744
+ delete process.env["SCRIPTC_CACHE_DIR"];
745
+ else
746
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
747
+ if (oldNoCache === undefined)
748
+ delete process.env["SCRIPTC_NO_CACHE"];
749
+ else
750
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
751
+ if (oldDisableCcache === undefined)
752
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
753
+ else
754
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
755
+ if (oldPath === undefined)
756
+ delete process.env["PATH"];
757
+ else
758
+ process.env["PATH"] = oldPath;
759
+ if (oldRealClang === undefined)
760
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
761
+ else
762
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
763
+ if (oldWrapperObject === undefined)
764
+ delete process.env["SCRIPTC_TEST_WRAPPER_OBJECT"];
765
+ else
766
+ process.env["SCRIPTC_TEST_WRAPPER_OBJECT"] = oldWrapperObject;
767
+ }
768
+ });
769
+ test.skipIf(process.platform !== "darwin")("cached Darwin executables preserve the requested code-signature identifier", async () => {
770
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-darwin-identifier-"));
771
+ scratch.push(dir);
772
+ const cacheRoot = join(dir, "cache");
773
+ const cPath = join(dir, "program.c");
774
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
775
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
776
+ const codeSignatureIdentifier = (path) => {
777
+ const result = spawnSync("codesign", ["-d", "-vv", path], { encoding: "utf8" });
778
+ expect(result.status).toBe(0);
779
+ return /^Identifier=(.+)$/m.exec(`${result.stdout}\n${result.stderr}`)?.[1];
780
+ };
781
+ try {
782
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
783
+ delete process.env["SCRIPTC_NO_CACHE"];
784
+ await writeFile(cPath, "int main(void) { return 0; }\n");
785
+ const firstOut = join(dir, "requested-one");
786
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
787
+ expect(codeSignatureIdentifier(firstOut)).toBe("requested-one");
788
+ const secondOut = join(dir, "requested-two");
789
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
790
+ expect(codeSignatureIdentifier(secondOut)).toBe("requested-two");
791
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
792
+ }
793
+ finally {
794
+ if (oldCacheDir === undefined)
795
+ delete process.env["SCRIPTC_CACHE_DIR"];
796
+ else
797
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
798
+ if (oldNoCache === undefined)
799
+ delete process.env["SCRIPTC_NO_CACHE"];
800
+ else
801
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
802
+ }
803
+ });
804
+ test.skipIf(process.platform === "win32")("implicit system-header changes invalidate complete artifacts and runtime objects", async () => {
805
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-system-header-"));
806
+ scratch.push(dir);
807
+ const cacheRoot = join(dir, "cache");
808
+ const binDir = join(dir, "bin");
809
+ const header = join(dir, "implicit-probe.h");
810
+ const cPath = join(dir, "program.c");
811
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
812
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
813
+ const oldPath = process.env["PATH"];
814
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
815
+ const oldImplicitHeader = process.env["SCRIPTC_TEST_IMPLICIT_HEADER"];
816
+ const realClang = (oldPath ?? "")
817
+ .split(delimiter)
818
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
819
+ .find((candidate) => existsSync(candidate));
820
+ expect(realClang).toBeDefined();
821
+ try {
822
+ await mkdir(binDir);
823
+ const wrapper = join(binDir, "clang");
824
+ await writeFile(wrapper, `#!/bin/sh
825
+ exec "$SCRIPTC_TEST_REAL_CLANG" -include "$SCRIPTC_TEST_IMPLICIT_HEADER" "$@"
826
+ `);
827
+ await chmod(wrapper, 0o755);
828
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts(SCRIPTC_IMPLICIT_PROBE); return 0; }\n');
829
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
830
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
831
+ process.env["SCRIPTC_TEST_IMPLICIT_HEADER"] = header;
832
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
833
+ trustInstrumentedCompilerWrapper();
834
+ delete process.env["SCRIPTC_NO_CACHE"];
835
+ await writeFile(header, '#define SCRIPTC_IMPLICIT_PROBE "one"\n');
836
+ const firstOut = join(dir, "first");
837
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
838
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
839
+ // The compiler executable, PATH, source, and environment spellings are
840
+ // unchanged; only a header in the driver's implicit dependency graph moves.
841
+ await writeFile(header, '#define SCRIPTC_IMPLICIT_PROBE "two"\n');
842
+ const secondOut = firstOut;
843
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
844
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("two");
845
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
846
+ const objectSets = (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
847
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
848
+ expect(objectSets).toHaveLength(2);
849
+ }
850
+ finally {
851
+ if (oldCacheDir === undefined)
852
+ delete process.env["SCRIPTC_CACHE_DIR"];
853
+ else
854
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
855
+ if (oldNoCache === undefined)
856
+ delete process.env["SCRIPTC_NO_CACHE"];
857
+ else
858
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
859
+ if (oldPath === undefined)
860
+ delete process.env["PATH"];
861
+ else
862
+ process.env["PATH"] = oldPath;
863
+ if (oldRealClang === undefined)
864
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
865
+ else
866
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
867
+ if (oldImplicitHeader === undefined)
868
+ delete process.env["SCRIPTC_TEST_IMPLICIT_HEADER"];
869
+ else
870
+ process.env["SCRIPTC_TEST_IMPLICIT_HEADER"] = oldImplicitHeader;
871
+ }
872
+ });
873
+ test.skipIf(process.platform === "win32")("headers selected only by the caller translation unit invalidate complete artifacts", async () => {
874
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-program-header-"));
875
+ scratch.push(dir);
876
+ const cacheRoot = join(dir, "cache");
877
+ const binDir = join(dir, "bin");
878
+ const headers = join(dir, "system-headers");
879
+ const header = join(headers, "scriptc_program_probe.h");
880
+ const cPath = join(dir, "program.c");
881
+ const outPath = join(dir, "program");
882
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
883
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
884
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
885
+ const oldPath = process.env["PATH"];
886
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
887
+ const oldHeaderDir = process.env["SCRIPTC_TEST_PROGRAM_HEADER_DIR"];
888
+ const realClang = (oldPath ?? "")
889
+ .split(delimiter)
890
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
891
+ .find((candidate) => existsSync(candidate));
892
+ expect(realClang).toBeDefined();
893
+ try {
894
+ await Promise.all([mkdir(binDir), mkdir(headers)]);
895
+ const wrapper = join(binDir, "clang");
896
+ await writeFile(wrapper, `#!/bin/sh
897
+ case "$1" in
898
+ --version|-print-prog-name=*) exec "$SCRIPTC_TEST_REAL_CLANG" "$@" ;;
899
+ esac
900
+ exec "$SCRIPTC_TEST_REAL_CLANG" -isystem "$SCRIPTC_TEST_PROGRAM_HEADER_DIR" "$@"
901
+ `);
902
+ await chmod(wrapper, 0o755);
903
+ await writeFile(cPath, '#include <stdio.h>\n#include <scriptc_program_probe.h>\nint main(void) { puts(SCRIPTC_PROGRAM_HEADER); return 0; }\n');
904
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
905
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
906
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
907
+ process.env["SCRIPTC_TEST_PROGRAM_HEADER_DIR"] = headers;
908
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
909
+ trustInstrumentedCompilerWrapper();
910
+ delete process.env["SCRIPTC_NO_CACHE"];
911
+ await writeFile(header, '#define SCRIPTC_PROGRAM_HEADER "one"\n');
912
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
913
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("one");
914
+ // The runtime/vendor seed probe never includes this spelling; only a
915
+ // dependency pass over the caller TU can observe its bytes.
916
+ await writeFile(header, '#define SCRIPTC_PROGRAM_HEADER "two"\n');
917
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
918
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("two");
919
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
920
+ await writeFile(header, '#define SCRIPTC_PROGRAM_HEADER "lib-one-unique"\n');
921
+ const archivePath = join(dir, "program.lib.a");
922
+ await compileLibArchive({ cPath, outPath: archivePath, cacheIdentity: TEST_CACHE_IDENTITY });
923
+ await writeFile(header, '#define SCRIPTC_PROGRAM_HEADER "lib-two-unique"\n');
924
+ await compileLibArchive({ cPath, outPath: archivePath, cacheIdentity: TEST_CACHE_IDENTITY });
925
+ const archive = await readFile(archivePath);
926
+ expect(archive.includes(Buffer.from("lib-two-unique"))).toBe(true);
927
+ expect(archive.includes(Buffer.from("lib-one-unique"))).toBe(false);
928
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(2);
929
+ }
930
+ finally {
931
+ if (oldCacheDir === undefined)
932
+ delete process.env["SCRIPTC_CACHE_DIR"];
933
+ else
934
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
935
+ if (oldNoCache === undefined)
936
+ delete process.env["SCRIPTC_NO_CACHE"];
937
+ else
938
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
939
+ if (oldDisableCcache === undefined)
940
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
941
+ else
942
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
943
+ if (oldPath === undefined)
944
+ delete process.env["PATH"];
945
+ else
946
+ process.env["PATH"] = oldPath;
947
+ if (oldRealClang === undefined)
948
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
949
+ else
950
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
951
+ if (oldHeaderDir === undefined)
952
+ delete process.env["SCRIPTC_TEST_PROGRAM_HEADER_DIR"];
953
+ else
954
+ process.env["SCRIPTC_TEST_PROGRAM_HEADER_DIR"] = oldHeaderDir;
955
+ }
956
+ });
957
+ test.skipIf(process.platform === "win32")("fresh dependency discovery detects an implicit include redirected to a new path", async () => {
958
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-header-redirect-"));
959
+ scratch.push(dir);
960
+ const cacheRoot = join(dir, "cache");
961
+ const binDir = join(dir, "bin");
962
+ const selector = join(dir, "selected-header");
963
+ const firstHeader = join(dir, "implicit-one.h");
964
+ const secondHeader = join(dir, "implicit-two.h");
965
+ const cPath = join(dir, "program.c");
966
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
967
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
968
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
969
+ const oldPath = process.env["PATH"];
970
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
971
+ const oldSelector = process.env["SCRIPTC_TEST_IMPLICIT_SELECTOR"];
972
+ const realClang = (oldPath ?? "")
973
+ .split(delimiter)
974
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
975
+ .find((candidate) => existsSync(candidate));
976
+ expect(realClang).toBeDefined();
977
+ try {
978
+ await mkdir(binDir);
979
+ const wrapper = join(binDir, "clang");
980
+ await writeFile(wrapper, `#!/bin/sh
981
+ IFS= read -r selected < "$SCRIPTC_TEST_IMPLICIT_SELECTOR"
982
+ exec "$SCRIPTC_TEST_REAL_CLANG" -include "$selected" "$@"
983
+ `);
984
+ await chmod(wrapper, 0o755);
985
+ await Promise.all([
986
+ writeFile(firstHeader, '#define SCRIPTC_IMPLICIT_REDIRECT "one"\n'),
987
+ writeFile(secondHeader, '#define SCRIPTC_IMPLICIT_REDIRECT "two"\n'),
988
+ writeFile(cPath, '#include <stdio.h>\nint main(void) { puts(SCRIPTC_IMPLICIT_REDIRECT); return 0; }\n'),
989
+ ]);
990
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
991
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
992
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
993
+ process.env["SCRIPTC_TEST_IMPLICIT_SELECTOR"] = selector;
994
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
995
+ trustInstrumentedCompilerWrapper();
996
+ delete process.env["SCRIPTC_NO_CACHE"];
997
+ await writeFile(selector, `${firstHeader}\n`);
998
+ const firstOut = join(dir, "first");
999
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
1000
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
1001
+ // Keep the old header and every cache/environment spelling unchanged.
1002
+ // Only a fresh `clang -M` can observe that the wrapper now selects a
1003
+ // different path whose bytes belong to a different cache identity.
1004
+ await writeFile(selector, `${secondHeader}\n`);
1005
+ const secondOut = firstOut;
1006
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
1007
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("two");
1008
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
1009
+ }
1010
+ finally {
1011
+ if (oldCacheDir === undefined)
1012
+ delete process.env["SCRIPTC_CACHE_DIR"];
1013
+ else
1014
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1015
+ if (oldNoCache === undefined)
1016
+ delete process.env["SCRIPTC_NO_CACHE"];
1017
+ else
1018
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1019
+ if (oldDisableCcache === undefined)
1020
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1021
+ else
1022
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
1023
+ if (oldPath === undefined)
1024
+ delete process.env["PATH"];
1025
+ else
1026
+ process.env["PATH"] = oldPath;
1027
+ if (oldRealClang === undefined)
1028
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
1029
+ else
1030
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
1031
+ if (oldSelector === undefined)
1032
+ delete process.env["SCRIPTC_TEST_IMPLICIT_SELECTOR"];
1033
+ else
1034
+ process.env["SCRIPTC_TEST_IMPLICIT_SELECTOR"] = oldSelector;
1035
+ }
1036
+ });
1037
+ test.skipIf(process.platform === "win32")("a missing compiler cannot reuse a dependency list that may have new higher-priority headers", async () => {
1038
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-missing-compiler-header-"));
1039
+ scratch.push(dir);
1040
+ const cacheRoot = join(dir, "cache");
1041
+ const cPath = join(dir, "program.c");
1042
+ const outPath = join(dir, "program");
1043
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1044
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1045
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1046
+ const oldPath = process.env["PATH"];
1047
+ try {
1048
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1049
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
1050
+ delete process.env["SCRIPTC_NO_CACHE"];
1051
+ await writeFile(cPath, '#include "scr_runtime.h"\n#include <stdio.h>\n#ifndef SHADOW_VALUE\n#define SHADOW_VALUE "old"\n#endif\nint main(void) { puts(SHADOW_VALUE); return 0; }\n');
1052
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
1053
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("old");
1054
+ // The old runtime header remains byte-identical, but this new local
1055
+ // header wins quote-include resolution. Without a compiler, rehashing
1056
+ // only the old dependency path cannot discover the new selection.
1057
+ await writeFile(join(dir, "scr_runtime.h"), '#define SHADOW_VALUE "new"\n');
1058
+ process.env["PATH"] = "";
1059
+ await rm(outPath, { force: true });
1060
+ await expect(compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY })).rejects.toThrow(/failed compiling/);
1061
+ process.env["PATH"] = oldPath;
1062
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
1063
+ expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("new");
1064
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
1065
+ }
1066
+ finally {
1067
+ if (oldCacheDir === undefined)
1068
+ delete process.env["SCRIPTC_CACHE_DIR"];
1069
+ else
1070
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1071
+ if (oldNoCache === undefined)
1072
+ delete process.env["SCRIPTC_NO_CACHE"];
1073
+ else
1074
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1075
+ if (oldDisableCcache === undefined)
1076
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1077
+ else
1078
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
1079
+ if (oldPath === undefined)
1080
+ delete process.env["PATH"];
1081
+ else
1082
+ process.env["PATH"] = oldPath;
1083
+ }
1084
+ });
1085
+ test.skipIf(process.platform === "win32")("implicit linker inputs join the complete artifact identity", async () => {
1086
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-implicit-link-"));
1087
+ scratch.push(dir);
1088
+ const cacheRoot = join(dir, "cache");
1089
+ const binDir = join(dir, "bin");
1090
+ const helperSource = join(dir, "implicit-helper.c");
1091
+ const helperObject = join(dir, "implicit-helper.o");
1092
+ const cPath = join(dir, "program.c");
1093
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1094
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1095
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1096
+ const oldPath = process.env["PATH"];
1097
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
1098
+ const oldImplicitObject = process.env["SCRIPTC_TEST_IMPLICIT_OBJECT"];
1099
+ const realClang = (oldPath ?? "")
1100
+ .split(delimiter)
1101
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
1102
+ .find((candidate) => existsSync(candidate));
1103
+ expect(realClang).toBeDefined();
1104
+ const buildHelper = async (value) => {
1105
+ await writeFile(helperSource, `int scriptc_implicit_helper(void) { return ${value}; }\n`);
1106
+ execFileSync(realClang, ["-c", helperSource, "-o", helperObject]);
1107
+ };
1108
+ try {
1109
+ await mkdir(binDir);
1110
+ const wrapper = join(binDir, "clang");
1111
+ await writeFile(wrapper, `#!/bin/sh
1112
+ compile_only=0
1113
+ dependency_only=0
1114
+ has_output=0
1115
+ for arg in "$@"; do
1116
+ case "$arg" in
1117
+ -c) compile_only=1 ;;
1118
+ -M|-MM) dependency_only=1 ;;
1119
+ -o) has_output=1 ;;
1120
+ esac
1121
+ done
1122
+ if [ "$has_output" = 1 ] && [ "$compile_only" = 0 ] && [ "$dependency_only" = 0 ]; then
1123
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@" "$SCRIPTC_TEST_IMPLICIT_OBJECT"
1124
+ fi
1125
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
1126
+ `);
1127
+ await chmod(wrapper, 0o755);
1128
+ await writeFile(cPath, '#include <stdio.h>\nint scriptc_implicit_helper(void);\nint main(void) { printf("%d\\n", scriptc_implicit_helper()); return 0; }\n');
1129
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1130
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
1131
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
1132
+ process.env["SCRIPTC_TEST_IMPLICIT_OBJECT"] = helperObject;
1133
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
1134
+ trustInstrumentedCompilerWrapper();
1135
+ delete process.env["SCRIPTC_NO_CACHE"];
1136
+ await buildHelper(1);
1137
+ const firstOut = join(dir, "first");
1138
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
1139
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("1");
1140
+ await buildHelper(2);
1141
+ const secondOut = firstOut;
1142
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
1143
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("2");
1144
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
1145
+ }
1146
+ finally {
1147
+ if (oldCacheDir === undefined)
1148
+ delete process.env["SCRIPTC_CACHE_DIR"];
1149
+ else
1150
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1151
+ if (oldNoCache === undefined)
1152
+ delete process.env["SCRIPTC_NO_CACHE"];
1153
+ else
1154
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1155
+ if (oldDisableCcache === undefined)
1156
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1157
+ else
1158
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
1159
+ if (oldPath === undefined)
1160
+ delete process.env["PATH"];
1161
+ else
1162
+ process.env["PATH"] = oldPath;
1163
+ if (oldRealClang === undefined)
1164
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
1165
+ else
1166
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
1167
+ if (oldImplicitObject === undefined)
1168
+ delete process.env["SCRIPTC_TEST_IMPLICIT_OBJECT"];
1169
+ else
1170
+ process.env["SCRIPTC_TEST_IMPLICIT_OBJECT"] = oldImplicitObject;
1171
+ }
1172
+ });
1173
+ test.skipIf(process.platform === "win32" || zigExecutable === undefined)("zig COFF dry-run inputs retain complete cross-target cache hits", async () => {
1174
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-zig-coff-link-"));
1175
+ scratch.push(dir);
1176
+ const cacheRoot = join(dir, "cache");
1177
+ const cPath = join(dir, "program.c");
1178
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1179
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1180
+ const oldCc = process.env["SCRIPTC_CC"];
1181
+ const oldTarget = process.env["SCRIPTC_TARGET"];
1182
+ const oldPath = process.env["PATH"];
1183
+ try {
1184
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1185
+ process.env["SCRIPTC_CC"] = "zigcc";
1186
+ process.env["SCRIPTC_TARGET"] = "x86_64-windows-gnu";
1187
+ delete process.env["SCRIPTC_NO_CACHE"];
1188
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1189
+ const firstOut = join(dir, "first.exe");
1190
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
1191
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(1);
1192
+ // lld-link rejects GNU ld's `-t`; the Zig `-###` fallback must still
1193
+ // capture every absolute CRT/import-library input. Pin the object-cache
1194
+ // mtimes: a complete hit never stages them, while a miss promotes them.
1195
+ const [objectSet] = (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
1196
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
1197
+ expect(objectSet).toBeDefined();
1198
+ const objectDir = join(cacheRoot, "obj", objectSet.name);
1199
+ const objectNames = (await readdir(objectDir)).filter((name) => name.endsWith(".o"));
1200
+ const pinnedTime = new Date("2000-01-01T00:00:00.000Z");
1201
+ await Promise.all(objectNames.map((name) => utimes(join(objectDir, name), pinnedTime, pinnedTime)));
1202
+ const hitOut = join(dir, "hit.exe");
1203
+ await compileC({ cPath, outPath: hitOut, cacheIdentity: TEST_CACHE_IDENTITY });
1204
+ expect(await readFile(hitOut)).toEqual(await readFile(firstOut));
1205
+ for (const name of objectNames) {
1206
+ expect((await stat(join(objectDir, name))).mtimeMs).toBe(pinnedTime.getTime());
1207
+ }
1208
+ }
1209
+ finally {
1210
+ if (oldCacheDir === undefined)
1211
+ delete process.env["SCRIPTC_CACHE_DIR"];
1212
+ else
1213
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1214
+ if (oldNoCache === undefined)
1215
+ delete process.env["SCRIPTC_NO_CACHE"];
1216
+ else
1217
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1218
+ if (oldCc === undefined)
1219
+ delete process.env["SCRIPTC_CC"];
1220
+ else
1221
+ process.env["SCRIPTC_CC"] = oldCc;
1222
+ if (oldTarget === undefined)
1223
+ delete process.env["SCRIPTC_TARGET"];
1224
+ else
1225
+ process.env["SCRIPTC_TARGET"] = oldTarget;
1226
+ if (oldPath === undefined)
1227
+ delete process.env["PATH"];
1228
+ else
1229
+ process.env["PATH"] = oldPath;
1230
+ }
1231
+ });
1232
+ test.skipIf(process.platform === "win32")("compiler metadata-probe failures fall back to a normal build", async () => {
1233
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-probe-fallback-"));
1234
+ scratch.push(dir);
1235
+ const cacheRoot = join(dir, "cache");
1236
+ const binDir = join(dir, "bin");
1237
+ const cPath = join(dir, "program.c");
1238
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1239
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1240
+ const oldPath = process.env["PATH"];
1241
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
1242
+ const realClang = (oldPath ?? "")
1243
+ .split(delimiter)
1244
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
1245
+ .find((candidate) => existsSync(candidate));
1246
+ expect(realClang).toBeDefined();
1247
+ try {
1248
+ await mkdir(binDir);
1249
+ const wrapper = join(binDir, "clang");
1250
+ await writeFile(wrapper, `#!/bin/sh
1251
+ if [ "$1" = "--version" ]; then exit 7; fi
1252
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
1253
+ `);
1254
+ await chmod(wrapper, 0o755);
1255
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1256
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1257
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
1258
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
1259
+ delete process.env["SCRIPTC_NO_CACHE"];
1260
+ const outPath = join(dir, "out");
1261
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
1262
+ expect((await stat(outPath)).isFile()).toBe(true);
1263
+ await expect(stat(join(cacheRoot, "bin"))).rejects.toMatchObject({ code: "ENOENT" });
1264
+ }
1265
+ finally {
1266
+ if (oldCacheDir === undefined)
1267
+ delete process.env["SCRIPTC_CACHE_DIR"];
1268
+ else
1269
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1270
+ if (oldNoCache === undefined)
1271
+ delete process.env["SCRIPTC_NO_CACHE"];
1272
+ else
1273
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1274
+ if (oldPath === undefined)
1275
+ delete process.env["PATH"];
1276
+ else
1277
+ process.env["PATH"] = oldPath;
1278
+ if (oldRealClang === undefined)
1279
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
1280
+ else
1281
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
1282
+ }
1283
+ });
1284
+ test("staged runtime objects survive removal of their cache names and are promoted in the LRU", async () => {
1285
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-object-stage-"));
1286
+ scratch.push(dir);
1287
+ const cachedObject = join(dir, "cache", "scr_number.o");
1288
+ const source = join(dir, "runtime", "scr_number.c");
1289
+ await mkdir(join(dir, "cache"), { recursive: true });
1290
+ await writeFile(cachedObject, "cached object bytes");
1291
+ const pinnedTime = new Date("2000-01-01T00:00:00.000Z");
1292
+ await utimes(cachedObject, pinnedTime, pinnedTime);
1293
+ const staged = await stageRuntimeObjects(new Map([[source, cachedObject]]), join(dir, "stage"));
1294
+ expect((await stat(cachedObject)).mtimeMs).toBeGreaterThan(pinnedTime.getTime());
1295
+ await rm(cachedObject);
1296
+ expect(await readFile(staged.get(source))).toEqual(Buffer.from("cached object bytes"));
1297
+ });
1298
+ test("cache hits honor the current umask", async () => {
1299
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-environment-"));
1300
+ scratch.push(dir);
1301
+ const cacheRoot = join(dir, "cache");
1302
+ const cPath = join(dir, "program.c");
1303
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1304
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1305
+ const oldUmask = process.umask();
1306
+ try {
1307
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("one"); return 0; }\n');
1308
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1309
+ delete process.env["SCRIPTC_NO_CACHE"];
1310
+ process.umask(0o022);
1311
+ const firstOut = join(dir, "first");
1312
+ await compileC({ cPath, outPath: firstOut, cacheIdentity: TEST_CACHE_IDENTITY });
1313
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
1314
+ if (process.platform !== "win32") {
1315
+ expect((await stat(firstOut)).mode & 0o777).toBe(0o755);
1316
+ expect((await stat(cacheRoot)).mode & 0o777).toBe(0o700);
1317
+ const [cachedBinary] = await completeArtifacts(cacheRoot, "bin");
1318
+ expect((await stat(join(cacheRoot, "bin", cachedBinary))).mode & 0o777).toBe(0o600);
1319
+ }
1320
+ // A hit populated under a permissive umask adopts the current restrictive
1321
+ // one instead of restoring the cached file's broader mode.
1322
+ process.umask(0o077);
1323
+ const hitOut = firstOut;
1324
+ await compileC({ cPath, outPath: hitOut, cacheIdentity: TEST_CACHE_IDENTITY });
1325
+ expect(execFileSync(hitOut, { encoding: "utf8" }).trim()).toBe("one");
1326
+ if (process.platform !== "win32")
1327
+ expect((await stat(hitOut)).mode & 0o777).toBe(0o700);
1328
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(1);
1329
+ process.umask(0o022);
1330
+ const firstArchivePath = join(dir, "first.lib.a");
1331
+ await compileLibArchive({ cPath, outPath: firstArchivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1332
+ const firstArchive = await readFile(firstArchivePath);
1333
+ if (process.platform !== "win32") {
1334
+ const [cachedArchive] = await completeArtifacts(cacheRoot, "lib");
1335
+ expect((await stat(join(cacheRoot, "lib", cachedArchive))).mode & 0o777).toBe(0o600);
1336
+ }
1337
+ process.umask(0o077);
1338
+ const hitArchivePath = join(dir, "hit.lib.a");
1339
+ await compileLibArchive({ cPath, outPath: hitArchivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1340
+ expect(await readFile(hitArchivePath)).toEqual(firstArchive);
1341
+ if (process.platform !== "win32")
1342
+ expect((await stat(hitArchivePath)).mode & 0o777).toBe(0o600);
1343
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(1);
1344
+ }
1345
+ finally {
1346
+ process.umask(oldUmask);
1347
+ if (oldCacheDir === undefined)
1348
+ delete process.env["SCRIPTC_CACHE_DIR"];
1349
+ else
1350
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1351
+ if (oldNoCache === undefined)
1352
+ delete process.env["SCRIPTC_NO_CACHE"];
1353
+ else
1354
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1355
+ }
1356
+ });
1357
+ test.skipIf(process.platform === "win32")("an existing cache override is never made private by mutating its permissions", async () => {
1358
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-existing-root-"));
1359
+ scratch.push(dir);
1360
+ const cacheRoot = join(dir, "shared-cache");
1361
+ const cPath = join(dir, "program.c");
1362
+ const outPath = join(dir, "program");
1363
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1364
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1365
+ try {
1366
+ await mkdir(cacheRoot);
1367
+ await chmod(cacheRoot, 0o755);
1368
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1369
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1370
+ delete process.env["SCRIPTC_NO_CACHE"];
1371
+ await compileC({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
1372
+ expect((await stat(outPath)).isFile()).toBe(true);
1373
+ expect((await stat(cacheRoot)).mode & 0o777).toBe(0o755);
1374
+ await expect(stat(join(cacheRoot, "bin"))).rejects.toMatchObject({ code: "ENOENT" });
1375
+ }
1376
+ finally {
1377
+ if (oldCacheDir === undefined)
1378
+ delete process.env["SCRIPTC_CACHE_DIR"];
1379
+ else
1380
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1381
+ if (oldNoCache === undefined)
1382
+ delete process.env["SCRIPTC_NO_CACHE"];
1383
+ else
1384
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1385
+ }
1386
+ });
1387
+ test.skipIf(process.platform === "win32")("cache installs support destination basenames near NAME_MAX", async () => {
1388
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-long-output-"));
1389
+ scratch.push(dir);
1390
+ const cacheRoot = join(dir, "cache");
1391
+ const cPath = join(dir, "program.c");
1392
+ const executablePath = join(dir, "x".repeat(235));
1393
+ const archivePath = join(dir, `${"a".repeat(229)}.lib.a`);
1394
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1395
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1396
+ try {
1397
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1398
+ delete process.env["SCRIPTC_NO_CACHE"];
1399
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1400
+ await compileC({ cPath, outPath: executablePath, cacheIdentity: TEST_CACHE_IDENTITY });
1401
+ expect((await stat(executablePath)).isFile()).toBe(true);
1402
+ await compileC({ cPath, outPath: executablePath, cacheIdentity: TEST_CACHE_IDENTITY });
1403
+ expect((await stat(executablePath)).isFile()).toBe(true);
1404
+ await compileLibArchive({ cPath, outPath: archivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1405
+ const firstArchive = await readFile(archivePath);
1406
+ await compileLibArchive({ cPath, outPath: archivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1407
+ expect(await readFile(archivePath)).toEqual(firstArchive);
1408
+ }
1409
+ finally {
1410
+ if (oldCacheDir === undefined)
1411
+ delete process.env["SCRIPTC_CACHE_DIR"];
1412
+ else
1413
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1414
+ if (oldNoCache === undefined)
1415
+ delete process.env["SCRIPTC_NO_CACHE"];
1416
+ else
1417
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1418
+ }
1419
+ });
1420
+ test("the configured cache maximum is enforced after writes", async () => {
1421
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-cap-"));
1422
+ scratch.push(dir);
1423
+ const cacheRoot = join(dir, "cache");
1424
+ const cPath = join(dir, "program.c");
1425
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1426
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1427
+ const oldMax = process.env["SCRIPTC_CACHE_MAX_MB"];
1428
+ const capBytes = 0.5 * 1024 * 1024;
1429
+ try {
1430
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1431
+ process.env["SCRIPTC_CACHE_MAX_MB"] = "0.5";
1432
+ delete process.env["SCRIPTC_NO_CACHE"];
1433
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1434
+ await compileC({
1435
+ cPath,
1436
+ outPath: join(dir, "out"),
1437
+ cacheIdentity: TEST_CACHE_IDENTITY,
1438
+ });
1439
+ expect(await cacheTreeBytes(cacheRoot)).toBeLessThanOrEqual(capBytes);
1440
+ // Long-lived callers write repeatedly through one process. Every write,
1441
+ // not only the first or a periodic sample, must leave the tree under cap.
1442
+ await writeFile(cPath, "int main(void) { return 1; }\n");
1443
+ await compileC({
1444
+ cPath,
1445
+ outPath: join(dir, "out"),
1446
+ cacheIdentity: TEST_CACHE_IDENTITY,
1447
+ });
1448
+ expect(await cacheTreeBytes(cacheRoot)).toBeLessThanOrEqual(capBytes);
1449
+ }
1450
+ finally {
1451
+ if (oldCacheDir === undefined)
1452
+ delete process.env["SCRIPTC_CACHE_DIR"];
1453
+ else
1454
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1455
+ if (oldNoCache === undefined)
1456
+ delete process.env["SCRIPTC_NO_CACHE"];
1457
+ else
1458
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1459
+ if (oldMax === undefined)
1460
+ delete process.env["SCRIPTC_CACHE_MAX_MB"];
1461
+ else
1462
+ process.env["SCRIPTC_CACHE_MAX_MB"] = oldMax;
1463
+ }
1464
+ });
1465
+ test("mutable compiler inputs bypass caches when files change in place", async () => {
1466
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-ambient-input-"));
1467
+ scratch.push(dir);
1468
+ const cacheRoot = join(dir, "cache");
1469
+ const vendorCacheRoot = join(dir, "vendor-cache");
1470
+ const headers = join(dir, "headers");
1471
+ const headerPath = join(headers, "cache_probe.h");
1472
+ const cPath = join(dir, "program.c");
1473
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1474
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1475
+ const oldCpath = process.env["CPATH"];
1476
+ const oldVendorCacheDir = process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1477
+ try {
1478
+ await mkdir(headers);
1479
+ await writeFile(cPath, '#include <stdio.h>\n#include <cache_probe.h>\nint main(void) { puts(CACHE_PROBE); return 0; }\n');
1480
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1481
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = vendorCacheRoot;
1482
+ delete process.env["SCRIPTC_NO_CACHE"];
1483
+ process.env["CPATH"] = headers;
1484
+ await writeFile(headerPath, '#define CACHE_PROBE "one"\n');
1485
+ const firstOut = join(dir, "first");
1486
+ await compileC({
1487
+ cPath,
1488
+ outPath: firstOut,
1489
+ cacheIdentity: TEST_CACHE_IDENTITY,
1490
+ regex: true,
1491
+ });
1492
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
1493
+ // The environment spelling is unchanged; only a file behind it moves.
1494
+ await writeFile(headerPath, '#define CACHE_PROBE "two"\n');
1495
+ const secondOut = join(dir, "second");
1496
+ await compileC({
1497
+ cPath,
1498
+ outPath: secondOut,
1499
+ cacheIdentity: TEST_CACHE_IDENTITY,
1500
+ regex: true,
1501
+ });
1502
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("two");
1503
+ const firstArchivePath = join(dir, "first.lib.a");
1504
+ await compileLibArchive({ cPath, outPath: firstArchivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1505
+ const firstArchive = await readFile(firstArchivePath);
1506
+ await writeFile(headerPath, '#define CACHE_PROBE "three"\n');
1507
+ const secondArchivePath = join(dir, "second.lib.a");
1508
+ await compileLibArchive({ cPath, outPath: secondArchivePath, cacheIdentity: TEST_CACHE_IDENTITY });
1509
+ expect(await readFile(secondArchivePath)).not.toEqual(firstArchive);
1510
+ // CPATH can mutate behind a stable string, so no cache tier is populated.
1511
+ await expect(stat(cacheRoot)).rejects.toMatchObject({ code: "ENOENT" });
1512
+ await expect(stat(vendorCacheRoot)).rejects.toMatchObject({ code: "ENOENT" });
1513
+ }
1514
+ finally {
1515
+ if (oldCacheDir === undefined)
1516
+ delete process.env["SCRIPTC_CACHE_DIR"];
1517
+ else
1518
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1519
+ if (oldNoCache === undefined)
1520
+ delete process.env["SCRIPTC_NO_CACHE"];
1521
+ else
1522
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1523
+ if (oldCpath === undefined)
1524
+ delete process.env["CPATH"];
1525
+ else
1526
+ process.env["CPATH"] = oldCpath;
1527
+ if (oldVendorCacheDir === undefined)
1528
+ delete process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1529
+ else
1530
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = oldVendorCacheDir;
1531
+ }
1532
+ });
1533
+ test("the hard disable bypasses vendor prerequisite caches", async () => {
1534
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-no-cache-vendor-"));
1535
+ scratch.push(dir);
1536
+ const cacheRoot = join(dir, "cache");
1537
+ const vendorCacheRoot = join(dir, "vendor-cache");
1538
+ const cPath = join(dir, "program.c");
1539
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1540
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1541
+ const oldVendorCacheDir = process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1542
+ const corruption = Buffer.from("scriptc-corrupt-vendor-object\n");
1543
+ try {
1544
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1545
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = vendorCacheRoot;
1546
+ delete process.env["SCRIPTC_NO_CACHE"];
1547
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1548
+ await compileC({
1549
+ cPath,
1550
+ outPath: join(dir, "populated"),
1551
+ cacheIdentity: TEST_CACHE_IDENTITY,
1552
+ regex: true,
1553
+ });
1554
+ const lreCache = (await readdir(vendorCacheRoot)).find((name) => name.includes("-lre-"));
1555
+ expect(lreCache).toBeDefined();
1556
+ const cachedObject = join(vendorCacheRoot, lreCache, "libregexp.o");
1557
+ await writeFile(cachedObject, corruption);
1558
+ process.env["SCRIPTC_NO_CACHE"] = "1";
1559
+ await compileC({
1560
+ cPath,
1561
+ outPath: join(dir, "disabled"),
1562
+ cacheIdentity: TEST_CACHE_IDENTITY,
1563
+ regex: true,
1564
+ });
1565
+ await compileLibArchive({
1566
+ cPath,
1567
+ outPath: join(dir, "disabled.lib.a"),
1568
+ cacheIdentity: TEST_CACHE_IDENTITY,
1569
+ regex: true,
1570
+ });
1571
+ // Neither disabled build consulted or repaired the package-local entry;
1572
+ // each built its prerequisite in a private, disposable temp root instead.
1573
+ expect(await readFile(cachedObject)).toEqual(corruption);
1574
+ }
1575
+ finally {
1576
+ if (oldCacheDir === undefined)
1577
+ delete process.env["SCRIPTC_CACHE_DIR"];
1578
+ else
1579
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1580
+ if (oldNoCache === undefined)
1581
+ delete process.env["SCRIPTC_NO_CACHE"];
1582
+ else
1583
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1584
+ if (oldVendorCacheDir === undefined)
1585
+ delete process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1586
+ else
1587
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = oldVendorCacheDir;
1588
+ }
1589
+ });
1590
+ test.skipIf(process.platform !== "darwin")("vendor object caches separate deployment-target environments", async () => {
1591
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-vendor-environment-"));
1592
+ scratch.push(dir);
1593
+ const cacheRoot = join(dir, "cache");
1594
+ const vendorCacheRoot = join(dir, "vendor-cache");
1595
+ const cPath = join(dir, "program.c");
1596
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1597
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1598
+ const oldVendorCacheDir = process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1599
+ const oldDeploymentTarget = process.env["MACOSX_DEPLOYMENT_TARGET"];
1600
+ try {
1601
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1602
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = vendorCacheRoot;
1603
+ delete process.env["SCRIPTC_NO_CACHE"];
1604
+ await writeFile(cPath, "int main(void) { return 0; }\n");
1605
+ process.env["MACOSX_DEPLOYMENT_TARGET"] = "14.0";
1606
+ await compileC({
1607
+ cPath,
1608
+ outPath: join(dir, "newer"),
1609
+ cacheIdentity: TEST_CACHE_IDENTITY,
1610
+ regex: true,
1611
+ });
1612
+ process.env["MACOSX_DEPLOYMENT_TARGET"] = "11.0";
1613
+ await compileC({
1614
+ cPath,
1615
+ outPath: join(dir, "older"),
1616
+ cacheIdentity: TEST_CACHE_IDENTITY,
1617
+ regex: true,
1618
+ });
1619
+ const lreCaches = (await readdir(vendorCacheRoot)).filter((name) => name.includes("-lre-"));
1620
+ expect(lreCaches).toHaveLength(2);
1621
+ const minVersions = lreCaches.map((cache) => execFileSync("vtool", ["-show-build", join(vendorCacheRoot, cache, "libregexp.o")], {
1622
+ encoding: "utf8",
1623
+ }).match(/minos\s+(\S+)/)?.[1]);
1624
+ expect(new Set(minVersions)).toEqual(new Set(["11.0", "14.0"]));
1625
+ }
1626
+ finally {
1627
+ if (oldCacheDir === undefined)
1628
+ delete process.env["SCRIPTC_CACHE_DIR"];
1629
+ else
1630
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1631
+ if (oldNoCache === undefined)
1632
+ delete process.env["SCRIPTC_NO_CACHE"];
1633
+ else
1634
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1635
+ if (oldVendorCacheDir === undefined)
1636
+ delete process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1637
+ else
1638
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = oldVendorCacheDir;
1639
+ if (oldDeploymentTarget === undefined)
1640
+ delete process.env["MACOSX_DEPLOYMENT_TARGET"];
1641
+ else
1642
+ process.env["MACOSX_DEPLOYMENT_TARGET"] = oldDeploymentTarget;
1643
+ }
1644
+ });
1645
+ test("complete binary hits precede missing vendor prerequisite materialization", async () => {
1646
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-vendor-hit-"));
1647
+ scratch.push(dir);
1648
+ const cacheRoot = join(dir, "cache");
1649
+ const vendorCacheRoot = join(dir, "vendor-cache");
1650
+ const cPath = join(dir, "program.c");
1651
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1652
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1653
+ const oldVendorCacheDir = process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1654
+ try {
1655
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1656
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = vendorCacheRoot;
1657
+ delete process.env["SCRIPTC_NO_CACHE"];
1658
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("cached"); return 0; }\n');
1659
+ const firstOut = join(dir, "first");
1660
+ await compileC({
1661
+ cPath,
1662
+ outPath: firstOut,
1663
+ cacheIdentity: TEST_CACHE_IDENTITY,
1664
+ regex: true,
1665
+ });
1666
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("cached");
1667
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(1);
1668
+ expect((await stat(vendorCacheRoot)).isDirectory()).toBe(true);
1669
+ // Simulate a package reinstall: the per-package vendor build disappears,
1670
+ // while the per-user complete executable remains. The hit must not
1671
+ // recreate that directory even though the compiler remains available for
1672
+ // fresh dependency discovery.
1673
+ await rm(vendorCacheRoot, { recursive: true, force: true });
1674
+ const hitOut = firstOut;
1675
+ await compileC({
1676
+ cPath,
1677
+ outPath: hitOut,
1678
+ cacheIdentity: TEST_CACHE_IDENTITY,
1679
+ regex: true,
1680
+ });
1681
+ expect(execFileSync(hitOut, { encoding: "utf8" }).trim()).toBe("cached");
1682
+ await expect(stat(vendorCacheRoot)).rejects.toMatchObject({ code: "ENOENT" });
1683
+ }
1684
+ finally {
1685
+ if (oldCacheDir === undefined)
1686
+ delete process.env["SCRIPTC_CACHE_DIR"];
1687
+ else
1688
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1689
+ if (oldNoCache === undefined)
1690
+ delete process.env["SCRIPTC_NO_CACHE"];
1691
+ else
1692
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1693
+ if (oldVendorCacheDir === undefined)
1694
+ delete process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"];
1695
+ else
1696
+ process.env["SCRIPTC_TEST_VENDOR_CACHE_DIR"] = oldVendorCacheDir;
1697
+ }
1698
+ });
1699
+ test("arbitrary C bypasses persistent artifacts so same-path header edits cannot go stale", async () => {
1700
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-arbitrary-c-cache-"));
1701
+ scratch.push(dir);
1702
+ const cacheRoot = join(dir, "cache");
1703
+ const cPath = join(dir, "program.c");
1704
+ const headerPath = join(dir, "value.h");
1705
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1706
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1707
+ try {
1708
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1709
+ delete process.env["SCRIPTC_NO_CACHE"];
1710
+ await writeFile(cPath, '#include <stdio.h>\n#include "value.h"\nint main(void) { puts(VALUE); return 0; }\n');
1711
+ await writeFile(headerPath, '#define VALUE "one"\n');
1712
+ const firstOut = join(dir, "first");
1713
+ await compileC({ cPath, outPath: firstOut });
1714
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
1715
+ await writeFile(headerPath, '#define VALUE "two"\n');
1716
+ const secondOut = join(dir, "second");
1717
+ await compileC({ cPath, outPath: secondOut });
1718
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("two");
1719
+ await expect(stat(join(cacheRoot, "bin"))).rejects.toMatchObject({ code: "ENOENT" });
1720
+ const archiveOut = join(dir, "program.lib.a");
1721
+ await compileLibArchive({ cPath, outPath: archiveOut });
1722
+ expect((await stat(archiveOut)).isFile()).toBe(true);
1723
+ await expect(stat(join(cacheRoot, "lib"))).rejects.toMatchObject({ code: "ENOENT" });
1724
+ }
1725
+ finally {
1726
+ if (oldCacheDir === undefined)
1727
+ delete process.env["SCRIPTC_CACHE_DIR"];
1728
+ else
1729
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1730
+ if (oldNoCache === undefined)
1731
+ delete process.env["SCRIPTC_NO_CACHE"];
1732
+ else
1733
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1734
+ }
1735
+ });
1736
+ test("cached translation units keep the compiler-visible source path in their identity", async () => {
1737
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-source-path-"));
1738
+ scratch.push(dir);
1739
+ const cacheRoot = join(dir, "cache");
1740
+ const aPath = join(dir, "a.c");
1741
+ const bPath = join(dir, "b.c");
1742
+ const headerPath = join(dir, "cache_path_header.h");
1743
+ const source = '#include <stdio.h>\n#include "cache_path_header.h"\nint main(void) { puts(__FILE__); puts(CACHE_PATH_HEADER); return 0; }\n';
1744
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1745
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1746
+ try {
1747
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1748
+ delete process.env["SCRIPTC_NO_CACHE"];
1749
+ await Promise.all([
1750
+ writeFile(aPath, source),
1751
+ writeFile(bPath, source),
1752
+ writeFile(headerPath, '#define CACHE_PATH_HEADER "header"\n'),
1753
+ ]);
1754
+ const aOut = join(dir, "a-out", "program");
1755
+ const bOut = join(dir, "b-out", "program");
1756
+ await Promise.all([mkdir(join(dir, "a-out")), mkdir(join(dir, "b-out"))]);
1757
+ await compileC({ cPath: aPath, outPath: aOut, cacheIdentity: TEST_CACHE_IDENTITY });
1758
+ await compileC({ cPath: bPath, outPath: bOut, cacheIdentity: TEST_CACHE_IDENTITY });
1759
+ expect(execFileSync(aOut, { encoding: "utf8" }).trim()).toBe(`${aPath}\nheader`);
1760
+ expect(execFileSync(bOut, { encoding: "utf8" }).trim()).toBe(`${bPath}\nheader`);
1761
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
1762
+ }
1763
+ finally {
1764
+ if (oldCacheDir === undefined)
1765
+ delete process.env["SCRIPTC_CACHE_DIR"];
1766
+ else
1767
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1768
+ if (oldNoCache === undefined)
1769
+ delete process.env["SCRIPTC_NO_CACHE"];
1770
+ else
1771
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1772
+ }
1773
+ });
1774
+ test.skipIf(process.platform === "win32")("cache publication compiles the exact translation-unit bytes used by its key", async () => {
1775
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-tu-race-"));
1776
+ scratch.push(dir);
1777
+ const cacheRoot = join(dir, "cache");
1778
+ const binDir = join(dir, "bin");
1779
+ const cPath = join(dir, "program.c");
1780
+ const signal = join(dir, "compile-started");
1781
+ const release = join(dir, "compile-release");
1782
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1783
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1784
+ const oldPath = process.env["PATH"];
1785
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
1786
+ const oldRaceSource = process.env["SCRIPTC_TEST_RACE_SOURCE"];
1787
+ const oldRaceSignal = process.env["SCRIPTC_TEST_RACE_SIGNAL"];
1788
+ const oldRaceRelease = process.env["SCRIPTC_TEST_RACE_RELEASE"];
1789
+ const originalClang = (oldPath ?? "")
1790
+ .split(delimiter)
1791
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
1792
+ .find((candidate) => existsSync(candidate));
1793
+ expect(originalClang).toBeDefined();
1794
+ const source = (value) => `#include <stdio.h>\nint main(void) { puts("${value}"); return 0; }\n`;
1795
+ const waitForSignal = async () => {
1796
+ const deadline = Date.now() + 10_000;
1797
+ while (!existsSync(signal)) {
1798
+ if (Date.now() > deadline)
1799
+ throw new Error("timed out waiting for the compiler wrapper");
1800
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10));
1801
+ }
1802
+ };
1803
+ try {
1804
+ await mkdir(binDir);
1805
+ const wrapper = join(binDir, "clang");
1806
+ await writeFile(wrapper, `#!/bin/sh
1807
+ for arg in "$@"; do
1808
+ case "$arg" in
1809
+ "$SCRIPTC_TEST_RACE_SOURCE"|"-ffile-prefix-map="*"$SCRIPTC_TEST_RACE_SOURCE")
1810
+ : > "$SCRIPTC_TEST_RACE_SIGNAL"
1811
+ while [ ! -e "$SCRIPTC_TEST_RACE_RELEASE" ]; do sleep 0.01; done
1812
+ break
1813
+ ;;
1814
+ esac
1815
+ done
1816
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
1817
+ `);
1818
+ // ccache availability is memoized across this file. If an earlier test
1819
+ // found it, keep the controlled compiler wrapper in the path by making
1820
+ // a cache shim that simply forwards its compiler argv.
1821
+ const ccacheWrapper = join(binDir, "ccache");
1822
+ await writeFile(ccacheWrapper, `#!/bin/sh
1823
+ if [ "$1" = "--version" ]; then exit 1; fi
1824
+ exec "$@"
1825
+ `);
1826
+ await Promise.all([chmod(wrapper, 0o755), chmod(ccacheWrapper, 0o755)]);
1827
+ await writeFile(cPath, source("one"));
1828
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1829
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = originalClang;
1830
+ process.env["SCRIPTC_TEST_RACE_SOURCE"] = cPath;
1831
+ process.env["SCRIPTC_TEST_RACE_SIGNAL"] = signal;
1832
+ process.env["SCRIPTC_TEST_RACE_RELEASE"] = release;
1833
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
1834
+ trustInstrumentedCompilerWrapper();
1835
+ delete process.env["SCRIPTC_NO_CACHE"];
1836
+ const firstOut = join(dir, "first");
1837
+ const firstBuild = compileC({
1838
+ cPath,
1839
+ outPath: firstOut,
1840
+ cacheIdentity: TEST_CACHE_IDENTITY,
1841
+ });
1842
+ await Promise.race([
1843
+ waitForSignal(),
1844
+ firstBuild.then(() => {
1845
+ throw new Error("compile completed without reaching the race barrier");
1846
+ }),
1847
+ ]);
1848
+ // The caller-visible generated path changes after its bytes have joined
1849
+ // the key but before clang is allowed to read its input.
1850
+ await writeFile(cPath, source("two"));
1851
+ await writeFile(release, "go");
1852
+ await firstBuild;
1853
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("one");
1854
+ const secondOut = join(dir, "second");
1855
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
1856
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("two");
1857
+ await writeFile(cPath, source("one"));
1858
+ const hitOut = firstOut;
1859
+ await compileC({ cPath, outPath: hitOut, cacheIdentity: TEST_CACHE_IDENTITY });
1860
+ expect(execFileSync(hitOut, { encoding: "utf8" }).trim()).toBe("one");
1861
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(2);
1862
+ await Promise.all([rm(signal, { force: true }), rm(release, { force: true })]);
1863
+ const libPath = join(dir, "library.c");
1864
+ const libOne = "const char *scriptc_cache_race_value = \"lib-race-one-unique\";\n";
1865
+ const libTwo = "const char *scriptc_cache_race_value = \"lib-race-two-unique\";\n";
1866
+ process.env["SCRIPTC_TEST_RACE_SOURCE"] = libPath;
1867
+ await writeFile(libPath, libOne);
1868
+ const firstArchivePath = join(dir, "first.lib.a");
1869
+ const firstArchiveBuild = compileLibArchive({
1870
+ cPath: libPath,
1871
+ outPath: firstArchivePath,
1872
+ cacheIdentity: TEST_CACHE_IDENTITY,
1873
+ });
1874
+ await Promise.race([
1875
+ waitForSignal(),
1876
+ firstArchiveBuild.then(() => {
1877
+ throw new Error("archive compile completed without reaching the race barrier");
1878
+ }),
1879
+ ]);
1880
+ await writeFile(libPath, libTwo);
1881
+ await writeFile(release, "go");
1882
+ await firstArchiveBuild;
1883
+ const firstArchive = await readFile(firstArchivePath);
1884
+ expect(firstArchive.includes(Buffer.from("lib-race-one-unique"))).toBe(true);
1885
+ expect(firstArchive.includes(Buffer.from("lib-race-two-unique"))).toBe(false);
1886
+ await writeFile(libPath, libOne);
1887
+ const hitArchivePath = join(dir, "hit.lib.a");
1888
+ await compileLibArchive({
1889
+ cPath: libPath,
1890
+ outPath: hitArchivePath,
1891
+ cacheIdentity: TEST_CACHE_IDENTITY,
1892
+ });
1893
+ expect(await readFile(hitArchivePath)).toEqual(firstArchive);
1894
+ }
1895
+ finally {
1896
+ if (oldCacheDir === undefined)
1897
+ delete process.env["SCRIPTC_CACHE_DIR"];
1898
+ else
1899
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
1900
+ if (oldNoCache === undefined)
1901
+ delete process.env["SCRIPTC_NO_CACHE"];
1902
+ else
1903
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
1904
+ if (oldPath === undefined)
1905
+ delete process.env["PATH"];
1906
+ else
1907
+ process.env["PATH"] = oldPath;
1908
+ if (oldRealClang === undefined)
1909
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
1910
+ else
1911
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
1912
+ if (oldRaceSource === undefined)
1913
+ delete process.env["SCRIPTC_TEST_RACE_SOURCE"];
1914
+ else
1915
+ process.env["SCRIPTC_TEST_RACE_SOURCE"] = oldRaceSource;
1916
+ if (oldRaceSignal === undefined)
1917
+ delete process.env["SCRIPTC_TEST_RACE_SIGNAL"];
1918
+ else
1919
+ process.env["SCRIPTC_TEST_RACE_SIGNAL"] = oldRaceSignal;
1920
+ if (oldRaceRelease === undefined)
1921
+ delete process.env["SCRIPTC_TEST_RACE_RELEASE"];
1922
+ else
1923
+ process.env["SCRIPTC_TEST_RACE_RELEASE"] = oldRaceRelease;
1924
+ }
1925
+ });
1926
+ test.skipIf(process.platform === "win32")("implicit headers changed during the final compile cannot poison the old key", async () => {
1927
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-cache-final-header-race-"));
1928
+ scratch.push(dir);
1929
+ const cacheRoot = join(dir, "cache");
1930
+ const binDir = join(dir, "bin");
1931
+ const header = join(dir, "implicit-race.h");
1932
+ const cPath = join(dir, "program.c");
1933
+ const signal = join(dir, "final-compile-started");
1934
+ const release = join(dir, "final-compile-release");
1935
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
1936
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
1937
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
1938
+ const oldPath = process.env["PATH"];
1939
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
1940
+ const oldImplicitHeader = process.env["SCRIPTC_TEST_IMPLICIT_HEADER"];
1941
+ const oldRaceSignal = process.env["SCRIPTC_TEST_FINAL_RACE_SIGNAL"];
1942
+ const oldRaceRelease = process.env["SCRIPTC_TEST_FINAL_RACE_RELEASE"];
1943
+ const realClang = (oldPath ?? "")
1944
+ .split(delimiter)
1945
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
1946
+ .find((candidate) => existsSync(candidate));
1947
+ expect(realClang).toBeDefined();
1948
+ const waitForSignal = async () => {
1949
+ const deadline = Date.now() + 10_000;
1950
+ while (!existsSync(signal)) {
1951
+ if (Date.now() > deadline)
1952
+ throw new Error("timed out waiting for final compilation");
1953
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10));
1954
+ }
1955
+ };
1956
+ try {
1957
+ await mkdir(binDir);
1958
+ const wrapper = join(binDir, "clang");
1959
+ await writeFile(wrapper, `#!/bin/sh
1960
+ for arg in "$@"; do
1961
+ case "$arg" in
1962
+ */scriptc-cache-build-*/program.c|*/scriptc-cache-build-*/program.ll)
1963
+ if [ ! -e "$SCRIPTC_TEST_FINAL_RACE_SIGNAL" ]; then
1964
+ : > "$SCRIPTC_TEST_FINAL_RACE_SIGNAL"
1965
+ while [ ! -e "$SCRIPTC_TEST_FINAL_RACE_RELEASE" ]; do sleep 0.01; done
1966
+ fi
1967
+ break
1968
+ ;;
1969
+ esac
1970
+ done
1971
+ exec "$SCRIPTC_TEST_REAL_CLANG" -include "$SCRIPTC_TEST_IMPLICIT_HEADER" "$@"
1972
+ `);
1973
+ await chmod(wrapper, 0o755);
1974
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts(SCRIPTC_IMPLICIT_RACE); return 0; }\n');
1975
+ await writeFile(header, '#define SCRIPTC_IMPLICIT_RACE "one"\n');
1976
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
1977
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
1978
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
1979
+ process.env["SCRIPTC_TEST_IMPLICIT_HEADER"] = header;
1980
+ process.env["SCRIPTC_TEST_FINAL_RACE_SIGNAL"] = signal;
1981
+ process.env["SCRIPTC_TEST_FINAL_RACE_RELEASE"] = release;
1982
+ process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
1983
+ trustInstrumentedCompilerWrapper();
1984
+ delete process.env["SCRIPTC_NO_CACHE"];
1985
+ const firstOut = join(dir, "first");
1986
+ const firstBuild = compileC({
1987
+ cPath,
1988
+ outPath: firstOut,
1989
+ cacheIdentity: TEST_CACHE_IDENTITY,
1990
+ });
1991
+ await Promise.race([
1992
+ waitForSignal(),
1993
+ firstBuild.then(() => {
1994
+ throw new Error("compile completed without reaching the final-build barrier");
1995
+ }),
1996
+ ]);
1997
+ await writeFile(header, '#define SCRIPTC_IMPLICIT_RACE "two"\n');
1998
+ await writeFile(release, "go");
1999
+ await firstBuild;
2000
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("two");
2001
+ // The first executable was compiled from `two` after its key captured
2002
+ // `one`, so it must not be published. Restoring `one` must rebuild it.
2003
+ await writeFile(header, '#define SCRIPTC_IMPLICIT_RACE "one"\n');
2004
+ const secondOut = join(dir, "second");
2005
+ await compileC({ cPath, outPath: secondOut, cacheIdentity: TEST_CACHE_IDENTITY });
2006
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("one");
2007
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(1);
2008
+ }
2009
+ finally {
2010
+ if (oldCacheDir === undefined)
2011
+ delete process.env["SCRIPTC_CACHE_DIR"];
2012
+ else
2013
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2014
+ if (oldNoCache === undefined)
2015
+ delete process.env["SCRIPTC_NO_CACHE"];
2016
+ else
2017
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2018
+ if (oldDisableCcache === undefined)
2019
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
2020
+ else
2021
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
2022
+ if (oldPath === undefined)
2023
+ delete process.env["PATH"];
2024
+ else
2025
+ process.env["PATH"] = oldPath;
2026
+ if (oldRealClang === undefined)
2027
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
2028
+ else
2029
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
2030
+ if (oldImplicitHeader === undefined)
2031
+ delete process.env["SCRIPTC_TEST_IMPLICIT_HEADER"];
2032
+ else
2033
+ process.env["SCRIPTC_TEST_IMPLICIT_HEADER"] = oldImplicitHeader;
2034
+ if (oldRaceSignal === undefined)
2035
+ delete process.env["SCRIPTC_TEST_FINAL_RACE_SIGNAL"];
2036
+ else
2037
+ process.env["SCRIPTC_TEST_FINAL_RACE_SIGNAL"] = oldRaceSignal;
2038
+ if (oldRaceRelease === undefined)
2039
+ delete process.env["SCRIPTC_TEST_FINAL_RACE_RELEASE"];
2040
+ else
2041
+ process.env["SCRIPTC_TEST_FINAL_RACE_RELEASE"] = oldRaceRelease;
2042
+ }
2043
+ });
2044
+ test.skipIf(process.platform === "win32")("runtime edits during object compilation cannot poison the old fingerprint", async () => {
2045
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-runtime-cache-race-"));
2046
+ scratch.push(dir);
2047
+ const cacheRoot = join(dir, "cache");
2048
+ const fakeRuntime = join(dir, "runtime", "src");
2049
+ const originalRuntime = runtimeSrcDir();
2050
+ await Promise.all([
2051
+ cp(originalRuntime, fakeRuntime, { recursive: true }),
2052
+ mkdir(join(dir, "runtime", "vendor"), { recursive: true }).then(() => cp(join(originalRuntime, "..", "vendor", "ryu"), join(dir, "runtime", "vendor", "ryu"), {
2053
+ recursive: true,
2054
+ })),
2055
+ ]);
2056
+ const binDir = join(dir, "bin");
2057
+ const cPath = join(dir, "program.c");
2058
+ const raceSource = join(fakeRuntime, "scr_number.c");
2059
+ const signal = join(dir, "runtime-compile-started");
2060
+ const release = join(dir, "runtime-compile-release");
2061
+ const marker = "scriptc-runtime-race-marker-unique";
2062
+ const originalRuntimeSource = await readFile(raceSource, "utf8");
2063
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2064
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2065
+ const oldRuntimeDir = process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
2066
+ const oldDisableCcache = process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
2067
+ const oldPath = process.env["PATH"];
2068
+ const oldRealClang = process.env["SCRIPTC_TEST_REAL_CLANG"];
2069
+ const oldRaceSource = process.env["SCRIPTC_TEST_RUNTIME_RACE_SOURCE"];
2070
+ const oldRaceSignal = process.env["SCRIPTC_TEST_RUNTIME_RACE_SIGNAL"];
2071
+ const oldRaceRelease = process.env["SCRIPTC_TEST_RUNTIME_RACE_RELEASE"];
2072
+ const realClang = (oldPath ?? "")
2073
+ .split(delimiter)
2074
+ .map((entry) => join(entry === "" ? process.cwd() : entry, "clang"))
2075
+ .find((candidate) => existsSync(candidate));
2076
+ expect(realClang).toBeDefined();
2077
+ try {
2078
+ await mkdir(binDir);
2079
+ const wrapper = join(binDir, "clang");
2080
+ await writeFile(wrapper, `#!/bin/sh
2081
+ for arg in "$@"; do
2082
+ if [ "$arg" = "$SCRIPTC_TEST_RUNTIME_RACE_SOURCE" ]; then
2083
+ : > "$SCRIPTC_TEST_RUNTIME_RACE_SIGNAL"
2084
+ while [ ! -e "$SCRIPTC_TEST_RUNTIME_RACE_RELEASE" ]; do sleep 0.01; done
2085
+ break
2086
+ fi
2087
+ done
2088
+ exec "$SCRIPTC_TEST_REAL_CLANG" "$@"
2089
+ `);
2090
+ await chmod(wrapper, 0o755);
2091
+ await writeFile(cPath, "int main(void) { return 0; }\n");
2092
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
2093
+ process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"] = fakeRuntime;
2094
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = realClang;
2095
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_SOURCE"] = raceSource;
2096
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_SIGNAL"] = signal;
2097
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_RELEASE"] = release;
2098
+ // Keep ccache out of this controlled race: its own object store is not
2099
+ // the cache under test and can obscure which compiler read happened.
2100
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = "1";
2101
+ process.env["PATH"] = `${binDir}${delimiter}/usr/bin${delimiter}/bin`;
2102
+ trustInstrumentedCompilerWrapper();
2103
+ delete process.env["SCRIPTC_NO_CACHE"];
2104
+ const firstOut = join(dir, "first");
2105
+ const firstBuild = compileC({
2106
+ cPath,
2107
+ outPath: firstOut,
2108
+ cacheIdentity: TEST_CACHE_IDENTITY,
2109
+ });
2110
+ const deadline = Date.now() + 10_000;
2111
+ while (!existsSync(signal)) {
2112
+ if (Date.now() > deadline)
2113
+ throw new Error("timed out waiting for runtime compile");
2114
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10));
2115
+ }
2116
+ await writeFile(raceSource, `${originalRuntimeSource}\nconst char scriptc_runtime_race_marker[] = "${marker}";\n`);
2117
+ await writeFile(release, "go");
2118
+ await firstBuild;
2119
+ expect((await readFile(firstOut)).includes(Buffer.from(marker))).toBe(true);
2120
+ // Restore the fingerprint, then force a program-cache miss. The old
2121
+ // fingerprint must not have retained the object built from edited bytes.
2122
+ await writeFile(raceSource, originalRuntimeSource);
2123
+ await writeFile(cPath, "int main(void) { return 0; } /* second */\n");
2124
+ const secondOut = join(dir, "second");
2125
+ await compileC({
2126
+ cPath,
2127
+ outPath: secondOut,
2128
+ cacheIdentity: TEST_CACHE_IDENTITY,
2129
+ });
2130
+ expect((await readFile(secondOut)).includes(Buffer.from(marker))).toBe(false);
2131
+ expect(await completeArtifacts(cacheRoot, "bin")).toHaveLength(1);
2132
+ }
2133
+ finally {
2134
+ if (oldCacheDir === undefined)
2135
+ delete process.env["SCRIPTC_CACHE_DIR"];
2136
+ else
2137
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2138
+ if (oldNoCache === undefined)
2139
+ delete process.env["SCRIPTC_NO_CACHE"];
2140
+ else
2141
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2142
+ if (oldRuntimeDir === undefined)
2143
+ delete process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"];
2144
+ else
2145
+ process.env["SCRIPTC_TEST_RUNTIME_SRC_DIR"] = oldRuntimeDir;
2146
+ if (oldDisableCcache === undefined)
2147
+ delete process.env["SCRIPTC_TEST_DISABLE_CCACHE"];
2148
+ else
2149
+ process.env["SCRIPTC_TEST_DISABLE_CCACHE"] = oldDisableCcache;
2150
+ if (oldPath === undefined)
2151
+ delete process.env["PATH"];
2152
+ else
2153
+ process.env["PATH"] = oldPath;
2154
+ if (oldRealClang === undefined)
2155
+ delete process.env["SCRIPTC_TEST_REAL_CLANG"];
2156
+ else
2157
+ process.env["SCRIPTC_TEST_REAL_CLANG"] = oldRealClang;
2158
+ if (oldRaceSource === undefined)
2159
+ delete process.env["SCRIPTC_TEST_RUNTIME_RACE_SOURCE"];
2160
+ else
2161
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_SOURCE"] = oldRaceSource;
2162
+ if (oldRaceSignal === undefined)
2163
+ delete process.env["SCRIPTC_TEST_RUNTIME_RACE_SIGNAL"];
2164
+ else
2165
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_SIGNAL"] = oldRaceSignal;
2166
+ if (oldRaceRelease === undefined)
2167
+ delete process.env["SCRIPTC_TEST_RUNTIME_RACE_RELEASE"];
2168
+ else
2169
+ process.env["SCRIPTC_TEST_RUNTIME_RACE_RELEASE"] = oldRaceRelease;
2170
+ }
2171
+ });
2172
+ test("explicit native link inputs relink while runtime objects remain cached", async () => {
2173
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-link-input-cache-"));
2174
+ scratch.push(dir);
2175
+ const cacheRoot = join(dir, "cache");
2176
+ const cPath = join(dir, "program.c");
2177
+ const nativeSource = join(dir, "probe.c");
2178
+ const nativeObject = join(dir, "probe.o");
2179
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2180
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2181
+ const rebuildObject = async (value) => {
2182
+ await writeFile(nativeSource, `int scriptc_cache_probe(void) { return ${value}; }\n`);
2183
+ execFileSync("clang", ["-c", nativeSource, "-o", nativeObject]);
2184
+ };
2185
+ try {
2186
+ await writeFile(cPath, '#include <stdio.h>\nextern int scriptc_cache_probe(void);\nint main(void) { printf("%d\\n", scriptc_cache_probe()); return 0; }\n');
2187
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
2188
+ delete process.env["SCRIPTC_NO_CACHE"];
2189
+ await rebuildObject(1);
2190
+ const firstOut = join(dir, "first");
2191
+ await compileC({
2192
+ cPath,
2193
+ outPath: firstOut,
2194
+ cacheIdentity: TEST_CACHE_IDENTITY,
2195
+ linkInputs: [nativeObject],
2196
+ });
2197
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("1");
2198
+ await rebuildObject(2);
2199
+ const secondOut = join(dir, "second");
2200
+ await compileC({
2201
+ cPath,
2202
+ outPath: secondOut,
2203
+ cacheIdentity: TEST_CACHE_IDENTITY,
2204
+ linkInputs: [nativeObject],
2205
+ });
2206
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("2");
2207
+ // A path may name a thin archive or linker script whose own bytes do not
2208
+ // cover its dependencies, so every native-input build performs this link.
2209
+ await expect(stat(join(cacheRoot, "bin"))).rejects.toMatchObject({ code: "ENOENT" });
2210
+ const objectSets = await readdir(join(cacheRoot, "obj"), { withFileTypes: true });
2211
+ expect(objectSets.some((entry) => entry.isDirectory() && !entry.name.startsWith("build-"))).toBe(true);
2212
+ }
2213
+ finally {
2214
+ if (oldCacheDir === undefined)
2215
+ delete process.env["SCRIPTC_CACHE_DIR"];
2216
+ else
2217
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2218
+ if (oldNoCache === undefined)
2219
+ delete process.env["SCRIPTC_NO_CACHE"];
2220
+ else
2221
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2222
+ }
2223
+ });
2224
+ test("system libraries relink after an in-place rebuild while runtime objects remain cached", async () => {
2225
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-system-library-cache-"));
2226
+ scratch.push(dir);
2227
+ const cacheRoot = join(dir, "cache");
2228
+ const libDir = join(dir, "lib");
2229
+ const cPath = join(dir, "program.c");
2230
+ const libSource = join(dir, "probe.c");
2231
+ const libObject = join(dir, "probe.o");
2232
+ const library = join(libDir, "libscriptc_cache_probe.a");
2233
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2234
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2235
+ const oldLibraryPath = process.env["LIBRARY_PATH"];
2236
+ const rebuildLibrary = async (value) => {
2237
+ await writeFile(libSource, `int scriptc_cache_probe(void) { return ${value}; }\n`);
2238
+ execFileSync("clang", ["-c", libSource, "-o", libObject]);
2239
+ execFileSync("ar", ["rcs", library, libObject]);
2240
+ };
2241
+ try {
2242
+ await mkdir(libDir);
2243
+ await writeFile(cPath, '#include <stdio.h>\nextern int scriptc_cache_probe(void);\nint main(void) { printf("%d\\n", scriptc_cache_probe()); return 0; }\n');
2244
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
2245
+ delete process.env["SCRIPTC_NO_CACHE"];
2246
+ process.env["LIBRARY_PATH"] = libDir;
2247
+ await rebuildLibrary(1);
2248
+ const firstOut = join(dir, "first");
2249
+ await compileC({
2250
+ cPath,
2251
+ outPath: firstOut,
2252
+ cacheIdentity: TEST_CACHE_IDENTITY,
2253
+ systemLibraries: ["scriptc_cache_probe"],
2254
+ });
2255
+ expect(execFileSync(firstOut, { encoding: "utf8" }).trim()).toBe("1");
2256
+ await rebuildLibrary(2);
2257
+ const secondOut = join(dir, "second");
2258
+ await compileC({
2259
+ cPath,
2260
+ outPath: secondOut,
2261
+ cacheIdentity: TEST_CACHE_IDENTITY,
2262
+ systemLibraries: ["scriptc_cache_probe"],
2263
+ });
2264
+ expect(execFileSync(secondOut, { encoding: "utf8" }).trim()).toBe("2");
2265
+ // The ambient library prevents a stale complete-binary hit, but the safe
2266
+ // runtime-object half of the persistent cache remains active.
2267
+ await expect(stat(join(cacheRoot, "bin"))).rejects.toMatchObject({ code: "ENOENT" });
2268
+ const objectSets = await readdir(join(cacheRoot, "obj"), { withFileTypes: true });
2269
+ expect(objectSets.some((entry) => entry.isDirectory() && !entry.name.startsWith("build-"))).toBe(true);
2270
+ }
2271
+ finally {
2272
+ if (oldCacheDir === undefined)
2273
+ delete process.env["SCRIPTC_CACHE_DIR"];
2274
+ else
2275
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2276
+ if (oldNoCache === undefined)
2277
+ delete process.env["SCRIPTC_NO_CACHE"];
2278
+ else
2279
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2280
+ if (oldLibraryPath === undefined)
2281
+ delete process.env["LIBRARY_PATH"];
2282
+ else
2283
+ process.env["LIBRARY_PATH"] = oldLibraryPath;
2284
+ }
2285
+ });
2286
+ test("damaged complete artifacts are rejected and rebuilt", async () => {
2287
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-artifact-integrity-"));
2288
+ scratch.push(dir);
2289
+ const cacheRoot = join(dir, "cache");
2290
+ const cPath = join(dir, "program.c");
2291
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2292
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2293
+ const corruption = Buffer.from("scriptc-corrupt-complete-artifact\n");
2294
+ try {
2295
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
2296
+ delete process.env["SCRIPTC_NO_CACHE"];
2297
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("valid"); return 0; }\n');
2298
+ await compileC({
2299
+ cPath,
2300
+ outPath: join(dir, "program"),
2301
+ cacheIdentity: TEST_CACHE_IDENTITY,
2302
+ });
2303
+ const [cachedBinary] = await completeArtifacts(cacheRoot, "bin");
2304
+ expect(cachedBinary).toBeDefined();
2305
+ expect((await stat(`${join(cacheRoot, "bin", cachedBinary)}.sha256`)).isFile()).toBe(true);
2306
+ await writeFile(join(cacheRoot, "bin", cachedBinary), corruption);
2307
+ const repairedBinary = join(dir, "program");
2308
+ await compileC({
2309
+ cPath,
2310
+ outPath: repairedBinary,
2311
+ cacheIdentity: TEST_CACHE_IDENTITY,
2312
+ });
2313
+ expect(execFileSync(repairedBinary, { encoding: "utf8" }).trim()).toBe("valid");
2314
+ expect(await readFile(join(cacheRoot, "bin", cachedBinary))).not.toEqual(corruption);
2315
+ await writeFile(cPath, "int scriptc_artifact_integrity_probe = 1;\n");
2316
+ await compileLibArchive({
2317
+ cPath,
2318
+ outPath: join(dir, "first.lib.a"),
2319
+ cacheIdentity: TEST_CACHE_IDENTITY,
2320
+ });
2321
+ const [cachedArchive] = await completeArtifacts(cacheRoot, "lib");
2322
+ expect(cachedArchive).toBeDefined();
2323
+ expect((await stat(`${join(cacheRoot, "lib", cachedArchive)}.sha256`)).isFile()).toBe(true);
2324
+ await writeFile(join(cacheRoot, "lib", cachedArchive), corruption);
2325
+ const repairedArchive = join(dir, "repaired.lib.a");
2326
+ await compileLibArchive({
2327
+ cPath,
2328
+ outPath: repairedArchive,
2329
+ cacheIdentity: TEST_CACHE_IDENTITY,
2330
+ });
2331
+ expect((await readFile(repairedArchive)).includes(corruption)).toBe(false);
2332
+ expect(execFileSync("ar", ["t", repairedArchive], { encoding: "utf8" })).toContain("program.program.o");
2333
+ }
2334
+ finally {
2335
+ if (oldCacheDir === undefined)
2336
+ delete process.env["SCRIPTC_CACHE_DIR"];
2337
+ else
2338
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2339
+ if (oldNoCache === undefined)
2340
+ delete process.env["SCRIPTC_NO_CACHE"];
2341
+ else
2342
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2343
+ }
2344
+ });
2345
+ test("damaged runtime objects are rebuilt before linking or archiving", async () => {
2346
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-object-integrity-"));
2347
+ scratch.push(dir);
2348
+ const cacheRoot = join(dir, "cache");
2349
+ const cPath = join(dir, "program.c");
2350
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2351
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2352
+ const corruption = Buffer.from("scriptc-corrupt-object-unique-marker");
2353
+ const objectSets = async () => (await readdir(join(cacheRoot, "obj"), { withFileTypes: true }))
2354
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("build-"))
2355
+ .map((entry) => entry.name);
2356
+ const corruptOneObject = async (set) => {
2357
+ const objectDir = join(cacheRoot, "obj", set);
2358
+ const object = (await readdir(objectDir)).find((name) => name.endsWith(".o"));
2359
+ expect(object).toBeDefined();
2360
+ await writeFile(join(objectDir, object), corruption);
2361
+ expect((await stat(`${join(objectDir, object)}.sha256`)).isFile()).toBe(true);
2362
+ return join(objectDir, object);
2363
+ };
2364
+ try {
2365
+ process.env["SCRIPTC_CACHE_DIR"] = cacheRoot;
2366
+ delete process.env["SCRIPTC_NO_CACHE"];
2367
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("one"); return 0; }\n');
2368
+ await compileC({
2369
+ cPath,
2370
+ outPath: join(dir, "one"),
2371
+ cacheIdentity: TEST_CACHE_IDENTITY,
2372
+ });
2373
+ const [executableSet] = await objectSets();
2374
+ const damagedExecutableObject = await corruptOneObject(executableSet);
2375
+ await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("two"); return 0; }\n');
2376
+ const repairedOut = join(dir, "two");
2377
+ await compileC({
2378
+ cPath,
2379
+ outPath: repairedOut,
2380
+ cacheIdentity: TEST_CACHE_IDENTITY,
2381
+ });
2382
+ expect(execFileSync(repairedOut, { encoding: "utf8" }).trim()).toBe("two");
2383
+ expect(await readFile(damagedExecutableObject)).not.toEqual(corruption);
2384
+ const beforeLibrary = new Set(await objectSets());
2385
+ await writeFile(cPath, "int scriptc_integrity_probe = 1;\n");
2386
+ await compileLibArchive({
2387
+ cPath,
2388
+ outPath: join(dir, "one.lib.a"),
2389
+ cacheIdentity: TEST_CACHE_IDENTITY,
2390
+ });
2391
+ const librarySet = (await objectSets()).find((set) => !beforeLibrary.has(set));
2392
+ expect(librarySet).toBeDefined();
2393
+ await corruptOneObject(librarySet);
2394
+ await writeFile(cPath, "int scriptc_integrity_probe = 2;\n");
2395
+ const repairedArchive = join(dir, "two.lib.a");
2396
+ await compileLibArchive({
2397
+ cPath,
2398
+ outPath: repairedArchive,
2399
+ cacheIdentity: TEST_CACHE_IDENTITY,
2400
+ });
2401
+ expect((await readFile(repairedArchive)).includes(corruption)).toBe(false);
2402
+ }
2403
+ finally {
2404
+ if (oldCacheDir === undefined)
2405
+ delete process.env["SCRIPTC_CACHE_DIR"];
2406
+ else
2407
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2408
+ if (oldNoCache === undefined)
2409
+ delete process.env["SCRIPTC_NO_CACHE"];
2410
+ else
2411
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2412
+ }
2413
+ });
2414
+ test("library archives hit by content, invalidate on edits, and reuse runtime objects", async () => {
2415
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-lib-cache-"));
2416
+ scratch.push(dir);
2417
+ const xdgCacheHome = join(dir, "xdg-cache");
2418
+ const cacheRoot = join(xdgCacheHome, "scriptc", "build");
2419
+ const cPath = join(dir, "program.c");
2420
+ const outPath = join(dir, "program.lib.a");
2421
+ const oldCacheDir = process.env["SCRIPTC_CACHE_DIR"];
2422
+ const oldNoCache = process.env["SCRIPTC_NO_CACHE"];
2423
+ const oldXdgCacheHome = process.env["XDG_CACHE_HOME"];
2424
+ const oldPath = process.env["PATH"];
2425
+ try {
2426
+ // No SCRIPTC_CACHE_DIR: this exercises the production-default activation,
2427
+ // with XDG_CACHE_HOME redirecting the per-user root into disposable space.
2428
+ delete process.env["SCRIPTC_CACHE_DIR"];
2429
+ process.env["XDG_CACHE_HOME"] = xdgCacheHome;
2430
+ delete process.env["SCRIPTC_NO_CACHE"];
2431
+ await writeFile(cPath, "int scriptc_cache_probe = 1;\n");
2432
+ await compileLibArchive({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
2433
+ const firstArchive = await readFile(outPath);
2434
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(1);
2435
+ const objectSets = await readdir(join(cacheRoot, "obj"), { withFileTypes: true });
2436
+ const objectSet = objectSets.find((entry) => entry.isDirectory() && !entry.name.startsWith("build-"));
2437
+ expect(objectSet).toBeDefined();
2438
+ const objectDir = join(cacheRoot, "obj", objectSet.name);
2439
+ const objectNames = (await readdir(objectDir)).filter((name) => name.endsWith(".o"));
2440
+ expect(objectNames.length).toBeGreaterThan(10);
2441
+ const pinnedTime = new Date("2000-01-01T00:00:00.000Z");
2442
+ await Promise.all(objectNames.map((name) => utimes(join(objectDir, name), pinnedTime, pinnedTime)));
2443
+ // A content hit still performs fresh compiler/toolchain discovery, but it
2444
+ // must not stage or promote the cached runtime objects.
2445
+ await rm(outPath, { force: true });
2446
+ await compileLibArchive({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
2447
+ expect(await readFile(outPath)).toEqual(firstArchive);
2448
+ for (const name of objectNames) {
2449
+ expect((await stat(join(objectDir, name))).mtimeMs).toBe(pinnedTime.getTime());
2450
+ }
2451
+ // The hard disable bypasses the same valid entry in both directions.
2452
+ process.env["SCRIPTC_NO_CACHE"] = "1";
2453
+ process.env["PATH"] = "";
2454
+ await rm(outPath, { force: true });
2455
+ await expect(compileLibArchive({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY })).rejects.toThrow(/failed compiling/);
2456
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(1);
2457
+ // An edit invalidates the complete archive but retains the library-flavor
2458
+ // runtime object set. Only the changed program TU is compiled again.
2459
+ process.env["PATH"] = oldPath;
2460
+ delete process.env["SCRIPTC_NO_CACHE"];
2461
+ await writeFile(cPath, "int scriptc_cache_probe = 2;\n");
2462
+ await compileLibArchive({ cPath, outPath, cacheIdentity: TEST_CACHE_IDENTITY });
2463
+ const editedArchive = await readFile(outPath);
2464
+ expect(editedArchive).not.toEqual(firstArchive);
2465
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(2);
2466
+ for (const name of objectNames) {
2467
+ expect((await stat(join(objectDir, name))).mtimeMs).toBeGreaterThan(pinnedTime.getTime());
2468
+ }
2469
+ // BSD ar preserves member mtimes, so containers assembled from old cached
2470
+ // objects need not be byte-identical to a fresh archive. Their object
2471
+ // members must be byte-identical, and a successful disabled build must
2472
+ // publish nothing into the cache.
2473
+ process.env["SCRIPTC_NO_CACHE"] = "1";
2474
+ const uncachedOut = join(dir, "program.uncached.lib.a");
2475
+ await compileLibArchive({ cPath, outPath: uncachedOut, cacheIdentity: TEST_CACHE_IDENTITY });
2476
+ const cachedMembers = join(dir, "cached-members");
2477
+ const uncachedMembers = join(dir, "uncached-members");
2478
+ await Promise.all([mkdir(cachedMembers), mkdir(uncachedMembers)]);
2479
+ execFileSync("ar", ["x", outPath], { cwd: cachedMembers });
2480
+ execFileSync("ar", ["x", uncachedOut], { cwd: uncachedMembers });
2481
+ const memberNames = await readdir(cachedMembers);
2482
+ expect(await readdir(uncachedMembers)).toEqual(memberNames);
2483
+ for (const name of memberNames) {
2484
+ expect(await readFile(join(uncachedMembers, name))).toEqual(await readFile(join(cachedMembers, name)));
2485
+ }
2486
+ expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(2);
2487
+ }
2488
+ finally {
2489
+ if (oldCacheDir === undefined)
2490
+ delete process.env["SCRIPTC_CACHE_DIR"];
2491
+ else
2492
+ process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
2493
+ if (oldNoCache === undefined)
2494
+ delete process.env["SCRIPTC_NO_CACHE"];
2495
+ else
2496
+ process.env["SCRIPTC_NO_CACHE"] = oldNoCache;
2497
+ if (oldXdgCacheHome === undefined)
2498
+ delete process.env["XDG_CACHE_HOME"];
2499
+ else
2500
+ process.env["XDG_CACHE_HOME"] = oldXdgCacheHome;
2501
+ if (oldPath === undefined)
2502
+ delete process.env["PATH"];
2503
+ else
2504
+ process.env["PATH"] = oldPath;
2505
+ }
2506
+ });
2507
+ //# sourceMappingURL=cc-cache.test.js.map