stim 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/dist/android-CC5bUer7.mjs +204 -0
  4. package/dist/android-DnZlT3aU.mjs +2279 -0
  5. package/dist/android-cas-DGlTStWu.mjs +1078 -0
  6. package/dist/android-cas-compiler.d.mts +1 -0
  7. package/dist/android-cas-compiler.mjs +71 -0
  8. package/dist/android-xmyGBNy5.mjs +665 -0
  9. package/dist/app-install-oZIQRbdD.mjs +1141 -0
  10. package/dist/build-lock-vE45ZzJZ.mjs +645 -0
  11. package/dist/build-slots-CgacA9zQ.mjs +152 -0
  12. package/dist/cache-manifest-oeH18ERr.mjs +66 -0
  13. package/dist/cache-manifest.d.mts +27 -0
  14. package/dist/cache-manifest.mjs +2 -0
  15. package/dist/cli.d.mts +1 -0
  16. package/dist/cli.mjs +40 -0
  17. package/dist/collector-run.d.mts +119 -0
  18. package/dist/collector-run.mjs +541 -0
  19. package/dist/command-output-8wHMEde0.mjs +289 -0
  20. package/dist/config-D7uu8Gbq.mjs +381 -0
  21. package/dist/deps-64wVnpZc.mjs +435 -0
  22. package/dist/dev-client-Bvky1hya.mjs +557 -0
  23. package/dist/device-DJKv5Qej.mjs +250 -0
  24. package/dist/device-lease-D-G3SgVe.mjs +335 -0
  25. package/dist/device-pool-BlHXnYYs.mjs +408 -0
  26. package/dist/device-remote-CCgkG82J.mjs +1209 -0
  27. package/dist/doctor-BRF9aAJ3.mjs +530 -0
  28. package/dist/doctor-DlVxTtMq.mjs +1137 -0
  29. package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
  30. package/dist/errors-B6W7FkxQ.mjs +15 -0
  31. package/dist/exec-bsN9MJXb.mjs +63 -0
  32. package/dist/gc-DhXdgSdt.mjs +1651 -0
  33. package/dist/guide-DrhrDBo5.mjs +4319 -0
  34. package/dist/ios-BZxKPuqP.mjs +382 -0
  35. package/dist/ios-CK1Vrk6Q.mjs +2721 -0
  36. package/dist/ios-device-CYbuRNXR.mjs +81 -0
  37. package/dist/ios-device-CdSm0GDw.mjs +413 -0
  38. package/dist/logs-Dxoy16Qv.mjs +176 -0
  39. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  40. package/dist/metro-B01H3EuD.mjs +184 -0
  41. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  42. package/dist/ndjson-DcAtEx_K.mjs +131 -0
  43. package/dist/ndjson-DvHSsNU9.d.mts +16 -0
  44. package/dist/ownership-B9QIdQOw.mjs +475 -0
  45. package/dist/ownership-D8HmSv2E.mjs +60 -0
  46. package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
  47. package/dist/process-output-C7IlIzd7.mjs +40 -0
  48. package/dist/project-COj6nzzh.mjs +243 -0
  49. package/dist/reclaim-BPvN2HlP.mjs +347 -0
  50. package/dist/reload-DYZEBbZy.mjs +332 -0
  51. package/dist/remote-cache-DOi5-zLj.mjs +934 -0
  52. package/dist/server-bare-BYHqEeyw.mjs +352 -0
  53. package/dist/server-expo-BEMr_m6p.mjs +312 -0
  54. package/dist/server-expo-rb0k-0w7.mjs +2 -0
  55. package/dist/settings-BSwrY4FZ.mjs +687 -0
  56. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  57. package/dist/start-ByCylYtB.mjs +822 -0
  58. package/dist/state-IYEY-zi8.mjs +268 -0
  59. package/dist/stats-DZ4Q5J-7.mjs +62 -0
  60. package/dist/stats-wqxPm7Tv.mjs +307 -0
  61. package/dist/status-Dia0XTqG.mjs +363 -0
  62. package/dist/stop-BApyaF21.mjs +591 -0
  63. package/dist/stop-D6o5zAdm.mjs +3 -0
  64. package/dist/supervisor-run.d.mts +84 -0
  65. package/dist/supervisor-run.mjs +210 -0
  66. package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
  67. package/dist/worktree-Dx3ZC5lS.mjs +539 -0
  68. package/dist/worktree-DxEmUI_n.mjs +1322 -0
  69. package/dist/xcode-CDB6fytw.mjs +1705 -0
  70. package/package.json +61 -8
  71. package/shim/android-cas-pch.cmake +35 -0
  72. package/shim/android-cas.gradle +29 -0
  73. package/shim/android-cas.toolchain.cmake +12 -0
  74. package/shim/android-optimizations.gradle +30 -0
  75. package/shim/bundle-response.cjs +46 -0
  76. package/shim/bundle-response.d.cts +5 -0
  77. package/shim/expo-metro-config.cjs +188 -0
  78. package/shim/metro-cache-generation.cjs +20 -0
  79. package/shim/metro-cache-generation.d.cts +5 -0
  80. package/skill/SKILL.md +14 -0
@@ -0,0 +1,1078 @@
1
+ import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
+ import { k as withDirLock, u as getConfigDir } from "./config-D7uu8Gbq.mjs";
3
+ import { r as register } from "./cache-manifest-oeH18ERr.mjs";
4
+ import { D as workspaceLogsDir, x as sharedCcache, y as ensureWorkspaceStorage } from "./state-IYEY-zi8.mjs";
5
+ import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
+ import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
7
+ import { n as androidHome } from "./android-xmyGBNy5.mjs";
8
+ import { f as startBuildHeartbeat, r as HEARTBEAT_INTERVAL_MS } from "./xcode-CDB6fytw.mjs";
9
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { dirname, isAbsolute, join, relative } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import chalk from "chalk";
14
+ const MAX_MESSAGE_LENGTH = 300;
15
+ const KOTLIN_URI = /^e:\s+file:\/\/(\/\S+?):(\d+):(\d+)\s+(.*)$/;
16
+ const KOTLIN_PAREN = /^e:\s+(\S+?):\s*\((\d+),\s*(\d+)\):\s*(.*)$/;
17
+ const KOTLIN_BARE = /^e:\s+(.*)$/;
18
+ const FILE_LINE_ERROR = /^([^\s:]+):(\d+):(?:(\d+):)?\s*(?:AAPT:\s*)?error:\s*(.*)$/i;
19
+ const AAPT_PREFIXED = /^ERROR:\s*([^\s:]+):(\d+):(?:(\d+):)?\s*(.*)$/;
20
+ const BARE_ERROR = /^error:\s*(.*)$/i;
21
+ const FATAL_ERROR = /FATAL_ERROR/;
22
+ const TASK_FAILED = /^>\s*Task\s+(:[A-Za-z0-9_.:-]+)\s+FAILED\b/;
23
+ const FAILURE_HEADER = /^FAILURE:\s*Build (?:failed|completed with)/;
24
+ const WHAT_WENT_WRONG = /^\*\s*What went wrong:/;
25
+ const SECTION = /^\*\s*[A-Za-z]/;
26
+ const COULD_NOT_RESOLVE = /^>?\s*(Could not (?:resolve|find|download|GET) .+)$/;
27
+ const EXECUTION_FAILED = /^Execution failed for task\s+'(?::[^']+)'/;
28
+ const CAUSE_LINE = /^>\s+\S/;
29
+ const JAVA_EXCEPTION = /^(?:[a-z][A-Za-z0-9_]*\.){2,}[A-Za-z0-9_$]*(?:Exception|Error)\b/;
30
+ const TASK_LINE = /^>\s*Task\s+:/;
31
+ const BUILD_TAIL = /^(?:BUILD (?:FAILED|SUCCESSFUL) in\b|Deprecated Gradle features were used\b|You can use '--warning-mode|For more on this, please refer to\b|\d+ actionable task|Configuration cache entry\b)/;
32
+ const MAX_CAUSE_LINES = 6;
33
+ const REMEDIES = [
34
+ [/JAVA_HOME is (?:set to an invalid directory|not set)/i, "Point JAVA_HOME at a JDK 17 install (`export JAVA_HOME=$(/usr/libexec/java_home -v 17)`) and run again."],
35
+ [/SDK location not found|ANDROID_HOME|ANDROID_SDK_ROOT|sdk\.dir/i, "Set ANDROID_HOME to the Android SDK (usually ~/Library/Android/sdk), or write sdk.dir into android/local.properties."],
36
+ [/licen[cs]es? (?:have not been|has not been) accepted|You have not accepted the license/i, "Accept the SDK licences: `$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses`."],
37
+ [/Failed to install the following (?:Android )?SDK packages|package is not installed/i, "Install the missing SDK package with `$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \"<package>\"`."],
38
+ [/Unsupported class file major version|invalid source release|compiled by a more recent version of the Java|Could not determine java version/i, "The JDK does not match what this project builds with; select JDK 17 (`export JAVA_HOME=$(/usr/libexec/java_home -v 17)`)."],
39
+ [/Could not (?:resolve|find|download|GET)/i, "Check network access and the repositories block. After a bad cache entry, `./gradlew --refresh-dependencies assembleDebug` in android/ re-resolves."],
40
+ [/Android resource linking failed|AAPT/i, "Fix the resource file and line named above (under android/app/src/main/res); aapt2 reports the exact element it could not link."]
41
+ ];
42
+ function remedyFor(message) {
43
+ const text = String(message || "");
44
+ for (const [pattern, remedy] of REMEDIES) if (pattern.test(text)) return remedy;
45
+ return null;
46
+ }
47
+ function extractGradleDiagnostics(text) {
48
+ if (typeof text !== "string" || text.trim() === "") return [];
49
+ const lines = text.split("\n");
50
+ const found = [];
51
+ const seen = /* @__PURE__ */ new Set();
52
+ const add = (diag) => {
53
+ const message = clip(diag.message);
54
+ if (!message) return;
55
+ const key = `${diag.file || ""}|${diag.line ?? ""}|${message}`;
56
+ if (seen.has(key)) return;
57
+ seen.add(key);
58
+ const record = { message };
59
+ if (diag.file) record.file = diag.file;
60
+ if (diag.line != null) record.line = diag.line;
61
+ if (diag.column != null) record.column = diag.column;
62
+ const remedy = remedyFor(`${diag.file || ""} ${message}`);
63
+ if (remedy) record.remedy = remedy;
64
+ found.push(record);
65
+ };
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = stripCarriage(lines[i]).trim();
68
+ if (!line) continue;
69
+ if (FAILURE_HEADER.test(line)) {
70
+ const block = readWhatWentWrong(lines, i);
71
+ if (block) {
72
+ add({ message: block.message });
73
+ for (const highlight of block.highlights) add({ message: highlight });
74
+ i = block.endIndex;
75
+ }
76
+ continue;
77
+ }
78
+ if (WHAT_WENT_WRONG.test(line)) {
79
+ const block = readWhatWentWrong(lines, i - 1);
80
+ if (block) {
81
+ add({ message: block.message });
82
+ for (const highlight of block.highlights) add({ message: highlight });
83
+ i = block.endIndex;
84
+ }
85
+ continue;
86
+ }
87
+ if (EXECUTION_FAILED.test(line)) {
88
+ const chain = readCauseChain(lines, i);
89
+ add({ message: chain.message });
90
+ for (const highlight of chain.highlights) add({ message: highlight });
91
+ i = chain.endIndex;
92
+ continue;
93
+ }
94
+ if (FATAL_ERROR.test(line)) {
95
+ add({ message: line });
96
+ continue;
97
+ }
98
+ const orphanCause = CAUSE_LINE.test(line) ? line.replace(/^>\s*/, "") : "";
99
+ if (JAVA_EXCEPTION.test(orphanCause)) {
100
+ add({ message: orphanCause });
101
+ continue;
102
+ }
103
+ const kotlinUri = KOTLIN_URI.exec(line);
104
+ if (kotlinUri) {
105
+ const uriFile = kotlinUri[1];
106
+ const uriMsg = kotlinUri[4];
107
+ if (uriFile === void 0 || uriMsg === void 0) continue;
108
+ add({
109
+ file: decodePath(uriFile),
110
+ line: Number(kotlinUri[2]),
111
+ column: Number(kotlinUri[3]),
112
+ message: uriMsg
113
+ });
114
+ continue;
115
+ }
116
+ const kotlinParen = KOTLIN_PAREN.exec(line);
117
+ if (kotlinParen) {
118
+ const parenMsg = kotlinParen[4];
119
+ if (parenMsg === void 0) continue;
120
+ add({
121
+ file: kotlinParen[1],
122
+ line: Number(kotlinParen[2]),
123
+ column: Number(kotlinParen[3]),
124
+ message: parenMsg
125
+ });
126
+ continue;
127
+ }
128
+ const aapt = AAPT_PREFIXED.exec(line);
129
+ if (aapt) {
130
+ const aaptMsg = aapt[4];
131
+ if (aaptMsg === void 0) continue;
132
+ add({
133
+ file: aapt[1],
134
+ line: Number(aapt[2]),
135
+ column: aapt[3] ? Number(aapt[3]) : void 0,
136
+ message: stripAaptPrefix(aaptMsg)
137
+ });
138
+ continue;
139
+ }
140
+ const fileError = FILE_LINE_ERROR.exec(line);
141
+ if (fileError) {
142
+ const errMsg = fileError[4];
143
+ if (errMsg === void 0) continue;
144
+ add({
145
+ file: fileError[1],
146
+ line: Number(fileError[2]),
147
+ column: fileError[3] ? Number(fileError[3]) : void 0,
148
+ message: errMsg
149
+ });
150
+ continue;
151
+ }
152
+ const kotlinBare = KOTLIN_BARE.exec(line);
153
+ if (kotlinBare) {
154
+ const bareMsg = kotlinBare[1];
155
+ if (bareMsg === void 0) continue;
156
+ add({ message: bareMsg });
157
+ continue;
158
+ }
159
+ const bare = BARE_ERROR.exec(line);
160
+ if (bare) {
161
+ const bareErrMsg = bare[1];
162
+ if (bareErrMsg === void 0) continue;
163
+ add({ message: bareErrMsg });
164
+ continue;
165
+ }
166
+ const resolve = COULD_NOT_RESOLVE.exec(line);
167
+ if (resolve) {
168
+ const resolveMsg = resolve[1];
169
+ if (resolveMsg === void 0) continue;
170
+ add({ message: resolveMsg });
171
+ continue;
172
+ }
173
+ const task = TASK_FAILED.exec(line);
174
+ if (task) {
175
+ add({ message: `Task ${task[1]} FAILED` });
176
+ continue;
177
+ }
178
+ }
179
+ return found;
180
+ }
181
+ function capDiagnostics(diagnostics, limit = 10) {
182
+ const all = Array.isArray(diagnostics) ? diagnostics : [];
183
+ if (all.length <= limit) return {
184
+ shown: all.slice(),
185
+ truncated: 0
186
+ };
187
+ return {
188
+ shown: all.slice(0, limit),
189
+ truncated: all.length - limit
190
+ };
191
+ }
192
+ function formatDiagnostic(diag) {
193
+ if (!diag) return "";
194
+ return `${diag.file ? `${diag.file}${diag.line != null ? `:${diag.line}` : ""}${diag.line != null && diag.column != null ? `:${diag.column}` : ""}: ` : ""}${diag.message}`;
195
+ }
196
+ function readWhatWentWrong(lines, start) {
197
+ let i = start + 1;
198
+ while (i < lines.length && !WHAT_WENT_WRONG.test(stripCarriage(lines[i]).trim())) {
199
+ const probe = stripCarriage(lines[i]).trim();
200
+ if (probe !== "" && !FAILURE_HEADER.test(probe)) return null;
201
+ i++;
202
+ }
203
+ if (i >= lines.length) return null;
204
+ const parts = [];
205
+ let j = i + 1;
206
+ for (; j < lines.length; j++) {
207
+ const line = stripCarriage(lines[j]).trim();
208
+ if (SECTION.test(line)) break;
209
+ if (TASK_LINE.test(line)) break;
210
+ if (BUILD_TAIL.test(line)) continue;
211
+ if (line === "") {
212
+ if (parts.length) break;
213
+ continue;
214
+ }
215
+ parts.push(line.replace(/^>\s*/, ""));
216
+ }
217
+ if (parts.length === 0) return null;
218
+ return {
219
+ message: parts.join(" "),
220
+ highlights: highlightsOf(parts),
221
+ endIndex: j - 1
222
+ };
223
+ }
224
+ function readCauseChain(lines, start) {
225
+ const parts = [stripCarriage(lines[start]).trim()];
226
+ let end = start;
227
+ for (let j = start + 1; j < lines.length && parts.length < MAX_CAUSE_LINES; j++) {
228
+ const line = stripCarriage(lines[j]).trim();
229
+ if (!CAUSE_LINE.test(line) || TASK_LINE.test(line)) break;
230
+ parts.push(line.replace(/^>\s*/, ""));
231
+ end = j;
232
+ }
233
+ return {
234
+ message: parts.join(" "),
235
+ highlights: highlightsOf(parts),
236
+ endIndex: end
237
+ };
238
+ }
239
+ function highlightsOf(parts) {
240
+ const joined = clip(parts.join(" "));
241
+ return parts.filter((part) => (FATAL_ERROR.test(part) || JAVA_EXCEPTION.test(part)) && !joined.includes(clip(part)));
242
+ }
243
+ function stripAaptPrefix(message) {
244
+ return String(message).replace(/^AAPT:\s*/, "").replace(/^error:\s*/i, "");
245
+ }
246
+ function decodePath(path) {
247
+ try {
248
+ return decodeURIComponent(path);
249
+ } catch {
250
+ return path;
251
+ }
252
+ }
253
+ function stripCarriage(line) {
254
+ const text = String(line ?? "");
255
+ const idx = text.lastIndexOf("\r");
256
+ return idx === -1 ? text : text.slice(idx + 1);
257
+ }
258
+ function clip(message) {
259
+ const text = String(message ?? "").trim();
260
+ if (text.length <= MAX_MESSAGE_LENGTH) return text;
261
+ return `${text.slice(0, 297)}...`;
262
+ }
263
+ const CCACHE_SLOPPINESS = "pch_defines,time_macros";
264
+ const STATS_LOG_FILE = "ccache-stats.log";
265
+ const CCACHE_UNAVAILABLE = {
266
+ status: "unavailable",
267
+ hits: null,
268
+ misses: null,
269
+ hitRatePercent: null
270
+ };
271
+ const CCACHE_NOT_RUN = {
272
+ status: "not-run",
273
+ hits: null,
274
+ misses: null,
275
+ hitRatePercent: null
276
+ };
277
+ function ccacheStatsLog(root) {
278
+ return join(workspaceLogsDir(root), STATS_LOG_FILE);
279
+ }
280
+ function ccacheEnvironment({ binary, dir, workspaceRoot, statsLog }) {
281
+ return {
282
+ CMAKE_C_COMPILER_LAUNCHER: binary,
283
+ CMAKE_CXX_COMPILER_LAUNCHER: binary,
284
+ CCACHE_DIR: dir,
285
+ CCACHE_BASEDIR: workspaceRoot,
286
+ CCACHE_NOHASHDIR: "true",
287
+ CCACHE_SLOPPINESS,
288
+ CCACHE_MAXSIZE: "5G",
289
+ CCACHE_STATSLOG: statsLog
290
+ };
291
+ }
292
+ function parseCcacheBinary(output) {
293
+ const first = String(output ?? "").split("\n")[0]?.trim();
294
+ if (!first || !isAbsolute(first)) return null;
295
+ return first;
296
+ }
297
+ const CMAKE_LAUNCHER = /CMAKE_(C|CXX)_COMPILER_LAUNCHER\b/;
298
+ function projectCmakeLauncher(source) {
299
+ if (typeof source !== "string") return null;
300
+ const match = CMAKE_LAUNCHER.exec(source);
301
+ return match ? `CMAKE_${match[1]}_COMPILER_LAUNCHER` : null;
302
+ }
303
+ const HIT_COUNTERS = /* @__PURE__ */ new Set(["direct_cache_hit", "preprocessed_cache_hit"]);
304
+ function parseCcacheStatsLog(text) {
305
+ if (typeof text !== "string") return null;
306
+ let hits = 0;
307
+ let misses = 0;
308
+ for (const raw of text.split("\n")) {
309
+ const line = raw.trim();
310
+ if (HIT_COUNTERS.has(line)) hits += 1;
311
+ else if (line === "cache_miss") misses += 1;
312
+ }
313
+ const total = hits + misses;
314
+ if (total === 0) return null;
315
+ return {
316
+ status: "reported",
317
+ hits,
318
+ misses,
319
+ hitRatePercent: Math.round(hits / total * 1e3) / 10
320
+ };
321
+ }
322
+ function readCcacheActivity(statsLog) {
323
+ let text;
324
+ try {
325
+ text = readFileSync(statsLog, "utf-8");
326
+ } catch {
327
+ return CCACHE_UNAVAILABLE;
328
+ }
329
+ return parseCcacheStatsLog(text) ?? CCACHE_UNAVAILABLE;
330
+ }
331
+ function ccacheActivityLine(activity) {
332
+ if (activity.status === "reported") return `${activity.hits} hits / ${activity.misses} misses (${activity.hitRatePercent}%)`;
333
+ if (activity.status === "not-run") return "not run; artifact cache supplied the app";
334
+ return "unavailable; no C++ compile went through ccache";
335
+ }
336
+ function lookupCcache() {
337
+ try {
338
+ return parseCcacheBinary(getExecutor().runQuiet("command -v ccache", { timeoutMs: 5e3 }));
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+ function readOrNull$1(file) {
344
+ try {
345
+ return readFileSync(file, "utf-8");
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+ function canonical(root) {
351
+ try {
352
+ return realpathSync(root);
353
+ } catch {
354
+ return root;
355
+ }
356
+ }
357
+ function resolveCcache({ root, dir = sharedCcache(), lookup = lookupCcache, onNote = (line) => console.error(line) }) {
358
+ const binary = lookup();
359
+ if (!binary) {
360
+ onNote(chalk.dim(phaseLine("cache", "ccache off (not installed; brew install ccache)")));
361
+ return null;
362
+ }
363
+ const declared = projectCmakeLauncher(readOrNull$1(join(root, "android", "app", "build.gradle")));
364
+ if (declared) {
365
+ onNote(chalk.dim(phaseLine("cache", `ccache off (android/app/build.gradle sets ${declared} itself)`)));
366
+ return null;
367
+ }
368
+ const statsLog = ccacheStatsLog(root);
369
+ register({
370
+ dir,
371
+ name: "ccache",
372
+ prune: "atomic",
373
+ note: "ccache keeps itself under CCACHE_MAXSIZE, so it is emptied whole or not at all"
374
+ });
375
+ onNote(chalk.dim(phaseLine("cache", `ccache on (${dir})`)));
376
+ return {
377
+ dir,
378
+ statsLog,
379
+ env: ccacheEnvironment({
380
+ binary,
381
+ dir,
382
+ workspaceRoot: canonical(root),
383
+ statsLog
384
+ })
385
+ };
386
+ }
387
+ //#endregion
388
+ //#region src/engine/gradle.ts
389
+ const BUILD_ERROR = "STIM_BUILD_FAILED";
390
+ const ASSEMBLE_TASK = "assembleDebug";
391
+ function assembleTaskFor(variant) {
392
+ const name = typeof variant === "string" ? variant.trim() : "";
393
+ if (!name) return ASSEMBLE_TASK;
394
+ return `assemble${name[0].toUpperCase()}${name.slice(1)}`;
395
+ }
396
+ const LAST_LINES = 20;
397
+ const TRANSCRIPT_LINES = 2e3;
398
+ function androidDir(root) {
399
+ return join(root, "android");
400
+ }
401
+ function gradlewPath(root) {
402
+ return join(androidDir(root), "gradlew");
403
+ }
404
+ function apkOutputsDir(root) {
405
+ return join(androidDir(root), "app", "build", "outputs", "apk");
406
+ }
407
+ function debugApkDir(root) {
408
+ return join(apkOutputsDir(root), "debug");
409
+ }
410
+ function discoverAndroidProject(root) {
411
+ const dir = androidDir(root);
412
+ if (!existsSync(dir)) return {
413
+ failed: true,
414
+ code: BUILD_ERROR,
415
+ reason: `No android/ directory in ${root}.`,
416
+ remedy: "Generate it (`npx expo prebuild -p android`, which `stim android` runs itself on an Expo project) or check out the native sources."
417
+ };
418
+ const gradlew = gradlewPath(root);
419
+ if (!existsSync(gradlew)) return {
420
+ failed: true,
421
+ code: BUILD_ERROR,
422
+ reason: `${gradlew} does not exist, so there is no gradle wrapper to build with.`,
423
+ remedy: "Restore the wrapper (`gradle wrapper` in android/, or regenerate the project with `npx expo prebuild -p android --clean`)."
424
+ };
425
+ return {
426
+ androidDir: dir,
427
+ gradlew
428
+ };
429
+ }
430
+ function androidSdkRefusal({ sdkPath, sdkExists, hasLocalProperties }) {
431
+ if (sdkExists || hasLocalProperties) return null;
432
+ return {
433
+ code: BUILD_ERROR,
434
+ reason: `No Android SDK at ${sdkPath}.`,
435
+ remedy: "Set ANDROID_HOME to the Android SDK (Android Studio installs it at ~/Library/Android/sdk), or write sdk.dir into android/local.properties. JAVA_HOME must point at a JDK 17 install as well."
436
+ };
437
+ }
438
+ function pickDebugApk(files) {
439
+ const list = (Array.isArray(files) ? files : []).filter((f) => typeof f === "string" && f.endsWith(".apk")).filter((f) => !/-(?:unsigned|unaligned)\.apk$/.test(f));
440
+ if (list.length === 0) return null;
441
+ const named = list.find((f) => baseName(f) === "app-debug.apk");
442
+ if (named) return named;
443
+ const debug = list.filter((f) => baseName(f).endsWith("-debug.apk"));
444
+ return (debug.length ? debug : list).toSorted((a, b) => baseName(a).length - baseName(b).length || baseName(a).localeCompare(baseName(b)))[0];
445
+ }
446
+ function parseOutputMetadata(text) {
447
+ let parsed;
448
+ try {
449
+ parsed = JSON.parse(String(text));
450
+ } catch {
451
+ return null;
452
+ }
453
+ return pickDebugApk((Array.isArray(parsed?.elements) ? parsed.elements : []).map((e) => e?.outputFile).filter((f) => typeof f === "string"));
454
+ }
455
+ const TRANSCRIPT_APK = [/(?:Wrote APK to|APK (?:written|generated|copied) (?:to|at)|Built the following APKs?:)\s*(\S+\.apk)/i, /Installing APK '([^']+\.apk)'/i];
456
+ function parseApkFromTranscript(text) {
457
+ if (typeof text !== "string") return null;
458
+ for (const raw of text.split("\n")) {
459
+ const line = raw.trim();
460
+ for (const pattern of TRANSCRIPT_APK) {
461
+ const m = pattern.exec(line);
462
+ if (m) return m[1];
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+ function variantNameOf(segments) {
468
+ return (Array.isArray(segments) ? segments : []).filter((s) => typeof s === "string" && s.trim()).map((s, i) => i === 0 ? s : `${s[0].toUpperCase()}${s.slice(1)}`).join("");
469
+ }
470
+ function apkInDir(dir) {
471
+ const metadata = readOrNull(join(dir, "output-metadata.json"));
472
+ if (metadata) {
473
+ const named = parseOutputMetadata(metadata);
474
+ if (named) {
475
+ const abs = named.startsWith("/") ? named : join(dir, named);
476
+ if (existsSync(abs)) return abs;
477
+ }
478
+ }
479
+ const listed = pickDebugApk(safeList(dir));
480
+ return listed ? join(dir, listed) : null;
481
+ }
482
+ function listApkSubdirs(base, prefix = [], depth = 0) {
483
+ if (depth > 3) return [];
484
+ const dirs = [];
485
+ for (const name of safeList(base)) {
486
+ const path = join(base, name);
487
+ let isDir = false;
488
+ try {
489
+ isDir = statSync(path).isDirectory();
490
+ } catch {
491
+ continue;
492
+ }
493
+ if (!isDir) continue;
494
+ const rel = [...prefix, name];
495
+ dirs.push(rel);
496
+ dirs.push(...listApkSubdirs(path, rel, depth + 1));
497
+ }
498
+ return dirs;
499
+ }
500
+ function findVariantApkDir(root, variant) {
501
+ const base = apkOutputsDir(root);
502
+ const wanted = variant.trim().toLowerCase();
503
+ for (const segments of listApkSubdirs(base)) if (variantNameOf(segments).toLowerCase() === wanted) return join(base, ...segments);
504
+ return null;
505
+ }
506
+ function findDebugApksUnder(base) {
507
+ const found = [];
508
+ for (const segments of [[], ...listApkSubdirs(base)]) {
509
+ const dir = join(base, ...segments);
510
+ for (const name of safeList(dir)) {
511
+ if (!name.endsWith("-debug.apk")) continue;
512
+ if (/-(?:unsigned|unaligned)\.apk$/.test(name)) continue;
513
+ found.push(join(dir, name));
514
+ }
515
+ }
516
+ return found.toSorted();
517
+ }
518
+ function locateApk(root, transcript = "", variant = null) {
519
+ const fromTranscript = parseApkFromTranscript(transcript);
520
+ if (fromTranscript) {
521
+ const abs = fromTranscript.startsWith("/") ? fromTranscript : join(androidDir(root), fromTranscript);
522
+ if (existsSync(abs)) return { apkPath: abs };
523
+ }
524
+ if (variant) {
525
+ const dir = findVariantApkDir(root, variant);
526
+ return { apkPath: dir ? apkInDir(dir) : null };
527
+ }
528
+ const direct = apkInDir(debugApkDir(root));
529
+ if (direct) return { apkPath: direct };
530
+ const found = findDebugApksUnder(apkOutputsDir(root));
531
+ if (found.length === 1) {
532
+ const apk = found[0];
533
+ const suggested = variantNameOf(relative(apkOutputsDir(root), apk).split("/").slice(0, -1));
534
+ return {
535
+ apkPath: apk,
536
+ note: `no APK in ${relative(androidDir(root), debugApkDir(root))}; using ${relative(androidDir(root), apk)}${suggested ? ` -- set the android.variant setting to "${suggested}" to build this variant explicitly` : ""}`
537
+ };
538
+ }
539
+ if (found.length > 1) return {
540
+ apkPath: null,
541
+ candidates: found
542
+ };
543
+ return { apkPath: null };
544
+ }
545
+ const UNKNOWN_FLAVORS = {
546
+ known: false,
547
+ dimensions: []
548
+ };
549
+ function stripGroovyComments(text) {
550
+ let out = "";
551
+ let quote = "";
552
+ let i = 0;
553
+ while (i < text.length) {
554
+ const ch = text[i];
555
+ const next = text[i + 1];
556
+ if (quote) {
557
+ out += ch;
558
+ if (ch === "\\") {
559
+ out += next ?? "";
560
+ i += 2;
561
+ continue;
562
+ }
563
+ if (ch === quote) quote = "";
564
+ i += 1;
565
+ continue;
566
+ }
567
+ if (ch === "\"" || ch === "'") {
568
+ quote = ch;
569
+ out += ch;
570
+ i += 1;
571
+ continue;
572
+ }
573
+ if (ch === "/" && next === "/") {
574
+ while (i < text.length && text[i] !== "\n") i += 1;
575
+ continue;
576
+ }
577
+ if (ch === "/" && next === "*") {
578
+ i += 2;
579
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
580
+ i += 2;
581
+ out += " ";
582
+ continue;
583
+ }
584
+ out += ch;
585
+ i += 1;
586
+ }
587
+ return out;
588
+ }
589
+ function closingBrace(text, open) {
590
+ let depth = 1;
591
+ let quote = "";
592
+ for (let i = open; i < text.length; i += 1) {
593
+ const ch = text[i];
594
+ if (quote) {
595
+ if (ch === "\\") i += 1;
596
+ else if (ch === quote) quote = "";
597
+ continue;
598
+ }
599
+ if (ch === "\"" || ch === "'") quote = ch;
600
+ else if (ch === "{") depth += 1;
601
+ else if (ch === "}") {
602
+ depth -= 1;
603
+ if (depth === 0) return i;
604
+ }
605
+ }
606
+ return -1;
607
+ }
608
+ function productFlavorsBody(text) {
609
+ const match = /(?:^|[^\w.])productFlavors\s*\{/.exec(text);
610
+ if (!match) return null;
611
+ const open = match.index + match[0].length;
612
+ const close = closingBrace(text, open);
613
+ return close < 0 ? null : text.slice(open, close);
614
+ }
615
+ function parseFlavorBlocks(body) {
616
+ const entry = /([A-Za-z_][A-Za-z0-9_]*)\s*\{/y;
617
+ const blocks = [];
618
+ let i = 0;
619
+ while (i < body.length) {
620
+ if (/\s/.test(body[i])) {
621
+ i += 1;
622
+ continue;
623
+ }
624
+ entry.lastIndex = i;
625
+ const match = entry.exec(body);
626
+ if (!match) return null;
627
+ const open = i + match[0].length;
628
+ const close = closingBrace(body, open);
629
+ if (close < 0) return null;
630
+ blocks.push({
631
+ name: match[1],
632
+ body: body.slice(open, close)
633
+ });
634
+ i = close + 1;
635
+ }
636
+ return blocks;
637
+ }
638
+ function parseDimensionNames(text) {
639
+ const statements = [...text.matchAll(/\bflavorDimensions\b([^\n]*)/g)];
640
+ if (statements.length === 0) return void 0;
641
+ const names = [];
642
+ for (const statement of statements) {
643
+ const line = statement[1] ?? "";
644
+ for (const quoted of line.matchAll(/(["'])([^"']*)\1/g)) names.push(quoted[2]);
645
+ if (line.replace(/(["'])[^"']*\1/g, "").replace(/[[\]()=+,;]/g, "").trim() !== "") return null;
646
+ }
647
+ return names.length ? names : null;
648
+ }
649
+ function declaredDimensionOf(flavorBody) {
650
+ const match = /\bdimension\b\s*=?\s*(["'])([^"']*)\1/.exec(flavorBody);
651
+ return match ? match[2] : null;
652
+ }
653
+ function parseProductFlavors(source) {
654
+ if (typeof source !== "string") return UNKNOWN_FLAVORS;
655
+ const text = stripGroovyComments(source);
656
+ if (/\bvariantFilter\b/.test(text)) return UNKNOWN_FLAVORS;
657
+ if (!/\bproductFlavors\b/.test(text)) return {
658
+ known: true,
659
+ dimensions: []
660
+ };
661
+ const body = productFlavorsBody(text);
662
+ if (body === null) return UNKNOWN_FLAVORS;
663
+ const flavors = parseFlavorBlocks(body);
664
+ if (!flavors) return UNKNOWN_FLAVORS;
665
+ if (flavors.length === 0) return {
666
+ known: true,
667
+ dimensions: []
668
+ };
669
+ const declared = parseDimensionNames(text);
670
+ if (declared === null) return UNKNOWN_FLAVORS;
671
+ const named = flavors.map((flavor) => declaredDimensionOf(flavor.body));
672
+ const used = [...new Set(named.filter((name) => name !== null))];
673
+ if (!declared || declared.length === 1) {
674
+ const only = declared ? declared[0] : used[0];
675
+ if (used.some((name) => name !== only)) return UNKNOWN_FLAVORS;
676
+ return {
677
+ known: true,
678
+ dimensions: [flavors.map((flavor) => flavor.name)]
679
+ };
680
+ }
681
+ if (named.some((name) => name === null || !declared.includes(name))) return UNKNOWN_FLAVORS;
682
+ const dimensions = [];
683
+ for (const dimension of declared) {
684
+ const group = flavors.filter((_, i) => named[i] === dimension).map((flavor) => flavor.name);
685
+ if (group.length === 0) return UNKNOWN_FLAVORS;
686
+ dimensions.push(group);
687
+ }
688
+ return {
689
+ known: true,
690
+ dimensions
691
+ };
692
+ }
693
+ function readProductFlavors(root) {
694
+ return parseProductFlavors(readOrNull(join(androidDir(root), "app", "build.gradle")));
695
+ }
696
+ function productFlavorRefusal({ flavors, variant }) {
697
+ if (variant) return null;
698
+ if (!flavors.known || flavors.dimensions.length === 0) return null;
699
+ let combinations = [[]];
700
+ for (const group of flavors.dimensions) combinations = combinations.flatMap((combination) => group.map((flavor) => combination.concat(flavor)));
701
+ if (combinations.length < 2) return null;
702
+ const count = flavors.dimensions.reduce((total, group) => total + group.length, 0);
703
+ const variants = combinations.map((combination) => variantNameOf(combination.concat("debug")));
704
+ return {
705
+ code: "STIM_BAD_ARG",
706
+ reason: `android/app/build.gradle declares ${count} product flavors, so \`./gradlew ${ASSEMBLE_TASK}\` builds an APK for each of them and nothing says which flavor to install.`,
707
+ remedy: `Pass \`--variant ${variants[0]}\` or set the android.variant setting -- e.g. {"android": {"variant": "${variants[0]}"}} in .stim.json. The debug variants are: ${variants.join(", ")}.`
708
+ };
709
+ }
710
+ function gradleArgs(task, { buildCache = true, abi = null } = {}) {
711
+ return [
712
+ task,
713
+ ...buildCache ? ["--build-cache"] : ["--no-build-cache"],
714
+ ...abi ? [`-PreactNativeArchitectures=${abi}`] : []
715
+ ];
716
+ }
717
+ async function buildAndroid({ root, logWriter, variant = null, abi = null }, { spawnFn = null, now = Date.now, env = process.env, buildCache = true, ccache = null, cas = null, pch = "auto", compilerCacheDisabled = false, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line), onNote = (line) => console.error(line) } = {}) {
718
+ const project = discoverAndroidProject(root);
719
+ if (project.failed) return {
720
+ ...project,
721
+ diagnostics: [],
722
+ truncated: 0,
723
+ lastLines: [],
724
+ durationMs: 0
725
+ };
726
+ const sdk = androidHome();
727
+ const refusal = androidSdkRefusal({
728
+ sdkPath: sdk,
729
+ sdkExists: existsSync(sdk),
730
+ hasLocalProperties: existsSync(join(project.androidDir, "local.properties"))
731
+ });
732
+ if (refusal) return {
733
+ failed: true,
734
+ ...refusal,
735
+ diagnostics: [],
736
+ truncated: 0,
737
+ lastLines: [],
738
+ durationMs: 0
739
+ };
740
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
741
+ const task = assembleTaskFor(variant);
742
+ const args = gradleArgs(task, {
743
+ buildCache,
744
+ abi
745
+ });
746
+ if (cas) {
747
+ args.push("--init-script", cas.initScript);
748
+ onNote(chalk.dim(phaseLine("cache", `Apple Clang CAS on (${cas.dir})`)));
749
+ }
750
+ const nativeScript = ["../shim/android-optimizations.gradle", "../../shim/android-optimizations.gradle"].map((path) => fileURLToPath(new URL(path, import.meta.url))).find((path) => existsSync(path));
751
+ if (!nativeScript) throw new Error("Stim installation is missing shim/android-optimizations.gradle.");
752
+ args.push("--init-script", nativeScript);
753
+ const nativeBackend = cas ? cas.id : ccache ? ccache.env.CMAKE_CXX_COMPILER_LAUNCHER : compilerCacheDisabled ? "none" : "project";
754
+ const nativeEnv = {
755
+ STIM_ANDROID_NATIVE_PROFILE: createHash("sha256").update(readFileSync(nativeScript)).update(JSON.stringify({
756
+ backend: nativeBackend,
757
+ pch
758
+ })).digest("hex").slice(0, 16),
759
+ STIM_ANDROID_PCH: pch,
760
+ STIM_ANDROID_CCACHE: ccache && !cas ? "on" : compilerCacheDisabled || cas ? "off" : "project"
761
+ };
762
+ if (compilerCacheDisabled) Object.assign(nativeEnv, {
763
+ CCACHE_DISABLE: "1",
764
+ CMAKE_C_COMPILER_LAUNCHER: "",
765
+ CMAKE_CXX_COMPILER_LAUNCHER: ""
766
+ });
767
+ if (ccache && !cas && pch === "auto") onNote(chalk.dim(phaseLine("cache", "CMake PCH off by default for ccache reuse (explicit project settings preserved)")));
768
+ else if (pch !== "auto") onNote(chalk.dim(phaseLine("cache", `CMake PCH ${pch} by configuration`)));
769
+ if (buildCache) onNote(chalk.dim(phaseLine("cache", "gradle build cache on (--build-cache, shared under the Gradle user home)")));
770
+ logWriter?.write?.({
771
+ src: "build",
772
+ level: "info",
773
+ msg: `${project.gradlew} ${args.join(" ")}`,
774
+ event: "build_start"
775
+ });
776
+ const startedAt = now();
777
+ const tail = [];
778
+ const window = [];
779
+ const push = (line) => {
780
+ const msg = stripAnsi(String(line)).trimEnd();
781
+ if (!msg.trim()) return;
782
+ tail.push(msg);
783
+ if (tail.length > LAST_LINES) tail.shift();
784
+ window.push(msg);
785
+ if (window.length > TRANSCRIPT_LINES) window.shift();
786
+ logWriter?.write?.({
787
+ src: "build",
788
+ level: "debug",
789
+ msg,
790
+ raw: true,
791
+ event: "gradle"
792
+ });
793
+ };
794
+ if (ccache) resetStatsLog(ccache.statsLog);
795
+ let child;
796
+ try {
797
+ child = spawn(project.gradlew, args, {
798
+ cwd: project.androidDir,
799
+ stdio: [
800
+ "ignore",
801
+ "pipe",
802
+ "pipe"
803
+ ],
804
+ env: {
805
+ ...env,
806
+ ...ccache?.env,
807
+ ...cas?.env,
808
+ ...nativeEnv,
809
+ TERM: "dumb",
810
+ FORCE_COLOR: "0"
811
+ }
812
+ });
813
+ } catch (err) {
814
+ return spawnFailure(err, project, now() - startedAt);
815
+ }
816
+ const outReader = createLineReader(push);
817
+ const errReader = createLineReader(push);
818
+ child.stdout?.setEncoding?.("utf-8");
819
+ child.stderr?.setEncoding?.("utf-8");
820
+ child.stdout?.on("data", (chunk) => outReader.push(chunk));
821
+ child.stderr?.on("data", (chunk) => errReader.push(chunk));
822
+ const stopHeartbeat = startBuildHeartbeat({
823
+ intervalMs: heartbeatMs,
824
+ elapsed: () => now() - startedAt,
825
+ emit: onHeartbeat,
826
+ estimateMs
827
+ });
828
+ const result = await waitForChild(child);
829
+ stopHeartbeat();
830
+ outReader.flush();
831
+ errReader.flush();
832
+ const durationMs = now() - startedAt;
833
+ const transcript = window.join("\n");
834
+ const ccacheActivity = ccache ? readCcacheActivity(ccache.statsLog) : CCACHE_UNAVAILABLE;
835
+ if (result.error) return {
836
+ ...spawnFailure(result.error, project, durationMs),
837
+ lastLines: tail.slice(),
838
+ ccache: ccacheActivity
839
+ };
840
+ if (result.code !== 0) {
841
+ const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
842
+ const { shown, truncated } = capDiagnostics(extractGradleDiagnostics(transcript));
843
+ return {
844
+ failed: true,
845
+ code: BUILD_ERROR,
846
+ reason: `\`./gradlew ${task}\` failed (${how}).`,
847
+ diagnostics: shown,
848
+ truncated,
849
+ lastLines: tail.slice(),
850
+ durationMs,
851
+ ccache: ccacheActivity
852
+ };
853
+ }
854
+ const located = locateApk(root, transcript, variant);
855
+ if (!located.apkPath && located.candidates?.length) return {
856
+ failed: true,
857
+ code: BUILD_ERROR,
858
+ reason: `\`./gradlew ${task}\` left ${located.candidates.length} debug APKs under ${apkOutputsDir(root)}, and nothing says which flavor to install.`,
859
+ remedy: `Set the android.variant setting to the variant to install -- e.g. {"android": {"variant": "${variantNameOf(relative(apkOutputsDir(root), located.candidates[0]).split("/").slice(0, -1))}"}} in .stim.json.`,
860
+ diagnostics: [],
861
+ truncated: 0,
862
+ lastLines: located.candidates.map((c) => relative(androidDir(root), c)),
863
+ durationMs,
864
+ ccache: ccacheActivity
865
+ };
866
+ if (!located.apkPath) return {
867
+ failed: true,
868
+ code: BUILD_ERROR,
869
+ reason: variant ? `\`./gradlew ${task}\` succeeded but produced no APK under ${apkOutputsDir(root)} for variant "${variant}".` : `\`./gradlew ${task}\` succeeded but produced no APK in ${debugApkDir(root)}.`,
870
+ remedy: variant ? `Check that the android.variant setting ("${variant}") names a real variant (\`./gradlew :app:tasks\` lists the assemble tasks).` : `Check that ${task} builds the app module (\`./gradlew :app:${task}\` in android/) and that no flavour redirects the output.`,
871
+ diagnostics: [],
872
+ truncated: 0,
873
+ lastLines: tail.slice(),
874
+ durationMs,
875
+ ccache: ccacheActivity
876
+ };
877
+ return {
878
+ ok: true,
879
+ apkPath: located.apkPath,
880
+ apkNote: located.note ?? null,
881
+ durationMs,
882
+ lastLines: tail.slice(),
883
+ ccache: ccacheActivity
884
+ };
885
+ }
886
+ function resetStatsLog(statsLog) {
887
+ try {
888
+ mkdirSync(dirname(statsLog), { recursive: true });
889
+ rmSync(statsLog, { force: true });
890
+ } catch {}
891
+ }
892
+ function spawnFailure(err, project, durationMs) {
893
+ const nodeErr = err;
894
+ const message = String(nodeErr?.message || err || "");
895
+ const permissionDenied = nodeErr?.code === "EACCES" || /EACCES|permission denied/i.test(message);
896
+ return {
897
+ failed: true,
898
+ code: BUILD_ERROR,
899
+ reason: `Could not run ${project.gradlew}: ${message}`,
900
+ remedy: permissionDenied ? `Make the wrapper executable: \`chmod +x ${project.gradlew}\`.` : "Check that the gradle wrapper is intact (android/gradlew and android/gradle/wrapper/) and that JAVA_HOME points at a JDK 17 install.",
901
+ diagnostics: [],
902
+ truncated: 0,
903
+ lastLines: [],
904
+ durationMs
905
+ };
906
+ }
907
+ function baseName(file) {
908
+ const parts = String(file).split("/");
909
+ return parts[parts.length - 1] ?? String(file);
910
+ }
911
+ function safeList(dir) {
912
+ try {
913
+ return readdirSync(dir);
914
+ } catch {
915
+ return [];
916
+ }
917
+ }
918
+ function readOrNull(file) {
919
+ try {
920
+ return readFileSync(file, "utf-8");
921
+ } catch {
922
+ return null;
923
+ }
924
+ }
925
+ //#endregion
926
+ //#region src/engine/android-cas.ts
927
+ const CAS_TOOLCHAIN_FIELDS = [
928
+ "clang",
929
+ "clangxx",
930
+ "lld",
931
+ "ar",
932
+ "ranlib",
933
+ "ndk",
934
+ "resourceDir"
935
+ ];
936
+ const CAS_TOOLCHAIN_BINARIES = [
937
+ "clang",
938
+ "clangxx",
939
+ "lld",
940
+ "ar",
941
+ "ranlib"
942
+ ];
943
+ function executableFile(path) {
944
+ try {
945
+ accessSync(path, constants.X_OK);
946
+ return statSync(path).isFile();
947
+ } catch {
948
+ return false;
949
+ }
950
+ }
951
+ function directory(path) {
952
+ try {
953
+ return statSync(path).isDirectory();
954
+ } catch {
955
+ return false;
956
+ }
957
+ }
958
+ function readAndroidCasToolchain(manifest) {
959
+ const toolchain = JSON.parse(readFileSync(manifest, "utf8"));
960
+ const missing = CAS_TOOLCHAIN_FIELDS.filter((field) => typeof toolchain?.[field] !== "string");
961
+ if (missing.length > 0) throw new Error(`${manifest} declares no ${missing.join(", ")}.`);
962
+ const notExecutable = CAS_TOOLCHAIN_BINARIES.filter((field) => !executableFile(toolchain[field]));
963
+ if (notExecutable.length > 0) throw new Error(`${manifest} names no executable ${notExecutable.join(", ")}.`);
964
+ if (!directory(toolchain.resourceDir)) throw new Error(`${manifest} names resourceDir ${toolchain.resourceDir}, which is not a directory.`);
965
+ const ndkProperties = readFileSync(join(toolchain.ndk, "source.properties"), "utf8");
966
+ const ndkVersion = /Pkg.Revision\s*=\s*([^\r\n]+)/.exec(ndkProperties)?.[1]?.trim();
967
+ if (!ndkVersion) throw new Error(`Missing NDK version in ${toolchain.ndk}/source.properties.`);
968
+ return {
969
+ toolchain,
970
+ ndkVersion,
971
+ ndkProperties
972
+ };
973
+ }
974
+ function resolveAndroidCompilerCache({ optimizations, use, env = process.env }) {
975
+ if (optimizations.android.compilerCache !== "cas") return {
976
+ cas: null,
977
+ optimizations
978
+ };
979
+ const manifest = optimizations.android.casToolchain;
980
+ try {
981
+ return {
982
+ cas: use(manifest),
983
+ optimizations
984
+ };
985
+ } catch (error) {
986
+ const fromEnvironment = Boolean(env.STIM_ANDROID_CAS_TOOLCHAIN);
987
+ return {
988
+ cas: null,
989
+ optimizations: {
990
+ ...optimizations,
991
+ android: {
992
+ ...optimizations.android,
993
+ compilerCache: "ccache",
994
+ compilerCacheFallback: {
995
+ key: fromEnvironment ? "STIM_ANDROID_CAS_TOOLCHAIN" : "optimizations.android.casToolchain",
996
+ reason: `could not be used: ${error.message.replace(/\.$/, "")}`,
997
+ fromEnvironment,
998
+ manifest
999
+ }
1000
+ }
1001
+ }
1002
+ };
1003
+ }
1004
+ }
1005
+ function resolveAndroidCas(root, env = process.env) {
1006
+ const manifest = env.STIM_ANDROID_CAS_TOOLCHAIN;
1007
+ if (!manifest) return null;
1008
+ const { toolchain, ndkVersion, ndkProperties } = readAndroidCasToolchain(manifest);
1009
+ const scripts = [
1010
+ "shim/android-cas.gradle",
1011
+ "shim/android-cas.toolchain.cmake",
1012
+ "shim/android-cas-pch.cmake",
1013
+ "dist/android-cas-compiler.mjs"
1014
+ ].map((name) => {
1015
+ const path = [`../${name}`, `../../${name}`].map((candidate) => fileURLToPath(new URL(candidate, import.meta.url))).find((candidate) => existsSync(candidate));
1016
+ if (!path) throw new Error(`Stim installation is missing ${name}.`);
1017
+ return path;
1018
+ });
1019
+ const hash = createHash("sha256").update(JSON.stringify(toolchain));
1020
+ for (const path of [
1021
+ ...scripts,
1022
+ toolchain.clang,
1023
+ toolchain.clangxx,
1024
+ toolchain.lld,
1025
+ toolchain.ar,
1026
+ toolchain.ranlib
1027
+ ]) hash.update(readFileSync(path));
1028
+ hash.update(ndkProperties);
1029
+ const id = `apple-cas-${hash.digest("hex")}`;
1030
+ const dir = join(getConfigDir(), "android-cas", id);
1031
+ const state = join(ensureWorkspaceStorage(root), "android-cas", id);
1032
+ const context = join(state, "context.json");
1033
+ mkdirSync(state, { recursive: true });
1034
+ mkdirSync(dir, { recursive: true });
1035
+ withDirLock(join(state, "setup.lock"), () => {
1036
+ for (const name of ["clang", "clang++"]) {
1037
+ const path = join(state, name);
1038
+ const temporary = `${path}.${randomUUID()}.tmp`;
1039
+ symlinkSync(scripts[3], temporary);
1040
+ renameSync(temporary, path);
1041
+ }
1042
+ const temporary = `${context}.${process.pid}.tmp`;
1043
+ writeFileSync(temporary, JSON.stringify({
1044
+ ...toolchain,
1045
+ source: realpathSync(root),
1046
+ state,
1047
+ cache: dir
1048
+ }));
1049
+ renameSync(temporary, context);
1050
+ });
1051
+ register({
1052
+ dir,
1053
+ name: "android-cas",
1054
+ prune: "atomic",
1055
+ note: "Experimental Android compiler CAS"
1056
+ });
1057
+ return {
1058
+ id,
1059
+ dir,
1060
+ initScript: scripts[0],
1061
+ env: {
1062
+ STIM_ANDROID_CAS_CONTEXT: context,
1063
+ STIM_ANDROID_CAS_SOURCE_ROOT: realpathSync(root),
1064
+ STIM_ANDROID_CAS_STATE: state,
1065
+ STIM_ANDROID_CAS_NDK: toolchain.ndk,
1066
+ STIM_ANDROID_CAS_NDK_VERSION: ndkVersion,
1067
+ STIM_ANDROID_CAS_AR: toolchain.ar,
1068
+ STIM_ANDROID_CAS_RANLIB: toolchain.ranlib,
1069
+ STIM_ANDROID_CAS_CMAKE_TOOLCHAIN: scripts[1],
1070
+ STIM_ANDROID_CAS_CMAKE_PCH: scripts[2],
1071
+ CMAKE_C_COMPILER_LAUNCHER: "",
1072
+ CMAKE_CXX_COMPILER_LAUNCHER: "",
1073
+ CCACHE_DISABLE: "1"
1074
+ }
1075
+ };
1076
+ }
1077
+ //#endregion
1078
+ export { buildAndroid as a, CCACHE_NOT_RUN as c, projectCmakeLauncher as d, resolveCcache as f, apkOutputsDir as i, CCACHE_UNAVAILABLE as l, resolveAndroidCas as n, productFlavorRefusal as o, formatDiagnostic as p, resolveAndroidCompilerCache as r, readProductFlavors as s, readAndroidCasToolchain as t, ccacheActivityLine as u };