stim 1.6.0 → 1.7.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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/{android-BkYmndPp.mjs → android-DWssqHQ8.mjs} +165 -661
  3. package/dist/{android-DLJB8ljN.mjs → android-Da-G_o3n.mjs} +646 -548
  4. package/dist/{android-BW8i_YxU.mjs → android-DeYdjjBP.mjs} +6 -2
  5. package/dist/{android-cas-CrzI_Zha.mjs → android-cas-SWIjzeuL.mjs} +54 -18
  6. package/dist/android-cas-compiler.mjs +1 -1
  7. package/dist/{app-install--PAocWvq.mjs → app-install-DJg7bVv-.mjs} +36 -10
  8. package/dist/build-lock-CgtsB6TV.mjs +604 -0
  9. package/dist/{build-slots-CuPszJDu.mjs → build-slots-C-oAHRT1.mjs} +2 -2
  10. package/dist/{cache-manifest-uoOKhgUD.mjs → cache-manifest-CYJi8-Hx.mjs} +2 -2
  11. package/dist/cache-manifest.d.mts +1 -1
  12. package/dist/cache-manifest.mjs +1 -1
  13. package/dist/cli.mjs +14 -14
  14. package/dist/collector-run.d.mts +2 -0
  15. package/dist/collector-run.mjs +7 -7
  16. package/dist/{command-output-BuIb_ZOI.mjs → command-output-B3ycpsYv.mjs} +2 -2
  17. package/dist/{config-DMP7L7H6.mjs → config-B7iyKWkL.mjs} +56 -16
  18. package/dist/{dependency-state-CTL8A9p3.mjs → dependency-state-D0M7oKA7.mjs} +2 -2
  19. package/dist/{deps-C1zGnr61.mjs → deps-oCXVNEqQ.mjs} +6 -6
  20. package/dist/detached-entry-Bpo6ZY_I.mjs +65 -0
  21. package/dist/{dev-client-BaTXAAnC.mjs → dev-client-DDUkWgJL.mjs} +12 -12
  22. package/dist/{device-xoQxw8hR.mjs → device-CBSaC6BU.mjs} +7 -7
  23. package/dist/{device-XY_5eGdN.mjs → device-CLeDp-KH.mjs} +17 -15
  24. package/dist/{device-lease-DBW-IL4p.mjs → device-lease-Ffz_M5t0.mjs} +3 -3
  25. package/dist/{device-pool-C0s_83Og.mjs → device-pool-DNSHL5z8.mjs} +4 -4
  26. package/dist/{device-remote-D9wN3CdE.mjs → device-remote-BprxZp-m.mjs} +11 -11
  27. package/dist/{doctor-BRO8UKlK.mjs → doctor-CTAx9gMu.mjs} +90 -46
  28. package/dist/{error-diagnostics-B5STzxqN.mjs → error-diagnostics-D1Kaf-eT.mjs} +23 -17
  29. package/dist/{exec-CyylIdq9.mjs → exec-B29XNuhP.mjs} +14 -2
  30. package/dist/{gc-C-6grAQI.mjs → gc-CX0KRfLG.mjs} +17 -16
  31. package/dist/{guide-CYtd7xbA.mjs → guide-CPmAG9me.mjs} +38 -15
  32. package/dist/{guide-status-BF6RsJJp.mjs → guide-status-Craq9z0t.mjs} +3 -3
  33. package/dist/{ios-Ccm6c8Hj.mjs → ios-BRXYSxSc.mjs} +34 -37
  34. package/dist/{ios-device-DJOi9FR5.mjs → ios-device-CDbvn5H2.mjs} +2 -2
  35. package/dist/{ios-device-D3pzTKQE.mjs → ios-device-CFXA46ez.mjs} +1 -1
  36. package/dist/{ios-Deh5js5V.mjs → ios-lEJ5pKsq.mjs} +3 -3
  37. package/dist/{logs-D4Poojxy.mjs → logs-CakdBTtm.mjs} +6 -6
  38. package/dist/{logs-query-Cdp5aaCH.mjs → logs-query-BGL-Pl12.mjs} +1 -1
  39. package/dist/metro-C7bcJ0uo.mjs +233 -0
  40. package/dist/{named-ports-DH9SXd0f.mjs → named-ports-DzMOlmaX.mjs} +57 -42
  41. package/dist/{ownership-CD_XnpDu.mjs → ownership-Cp52QAK1.mjs} +3 -3
  42. package/dist/{ownership-CVY9cqCr.mjs → ownership-D5vkdm8H.mjs} +13 -13
  43. package/dist/{ports-A6uTJSPj.mjs → ports-BIbJaKNW.mjs} +3 -3
  44. package/dist/{process-output-C7IlIzd7.mjs → process-output-Dl5uKE03.mjs} +1 -1
  45. package/dist/{project-B_SDt4ip.mjs → project-DDZWf_16.mjs} +3 -3
  46. package/dist/{reclaim-WXTbXPX4.mjs → reclaim-D614jI6_.mjs} +11 -11
  47. package/dist/{reload-xlQvHKfN.mjs → reload-ChW46VDb.mjs} +9 -9
  48. package/dist/{server-bare-CCLb90FS.mjs → server-bare-BQkyxa_b.mjs} +1 -1
  49. package/dist/{server-expo-CX-zkgWl.mjs → server-expo-BVAA1OqJ.mjs} +23 -11
  50. package/dist/server-expo-YDDG3eP7.mjs +2 -0
  51. package/dist/{settings-CGHTt5vb.mjs → settings-BXXorVM2.mjs} +3 -3
  52. package/dist/{slot-launch-BYjnljAs.mjs → slot-launch-DbLvyrup.mjs} +4 -4
  53. package/dist/{start-DHYAyGzm.mjs → start-TTVILlUi.mjs} +111 -38
  54. package/dist/{state-CnWzvCWP.mjs → state-DZobzxLW.mjs} +2 -2
  55. package/dist/{stats-Fo9ACIv7.mjs → stats-BYcZ6n1h.mjs} +1 -1
  56. package/dist/{stats-DHrO0frH.mjs → stats-CLg5RuDS.mjs} +4 -4
  57. package/dist/{status-QeLZb8SM.mjs → status-DUNy_HZs.mjs} +14 -15
  58. package/dist/{stop-CHTaw65F.mjs → stop-CnUpnFT5.mjs} +2 -2
  59. package/dist/{stop-DsdOx9Hj.mjs → stop-ZrfODZ51.mjs} +13 -13
  60. package/dist/supervisor-run.mjs +7 -5
  61. package/dist/{teardown-sm1FwF6V.mjs → teardown-5J5UXz2P.mjs} +14 -17
  62. package/dist/{workspace-process-lock-Bz19_R9R.mjs → workspace-process-lock-BYfZz_S-.mjs} +1 -1
  63. package/dist/{workspace-state-WVwWw_hB.mjs → workspace-state-BT5E3SA2.mjs} +3 -3
  64. package/dist/{worktree-CCNPg_f0.mjs → worktree-BfE4lzHK.mjs} +21 -16
  65. package/dist/{worktree-DJnBbQZ9.mjs → worktree-FG_ZKKdb.mjs} +13 -9
  66. package/dist/{xcode-DUF7jrXj.mjs → xcode-B6YaWeg0.mjs} +8 -8
  67. package/package.json +12 -8
  68. package/shim/android-optimizations.gradle +6 -0
  69. package/dist/metro-ato6ef3J.mjs +0 -99
  70. package/dist/ports-BZprTJRa.mjs +0 -90
  71. package/dist/server-expo-CjeTJ273.mjs +0 -2
@@ -1,610 +1,10 @@
1
- import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { E as ownership_claim_exports, u as getConfigDir } from "./config-DMP7L7H6.mjs";
3
- import { r as register } from "./cache-manifest-uoOKhgUD.mjs";
4
- import { l as sharedBuildCache } from "./workspace-state-WVwWw_hB.mjs";
5
- import { r as pidExists } from "./metro-ato6ef3J.mjs";
6
- import { a as formatElapsed } from "./command-output-BuIb_ZOI.mjs";
7
- import { c as androidDataPartitionSizeBytes } from "./settings-CGHTt5vb.mjs";
8
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
9
- import { createHash } from "node:crypto";
10
- import { join } from "node:path";
11
- import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1 } from "fs";
12
- import { homedir } from "os";
13
- import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1 } from "path";
14
- import { buildCacheKey, resolveArtifact, storeArtifact } from "@stim-cli/core";
15
- import * as expoFingerprint from "@expo/fingerprint";
16
- import { buildUploadTimeoutMs } from "@stim-cli/cache";
17
- const ASSET_MANIFEST_FILE = "assets-manifest.json";
18
- const MAX_DEPTH = 4;
19
- function hashFile(file) {
20
- try {
21
- return createHash("sha256").update(readFileSync(file)).digest("hex");
22
- } catch {
23
- return null;
24
- }
25
- }
26
- function readAssetManifest(root) {
27
- if (!existsSync(root)) return null;
28
- const assets = [];
29
- let broken = false;
30
- const walk = (abs, rel, depth) => {
31
- if (depth > MAX_DEPTH || broken) return;
32
- let names;
33
- try {
34
- names = readdirSync(abs);
35
- } catch {
36
- broken = true;
37
- return;
38
- }
39
- for (const name of names) {
40
- const child = join(abs, name);
41
- let stat;
42
- try {
43
- stat = statSync(child);
44
- } catch {
45
- broken = true;
46
- return;
47
- }
48
- const path = rel === "" ? name : `${rel}/${name}`;
49
- if (stat.isDirectory()) {
50
- walk(child, path, depth + 1);
51
- continue;
52
- }
53
- if (!stat.isFile()) continue;
54
- const sha256 = hashFile(child);
55
- if (sha256 === null) {
56
- broken = true;
57
- return;
58
- }
59
- assets.push({
60
- path,
61
- sha256
62
- });
63
- }
64
- };
65
- walk(root, "", 0);
66
- if (broken) return null;
67
- assets.sort((a, b) => a.path.localeCompare(b.path));
68
- return {
69
- version: 1,
70
- assets
71
- };
72
- }
73
- function parseAssetManifest(text) {
74
- let parsed;
75
- try {
76
- parsed = JSON.parse(String(text));
77
- } catch {
78
- return null;
79
- }
80
- const obj = parsed;
81
- if (!obj || obj.version !== 1 || !Array.isArray(obj.assets)) return null;
82
- const assets = [];
83
- for (const raw of obj.assets) {
84
- const entry = raw;
85
- if (typeof entry?.path !== "string" || typeof entry?.sha256 !== "string") return null;
86
- assets.push({
87
- path: entry.path,
88
- sha256: entry.sha256
89
- });
90
- }
91
- return {
92
- version: 1,
93
- assets
94
- };
95
- }
96
- function pickGeneratedResDir(names, variant, reactChildren = []) {
97
- const wanted = (variant ?? "").trim().toLowerCase();
98
- const modern = names.filter((n) => /^createBundle.+JsAndAssets$/.test(n)).toSorted();
99
- if (wanted) {
100
- const match = modern.find((n) => n.toLowerCase() === `createbundle${wanted}jsandassets`);
101
- if (match) return match;
102
- } else if (modern.length === 1) return modern[0];
103
- if (!names.includes("react")) return null;
104
- const children = reactChildren.toSorted();
105
- if (wanted) {
106
- const match = children.find((n) => n.toLowerCase() === wanted);
107
- if (match) return `react/${match}`;
108
- } else if (children.length === 1) return `react/${children[0]}`;
109
- return null;
110
- }
111
- function androidModuleDirs(root, { list = safeList$1 } = {}) {
112
- const android = join(root, "android");
113
- return ["app", ...list(android).filter((name) => name !== "app" && existsSync(join(android, name, "build", "generated", "res"))).toSorted()];
114
- }
115
- function findGeneratedResDir(root, variant, { list = safeList$1 } = {}) {
116
- for (const module of androidModuleDirs(root, { list })) {
117
- const base = join(root, "android", module, "build", "generated", "res");
118
- const names = list(base);
119
- if (names.length === 0) continue;
120
- const picked = pickGeneratedResDir(names, variant, names.includes("react") ? list(join(base, "react")) : []);
121
- if (picked) return join(base, ...picked.split("/"));
122
- }
123
- return null;
124
- }
125
- function captureAssetManifest(root, { variant = null, findDir = findGeneratedResDir, read = readAssetManifest } = {}) {
126
- const dir = findDir(root, variant);
127
- return dir ? read(dir) : null;
128
- }
129
- function compareAssetManifests(fresh, stored) {
130
- const left = new Map(fresh.assets.map((a) => [a.path, a.sha256]));
131
- const right = new Map(stored.assets.map((a) => [a.path, a.sha256]));
132
- const added = [];
133
- const changed = [];
134
- const removed = [];
135
- for (const [path, sha256] of left) if (!right.has(path)) added.push(path);
136
- else if (right.get(path) !== sha256) changed.push(path);
137
- for (const path of right.keys()) if (!left.has(path)) removed.push(path);
138
- added.sort();
139
- changed.sort();
140
- removed.sort();
141
- return {
142
- same: added.length === 0 && changed.length === 0 && removed.length === 0,
143
- added,
144
- removed,
145
- changed,
146
- example: added[0] ?? changed[0] ?? removed[0] ?? null
147
- };
148
- }
149
- function assetDiffReason(diff) {
150
- const which = diff.added.length ? "added" : diff.changed.length ? "changed" : "removed";
151
- return `this workspace emits a different asset set than the cached build did (${diff.added.length} added, ${diff.changed.length} changed, ${diff.removed.length} removed; e.g. ${which} ${diff.example})`;
152
- }
153
- function safeList$1(dir) {
154
- try {
155
- return readdirSync(dir);
156
- } catch {
157
- return [];
158
- }
159
- }
160
- //#endregion
161
- //#region src/build-cache.ts
162
- function entryDir(platform, key, root = sharedBuildCache()) {
163
- return join$1(root, platform, key);
164
- }
165
- const FINGERPRINT_PLATFORMS = /* @__PURE__ */ new Set(["ios", "android"]);
166
- /**
167
- * Paths that exist in a working checkout and not in a fresh one, which
168
- * @expo/fingerprint does not ignore on its own. A fingerprint that counts them
169
- * cannot match across checkouts, which is the parity failure `doctor` reports.
170
- *
171
- * The list stays narrow on purpose: a path belongs here only when no native
172
- * build on any project can read it. Anything a single project can judge --
173
- * a lockfile with machine paths, a generated report -- belongs in that
174
- * project's `.fingerprintignore`, which still applies on top of this.
175
- */
176
- const DEFAULT_FINGERPRINT_IGNORES = ["**/android/local.properties", "**/android/.idea/**"];
177
- async function fingerprintProject(projectRoot, { platform, createFingerprint = expoFingerprint.createFingerprintAsync, debug = false } = {}) {
178
- const result = await createFingerprint(projectRoot, {
179
- ...platform && FINGERPRINT_PLATFORMS.has(platform) ? { platforms: [platform] } : void 0,
180
- ...debug ? { debug } : {},
181
- ignorePaths: DEFAULT_FINGERPRINT_IGNORES,
182
- silent: true
183
- });
184
- const hash = result?.hash ?? null;
185
- if (!hash) return null;
186
- return {
187
- hash,
188
- sources: Array.isArray(result?.sources) ? result.sources : []
189
- };
190
- }
191
- function registerOnce(root) {
192
- try {
193
- register({
194
- dir: root,
195
- name: "Build cache",
196
- prune: "entries",
197
- entriesDepth: 2,
198
- note: "built .app/.apk keyed on the native fingerprint"
199
- });
200
- } catch {}
201
- }
202
- function resolveBuild(platform, key, root = sharedBuildCache()) {
203
- return resolveArtifact(entryDir(platform, key, root));
204
- }
205
- function storeBuild(platform, key, buildPath, rootOrOptions = {}) {
206
- const options = typeof rootOrOptions === "string" ? { root: rootOrOptions } : rootOrOptions || {};
207
- const root = options.root || sharedBuildCache();
208
- if (!buildPath || !existsSync$1(buildPath)) throw new Error(`No build to store at ${buildPath}`);
209
- registerOnce(root);
210
- return storeArtifact(entryDir(platform, key, root), buildPath, {
211
- runFile: getExecutor().runFile,
212
- overwrite: Boolean(options.overwrite),
213
- writeMetadata: (staging) => {
214
- if (Array.isArray(options.sources)) try {
215
- writeFileSync$1(join$1(staging, SOURCES_FILE), JSON.stringify(options.sources));
216
- } catch {}
217
- if (options.assetManifest) try {
218
- writeFileSync$1(join$1(staging, ASSET_MANIFEST_FILE), JSON.stringify(options.assetManifest));
219
- } catch {}
220
- }
221
- });
222
- }
223
- const PROVIDER_DOWNLOAD_DIR = "cache-provider";
224
- function providerDownloadPath(workspacePath) {
225
- return join$1(workspacePath, PROVIDER_DOWNLOAD_DIR);
226
- }
227
- /**
228
- * Empties and registers the scratch directory a provider downloads into. It is
229
- * created only when a provider is about to be asked, and registered so `gc`
230
- * reports what an interrupted run left behind.
231
- */
232
- function prepareProviderDownloadDir(dir) {
233
- rmSync$1(dir, {
234
- recursive: true,
235
- force: true
236
- });
237
- mkdirSync$1(dir, { recursive: true });
238
- try {
239
- register({
240
- dir,
241
- name: "Cache provider downloads",
242
- prune: "entries",
243
- entriesDepth: 1,
244
- note: "artifacts fetched from the project cache provider; anything left here is from an interrupted run"
245
- });
246
- } catch {}
247
- }
248
- function filesystemBuildCapability({ root, sources, assetManifest, resolve = resolveBuild, store = storeBuild } = {}) {
249
- const stored = {
250
- ...root === void 0 ? {} : { root },
251
- ...sources === void 0 ? {} : { sources },
252
- ...assetManifest === void 0 ? {} : { assetManifest }
253
- };
254
- return {
255
- resolve: ({ platform, key }) => root === void 0 ? resolve(platform, key) : resolve(platform, key, root),
256
- store: ({ platform, key, sourcePath, overwrite }) => store(platform, key, sourcePath, {
257
- ...stored,
258
- overwrite
259
- })
260
- };
261
- }
262
- function providerUploadOutcome(result, name) {
263
- if (!result) return null;
264
- const label = name || "the cache provider";
265
- if (result.timedOut) return {
266
- line: `${label} upload was cancelled after ${buildUploadTimeoutMs()}ms`,
267
- warn: true
268
- };
269
- if (result.failed) return {
270
- line: `${label} upload failed: ${result.failed}`,
271
- warn: true
272
- };
273
- return {
274
- line: `uploaded (${label})`,
275
- warn: false
276
- };
277
- }
278
- const SOURCES_FILE = "fingerprint-sources.json";
279
- function storedSources(platform, key, root = sharedBuildCache()) {
280
- try {
281
- const parsed = JSON.parse(readFileSync$1(join$1(entryDir(platform, key, root), SOURCES_FILE), "utf-8"));
282
- return Array.isArray(parsed) ? parsed : null;
283
- } catch {
284
- return null;
285
- }
286
- }
287
- function storedAssetManifest(platform, key, root = sharedBuildCache()) {
288
- try {
289
- return parseAssetManifest(readFileSync$1(join$1(entryDir(platform, key, root), ASSET_MANIFEST_FILE), "utf-8"));
290
- } catch {
291
- return null;
292
- }
293
- }
294
- function sourceName(source) {
295
- const s = source;
296
- if (typeof s?.filePath === "string" && s.filePath !== "") return s.filePath;
297
- if (typeof s?.id === "string" && s.id !== "") return s.id;
298
- return null;
299
- }
300
- function diffItemName(item) {
301
- const o = item;
302
- for (const candidate of [
303
- o,
304
- o?.addedSource,
305
- o?.removedSource,
306
- o?.afterSource,
307
- o?.beforeSource
308
- ]) {
309
- const name = sourceName(candidate);
310
- if (name) return name;
311
- }
312
- return null;
313
- }
314
- function compareSourceLists(previous, current) {
315
- const previousByName = /* @__PURE__ */ new Map();
316
- for (const source of previous) {
317
- const name = sourceName(source);
318
- if (name !== null) previousByName.set(name, source.hash ?? null);
319
- }
320
- const changed = [];
321
- const seen = /* @__PURE__ */ new Set();
322
- for (const source of current) {
323
- const name = sourceName(source);
324
- if (name === null || seen.has(name)) continue;
325
- seen.add(name);
326
- const hash = source.hash;
327
- if (!previousByName.has(name) || previousByName.get(name) !== (hash ?? null)) changed.push(name);
328
- }
329
- for (const [name] of previousByName) if (!seen.has(name)) changed.push(name);
330
- return changed;
331
- }
332
- function diffFingerprintSources({ previous, previousHash = null, current, differ = null }) {
333
- if (typeof differ === "function") try {
334
- const items = differ({
335
- sources: previous,
336
- hash: previousHash ?? ""
337
- }, {
338
- sources: current.sources,
339
- hash: current.hash
340
- });
341
- if (Array.isArray(items)) {
342
- const names = [];
343
- const seen = /* @__PURE__ */ new Set();
344
- for (const item of items) {
345
- const name = diffItemName(item);
346
- if (name !== null && !seen.has(name)) {
347
- seen.add(name);
348
- names.push(name);
349
- }
350
- }
351
- return names;
352
- }
353
- } catch {}
354
- return compareSourceLists(previous, current.sources);
355
- }
356
- function fingerprintDiffSuffix(changed) {
357
- if (!changed.length) return "";
358
- const shown = changed.slice(0, 3).join(", ");
359
- return ` -- ${changed.length} source${changed.length === 1 ? "" : "s"} changed: ${shown}`;
360
- }
361
- function describeFingerprintMiss({ platform, current, lastBuild, root = sharedBuildCache(), differ = expoFingerprint.diffFingerprints }) {
362
- if (!lastBuild || lastBuild.platform !== platform) return null;
363
- const previousHash = typeof lastBuild.fingerprint === "string" ? lastBuild.fingerprint : null;
364
- const previousKey = typeof lastBuild.cacheKey === "string" ? lastBuild.cacheKey : null;
365
- if (!previousHash || !previousKey || previousHash === current.hash) return null;
366
- const previous = storedSources(platform, previousKey, root);
367
- if (!previous) return null;
368
- const changed = diffFingerprintSources({
369
- previous,
370
- previousHash,
371
- current,
372
- differ
373
- });
374
- return changed.length ? {
375
- changed,
376
- previousHash
377
- } : null;
378
- }
379
- async function refingerprintAfterMutation({ projectRoot, platform, previousHash, fingerprint = fingerprintProject }) {
380
- let computed = null;
381
- try {
382
- computed = await fingerprint(projectRoot, { platform });
383
- } catch {
384
- return null;
385
- }
386
- if (!computed?.hash) return null;
387
- return {
388
- hash: computed.hash,
389
- sources: computed.sources,
390
- moved: computed.hash !== previousHash
391
- };
392
- }
393
- function untrackedNativeFiles({ projectRoot, exec = getExecutor() }) {
394
- let out;
395
- try {
396
- out = exec.runFile("git", [
397
- "-C",
398
- projectRoot,
399
- "ls-files",
400
- "--others",
401
- "--exclude-standard",
402
- "--",
403
- "ios",
404
- "android"
405
- ]);
406
- } catch {
407
- return [];
408
- }
409
- return String(out || "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
410
- }
411
- function untrackedMissLine(files, cap = 3) {
412
- if (!Array.isArray(files) || files.length === 0) return null;
413
- const shown = files.slice(0, cap);
414
- const more = files.length > shown.length ? `, and ${files.length - shown.length} more` : "";
415
- return `no previous entry to diff against; ${files.length} untracked file${files.length === 1 ? "" : "s"} under ios/ or android/ are hashed like any other source: ${shown.join(", ")}${more} -- list the build-irrelevant ones in .fingerprintignore`;
416
- }
417
- function fingerprintDiffRecord({ changed, previousHash, hash }) {
418
- const shown = changed.slice(0, 20);
419
- return {
420
- src: "build",
421
- level: "info",
422
- event: "fingerprint_diff",
423
- msg: `fingerprint ${previousHash} -> ${hash}: ${changed.length} source${changed.length === 1 ? "" : "s"} changed: ` + shown.join(", ") + (changed.length > shown.length ? `, and ${changed.length - shown.length} more` : ""),
424
- changed: changed.length,
425
- sources: shown
426
- };
427
- }
428
- //#endregion
429
- //#region src/engine/build-lock.ts
430
- const LOCK_SUFFIX = ".lock";
431
- const WAIT_POLL_MS = 1e3;
432
- const WAIT_PROGRESS_MS = 3e4;
433
- const WAIT_CEILING_MS = 54e5;
434
- function sleepAsync(ms) {
435
- return new Promise((resolve) => setTimeout(resolve, ms));
436
- }
437
- function segment(value) {
438
- return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "unknown";
439
- }
440
- function buildLocksDir() {
441
- return join(getConfigDir(), "build-locks");
442
- }
443
- function buildLockPath(platform, key) {
444
- return join(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX}`);
445
- }
446
- function label(platform) {
447
- return `${platform} build`;
448
- }
449
- function toRecord(holder) {
450
- const details = holder.details;
451
- return {
452
- pid: holder.owner.pid,
453
- projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
454
- startedAt: holder.startedAt || null,
455
- logFile: typeof details.logFile === "string" ? details.logFile : null
456
- };
457
- }
458
- function readBuildLock(pathOrSpec) {
459
- const path = typeof pathOrSpec === "string" ? pathOrSpec : buildLockPath(pathOrSpec.platform, pathOrSpec.key);
460
- const survey = (0, ownership_claim_exports.readClaimSet)(path);
461
- const holder = survey.live[0] ?? survey.dead[0];
462
- return holder ? toRecord(holder) : null;
463
- }
464
- function acquireBuildLock({ platform, key, root = null, logFile = null }) {
465
- const path = buildLockPath(platform, key);
466
- const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
467
- root: path,
468
- mode: "exclusive",
469
- label: label(platform),
470
- details: {
471
- projectRoot: root,
472
- logFile
473
- }
474
- });
475
- const reaped = attempt.reaped[0];
476
- if (attempt.acquired) {
477
- const handle = {
478
- acquired: true,
479
- path,
480
- lock: {
481
- pid: attempt.acquired.owner.pid,
482
- projectRoot: root,
483
- startedAt: attempt.acquired.startedAt,
484
- logFile
485
- },
486
- claim: attempt.acquired
487
- };
488
- return reaped ? {
489
- ...handle,
490
- tookOver: toRecord(reaped)
491
- } : handle;
492
- }
493
- if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
494
- const holder = attempt.held ?? attempt.waitingFor?.[0];
495
- return holder ? {
496
- held: toRecord(holder),
497
- path
498
- } : { path };
499
- }
500
- function releaseBuildLock(handle) {
501
- return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
502
- }
503
- function listBuildLocks() {
504
- const dir = buildLocksDir();
505
- let names;
506
- try {
507
- names = readdirSync(dir);
508
- } catch {
509
- return [];
510
- }
511
- const locks = [];
512
- for (const name of names) {
513
- if (!name.endsWith(LOCK_SUFFIX)) continue;
514
- const path = join(dir, name);
515
- try {
516
- if (!statSync(path).isDirectory()) continue;
517
- } catch {
518
- continue;
519
- }
520
- const stem = name.slice(0, -5);
521
- const cut = stem.indexOf("-");
522
- const survey = (0, ownership_claim_exports.readClaimSet)(path);
523
- const holder = survey.live[0] ?? survey.dead[0];
524
- const info = holder ? toRecord(holder) : null;
525
- locks.push({
526
- path,
527
- name,
528
- platform: cut > 0 ? stem.slice(0, cut) : stem,
529
- key: cut > 0 ? stem.slice(cut + 1) : null,
530
- pid: info?.pid ?? null,
531
- projectRoot: info?.projectRoot ?? null,
532
- startedAt: info?.startedAt ?? null,
533
- logFile: info?.logFile ?? null,
534
- alive: survey.live.length > 0,
535
- unresolved: survey.unresolved.length > 0
536
- });
537
- }
538
- return locks;
539
- }
540
- function waitingLine({ projectRoot, pid, elapsedMs, logFile }) {
541
- const where = projectRoot || "another workspace";
542
- const tail = logFile ? ` -- tail ${logFile}` : "";
543
- return `${"build".padEnd(11)} waiting on ${where} (pid ${pid ?? "?"}, ${formatElapsed(elapsedMs)} elapsed)${tail} -- stim guide lifecycle concurrency`;
544
- }
545
- function takeoverLine({ projectRoot, pid, logFile, startedAt = null, now = Date.now }) {
546
- const where = projectRoot || "another workspace";
547
- const started = startedAt ? Date.parse(startedAt) : NaN;
548
- const ago = Number.isFinite(started) ? ` (it started ${formatElapsed(now() - started)} ago)` : "";
549
- const tail = logFile ? ` -- read ${logFile} before this one finishes` : "";
550
- return `RETRY: ${where}'s build of this fingerprint (pid ${pid ?? "?"}) FAILED without an artifact${ago}, and this run rebuilds the SAME inputs, so expect the same failure unless something changed${tail}`;
551
- }
552
- async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs = WAIT_POLL_MS, progressMs = WAIT_PROGRESS_MS, ceilingMs = WAIT_CEILING_MS, out = () => {}, now = Date.now, sleep = sleepAsync }) {
553
- const path = buildLockPath(platform, key);
554
- const started = now();
555
- let lastProgress = started;
556
- let holder = readBuildLock(path);
557
- for (;;) {
558
- const hit = resolve(platform, key);
559
- if (hit) return {
560
- hit,
561
- waitedMs: now() - started
562
- };
563
- const survey = (0, ownership_claim_exports.readClaimSet)(path);
564
- const unresolved = survey.unresolved[0];
565
- if (unresolved) throw new ownership_claim_exports.ClaimRefusedError({
566
- claimPath: unresolved.path,
567
- root: path,
568
- label: label(platform),
569
- reason: unresolved.reason
570
- });
571
- const live = survey.live[0];
572
- if (live) holder = toRecord(live);
573
- else {
574
- const dead = survey.dead[0];
575
- if (dead) return {
576
- builderFailed: `the builder (pid ${dead.owner.pid}) is gone`,
577
- holder: toRecord(dead),
578
- waitedMs: now() - started
579
- };
580
- return {
581
- lockReleased: true,
582
- holder,
583
- waitedMs: now() - started
584
- };
585
- }
586
- const elapsed = now() - started;
587
- if (elapsed >= ceilingMs) {
588
- const err = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsed)} for ${holder?.projectRoot || "another workspace"}'s ${platform} build of ${key} without an artifact, and pid ${holder?.pid} is still the live holder. The lock is ${path}; remove it if that process is not really building:\n ${(0, ownership_claim_exports.claimRemoveCommand)(path)}`);
589
- err.code = "STIM_BUILD_WAIT_TIMEOUT";
590
- err.lockPath = path;
591
- err.holder = holder ?? void 0;
592
- throw err;
593
- }
594
- if (now() - lastProgress >= progressMs) {
595
- lastProgress = now();
596
- out(waitingLine({
597
- projectRoot: holder.projectRoot,
598
- pid: holder.pid,
599
- elapsedMs: elapsed,
600
- logFile: holder.logFile
601
- }));
602
- }
603
- await sleep(intervalMs);
604
- }
605
- }
606
- //#endregion
607
- //#region src/sim/android.ts
1
+ import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
+ import { i as pidExists, o as signalProcessTree } from "./metro-C7bcJ0uo.mjs";
3
+ import { c as androidDataPartitionSizeBytes } from "./settings-BXXorVM2.mjs";
4
+ import { existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
5
+ import { homedir, tmpdir } from "os";
6
+ import { dirname, isAbsolute, join, resolve } from "path";
7
+ //#region src/devices/android.ts
608
8
  const ANDROID_ABIS = /* @__PURE__ */ new Set([
609
9
  "armeabi-v7a",
610
10
  "arm64-v8a",
@@ -613,7 +13,10 @@ const ANDROID_ABIS = /* @__PURE__ */ new Set([
613
13
  ]);
614
14
  const DEFAULT_AVD_DEVICE = "pixel_6";
615
15
  function androidHome() {
616
- return process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || join$1(homedir(), "Library", "Android", "sdk");
16
+ const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
17
+ if (configured) return configured;
18
+ if (process.platform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Android", "Sdk");
19
+ return join(homedir(), "Library", "Android", "sdk");
617
20
  }
618
21
  const SDK_TOOL_LOCATIONS = {
619
22
  emulator: ["emulator", "emulator"],
@@ -625,9 +28,22 @@ const SDK_TOOL_LOCATIONS = {
625
28
  "avdmanager"
626
29
  ]
627
30
  };
628
- function androidToolPath(tool) {
629
- const abs = join$1(androidHome(), ...SDK_TOOL_LOCATIONS[tool]);
630
- return existsSync$1(abs) ? abs : tool;
31
+ const WINDOWS_SDK_TOOL_EXTENSIONS = {
32
+ emulator: ".exe",
33
+ adb: ".exe",
34
+ avdmanager: ".bat"
35
+ };
36
+ function androidToolPath(tool, platform = process.platform) {
37
+ const location = SDK_TOOL_LOCATIONS[tool];
38
+ const bare = location.at(-1);
39
+ const name = platform === "win32" ? `${bare}${WINDOWS_SDK_TOOL_EXTENSIONS[tool]}` : bare;
40
+ const abs = join(androidHome(), ...location.slice(0, -1), name);
41
+ return existsSync(abs) ? abs : bare;
42
+ }
43
+ const WINDOWS_BUILD_TOOL_EXTENSIONS = { apksigner: ".bat" };
44
+ function androidBuildToolName(tool, platform = process.platform) {
45
+ if (platform !== "win32") return tool;
46
+ return `${tool}${WINDOWS_BUILD_TOOL_EXTENSIONS[tool] ?? ".exe"}`;
631
47
  }
632
48
  function newestBuildTools(names) {
633
49
  return [...Array.isArray(names) ? names : []].filter((n) => /^\d+(\.\d+)*(-\w+)?$/.test(String(n))).toSorted((a, b) => {
@@ -644,8 +60,8 @@ function buildToolsMajor(version) {
644
60
  const major = Number(String(version ?? "").split(/[.-]/)[0]);
645
61
  return Number.isFinite(major) ? major : 0;
646
62
  }
647
- function findBuildTool(tools, { home = androidHome(), readDir = readdirSync$1, exists = existsSync$1 } = {}) {
648
- const root = join$1(home, "build-tools");
63
+ function findBuildTool(tools, { home = androidHome(), readDir = readdirSync, exists = existsSync, platform = process.platform } = {}) {
64
+ const root = join(home, "build-tools");
649
65
  let versions = [];
650
66
  try {
651
67
  versions = readDir(root);
@@ -656,7 +72,7 @@ function findBuildTool(tools, { home = androidHome(), readDir = readdirSync$1, e
656
72
  const version = newestBuildTools(versions);
657
73
  if (!version) return null;
658
74
  for (const tool of tools) {
659
- const path = join$1(root, version, tool);
75
+ const path = join(root, version, androidBuildToolName(tool, platform));
660
76
  if (exists(path)) return {
661
77
  path,
662
78
  tool,
@@ -673,14 +89,14 @@ function androidTool(tool) {
673
89
  return resolved === tool ? tool : `"${resolved}"`;
674
90
  }
675
91
  function listInstalledSystemImages() {
676
- const root = join$1(androidHome(), "system-images");
92
+ const root = join(androidHome(), "system-images");
677
93
  const images = [];
678
- if (!existsSync$1(root)) return images;
94
+ if (!existsSync(root)) return images;
679
95
  for (const apiDir of safeList(root)) {
680
96
  const m = apiDir.match(/^android-(\d+)$/);
681
97
  if (!m) continue;
682
- const apiPath = join$1(root, apiDir);
683
- for (const tag of safeList(apiPath)) for (const arch of safeList(join$1(apiPath, tag))) images.push({
98
+ const apiPath = join(root, apiDir);
99
+ for (const tag of safeList(apiPath)) for (const arch of safeList(join(apiPath, tag))) images.push({
684
100
  api: Number(m[1]),
685
101
  tag,
686
102
  arch,
@@ -691,7 +107,7 @@ function listInstalledSystemImages() {
691
107
  }
692
108
  function safeList(dir) {
693
109
  try {
694
- return readdirSync$1(dir);
110
+ return readdirSync(dir);
695
111
  } catch {
696
112
  return [];
697
113
  }
@@ -761,17 +177,17 @@ function parseAvdSystemImage(configIni) {
761
177
  const separator = line.indexOf("=");
762
178
  if (separator < 0) continue;
763
179
  if (line.slice(0, separator).trim() !== "image.sysdir.1") continue;
764
- const dir = line.slice(separator + 1).trim().replace(/\/+$/, "");
180
+ const dir = line.slice(separator + 1).trim().replace(/[\\/]+$/, "");
765
181
  if (!dir) return null;
766
- return dir.split("/").join(";");
182
+ return dir.split(/[\\/]/).join(";");
767
183
  }
768
184
  return null;
769
185
  }
770
- function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8") } = {}) {
186
+ function ownedAvdSystemImage(avdName, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8") } = {}) {
771
187
  const directory = avdDirectory(avdName);
772
188
  if (!directory) return null;
773
189
  try {
774
- return parseAvdSystemImage(readFile(join$1(directory, "config.ini")));
190
+ return parseAvdSystemImage(readFile(join(directory, "config.ini")));
775
191
  } catch {
776
192
  return null;
777
193
  }
@@ -831,8 +247,22 @@ function parseAdbDevices(text) {
831
247
  function listAvds({ timeoutMs } = {}) {
832
248
  return parseAvdList(getExecutor().run(`${androidTool("emulator")} -list-avds`, { timeoutMs }));
833
249
  }
834
- function listAdbDevices({ timeoutMs } = {}) {
835
- return parseAdbDevices(getExecutor().run(`${androidTool("adb")} devices`, { timeoutMs }));
250
+ /**
251
+ * Working directory for the adb client and the emulator Stim starts. Both leave processes behind
252
+ * that inherit it: the adb server is spawned by whichever client finds none running, and the
253
+ * emulator launcher forks qemu and a crashpad handler that outlives a crash on exit. On Windows a
254
+ * process holds its working directory open, and Windows refuses to delete a directory with an open
255
+ * handle, so one started from a worktree breaks `worktree remove`.
256
+ * https://github.com/appandflow/stim/issues/914
257
+ */
258
+ function androidToolCwd(platform = process.platform) {
259
+ return platform === "win32" ? homedir() : void 0;
260
+ }
261
+ function listAdbDevices({ timeoutMs, platform = process.platform } = {}) {
262
+ return parseAdbDevices(getExecutor().run(`${androidTool("adb")} devices`, {
263
+ timeoutMs,
264
+ cwd: androidToolCwd(platform)
265
+ }));
836
266
  }
837
267
  const ADB_PROP_TIMEOUT_MS = 5e3;
838
268
  const EMULATOR_HARDWARE = /* @__PURE__ */ new Set([
@@ -971,22 +401,22 @@ function parseAvdRootIni(contents) {
971
401
  }
972
402
  function avdIniPaths(root, ini) {
973
403
  const parsed = parseAvdRootIni(ini);
974
- return [parsed.relativePath && !isAbsolute$1(parsed.relativePath) ? resolve$1(dirname$1(root), parsed.relativePath) : null, parsed.path && isAbsolute$1(parsed.path) ? parsed.path : null].filter((candidate) => candidate !== null);
404
+ return [parsed.relativePath && !isAbsolute(parsed.relativePath) ? resolve(dirname(root), parsed.relativePath) : null, parsed.path && isAbsolute(parsed.path) ? parsed.path : null].filter((candidate) => candidate !== null);
975
405
  }
976
406
  function avdStorageRoots() {
977
407
  const env = process.env;
978
408
  return [...new Set([
979
409
  env.ANDROID_AVD_HOME,
980
- env.ANDROID_EMULATOR_HOME ? join$1(env.ANDROID_EMULATOR_HOME, "avd") : null,
981
- env.ANDROID_USER_HOME ? join$1(env.ANDROID_USER_HOME, "avd") : null,
982
- env.ANDROID_SDK_HOME ? join$1(env.ANDROID_SDK_HOME, ".android", "avd") : null,
983
- env.ANDROID_SDK_HOME ? join$1(env.ANDROID_SDK_HOME, "avd") : null,
984
- join$1(homedir(), ".android", "avd")
410
+ env.ANDROID_EMULATOR_HOME ? join(env.ANDROID_EMULATOR_HOME, "avd") : null,
411
+ env.ANDROID_USER_HOME ? join(env.ANDROID_USER_HOME, "avd") : null,
412
+ env.ANDROID_SDK_HOME ? join(env.ANDROID_SDK_HOME, ".android", "avd") : null,
413
+ env.ANDROID_SDK_HOME ? join(env.ANDROID_SDK_HOME, "avd") : null,
414
+ join(homedir(), ".android", "avd")
985
415
  ].filter((value) => Boolean(value)))];
986
416
  }
987
417
  function avdPathExists(path) {
988
418
  try {
989
- lstatSync$1(path);
419
+ lstatSync(path);
990
420
  return true;
991
421
  } catch (error) {
992
422
  if (error.code === "ENOENT") return false;
@@ -997,17 +427,17 @@ function listOrphanedAvdDirectories(avdName) {
997
427
  const roots = avdStorageRoots();
998
428
  const stores = roots.filter(avdPathExists).map((root) => ({
999
429
  root,
1000
- canonicalRoot: realpathSync$1(root),
1001
- names: readdirSync$1(root)
430
+ canonicalRoot: realpathSync(root),
431
+ names: readdirSync(root)
1002
432
  }));
1003
433
  const registered = /* @__PURE__ */ new Set();
1004
434
  for (const { root, canonicalRoot, names } of stores) for (const name of names.filter((entry) => entry.endsWith(".ini"))) {
1005
- const path = join$1(root, name);
1006
- const ini = readFileSync$1(path, "utf8");
435
+ const path = join(root, name);
436
+ const ini = readFileSync(path, "utf8");
1007
437
  const targets = /* @__PURE__ */ new Set([...avdIniPaths(root, ini), ...avdIniPaths(canonicalRoot, ini)]);
1008
438
  if (!targets.size) throw new Error(`Cannot verify AVD registration at ${path}; its data was kept.`);
1009
439
  for (const target of targets) try {
1010
- registered.add(realpathSync$1(target));
440
+ registered.add(realpathSync(target));
1011
441
  } catch (error) {
1012
442
  const code = error.code;
1013
443
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
@@ -1019,11 +449,11 @@ function listOrphanedAvdDirectories(avdName) {
1019
449
  for (const entry of names) {
1020
450
  if (!/^stim-[A-Za-z0-9._-]+\.avd$/.test(entry)) continue;
1021
451
  const name = entry.slice(0, -4);
1022
- if (roots.some((candidate) => avdPathExists(join$1(candidate, `${name}.ini`)))) continue;
1023
- const directory = join$1(canonicalRoot, entry);
452
+ if (roots.some((candidate) => avdPathExists(join(candidate, `${name}.ini`)))) continue;
453
+ const directory = join(canonicalRoot, entry);
1024
454
  if (registered.has(directory) || !avdPathExists(directory)) continue;
1025
- const stat = lstatSync$1(directory);
1026
- if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync$1(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
455
+ const stat = lstatSync(directory);
456
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(directory) !== directory) throw new Error(`Cannot verify AVD data at ${directory}; it was kept.`);
1027
457
  if (!orphaned.some((candidate) => candidate.directory === directory)) orphaned.push({
1028
458
  name,
1029
459
  directory,
@@ -1034,17 +464,17 @@ function listOrphanedAvdDirectories(avdName) {
1034
464
  }
1035
465
  return orphaned;
1036
466
  }
1037
- function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync$1(path, "utf8"), realpath = realpathSync$1, isDirectory = (path) => statSync$1(path).isDirectory() } = {}) {
467
+ function ownedAvdDirectory(avdName, { env = process.env, home = homedir(), readFile = (path) => readFileSync(path, "utf8"), realpath = realpathSync, isDirectory = (path) => statSync(path).isDirectory() } = {}) {
1038
468
  if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return null;
1039
469
  const roots = [
1040
470
  env.ANDROID_AVD_HOME,
1041
- env.ANDROID_SDK_HOME ? join$1(env.ANDROID_SDK_HOME, "avd") : null,
1042
- join$1(home, ".android", "avd")
471
+ env.ANDROID_SDK_HOME ? join(env.ANDROID_SDK_HOME, "avd") : null,
472
+ join(home, ".android", "avd")
1043
473
  ];
1044
474
  for (const root of new Set(roots.filter((value) => Boolean(value)))) {
1045
475
  let ini;
1046
476
  try {
1047
- ini = readFile(join$1(root, `${avdName}.ini`));
477
+ ini = readFile(join(root, `${avdName}.ini`));
1048
478
  } catch (error) {
1049
479
  const code = error.code;
1050
480
  if (code === "ENOENT" || code === "ENOTDIR") continue;
@@ -1079,21 +509,21 @@ function withAvdConfigOverrides(contents, overrides) {
1079
509
  return updated.join(newline) + (trailingNewline ? newline : "");
1080
510
  }
1081
511
  let avdConfigWriteSequence = 0;
1082
- function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync$1(path, "utf8"), writeFile = (path, contents) => writeFileSync$1(path, contents, {
512
+ function configureNewOwnedAvd(avdName, { dataPartitionSizeGb, avdConfig = {} }, { avdDirectory = ownedAvdDirectory, readFile = (path) => readFileSync(path, "utf8"), writeFile = (path, contents) => writeFileSync(path, contents, {
1083
513
  encoding: "utf8",
1084
514
  flag: "wx"
1085
- }), rename = renameSync$1, remove = (path) => rmSync$1(path, { force: true }) } = {}) {
515
+ }), rename = renameSync, remove = (path) => rmSync(path, { force: true }) } = {}) {
1086
516
  const sizeBytes = androidDataPartitionSizeBytes(dataPartitionSizeGb);
1087
517
  const directory = avdDirectory(avdName);
1088
518
  if (!directory) throw new Error(`Could not resolve the content directory for newly created AVD ${avdName}.`);
1089
- const configPath = join$1(directory, "config.ini");
519
+ const configPath = join(directory, "config.ini");
1090
520
  const original = readFile(configPath);
1091
521
  const expected = {
1092
522
  ...avdConfig,
1093
523
  "disk.dataPartition.size": String(sizeBytes)
1094
524
  };
1095
525
  const updated = withAvdConfigOverrides(original, expected);
1096
- const tempPath = join$1(directory, `.config.ini.stim-${process.pid}-${++avdConfigWriteSequence}.tmp`);
526
+ const tempPath = join(directory, `.config.ini.stim-${process.pid}-${++avdConfigWriteSequence}.tmp`);
1097
527
  try {
1098
528
  writeFile(tempPath, updated);
1099
529
  rename(tempPath, configPath);
@@ -1117,7 +547,7 @@ function avdPoolConfiguration(dataPartitionSizeGb, avdConfig) {
1117
547
  function ownedAvdMatchesConfiguration(avdName, configuration) {
1118
548
  const directory = ownedAvdDirectory(avdName);
1119
549
  if (!directory) return false;
1120
- const contents = readFileSync$1(join$1(directory, "config.ini"), "utf8");
550
+ const contents = readFileSync(join(directory, "config.ini"), "utf8");
1121
551
  const expected = JSON.parse(configuration);
1122
552
  return Array.isArray(expected) && expected.length > 0 && expected.every((entry) => Array.isArray(entry) && entry.length === 2 && entry.every((value) => typeof value === "string") && contents.split(/\r?\n/).some((line) => line.trim() === `${entry[0]}=${entry[1]}`));
1123
553
  }
@@ -1184,14 +614,58 @@ async function resetAdoptedAvd(avdName, serial, keepPackage) {
1184
614
  }
1185
615
  }
1186
616
  }
1187
- function bootAndroidEmulator(avdName, consolePort, { logFile } = {}) {
617
+ function parseEmulatorVersion(text) {
618
+ const match = /Android emulator version (\d+)\./.exec(String(text ?? ""));
619
+ return match ? Number(match[1]) : null;
620
+ }
621
+ function emulatorMajorVersion() {
622
+ return parseEmulatorVersion(getExecutor().runQuiet(`${androidTool("emulator")} -version`, { timeoutMs: 1e4 }));
623
+ }
624
+ /** The first emulator whose launcher accepts `-crash-report-mode`; an older one refuses to start with it. */
625
+ const CRASH_REPORT_MODE_EMULATOR_VERSION = 37;
626
+ /**
627
+ * Where the emulator keeps crashpad's database on Windows: `AndroidEmulator\emu-crash-<version>.db`
628
+ * under GetTempPath, which reads TMP before TEMP.
629
+ */
630
+ function emulatorCrashDatabases(env = process.env) {
631
+ const root = join(env.TMP || env.TEMP || tmpdir(), "AndroidEmulator");
632
+ try {
633
+ return readdirSync(root).filter((name) => /^emu-crash-.*\.db$/.test(name)).map((name) => join(root, name));
634
+ } catch {
635
+ return [];
636
+ }
637
+ }
638
+ /**
639
+ * An emulator that crashed (this host's crashes on exit) leaves a report in crashpad's database,
640
+ * and the next launch blocks on a consent dialog before qemu boots: emulator.log says "Showing
641
+ * crashdialog to get consent." and adb never sees the device. `-crash-report-mode never` (emulator
642
+ * 37 and later) answers it: the pending report is deleted at startup and the emulator boots.
643
+ * `-no-metrics` does not; the dialog shows all the same. An older emulator gets the database
644
+ * removed instead, which has the same effect unless a crash handler is still writing into it.
645
+ * https://github.com/appandflow/stim/issues/918
646
+ */
647
+ function suppressEmulatorCrashConsent({ platform = process.platform, version = emulatorMajorVersion, databases = emulatorCrashDatabases, remove = (path) => rmSync(path, {
648
+ recursive: true,
649
+ force: true
650
+ }) } = {}) {
651
+ if (platform !== "win32") return [];
652
+ const major = version();
653
+ if (major !== null && major >= CRASH_REPORT_MODE_EMULATOR_VERSION) return ["-crash-report-mode", "never"];
654
+ for (const database of databases()) try {
655
+ remove(database);
656
+ } catch {}
657
+ return [];
658
+ }
659
+ function bootAndroidEmulator(avdName, consolePort, { logFile, platform = process.platform } = {}) {
1188
660
  const child = getExecutor().spawn(androidToolPath("emulator"), [
1189
661
  "-avd",
1190
662
  avdName,
1191
663
  "-port",
1192
664
  String(consolePort),
1193
- ...headlessEmulatorArgs()
665
+ ...headlessEmulatorArgs(),
666
+ ...suppressEmulatorCrashConsent({ platform })
1194
667
  ], {
668
+ cwd: androidToolCwd(platform),
1195
669
  detached: true,
1196
670
  stdio: emulatorStdio(logFile)
1197
671
  });
@@ -1201,8 +675,8 @@ function bootAndroidEmulator(avdName, consolePort, { logFile } = {}) {
1201
675
  function emulatorStdio(logFile) {
1202
676
  if (!logFile) return "ignore";
1203
677
  try {
1204
- mkdirSync$1(dirname$1(logFile), { recursive: true });
1205
- const fd = openSync$1(logFile, "w");
678
+ mkdirSync(dirname(logFile), { recursive: true });
679
+ const fd = openSync(logFile, "w");
1206
680
  return [
1207
681
  "ignore",
1208
682
  fd,
@@ -1254,14 +728,18 @@ function getprop(exec, serial, prop, timeoutMs) {
1254
728
  const out = exec.runQuiet(`${androidTool("adb")} -s ${serial} shell getprop ${prop}`, { timeoutMs });
1255
729
  return typeof out === "string" ? out.trim() : "";
1256
730
  }
731
+ const PACKAGE_MANAGER_PROBE = "shell pm path android";
732
+ function packageManagerReady(exec, serial, timeoutMs) {
733
+ const out = exec.runQuiet(`${androidTool("adb")} -s ${serial} ${PACKAGE_MANAGER_PROBE}`, { timeoutMs });
734
+ return typeof out === "string" && /^package:/m.test(out);
735
+ }
1257
736
  async function waitForBoot(serial, timeoutMs = 6e4, { aborted = () => false, pollMs = 1e3, commandTimeoutMs } = {}) {
1258
737
  const exec = getExecutor();
1259
738
  const start = Date.now();
1260
739
  let exited = false;
1261
740
  const probeTimeout = () => commandTimeoutMs === void 0 ? void 0 : Math.max(1, Math.min(commandTimeoutMs, timeoutMs - (Date.now() - start)));
1262
741
  while (Date.now() - start < timeoutMs) {
1263
- if (getprop(exec, serial, "sys.boot_completed", probeTimeout()) === "1") return { ok: true };
1264
- if (getprop(exec, serial, "dev.bootcomplete", probeTimeout()) === "1") return { ok: true };
742
+ if ((getprop(exec, serial, "sys.boot_completed", probeTimeout()) === "1" || getprop(exec, serial, "dev.bootcomplete", probeTimeout()) === "1") && packageManagerReady(exec, serial, probeTimeout())) return { ok: true };
1265
743
  if (aborted()) {
1266
744
  exited = true;
1267
745
  break;
@@ -1282,7 +760,8 @@ async function waitForBoot(serial, timeoutMs = 6e4, { aborted = () => false, pol
1282
760
  devices: diagnosticQuery("devices"),
1283
761
  sysBoot: diagnosticQuery(`-s ${serial} shell getprop sys.boot_completed`),
1284
762
  devBoot: diagnosticQuery(`-s ${serial} shell getprop dev.bootcomplete`),
1285
- bootAnim: diagnosticQuery(`-s ${serial} shell getprop init.svc.bootanim`)
763
+ bootAnim: diagnosticQuery(`-s ${serial} shell getprop init.svc.bootanim`),
764
+ packageManager: diagnosticQuery(`-s ${serial} ${PACKAGE_MANAGER_PROBE}`)
1286
765
  }
1287
766
  };
1288
767
  }
@@ -1298,11 +777,11 @@ function sleepSync(ms) {
1298
777
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1299
778
  }
1300
779
  function avdProcessLockPaths(avdDirectory, platform) {
1301
- return ["hardware-qemu.ini.lock", "userdata-qemu.img.lock"].map((name) => join$1(avdDirectory, name, ...platform === "win32" ? ["pid"] : []));
780
+ return ["hardware-qemu.ini.lock", "userdata-qemu.img.lock"].map((name) => join(avdDirectory, name, ...platform === "win32" ? ["pid"] : []));
1302
781
  }
1303
782
  function readAvdProcessId(path) {
1304
783
  try {
1305
- const pid = Number(readFileSync$1(path, "utf8").split("\0")[0]);
784
+ const pid = Number(readFileSync(path, "utf8").split("\0")[0]);
1306
785
  if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error(`Could not read the emulator PID from process lock ${path}.`);
1307
786
  return pid;
1308
787
  } catch (error) {
@@ -1330,7 +809,22 @@ function assertOwnedAvdStopped(avdName, { processAlive = pidExists, ...resolveOp
1330
809
  const { processId } = resolveAvdProcess(avdName, resolveOptions);
1331
810
  if (processId !== null && processAlive(processId)) throw new Error(`Owned AVD ${avdName} still has a live emulator process (${processId}).`);
1332
811
  }
1333
- function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, directoryExists = (path) => statSync$1(path).isDirectory(), now = Date.now, sleep = sleepSync } = {}) {
812
+ const CRASH_HANDLER_EXIT_TIMEOUT_MS = 15e3;
813
+ /**
814
+ * The crashpad handler qemu forks on Windows. It outlives an emulator that crashes on exit while
815
+ * it writes the dump, so a `stop` that returns on qemu's exit leaves it running.
816
+ * https://github.com/appandflow/stim/issues/918
817
+ */
818
+ function emulatorCrashHandlerPids(qemuPid) {
819
+ const out = getExecutor().runFileQuiet("powershell.exe", [
820
+ "-NoProfile",
821
+ "-NonInteractive",
822
+ "-Command",
823
+ `Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'crashpad_handler.exe' -and $_.ParentProcessId -eq ${qemuPid} } | ForEach-Object ProcessId`
824
+ ], { timeoutMs: 1e4 });
825
+ return String(out ?? "").split(/\r?\n/).map((line) => Number(line.trim())).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
826
+ }
827
+ function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID_EMULATOR_SHUTDOWN_TIMEOUT_MS, pollMs = ANDROID_EMULATOR_SHUTDOWN_POLL_MS, platform = process.platform, resolveDirectory = ownedAvdDirectory, readProcessId = readAvdProcessId, processAlive = pidExists, directoryExists = (path) => statSync(path).isDirectory(), crashHandlerPids = emulatorCrashHandlerPids, killCrashHandler = (pid) => signalProcessTree(pid, "SIGKILL", { platform }), now = Date.now, sleep = sleepSync } = {}) {
1334
828
  const { directory, processId } = resolveAvdProcess(avdName, {
1335
829
  platform,
1336
830
  resolveDirectory,
@@ -1346,6 +840,16 @@ function waitForAndroidEmulatorShutdown(avdName, shutdown, { timeoutMs = ANDROID
1346
840
  if (remaining <= 0) throw new Error(`Owned AVD ${avdName} did not finish shutting down within ${Math.ceil(timeoutMs / 1e3)}s.`);
1347
841
  sleep(Math.min(pollMs, remaining));
1348
842
  }
843
+ if (platform === "win32") {
844
+ const handlerDeadline = now() + CRASH_HANDLER_EXIT_TIMEOUT_MS;
845
+ for (const handler of crashHandlerPids(processId)) {
846
+ while (processAlive(handler) && now() < handlerDeadline) sleep(pollMs);
847
+ if (processAlive(handler)) {
848
+ killCrashHandler(handler);
849
+ if (processAlive(handler)) throw new Error(`Crashpad handler ${handler} for owned AVD ${avdName} stayed alive.`);
850
+ }
851
+ }
852
+ }
1349
853
  if (!directoryExists(directory)) throw new Error(`Could not verify the content directory for owned AVD ${avdName} after shutdown.`);
1350
854
  }
1351
855
  function getAvdNameForSerial(serial, { timeoutMs } = {}) {
@@ -1364,4 +868,4 @@ function resolveOwnedAvdSerial(avdName, { timeoutMs } = {}) {
1364
868
  return { notRunning: true };
1365
869
  }
1366
870
  //#endregion
1367
- export { providerDownloadPath as $, pickDefaultSystemImage as A, listBuildLocks as B, memoizeEmulatorProbe as C, ownedAvdName as D, ownedAvdMatchesConfiguration as E, shutdownAndroidEmulator as F, buildCacheKey as G, segment as H, waitForAndroidEmulatorShutdown as I, filesystemBuildCapability as J, describeFingerprintMiss as K, waitForBoot as L, resetAdoptedAvd as M, resolveOwnedAvdSerial as N, ownedAvdSystemImage as O, resolvePhysicalDevice as P, prepareProviderDownloadDir as Q, WAIT_CEILING_MS as R, listOrphanedAvdDirectories as S, ownedAvdDirectory as T, takeoverLine as U, releaseBuildLock as V, waitForBuild as W, fingerprintDiffSuffix as X, fingerprintDiffRecord as Y, fingerprintProject as Z, findBuildTool as _, androidSystemImageAbi as a, untrackedMissLine as at, listAvds as b, avdPoolConfiguration as c, captureAssetManifest as ct, configureNewOwnedAvd as d, providerUploadOutcome as et, createOwnedAvd as f, extractEmulatorFailure as g, emulatorFailureRemedy as h, androidPoolNoCandidatesRefusal as i, storedAssetManifest as it, probeEmulatorSerial as j, physicalDeviceModel as k, avdStorageRoots as l, compareAssetManifests as lt, emulatorDiskSpaceRemedy as m, androidHome as n, resolveBuild as nt, assertOwnedAvdStopped as o, untrackedNativeFiles as ot, deleteAvd as p, diffFingerprintSources as q, androidPoolCandidates as r, storeBuild as rt, avdPathExists as s, assetDiffReason as st, androidDeviceAbi as t, refingerprintAfterMutation as tt, bootAndroidEmulator as u, readAssetManifest as ut, getAvdNameForSerial as v, nextConsolePort as w, listInstalledSystemImages as x, listAdbDevices as y, acquireBuildLock as z };
871
+ export { ownedAvdName as A, waitForBoot as B, listAvds as C, nextConsolePort as D, memoizeEmulatorProbe as E, resetAdoptedAvd as F, resolveOwnedAvdSerial as I, resolvePhysicalDevice as L, physicalDeviceModel as M, pickDefaultSystemImage as N, ownedAvdDirectory as O, probeEmulatorSerial as P, shutdownAndroidEmulator as R, listAdbDevices as S, listOrphanedAvdDirectories as T, emulatorFailureRemedy as _, androidPoolNoCandidatesRefusal as a, getAvdNameForSerial as b, assertOwnedAvdStopped as c, avdStorageRoots as d, bootAndroidEmulator as f, emulatorDiskSpaceRemedy as g, deleteAvd as h, androidPoolCandidates as i, ownedAvdSystemImage as j, ownedAvdMatchesConfiguration as k, avdPathExists as l, createOwnedAvd as m, androidDeviceAbi as n, androidSystemImageAbi as o, configureNewOwnedAvd as p, androidHome as r, androidToolCwd as s, androidBuildToolName as t, avdPoolConfiguration as u, extractEmulatorFailure as v, listInstalledSystemImages as w, hostSystemImageArch as x, findBuildTool as y, waitForAndroidEmulatorShutdown as z };