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