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,2279 @@
1
+ import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
+ import { C as withConfigLock, S as upsertProject, f as getProject, g as loadConfig, l as getConcurrencyLimits, y as saveConfig } from "./config-D7uu8Gbq.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
+ import { D as workspaceLogsDir, T as workspaceDir, _ as writeWorkspaceState, g as writeWorkspaceLaunch, n as MODE_EXPO, p as readWorkspaceState, t as MODE_BARE, v as emulatorLogFile } from "./state-IYEY-zi8.mjs";
5
+ import { C as removeTemporaryEntry, S as makeTemporaryDirectory, g as repoRoot, s as gitCommonDir } from "./worktree-Dx3ZC5lS.mjs";
6
+ import { a as findProjectRoot, c as projectShortcut, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-COj6nzzh.mjs";
7
+ import { r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
8
+ import { i as resolveProjectMetro, r as pidExists } from "./metro-B01H3EuD.mjs";
9
+ import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-D8HmSv2E.mjs";
10
+ import { a as easAuthNote, c as loadProjectProvider, d as resolveEasCliBin, f as resolveRemote, i as checkEasAuth, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as uploadRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS, x as parkedMaxSetting } from "./remote-cache-DOi5-zLj.mjs";
11
+ import { A as settingFile, B as compilerCacheFallbackMessage, E as remoteDeviceSettingError, F as unknownSettingKeys, H as resolveOptimizations, L as detectProviders, M as settingShapeErrors, O as resolveCacheProviderConfig, P as tunnelModeSetting, T as remoteAndroidSetting, V as optimizationBuildProfile, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, k as resolveSettings, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, w as publicUrlSetting, z as artifactCachePolicy } from "./settings-BSwrY4FZ.mjs";
12
+ import { _ as launchErrorPreview, c as launchErrorReport, g as stepTimer, h as stepClock, i as formatDuration, l as phaseLine, n as appReadinessMessage, p as shortHash } from "./command-output-8wHMEde0.mjs";
13
+ import { D as verifyAndroidReleaseLaunch, E as unverifiedLaunchLines, O as verifyLaunch, S as launchAndroidReleaseApp, _ as installConflictKind, c as RELEASE_VERIFY_WAIT_MS, d as androidDevClientUrl, g as installAndroidApp, h as deviceShellArg, i as LAUNCH_BUNDLING, n as DEFAULT_METRO_PORT, o as LAUNCH_FATAL, s as LAUNCH_UNVERIFIED, t as ADB_INSTALL_TIMEOUT_MS, u as androidAppProcess, x as launchAndroidApp } from "./app-install-oZIQRbdD.mjs";
14
+ import { C as untrackedNativeFiles, D as readAssetManifest, E as compareAssetManifests, S as untrackedMissLine, T as captureAssetManifest, _ as providerUploadOutcome, b as storeBuild, c as buildCacheKey, d as filesystemBuildCapability, f as fingerprintDiffRecord, g as providerDownloadPath, h as prepareProviderDownloadDir, i as releaseBuildLock, l as describeFingerprintMiss, m as fingerprintProject, n as acquireBuildLock, o as takeoverLine, p as fingerprintDiffSuffix, s as waitForBuild, t as WAIT_CEILING_MS, v as refingerprintAfterMutation, w as assetDiffReason, x as storedAssetManifest, y as resolveBuild } from "./build-lock-vE45ZzJZ.mjs";
15
+ import { r as claimFailure } from "./ownership-claim-CeJLEpjD.mjs";
16
+ import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-BlHXnYYs.mjs";
17
+ import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-CgacA9zQ.mjs";
18
+ import { c as ensureWorkspaceStorageSafely, d as noMetroMessage, f as noMetroRemedy, i as detectEntryFile, n as pickDevClientScheme, o as needsPrebuild, p as resolveMetroWithRetry, s as runPrebuild, t as devClientScheme, u as launchOutcomeRecord } from "./dev-client-Bvky1hya.mjs";
19
+ import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-wqxPm7Tv.mjs";
20
+ import { E as physicalDeviceModel, O as probeEmulatorSerial, _ as listAdbDevices, a as androidSystemImageAbi, b as memoizeEmulatorProbe, f as emulatorDiskSpaceRemedy, h as findBuildTool, i as androidPoolNoCandidatesRefusal, j as resolvePhysicalDevice, k as resetAdoptedAvd, m as extractEmulatorFailure, n as androidHome, p as emulatorFailureRemedy, r as androidPoolCandidates, t as androidDeviceAbi, y as listInstalledSystemImages } from "./android-xmyGBNy5.mjs";
21
+ import { S as unknownAndroidSystemImageRefusal, _ as checkDeviceCapacity, b as ensureOwnedDevice, f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS, y as ensureBooted } from "./xcode-CDB6fytw.mjs";
22
+ import { a as ensureMetroReachable, b as ownedSessionName, l as resolveRemoteContext, n as binOnPath, o as ensureRemoteBootOwned, s as remoteAndroidDeps } from "./device-remote-CCgkG82J.mjs";
23
+ import { t as cleanLine } from "./server-expo-BEMr_m6p.mjs";
24
+ import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-DGlTStWu.mjs";
25
+ import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
26
+ import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-B5IJwv3-.mjs";
27
+ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from "node:fs";
28
+ import { InvalidArgumentError } from "commander";
29
+ import { basename, dirname, join, relative } from "node:path";
30
+ import chalk from "chalk";
31
+ import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
32
+ //#region src/engine/apk-swap.ts
33
+ const ANDROID_BUNDLE_NAME = "index.android.bundle";
34
+ const ANDROID_BUNDLE_ENTRY = `assets/${ANDROID_BUNDLE_NAME}`;
35
+ const LAST_LINES = 5;
36
+ function hermesEnabledFromGradleProperties(text) {
37
+ if (typeof text !== "string") return true;
38
+ let value = null;
39
+ for (const raw of text.split("\n")) {
40
+ const line = raw.trim();
41
+ if (line === "" || line.startsWith("#") || line.startsWith("!")) continue;
42
+ const match = /^hermesEnabled\s*[=:]\s*(.*)$/.exec(line);
43
+ if (match) value = match[1].trim();
44
+ }
45
+ return value === null || value.toLowerCase() !== "false";
46
+ }
47
+ function readAndroidHermesEnabled(root) {
48
+ try {
49
+ return hermesEnabledFromGradleProperties(readFileSync(join(root, "android", "gradle.properties"), "utf-8"));
50
+ } catch {
51
+ return true;
52
+ }
53
+ }
54
+ function hermescBinDir(platform = process.platform) {
55
+ return platform === "darwin" ? "osx-bin" : "linux64-bin";
56
+ }
57
+ function hermescCandidates(root, { platform = process.platform, reactNativePath = null } = {}) {
58
+ const bin = hermescBinDir(platform);
59
+ const rn = reactNativePath ?? join(root, "node_modules", "react-native");
60
+ return [.../* @__PURE__ */ new Set([
61
+ join(dirname(rn), "hermes-compiler", "hermesc", bin, "hermesc"),
62
+ join(root, "node_modules", "hermes-compiler", "hermesc", bin, "hermesc"),
63
+ join(root, "node_modules", "react-native", "sdks", "hermesc", bin, "hermesc"),
64
+ join(root, "node_modules", "react-native", "sdks", "hermes", "build", "bin", "hermesc")
65
+ ])];
66
+ }
67
+ function androidHermescPath(root, { exists = existsSync, platform = process.platform, reactNativePath = null } = {}) {
68
+ const candidates = hermescCandidates(root, {
69
+ platform,
70
+ reactNativePath
71
+ });
72
+ return candidates.find(exists) ?? candidates[candidates.length - 1];
73
+ }
74
+ function androidHermescArgs({ bundle, out }) {
75
+ return [
76
+ "-emit-binary",
77
+ "-O",
78
+ "-w",
79
+ "-out",
80
+ out,
81
+ bundle
82
+ ];
83
+ }
84
+ function androidBundleCommand({ isExpo, entryFile, bundleOutput, assetsDest }) {
85
+ if (isExpo) return {
86
+ file: "npx",
87
+ args: [
88
+ "expo",
89
+ "export:embed",
90
+ "--platform",
91
+ "android",
92
+ "--dev",
93
+ "false",
94
+ "--bundle-output",
95
+ bundleOutput,
96
+ "--assets-dest",
97
+ assetsDest
98
+ ]
99
+ };
100
+ return {
101
+ file: "npx",
102
+ args: [
103
+ "react-native",
104
+ "bundle",
105
+ "--platform",
106
+ "android",
107
+ "--dev",
108
+ "false",
109
+ "--entry-file",
110
+ entryFile,
111
+ "--bundle-output",
112
+ bundleOutput,
113
+ "--assets-dest",
114
+ assetsDest
115
+ ]
116
+ };
117
+ }
118
+ function isNothingToDelete(text) {
119
+ return /nothing to do|name not matched|no matches found/i.test(String(text ?? ""));
120
+ }
121
+ function zipalignArgs({ buildToolsMajor, input, output }) {
122
+ return [
123
+ ...buildToolsMajor >= 35 ? ["-P", "16"] : ["-p"],
124
+ "-f",
125
+ "-v",
126
+ "4",
127
+ input,
128
+ output
129
+ ];
130
+ }
131
+ function keystorePassArg(value) {
132
+ const text = typeof value === "string" ? value.trim() : "";
133
+ if (text === "") return "pass:android";
134
+ if (/^(?:pass|file|env):/.test(text) || text === "stdin") return text;
135
+ return `pass:${text}`;
136
+ }
137
+ function resolveKeystore(root, settings) {
138
+ const android = settings?.["android"];
139
+ const bag = android && typeof android === "object" && !Array.isArray(android) ? android : {};
140
+ const configured = bag["keystore"];
141
+ return {
142
+ path: typeof configured === "string" && configured.trim() !== "" ? configured.trim().startsWith("/") ? configured.trim() : join(root, configured.trim()) : join(root, "android", "app", "debug.keystore"),
143
+ pass: keystorePassArg(bag["keystorePassword"])
144
+ };
145
+ }
146
+ function apksignerArgs({ keystore, apkPath }) {
147
+ return [
148
+ "sign",
149
+ "--ks",
150
+ keystore.path,
151
+ "--ks-pass",
152
+ keystore.pass,
153
+ apkPath
154
+ ];
155
+ }
156
+ async function swapApkBundle({ root, isExpo, cachedApkPath, keystore, logWriter = null, exec = null, spawnFn = null, mkdtemp = () => makeTemporaryDirectory(cachedApkPath, "stim-apk-swap-"), exists = existsSync, hermesEnabled = null, buildTools = null, findTool = findBuildTool, storedAssets = null, readManifest = readAssetManifest, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, onHeartbeat = (line) => console.error(line) }) {
157
+ const e = exec || getExecutor();
158
+ const startedAt = now();
159
+ const elapsed = () => now() - startedAt;
160
+ let tmp;
161
+ const fail = (step, reason, lastLines = []) => {
162
+ logWriter?.write?.({
163
+ src: "build",
164
+ level: "error",
165
+ msg: `APK swap failed at ${step}: ${reason}`,
166
+ event: "apk_swap"
167
+ });
168
+ if (tmp) removeTemporaryEntry(tmp);
169
+ return {
170
+ failed: true,
171
+ step,
172
+ reason,
173
+ lastLines,
174
+ durationMs: elapsed()
175
+ };
176
+ };
177
+ let work;
178
+ let final;
179
+ try {
180
+ tmp = mkdtemp();
181
+ const base = basename(cachedApkPath);
182
+ work = join(tmp, `unaligned-${base}`);
183
+ final = join(tmp, base);
184
+ try {
185
+ e.runFile("cp", [
186
+ "-c",
187
+ cachedApkPath,
188
+ work
189
+ ]);
190
+ } catch {
191
+ e.runFile("cp", [cachedApkPath, work]);
192
+ }
193
+ } catch (err) {
194
+ return fail("copy", `could not copy ${cachedApkPath} aside: ${describe(err)}`);
195
+ }
196
+ const stage = join(tmp, "stage");
197
+ const bundleOutput = join(stage, "assets", ANDROID_BUNDLE_NAME);
198
+ const assetsDest = join(stage, "res");
199
+ const command = androidBundleCommand({
200
+ isExpo,
201
+ entryFile: isExpo ? "index.js" : detectEntryFile(root),
202
+ bundleOutput,
203
+ assetsDest
204
+ });
205
+ try {
206
+ mkdirSync(join(stage, "assets"), { recursive: true });
207
+ mkdirSync(assetsDest, { recursive: true });
208
+ } catch (err) {
209
+ return fail("bundle", `could not create ${stage}: ${describe(err)}`);
210
+ }
211
+ logWriter?.write?.({
212
+ src: "build",
213
+ level: "info",
214
+ msg: `${command.file} ${command.args.join(" ")}`,
215
+ event: "apk_swap"
216
+ });
217
+ const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
218
+ const transcript = [];
219
+ const push = (line) => {
220
+ const msg = cleanLine(line);
221
+ if (msg.trim() === "") return;
222
+ transcript.push(msg);
223
+ logWriter?.write?.({
224
+ src: "build",
225
+ level: "debug",
226
+ msg,
227
+ raw: true,
228
+ event: "apk_swap"
229
+ });
230
+ };
231
+ let child;
232
+ try {
233
+ child = spawn(command.file, command.args, {
234
+ cwd: root,
235
+ stdio: [
236
+ "ignore",
237
+ "pipe",
238
+ "pipe"
239
+ ],
240
+ env: {
241
+ ...process.env,
242
+ FORCE_COLOR: "0"
243
+ }
244
+ });
245
+ } catch (err) {
246
+ return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
247
+ }
248
+ const reader = {
249
+ out: createLineReader(push),
250
+ err: createLineReader(push)
251
+ };
252
+ child.stdout?.setEncoding?.("utf-8");
253
+ child.stderr?.setEncoding?.("utf-8");
254
+ child.stdout?.on("data", (chunk) => reader.out.push(chunk));
255
+ child.stderr?.on("data", (chunk) => reader.err.push(chunk));
256
+ const stopHeartbeat = startBuildHeartbeat({
257
+ intervalMs: heartbeatMs,
258
+ elapsed,
259
+ emit: onHeartbeat,
260
+ label: "swap"
261
+ });
262
+ let outcome;
263
+ try {
264
+ outcome = await waitForChild(child);
265
+ } finally {
266
+ stopHeartbeat();
267
+ }
268
+ reader.out.flush();
269
+ reader.err.flush();
270
+ if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
271
+ if (outcome.code !== 0) {
272
+ const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
273
+ return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
274
+ }
275
+ if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${ANDROID_BUNDLE_NAME} at ${bundleOutput}`);
276
+ let hermes = false;
277
+ let note;
278
+ if (hermesEnabled ?? readAndroidHermesEnabled(root)) {
279
+ const hermesc = androidHermescPath(root);
280
+ if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
281
+ else {
282
+ const hbc = `${bundleOutput}.hbc`;
283
+ try {
284
+ e.runFile(hermesc, androidHermescArgs({
285
+ bundle: bundleOutput,
286
+ out: hbc
287
+ }));
288
+ e.runFile("mv", [hbc, bundleOutput]);
289
+ hermes = true;
290
+ } catch (err) {
291
+ return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
292
+ }
293
+ }
294
+ }
295
+ const refuse = (reason, assetDiff) => {
296
+ logWriter?.write?.({
297
+ src: "build",
298
+ level: "warn",
299
+ msg: `APK swap refused: ${reason}`,
300
+ event: "apk_swap"
301
+ });
302
+ removeTemporaryEntry(tmp);
303
+ const result = {
304
+ assetMismatch: true,
305
+ reason,
306
+ durationMs: elapsed()
307
+ };
308
+ if (assetDiff) result.assetDiff = assetDiff;
309
+ return result;
310
+ };
311
+ if (!storedAssets) return refuse("this cache entry predates asset tracking (no assets-manifest.json beside the artifact), so its asset set cannot be proven to match this one");
312
+ const fresh = readManifest(assetsDest);
313
+ if (!fresh) return fail("assets", `could not hash the assets emitted into ${assetsDest}, so the asset set cannot be verified`);
314
+ const diff = compareAssetManifests(fresh, storedAssets);
315
+ if (!diff.same) return refuse(assetDiffReason(diff), diff);
316
+ try {
317
+ e.runFile("zip", [
318
+ "-d",
319
+ work,
320
+ ANDROID_BUNDLE_ENTRY
321
+ ]);
322
+ } catch (err) {
323
+ if (!isNothingToDelete(describe(err))) return fail("zip", `zip -d ${ANDROID_BUNDLE_ENTRY} failed on ${work}: ${describe(err)}`);
324
+ }
325
+ try {
326
+ e.runFile("zip", [
327
+ "-0",
328
+ "-r",
329
+ work,
330
+ "assets"
331
+ ], { cwd: stage });
332
+ } catch (err) {
333
+ return fail("zip", `zip -0 -r ${work} assets failed: ${describe(err)}`);
334
+ }
335
+ const tools = buildTools ?? findTool(["zipalign"]);
336
+ if (!tools) return fail("zipalign", "no zipalign found under the Android SDK build-tools; install one with `sdkmanager \"build-tools;36.0.0\"`");
337
+ try {
338
+ e.runFile(tools.path, zipalignArgs({
339
+ buildToolsMajor: tools.major,
340
+ input: work,
341
+ output: final
342
+ }));
343
+ } catch (err) {
344
+ return fail("zipalign", `zipalign failed on ${work}: ${describe(err)}`);
345
+ }
346
+ const signer = buildTools ? {
347
+ ...buildTools,
348
+ path: join(dirname(buildTools.path), "apksigner")
349
+ } : findTool(["apksigner"]);
350
+ if (!signer) return fail("apksigner", "no apksigner found under the Android SDK build-tools; install one with `sdkmanager \"build-tools;36.0.0\"`");
351
+ try {
352
+ e.runFile(signer.path, apksignerArgs({
353
+ keystore,
354
+ apkPath: final
355
+ }));
356
+ } catch (err) {
357
+ return fail("apksigner", `apksigner sign failed on ${final} with ${keystore.path}: ${describe(err)}`);
358
+ }
359
+ logWriter?.write?.({
360
+ src: "build",
361
+ level: "info",
362
+ msg: `APK swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${final} in ${elapsed()}ms`,
363
+ event: "apk_swap"
364
+ });
365
+ const result = {
366
+ ok: true,
367
+ apkPath: final,
368
+ tmpDir: tmp,
369
+ hermes,
370
+ durationMs: elapsed()
371
+ };
372
+ if (note) result.note = note;
373
+ return result;
374
+ }
375
+ function describe(err) {
376
+ const e = err;
377
+ const streams = [e?.stdout, e?.stderr].map((s) => s ? String(s).trim() : "").filter(Boolean).join("\n");
378
+ const message = e?.message ? String(e.message).trim() : String(err);
379
+ return streams ? `${message}: ${streams}` : message;
380
+ }
381
+ //#endregion
382
+ //#region src/commands/android/support.ts
383
+ const PLATFORM = "android";
384
+ function androidVariantSetting(settings) {
385
+ const android = settings?.["android"];
386
+ if (!android || typeof android !== "object" || Array.isArray(android)) return null;
387
+ const raw = android["variant"];
388
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
389
+ }
390
+ function resolveVariant(flag, settings) {
391
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || androidVariantSetting(settings);
392
+ }
393
+ function androidSystemImageSetting(settings) {
394
+ const android = settings?.["android"];
395
+ if (!android || typeof android !== "object" || Array.isArray(android)) return null;
396
+ const raw = android["systemImage"];
397
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
398
+ }
399
+ function resolveSystemImage(flag, settings) {
400
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || androidSystemImageSetting(settings);
401
+ }
402
+ function systemImageRefusal({ flag, resolved, physical, remoteBackend, listImages }) {
403
+ if (typeof flag === "string" && flag.trim() === "") return {
404
+ code: "STIM_BAD_ARG",
405
+ message: "--system-image was given an empty id.",
406
+ remedy: "Pass `--system-image <id>` with an sdkmanager package id, e.g. \"system-images;android-36;google_apis;arm64-v8a\"."
407
+ };
408
+ if (physical || remoteBackend) return null;
409
+ if (!resolved) return null;
410
+ let images;
411
+ try {
412
+ images = listImages();
413
+ } catch (error) {
414
+ return {
415
+ code: NO_DEVICE,
416
+ message: `Could not read the installed Android system images: ${error?.message || error}`,
417
+ remedy: "Check that ANDROID_HOME points at a readable SDK, then try again."
418
+ };
419
+ }
420
+ const refusal = unknownAndroidSystemImageRefusal(resolved, images);
421
+ return refusal ? {
422
+ code: "STIM_BAD_ARG",
423
+ message: refusal.message,
424
+ remedy: refusal.remedy
425
+ } : null;
426
+ }
427
+ function isReleaseVariant(variant) {
428
+ return typeof variant === "string" && /release$/i.test(variant.trim());
429
+ }
430
+ const NO_METRO = "STIM_NO_METRO";
431
+ const NO_FINGERPRINT = "STIM_NO_FINGERPRINT";
432
+ const NO_DEVICE = "STIM_NO_DEVICE";
433
+ const LAUNCH_FAILED = "STIM_LAUNCH_FAILED";
434
+ function findAapt(home = androidHome(), { readDir = readdirSync, exists = existsSync } = {}) {
435
+ const found = findBuildTool(["aapt", "aapt2"], {
436
+ home,
437
+ readDir,
438
+ exists
439
+ });
440
+ return found ? {
441
+ path: found.path,
442
+ tool: found.tool,
443
+ version: found.version
444
+ } : null;
445
+ }
446
+ function dumpApkManifest(apkPath, { exec = null, aapt = null } = {}) {
447
+ if (typeof apkPath !== "string" || apkPath.trim() === "") return null;
448
+ const tool = aapt || findAapt();
449
+ if (!tool) return null;
450
+ const e = exec || getExecutor();
451
+ const args = tool.tool === "aapt2" ? [
452
+ "dump",
453
+ "xmltree",
454
+ "--file",
455
+ "AndroidManifest.xml",
456
+ apkPath
457
+ ] : [
458
+ "dump",
459
+ "xmltree",
460
+ apkPath,
461
+ "AndroidManifest.xml"
462
+ ];
463
+ try {
464
+ const out = e.runFile(tool.path, args);
465
+ return typeof out === "string" && out.includes("E: manifest") ? out : null;
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ function parseXmltree(text) {
471
+ const root = {
472
+ tag: "#root",
473
+ attrs: {},
474
+ children: [],
475
+ indent: -1
476
+ };
477
+ const stack = [root];
478
+ for (const raw of String(text ?? "").split("\n")) {
479
+ const indent = raw.search(/\S/);
480
+ if (indent < 0) continue;
481
+ const line = raw.trim();
482
+ const element = /^E: ([\w.:-]+)/.exec(line);
483
+ if (element) {
484
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
485
+ const node = {
486
+ tag: element[1],
487
+ attrs: {},
488
+ children: [],
489
+ indent
490
+ };
491
+ stack[stack.length - 1].children.push(node);
492
+ stack.push(node);
493
+ continue;
494
+ }
495
+ const attr = /^A: ([^(=]+?)(?:\(0x[0-9a-f]+\))?=(.*)$/.exec(line);
496
+ if (attr && stack.length > 1) {
497
+ const name = attr[1].replace(/^http:\/\/schemas\.android\.com\/apk\/res\/android:/, "android:");
498
+ const value = /^"((?:[^"\\]|\\.)*)"/.exec(attr[2]);
499
+ stack[stack.length - 1].attrs[name] = value ? value[1] : null;
500
+ }
501
+ }
502
+ return root;
503
+ }
504
+ function eachNode(node, fn) {
505
+ fn(node);
506
+ for (const child of node.children) eachNode(child, fn);
507
+ }
508
+ function apkPackage(text) {
509
+ const pkg = parseXmltree(text).children.find((c) => c.tag === "manifest")?.attrs["package"];
510
+ return typeof pkg === "string" && pkg.trim() ? pkg.trim() : null;
511
+ }
512
+ function apkDevClientFacts(text) {
513
+ const root = parseXmltree(text);
514
+ const facts = {
515
+ devClient: false,
516
+ schemes: []
517
+ };
518
+ let launchable = null;
519
+ eachNode(root, (node) => {
520
+ const name = node.attrs["android:name"];
521
+ if (typeof name === "string" && name.startsWith("expo.modules.devlauncher")) facts.devClient = true;
522
+ if (node.tag !== "activity" && node.tag !== "activity-alias") return;
523
+ const filters = node.children.filter((c) => c.tag === "intent-filter");
524
+ if (!filters.some((f) => f.children.some((c) => c.tag === "action" && c.attrs["android:name"] === "android.intent.action.MAIN")) || launchable) return;
525
+ launchable = node;
526
+ for (const filter of filters) for (const data of filter.children) {
527
+ if (data.tag !== "data") continue;
528
+ const scheme = data.attrs["android:scheme"];
529
+ if (typeof scheme === "string" && scheme.trim()) facts.schemes.push(scheme.trim());
530
+ }
531
+ });
532
+ return facts;
533
+ }
534
+ function androidDevClientScheme(root, apkPath, { exec = null, dump = dumpApkManifest, aapt = null } = {}) {
535
+ const text = dump(apkPath, {
536
+ exec,
537
+ aapt
538
+ });
539
+ if (text) {
540
+ const facts = apkDevClientFacts(text);
541
+ if (!facts.devClient) return void 0;
542
+ const scheme = pickDevClientScheme(facts.schemes);
543
+ if (scheme) return scheme;
544
+ }
545
+ return devClientScheme(root, null);
546
+ }
547
+ function noDeviceDiagnostic({ reason, logFile, remedy, localEmulator = true, readLog = readEmulatorLogTail }) {
548
+ const text = localEmulator ? readLog(logFile) : "";
549
+ const logPath = text.trim() ? logFile : null;
550
+ const found = extractEmulatorFailure(text);
551
+ const diskRemedy = localEmulator ? emulatorDiskSpaceRemedy([reason, ...found]) : null;
552
+ if (found.length === 0) return {
553
+ message: reason,
554
+ remedy: diskRemedy ?? remedy,
555
+ lines: [],
556
+ logPath
557
+ };
558
+ return {
559
+ message: `${reason} The emulator reported: ${found[0]}`,
560
+ remedy: diskRemedy ?? emulatorFailureRemedy(found),
561
+ lines: found.slice(1),
562
+ logPath
563
+ };
564
+ }
565
+ function readEmulatorLogTail(file) {
566
+ try {
567
+ return readFileSync(file, "utf-8").split("\n").slice(-400).join("\n");
568
+ } catch {
569
+ return "";
570
+ }
571
+ }
572
+ function displayPath(root, path) {
573
+ const rel = relative(root, path);
574
+ return rel && !rel.startsWith("..") ? rel : path;
575
+ }
576
+ async function pooledAndroidDevice({ root, selectPool, listDevices, isEmulatorDevice, deviceModel, waitSeconds, noWait, now, warn }) {
577
+ const isEmulator = memoizeEmulatorProbe(isEmulatorDevice);
578
+ const pooled = await selectPool({
579
+ root,
580
+ platform: PLATFORM,
581
+ idLabel: "serial",
582
+ list: () => androidPoolCandidates(listDevices(), isEmulator).map((entry) => ({ id: entry.serial })),
583
+ noCandidates: () => {
584
+ const resolved = androidPoolNoCandidatesRefusal(listDevices(), isEmulator);
585
+ return {
586
+ message: resolved.error,
587
+ remedy: resolved.remedy
588
+ };
589
+ },
590
+ waitSeconds,
591
+ noWait,
592
+ now,
593
+ warn
594
+ });
595
+ if (pooled.status === "refused") {
596
+ const { code, message, remedy, lease } = pooled.refusal;
597
+ return {
598
+ code,
599
+ message,
600
+ remedy,
601
+ extra: lease === null ? {} : { lease }
602
+ };
603
+ }
604
+ return { device: {
605
+ serial: pooled.candidate.id,
606
+ deviceName: deviceModel(pooled.candidate.id) ?? pooled.candidate.id,
607
+ owned: false
608
+ } };
609
+ }
610
+ function androidBuildOptions({ release, physical, device, variant, deviceAbi, compiler, buildProfile, targetAbiOnly = true }) {
611
+ let abi = null;
612
+ if (targetAbiOnly && !release && physical && device.serial) abi = deviceAbi(device.serial);
613
+ if (targetAbiOnly && !release && !physical) abi = androidSystemImageAbi(device.systemImage);
614
+ const runOptions = {};
615
+ if (variant) runOptions.variant = variant;
616
+ if (abi) runOptions.abi = abi;
617
+ if (compiler) runOptions.compiler = compiler;
618
+ if (buildProfile) runOptions.buildProfile = buildProfile;
619
+ return {
620
+ abi,
621
+ runOptions,
622
+ remoteRunOptions: Object.keys(runOptions).length === 0 ? null : runOptions
623
+ };
624
+ }
625
+ //#endregion
626
+ //#region src/commands/android/result.ts
627
+ function androidFacts({ serial, avdName = null, deviceName = null, systemImage = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease }) {
628
+ return {
629
+ platform: PLATFORM,
630
+ serial: serial ?? null,
631
+ avdName: avdName ?? null,
632
+ deviceName: deviceName ?? avdName ?? null,
633
+ systemImage: systemImage ?? null,
634
+ fingerprint: fingerprint ?? null,
635
+ cacheKey: cacheKey ?? null,
636
+ variant: variant ?? null,
637
+ metroPort: metroPort ?? null,
638
+ cacheHit: cacheLevel(cacheHit),
639
+ cacheSkipped: Boolean(cacheSkipped),
640
+ waitedForBuild: waitedForBuild ? {
641
+ pid: waitedForBuild.pid ?? null,
642
+ ms: waitedForBuild.ms ?? 0
643
+ } : null,
644
+ appPath: appPath ?? null,
645
+ bundleId: bundleId ?? null,
646
+ installSkipped: Boolean(installSkipped),
647
+ launched: launched === "unverified" || launched === "bundling" ? launched : Boolean(launched),
648
+ ccache,
649
+ debugHttpHost: debugHttpHost ?? null,
650
+ debugHttpHostNote: debugHttpHostNote ?? null,
651
+ devClientUrl: devClientUrl ?? null,
652
+ logs: logs ?? null,
653
+ durationMs: typeof durationMs === "number" && Number.isFinite(durationMs) ? durationMs : null,
654
+ ...lease === void 0 ? {} : { lease }
655
+ };
656
+ }
657
+ function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null }) {
658
+ const record = {
659
+ platform: PLATFORM,
660
+ avdName: avdName ?? null,
661
+ deviceName: deviceName ?? avdName ?? null,
662
+ fingerprint: fingerprint ?? null,
663
+ cacheKey: cacheKey ?? null,
664
+ cacheHit: cacheLevel(cacheHit),
665
+ cacheSkipped: Boolean(cacheSkipped),
666
+ durationMs: Number.isFinite(durationMs) ? durationMs : null,
667
+ appPath: appPath ?? null,
668
+ bundleId: bundleId ?? null,
669
+ startedAt,
670
+ status
671
+ };
672
+ if (errorCode) record.errorCode = errorCode;
673
+ return record;
674
+ }
675
+ async function finishAndroidUpload(uploadPending, remote, phase) {
676
+ if (!uploadPending) return false;
677
+ const upload = await uploadPending;
678
+ if (upload?.uploaded) phase("cache", `uploaded (${remote?.name})`);
679
+ else if (upload?.timedOut) {
680
+ phase("cache", chalk.yellow(`${remote?.name} upload still running after ${formatDuration(UPLOAD_TIMEOUT_MS)}; not waiting`));
681
+ return true;
682
+ } else if (upload?.failed) {
683
+ const authNote = remote?.name === "eas" && isEasAuthFailureText(upload.failed) ? easAuthNote({
684
+ code: "logged-out",
685
+ reason: upload.failed,
686
+ phase: "upload"
687
+ }) : null;
688
+ phase("cache", chalk.yellow(authNote || `${remote?.name} upload failed: ${upload.failed}`));
689
+ }
690
+ return false;
691
+ }
692
+ function reportAndroidResult({ json, useBuildCache, variant, release, metroCheck, metroPort, logsDir, serial, apkPath, androidPackage, installSkipped, record, waitedForBuild, remote, providerName, launchState, launchWarning, launched, ccache, durationMs, writer, emit, lease, recordRun }) {
693
+ recordRun({
694
+ failed: false,
695
+ cacheHit: cacheLevel(record.cacheHit),
696
+ waited: waitedForBuild,
697
+ durationMs
698
+ });
699
+ const facts = androidFacts({
700
+ serial,
701
+ avdName: record.avdName,
702
+ deviceName: record.deviceName,
703
+ systemImage: record.systemImage,
704
+ debugHttpHost: launched.debugHttpHost ?? null,
705
+ debugHttpHostNote: launched.debugHttpHostNote ?? null,
706
+ devClientUrl: launched.devClientUrl ?? null,
707
+ fingerprint: record.fingerprint,
708
+ cacheKey: record.cacheKey,
709
+ variant,
710
+ metroPort,
711
+ cacheHit: record.cacheHit,
712
+ cacheSkipped: !useBuildCache,
713
+ waitedForBuild,
714
+ appPath: apkPath,
715
+ bundleId: androidPackage,
716
+ installSkipped,
717
+ launched: launchState,
718
+ ccache,
719
+ logs: logsDir,
720
+ durationMs,
721
+ lease
722
+ });
723
+ writer.close();
724
+ if (json) emit(JSON.stringify(facts));
725
+ else {
726
+ const summary = `${launchWarning ? "WARNING" : "OK"}: ${androidPackage} launched on ${serial}, ${release ? `${variant} (embedded JS, no Metro)` : `Metro port ${metroPort}`} (${cacheOutcome(record.cacheHit, remote?.name ?? providerName)})`;
727
+ const outcome = launchWarning ? chalk.yellow(`${summary} -- ${launchWarning}`) : launchState === "unverified" ? chalk.yellow(`${summary} -- launch UNVERIFIED`) : launchState === "bundling" ? chalk.green(`${summary} -- bundle requested, still building`) : chalk.green(summary);
728
+ const deviceName = record.avdName || record.deviceName || serial;
729
+ const cacheResult = useBuildCache ? cacheOutcome(record.cacheHit, remote?.name ?? providerName) : "bypassed; built";
730
+ const metroResult = release ? `embedded (${variant})` : !metroCheck ? `check skipped on port ${metroPort}` : launchState === "unverified" ? `state unverified on port ${metroPort}` : `running on port ${metroPort}`;
731
+ emit([
732
+ outcome,
733
+ phaseLine("device", `${deviceName} (${serial})`),
734
+ phaseLine("app", androidPackage),
735
+ phaseLine("metro", metroResult),
736
+ phaseLine("cache", cacheResult),
737
+ ...ccache.status === "not-run" ? [phaseLine("compilation cache", ccacheActivityLine(ccache))] : [],
738
+ phaseLine("logs", logsDir || "unavailable (remote device)")
739
+ ].join("\n"));
740
+ }
741
+ return facts;
742
+ }
743
+ function cacheOutcome(cacheHit, providerName = null) {
744
+ if (cacheHit === "remote") return `cache hit from ${providerName || "the remote cache"}`;
745
+ if (cacheHit === "local") return "cache hit";
746
+ return "built";
747
+ }
748
+ function persistLastBuild({ writeState, root, record, startedAt, durationMs, status, errorCode = null, out }) {
749
+ const lastBuild = lastBuildRecord({
750
+ ...record,
751
+ startedAt,
752
+ durationMs,
753
+ status,
754
+ errorCode
755
+ });
756
+ try {
757
+ writeState(root, { lastBuild });
758
+ } catch (err) {
759
+ out(phaseLine("state", chalk.yellow(`could not record lastBuild: ${err?.message || err}`)));
760
+ }
761
+ return lastBuild;
762
+ }
763
+ //#endregion
764
+ //#region src/commands/android/collector.ts
765
+ function collectorEntry() {
766
+ return spawnEntry("collector-run");
767
+ }
768
+ function collectorLogFile(root) {
769
+ return join(workspaceLogsDir(root), `collector-${PLATFORM}.log`);
770
+ }
771
+ const COLLECTOR_EXIT_WAIT_MS = 2e3;
772
+ const COLLECTOR_POLL_MS = 25;
773
+ function killPreviousCollector(root, { platform = PLATFORM, kill = (pid, signal) => process.kill(pid, signal), collectors = null, verify = verifyCollectorOwnership, isAlive = pidExists, note = (_line) => {} } = {}) {
774
+ const record = (collectors ?? readCollectors(root))?.[platform];
775
+ const pid = Number(record?.pid);
776
+ if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return null;
777
+ const ownership = verify({
778
+ pid,
779
+ platform,
780
+ root,
781
+ isAlive,
782
+ expected: record
783
+ });
784
+ if (ownership.status === "gone") return null;
785
+ if (ownership.status === "unverified") {
786
+ note(chalk.dim(`Previous ${platform} log collector (pid ${pid}): ${ownership.reason}, so it was not signalled -- starting a replacement anyway`));
787
+ return null;
788
+ }
789
+ try {
790
+ kill(pid, "SIGTERM");
791
+ return pid;
792
+ } catch {
793
+ return null;
794
+ }
795
+ }
796
+ async function startCollector({ root, serial, packageName, spawn, kill, alive = pidExists, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), out }) {
797
+ const previousPid = killPreviousCollector(root, {
798
+ kill,
799
+ isAlive: alive,
800
+ verify,
801
+ note: (line) => out(phaseLine("logs", line))
802
+ });
803
+ if (previousPid) {
804
+ const deadline = Date.now() + waitMs;
805
+ while (Date.now() < deadline && alive(previousPid)) await sleep(COLLECTOR_POLL_MS);
806
+ }
807
+ let stdio = "ignore";
808
+ try {
809
+ mkdirSync(workspaceLogsDir(root), { recursive: true });
810
+ const fd = openSync(collectorLogFile(root), "a");
811
+ stdio = [
812
+ "ignore",
813
+ fd,
814
+ fd
815
+ ];
816
+ } catch {}
817
+ try {
818
+ const child = spawn(process.execPath, [
819
+ collectorEntry(),
820
+ "--platform",
821
+ PLATFORM,
822
+ "--root",
823
+ root,
824
+ "--serial",
825
+ serial,
826
+ "--package",
827
+ packageName
828
+ ], {
829
+ cwd: root,
830
+ detached: true,
831
+ stdio,
832
+ env: process.env
833
+ });
834
+ child?.unref?.();
835
+ return child?.pid ?? null;
836
+ } catch (err) {
837
+ out(phaseLine("logs", chalk.yellow(`could not start the device log collector: ${err?.message || err}`)));
838
+ return null;
839
+ }
840
+ }
841
+ //#endregion
842
+ //#region src/commands/android/launch.ts
843
+ async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, verifyReleaseLaunched, verifyLaunched, serial, androidPackage, variant, metroCheck, logsDir, launchedAt, metroPort, isExpo, physical, scheme, component = null, phase }) {
844
+ const readNativeCrashes = () => remoteDevice ? [] : captureNativeCrashes({
845
+ root,
846
+ platform: "android",
847
+ deviceId: serial,
848
+ appId: androidPackage,
849
+ since: launchedAt
850
+ }, logsDir);
851
+ if (remoteRelease) {
852
+ phase("verify", chalk.yellow("UNVERIFIED: remote adapter launch accepted; process verification is unavailable"));
853
+ return { state: LAUNCH_UNVERIFIED };
854
+ }
855
+ if (release) {
856
+ const processCheck = await verifyReleaseLaunched({
857
+ serial,
858
+ packageName: androidPackage
859
+ });
860
+ const crashes = readNativeCrashes();
861
+ if (processCheck?.verified && !crashes.length) {
862
+ phase("verify", `process alive ${formatDuration(processCheck.waitedMs ?? 0)} after launch (${variant}: no bundle fetch to observe)`);
863
+ return { state: true };
864
+ }
865
+ if (processCheck?.reason === "probe-failed" && !crashes.length) {
866
+ phase("verify", chalk.yellow("UNVERIFIED: the app process check failed"));
867
+ return { state: LAUNCH_UNVERIFIED };
868
+ }
869
+ for (const line of launchErrorPreview(crashes, root)) phase("launch", chalk.red(line));
870
+ if (!crashes.length) phase("logs", "Process missing; no attributable native crash report captured. Read `stim logs --source device` for available output.");
871
+ phase("verify", chalk.yellow(crashes.length ? "FATAL: the app reported a native crash" : `FATAL: no ${androidPackage} process on ${serial} ${formatDuration(processCheck?.waitedMs ?? 0)} after launch`));
872
+ phase("", chalk.yellow("A release process exited before readiness. Run `stim logs --errors` for captured crash reports, or `stim logs --source device` for the full device output."));
873
+ return { state: LAUNCH_FATAL };
874
+ }
875
+ const verification = metroCheck ? await verifyLaunched({
876
+ onReadinessPending: () => phase("readiness", "waiting for app readiness (up to 30s after bundle load)"),
877
+ logsDir,
878
+ since: launchedAt,
879
+ metroPort,
880
+ platform: "android",
881
+ mode: isExpo ? MODE_EXPO : MODE_BARE,
882
+ readNativeCrashes,
883
+ processAlive: () => {
884
+ const pid = androidAppProcess(serial, androidPackage);
885
+ return pid === void 0 ? null : pid !== null;
886
+ }
887
+ }) : {
888
+ verified: false,
889
+ skipped: true
890
+ };
891
+ const nativeCrashes = verification.errors?.some((record) => record.event === "native_crash") ? [] : readNativeCrashes();
892
+ if (nativeCrashes.length) verification.fatal = true;
893
+ verification.errors = await errorDiagnostics([...verification.errors ?? [], ...nativeCrashes], {
894
+ root,
895
+ logsDir,
896
+ port: metroPort,
897
+ allowRequest: true
898
+ });
899
+ if (verification.readiness) phase("readiness", appReadinessMessage(verification.readiness, verification.waitedMs ?? 0));
900
+ if (verification?.fatal) {
901
+ const nativeFatal = verification.errors?.some((record) => record.event === "native_crash");
902
+ const deliveryFailed = verification.record?.event === "bundle_response_failed";
903
+ const reason = nativeFatal ? "the app reported a native crash" : verification.processAlive === false ? "the app process exited" : deliveryFailed ? "Metro bundle delivery failed" : "Metro could not build the bundle";
904
+ phase("verify", chalk.red(`FATAL after ${formatDuration(verification.waitedMs ?? 0)}: ${reason}`));
905
+ for (const line of launchErrorPreview(verification.errors ?? [], root)) phase("", chalk.red(line));
906
+ if (verification.processAlive === false && !verification.errors?.some((record) => record.event === "native_crash")) phase("logs", "No attributable native crash report captured yet. Run `stim logs --errors` again for delayed reports, or `stim logs --source device` for available output.");
907
+ if (nativeFatal || verification.processAlive === false) phase("remedy", chalk.yellow(`Fix the crash, then restart the app: \`adb -s ${deviceShellArg(serial)} shell am force-stop ${deviceShellArg(androidPackage)}\`, then run \`stim android\` again. A crashed Android process can remain alive behind the system crash dialog; Metro reload cannot recover it.`));
908
+ else if (verification.processAlive === true && metroPort !== null) {
909
+ const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
910
+ phase("remedy", chalk.yellow(`The native app is still running. ${deliveryFailed ? "Check the Metro logs and device connection, then" : "Fix the JavaScript or TypeScript error, then"} ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
911
+ }
912
+ return { state: LAUNCH_FATAL };
913
+ }
914
+ if (verification?.verified) {
915
+ phase("verify", `bundle loaded` + (verification.processAlive === true ? ", process alive" : "") + (verification.readiness ? "" : ", stable for 3s -- the first screen may still be rendering") + ` (${formatDuration(verification.waitedMs ?? 0)} total)`);
916
+ const report = launchErrorReport(verification.errors ?? [], root);
917
+ if (report.summary) phase("launch", chalk.dim(report.summary));
918
+ for (const line of report.lines) phase("launch", chalk.yellow(line));
919
+ if (report.lines.length > 0 && verification.processAlive === true && metroPort !== null) {
920
+ const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
921
+ phase("remedy", chalk.yellow(`The native app is still running. Fix the JavaScript or TypeScript error; Fast Refresh should apply the edit. If the error screen remains, ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
922
+ }
923
+ return {
924
+ state: true,
925
+ warning: report.lines.length > 0 ? "app errors detected; inspect the error above" : verification.readiness === "timed-out" || verification.readiness === "error" ? "app readiness not confirmed; inspect the UI and logs" : void 0
926
+ };
927
+ }
928
+ if (verification?.skipped) {
929
+ phase("verify", "skipped (--no-metro-check): the launch is reported as unverified");
930
+ return { state: LAUNCH_UNVERIFIED };
931
+ }
932
+ if (verification?.requested) {
933
+ phase("verify", `BUNDLING: the app asked port ${metroPort} for its bundle; build, delivery, or JavaScript loading was still pending after ${formatDuration(verification.waitedMs ?? 0)} (a cold bundle on a large graph outlasts this window)`);
934
+ phase("", chalk.dim("Nothing to do: `stim logs --source metro` shows the build finishing, usually within a minute."));
935
+ return { state: LAUNCH_BUNDLING };
936
+ }
937
+ phase("verify", chalk.yellow("UNVERIFIED: no bundle request reached this workspace's Metro"));
938
+ for (const line of unverifiedLaunchLines({
939
+ platform: PLATFORM,
940
+ metroPort: metroPort ?? 8081,
941
+ waitedMs: verification?.waitedMs,
942
+ bundleId: androidPackage,
943
+ serial,
944
+ component,
945
+ devClientUrl: scheme ? androidDevClientUrl(scheme, metroPort ?? 8081, physical) : null,
946
+ mode: isExpo ? MODE_EXPO : MODE_BARE
947
+ })) phase("", chalk.yellow(line));
948
+ return { state: LAUNCH_UNVERIFIED };
949
+ }
950
+ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, useBuildCache, variant, release, isExpo, metroPort, logsDir, emuLog, device, physical, remoteDevice, bootPromise, bootDuration, apkPath, androidPackage: initialPackage, swapDir, record, waitedForBuild, ccache, uploadPending, providerUpload, providerName, remote, abandonedRemote: remoteWasAbandoned, started, startedAt, writer, phase, fail, readApkPackage, install, launch, launchRelease, resolveDevClientScheme, verifyLaunched, verifyReleaseLaunched, spawn, kill, pidAlive, verifyCollector, writeLaunch, writeState, now, out, emit, recordRun }) {
951
+ let androidPackage = initialPackage;
952
+ let leaseWarned = false;
953
+ const raiseLeaseFor = (boundMs, beforeInstall) => {
954
+ const step = lease?.raise(boundMs);
955
+ if (!step || step.ok) return null;
956
+ if (beforeInstall) {
957
+ const refusal = lostRefusal(step.holder, step.expiresAt, now());
958
+ return fail(refusal.code, refusal.message, refusal.remedy, { lease: refusal.lease });
959
+ }
960
+ if (!leaseWarned) {
961
+ leaseWarned = true;
962
+ phase("lease", chalk.yellow(lostLine(step.holder, step.expiresAt, now())));
963
+ }
964
+ return null;
965
+ };
966
+ const booted = await bootPromise;
967
+ if (booted.failed) {
968
+ const diag = noDeviceDiagnostic({
969
+ reason: booted.reason ?? "The emulator did not boot.",
970
+ logFile: emuLog,
971
+ remedy: "Run `stim status` to see what Stim thinks it owns; re-running `stim android` creates a fresh owned AVD.",
972
+ localEmulator: !physical
973
+ });
974
+ return fail(NO_DEVICE, diag.message, diag.remedy, {
975
+ lines: diag.lines,
976
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
977
+ });
978
+ }
979
+ const serial = booted.serial;
980
+ phase("device", physical ? `${device.deviceName || serial} (${serial}) connected, not owned by Stim` : `${device.avdName || serial} (${serial}) ${device.adopted || device.adoptionPending ? "adopted" : "booted"} ${bootDuration()}`);
981
+ const packageFromApk = readApkPackage(apkPath);
982
+ if (packageFromApk && androidPackage && packageFromApk !== androidPackage) phase("install", chalk.dim(`applicationId ${packageFromApk} (from the APK; project files say ${androidPackage})`));
983
+ androidPackage = packageFromApk || androidPackage || detectAndroidPackage(root);
984
+ const adopting = !physical && !remoteDevice && Boolean(device.adoptionPending);
985
+ if (adopting) {
986
+ if (!androidPackage || !device.avdName) return fail(LAUNCH_FAILED, "Cannot clean an adopted emulator without its app package and AVD name.", "Retry once the APK applicationId can be read.");
987
+ try {
988
+ await resetAdoptedAvd(device.avdName, serial, androidPackage);
989
+ } catch (error) {
990
+ return fail(LAUNCH_FAILED, `Could not clean adopted emulator ${device.avdName}: ${String(error?.message || error)}`, "Fix the device cleanup error and retry; adoption remains pending and the app was not launched.");
991
+ }
992
+ }
993
+ const lostBeforeInstall = physical ? raiseLeaseFor(ADB_INSTALL_TIMEOUT_MS, true) : null;
994
+ if (lostBeforeInstall) return lostBeforeInstall;
995
+ const installTimer = stepTimer(now);
996
+ const installed = install({
997
+ serial,
998
+ apkPath,
999
+ packageName: androidPackage,
1000
+ allowUninstall: release || adopting
1001
+ });
1002
+ if (installed.failed) {
1003
+ const conflict = installConflictKind(installed.reason);
1004
+ const rerun = useBuildCache ? " Then run this command again; it installs the APK from cache without building it again." : " Then run this command again.";
1005
+ const installRemedy = conflict ? `${androidPackage} is already installed on ${serial} ` + (conflict === "signature" ? "with a different signer" : "at a higher versionCode") + `. Uninstall it first (\`adb -s ${serial} uninstall ${androidPackage}\`); its data goes with it.` + rerun : `Check that ${serial} is still connected (\`adb devices\`) and has room for the APK.`;
1006
+ return fail(installed.code || "STIM_INSTALL_FAILED", installed.reason, installRemedy, { lastBuildStatus: true });
1007
+ }
1008
+ if (adopting) try {
1009
+ withConfigLock(() => {
1010
+ const config = loadConfig();
1011
+ const current = config?.projects?.[root]?.platforms?.android;
1012
+ if (!config || !current || current.avdName !== device.avdName) throw new Error("The adopted emulator assignment changed during installation.");
1013
+ delete current.adoptionPending;
1014
+ saveConfig(config);
1015
+ });
1016
+ } catch (error) {
1017
+ return fail(LAUNCH_FAILED, `Could not finish adopting emulator ${device.avdName}: ${String(error?.message || error)}`, "Retry to reconcile the emulator assignment; the app was not launched.");
1018
+ }
1019
+ const installSkipped = Boolean(installed.skipped);
1020
+ phase("install", installSkipped ? `unchanged (${serial} already has this build) ${installTimer()}` : `${basename(apkPath)} -> ${serial} ${installTimer()}`);
1021
+ if (installed.note) {
1022
+ phase("install", chalk.yellow(installed.note));
1023
+ writer.write({
1024
+ src: "build",
1025
+ level: "warn",
1026
+ event: "install_uninstalled_first",
1027
+ msg: installed.note
1028
+ });
1029
+ }
1030
+ if (swapDir) try {
1031
+ rmSync(swapDir, {
1032
+ recursive: true,
1033
+ force: true
1034
+ });
1035
+ } catch {}
1036
+ if (androidPackage) upsertProject(root, { androidPackage });
1037
+ record.bundleId = androidPackage;
1038
+ if (!androidPackage) return fail(LAUNCH_FAILED, "Could not determine this app's Android package name, so there is nothing to launch.", "Set `expo.android.package` in app.json / app.config.js, or `namespace` in android/app/build.gradle.", { lastBuildStatus: true });
1039
+ if (physical) raiseLeaseFor(0, false);
1040
+ const scheme = release ? void 0 : resolveDevClientScheme(root, apkPath);
1041
+ const launchTimer = stepTimer(now);
1042
+ const launchedAt = now();
1043
+ writer.write({
1044
+ src: "build",
1045
+ level: "info",
1046
+ marker: true,
1047
+ event: "launch_attempt",
1048
+ ts: launchedAt,
1049
+ platform: "android",
1050
+ appId: androidPackage,
1051
+ deviceId: serial,
1052
+ remote: Boolean(remoteDevice),
1053
+ physical,
1054
+ msg: `launching ${androidPackage} on ${serial}`
1055
+ });
1056
+ const launched = release ? remoteDevice ? remoteDevice.launch({
1057
+ serial,
1058
+ packageName: androidPackage,
1059
+ metroPort: null
1060
+ }) : launchRelease({
1061
+ serial,
1062
+ packageName: androidPackage
1063
+ }) : launch({
1064
+ serial,
1065
+ packageName: androidPackage,
1066
+ metroPort: metroPort ?? 8081,
1067
+ devClientScheme: scheme,
1068
+ physical
1069
+ });
1070
+ if (launched.failed) {
1071
+ printNativeCrashReport({
1072
+ root,
1073
+ platform: "android",
1074
+ deviceId: serial,
1075
+ appId: androidPackage,
1076
+ since: launchedAt
1077
+ }, logsDir, (line) => phase("launch", chalk.red(line)), Boolean(remoteDevice));
1078
+ return fail(launched.code || "STIM_LAUNCH_FAILED", launched.reason, `Check the app installed correctly (\`adb -s ${serial} shell pm list packages ${androidPackage}\`).`, { lastBuildStatus: true });
1079
+ }
1080
+ writer.write({
1081
+ src: "build",
1082
+ level: "info",
1083
+ event: "app_launched",
1084
+ msg: release ? `launched ${androidPackage} on ${serial} (${variant}, embedded JS bundle, no Metro)` : `launched ${androidPackage} on ${serial} against Metro port ${metroPort}`
1085
+ });
1086
+ phase("launch", `${androidPackage} ${launchTimer()}`);
1087
+ if (!physical && !remoteDevice) try {
1088
+ writeLaunch(root, "android", {
1089
+ appId: androidPackage,
1090
+ deviceId: serial,
1091
+ metroPort,
1092
+ release,
1093
+ launchedAt: new Date(launchedAt).toISOString()
1094
+ });
1095
+ } catch (error) {
1096
+ out(phaseLine("state", chalk.yellow(`could not record Android launch: ${error?.message || error}`)));
1097
+ }
1098
+ const reversedSummary = (launched.reversed ?? []).join(", ") || `tcp:${metroPort}->tcp:${metroPort}`;
1099
+ if (!release && launched.debugHttpHost) phase("metro", `debug_http_host ${launched.debugHttpHost} + adb reverse ${reversedSummary}`);
1100
+ else if (!release) {
1101
+ phase("metro", chalk.yellow(`adb reverse ${reversedSummary}; ${launched.debugHttpHostNote || "debug_http_host not written"}`));
1102
+ writer.write({
1103
+ src: "build",
1104
+ level: "warn",
1105
+ event: "debug_http_host_failed",
1106
+ msg: `debug_http_host was not written for ${androidPackage} on ${serial}: ${launched.debugHttpHostNote || "unknown reason"}`
1107
+ });
1108
+ }
1109
+ if (launched.devClientNote) {
1110
+ phase("metro", chalk.yellow(launched.devClientNote));
1111
+ writer.write({
1112
+ src: "build",
1113
+ level: "warn",
1114
+ event: "dev_client_link_failed",
1115
+ msg: launched.devClientNote
1116
+ });
1117
+ }
1118
+ const uploadWasAbandoned = await finishAndroidUpload(uploadPending, remote, phase);
1119
+ const providerOutcome = providerUploadOutcome(providerUpload ? await providerUpload : null, providerName);
1120
+ if (providerOutcome) phase("cache", providerOutcome.warn ? chalk.yellow(providerOutcome.line) : providerOutcome.line);
1121
+ persistLastBuild({
1122
+ writeState,
1123
+ root,
1124
+ record,
1125
+ startedAt,
1126
+ durationMs: now() - started,
1127
+ status: "ok",
1128
+ out
1129
+ });
1130
+ const remoteRelease = Boolean(remoteDevice && release);
1131
+ if (!remoteRelease) {
1132
+ if (physical) raiseLeaseFor(0, false);
1133
+ const collectorPid = await startCollector({
1134
+ root,
1135
+ serial,
1136
+ packageName: androidPackage,
1137
+ spawn,
1138
+ kill,
1139
+ alive: pidAlive,
1140
+ verify: verifyCollector,
1141
+ out
1142
+ });
1143
+ phase("logs", `${displayPath(root, logsDir)}${collectorPid ? ` (collector pid ${collectorPid})` : ""}`);
1144
+ }
1145
+ if (physical) raiseLeaseFor(release ? RELEASE_VERIFY_WAIT_MS : DEBUG_VERIFY_STEP_MS, false);
1146
+ const { state: launchState, warning: launchWarning } = await verifyAndroidRun({
1147
+ root,
1148
+ release,
1149
+ remoteRelease,
1150
+ remoteDevice: Boolean(remoteDevice),
1151
+ verifyReleaseLaunched,
1152
+ verifyLaunched,
1153
+ serial,
1154
+ androidPackage,
1155
+ variant,
1156
+ metroCheck,
1157
+ logsDir,
1158
+ launchedAt,
1159
+ metroPort,
1160
+ isExpo,
1161
+ physical,
1162
+ scheme,
1163
+ component: launched.component ?? null,
1164
+ phase
1165
+ });
1166
+ if (launchState === "fatal") return fail(LAUNCH_FAILED, "The app failed its launch readiness check.", `Read the launch error above or run \`stim logs --errors\`. The full timeline is in ${logsDir}.`, {
1167
+ lastBuildStatus: true,
1168
+ logPath: displayPath(root, logsDir)
1169
+ });
1170
+ writer.write(launchOutcomeRecord({
1171
+ launchState,
1172
+ release,
1173
+ bundleId: androidPackage,
1174
+ configuration: variant,
1175
+ metroPort
1176
+ }));
1177
+ const leaseFacts = lease?.facts() ?? null;
1178
+ releaseLease();
1179
+ const facts = reportAndroidResult({
1180
+ json,
1181
+ useBuildCache,
1182
+ variant,
1183
+ release,
1184
+ metroCheck,
1185
+ metroPort,
1186
+ logsDir: remoteRelease ? null : logsDir,
1187
+ serial,
1188
+ apkPath,
1189
+ androidPackage,
1190
+ installSkipped,
1191
+ record,
1192
+ waitedForBuild,
1193
+ remote,
1194
+ providerName,
1195
+ launchState,
1196
+ launchWarning,
1197
+ launched,
1198
+ ccache,
1199
+ durationMs: now() - started,
1200
+ writer,
1201
+ emit,
1202
+ lease: physical ? leaseFacts : void 0,
1203
+ recordRun
1204
+ });
1205
+ if (remoteWasAbandoned || uploadWasAbandoned) exitAfterFlush(0);
1206
+ return {
1207
+ ok: true,
1208
+ facts
1209
+ };
1210
+ }
1211
+ //#endregion
1212
+ //#region src/commands/android.ts
1213
+ const FALLBACK_LINES = 5;
1214
+ function androidCompilerCache({ root, optimizations, settingsContext }) {
1215
+ const { cas, optimizations: resolved } = resolveAndroidCompilerCache({
1216
+ optimizations,
1217
+ use: (manifest) => resolveAndroidCas(root, {
1218
+ ...process.env,
1219
+ STIM_ANDROID_CAS_TOOLCHAIN: manifest
1220
+ })
1221
+ });
1222
+ const fallback = resolved.android.compilerCacheFallback;
1223
+ if (!fallback) return {
1224
+ cas,
1225
+ optimizations: resolved,
1226
+ warning: null
1227
+ };
1228
+ return {
1229
+ cas,
1230
+ optimizations: resolved,
1231
+ warning: `Warning: ${compilerCacheFallbackMessage({
1232
+ fallback,
1233
+ compilerCache: resolved.android.compilerCache === "none" ? "none" : "ccache",
1234
+ file: settingFile(settingsContext, fallback.key)
1235
+ })}`
1236
+ };
1237
+ }
1238
+ function androidCommand(program) {
1239
+ registerAndroid(program);
1240
+ }
1241
+ function registerAndroid(program) {
1242
+ program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. Never starts the bundler -- run `stim start` first.").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--no-metro-check", "Skip the reserved-port Metro health check (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. The build still happens here.", (value) => {
1243
+ if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
1244
+ throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
1245
+ }).option("--wait <seconds>", "How long to wait for another workspace to release the device it leases, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once). Only with --device.").option("--no-wait", "Install on a device another workspace leases instead of waiting: this run takes no lease and, when both workspaces build the same app id, the install terminates the holder's running app. Only with --device.").action(async (opts) => {
1246
+ const root = findProjectRoot(process.cwd());
1247
+ if (!root) {
1248
+ console.error(chalk.red("Not in a React Native project (no package.json found)."));
1249
+ process.exit(1);
1250
+ return;
1251
+ }
1252
+ if (!(await runAndroid({
1253
+ root,
1254
+ json: Boolean(opts.json),
1255
+ metroCheck: opts.metroCheck !== false,
1256
+ useBuildCache: opts.buildCache !== false,
1257
+ variant: opts.variant ?? null,
1258
+ systemImage: opts.systemImage ?? null,
1259
+ remoteDevice: opts.remote ?? null,
1260
+ device: opts.device ?? null,
1261
+ wait: opts.wait,
1262
+ waitConflict: waitFlagConflict(process.argv)
1263
+ })).ok) process.exit(1);
1264
+ });
1265
+ }
1266
+ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, variant: variantFlag = null, systemImage: systemImageFlag = null, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, ensureDeviceBooted = ensureBooted, resolveMetro = resolveProjectMetro, resolveMetroRetrying = resolveMetroWithRetry, readState = readWorkspaceState, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, needsPrebuildFor = needsPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
1267
+ return {
1268
+ root,
1269
+ json,
1270
+ commandRemoteBackend,
1271
+ resolveSettingsFor,
1272
+ resolveRemoteDeviceContext,
1273
+ makeRemoteDeviceDeps,
1274
+ resolveEasBin,
1275
+ ensureRemoteMetro,
1276
+ ensureRemoteOwned,
1277
+ detectRemoteProviders,
1278
+ metroCheck,
1279
+ useBuildCache,
1280
+ variantFlag,
1281
+ systemImageFlag,
1282
+ deviceFlag,
1283
+ waitFlag,
1284
+ waitConflict,
1285
+ acquireLease,
1286
+ makeRunLease,
1287
+ selectPool,
1288
+ onLeaseSignal,
1289
+ listDevices,
1290
+ deviceModel,
1291
+ deviceAbi,
1292
+ isEmulatorDevice,
1293
+ readApkPackage,
1294
+ getLimits,
1295
+ checkCapacity,
1296
+ acquireSlot,
1297
+ releaseSlot,
1298
+ ensureDevice,
1299
+ listSystemImages,
1300
+ ensureDeviceBooted,
1301
+ resolveMetro,
1302
+ resolveMetroRetrying,
1303
+ readState,
1304
+ pidAlive,
1305
+ verifyCollector,
1306
+ verifyLaunched,
1307
+ ensureStorage,
1308
+ fingerprint,
1309
+ untracked,
1310
+ resolveCached,
1311
+ storeCached,
1312
+ storedAssets,
1313
+ captureAssets,
1314
+ acquireLock,
1315
+ releaseLock,
1316
+ waitForBuild: waitForBuild$1,
1317
+ loadProvider,
1318
+ easAuth,
1319
+ resolveRemoteBuild,
1320
+ uploadRemoteBuild,
1321
+ resolveCacheProvider,
1322
+ loadCacheProviderModule,
1323
+ needsPrebuildFor,
1324
+ prebuild,
1325
+ build,
1326
+ ccacheFor,
1327
+ install,
1328
+ launch,
1329
+ launchRelease,
1330
+ verifyReleaseLaunched,
1331
+ swapApk,
1332
+ resolveDevClientScheme,
1333
+ spawn,
1334
+ kill,
1335
+ createWriter,
1336
+ writeLaunch,
1337
+ writeState,
1338
+ recordStats,
1339
+ readEstimates,
1340
+ now,
1341
+ out,
1342
+ emit
1343
+ };
1344
+ }
1345
+ async function runAndroid(options = {}) {
1346
+ let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, variantFlag, systemImageFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, ensureDeviceBooted, resolveMetro, resolveMetroRetrying, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, needsPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = resolveRunAndroidOptions(options);
1347
+ const started = now();
1348
+ const startedAt = new Date(started).toISOString();
1349
+ const projectProblem = appProjectProblem(root);
1350
+ if (projectProblem) {
1351
+ const { message, remedy } = projectProblem;
1352
+ out(phaseLine("error", chalk.red(`STIM_NO_PROJECT: ${message}`)));
1353
+ out(phaseLine("remedy", remedy));
1354
+ if (json) emit(JSON.stringify({
1355
+ code: "STIM_NO_PROJECT",
1356
+ message,
1357
+ remedy
1358
+ }));
1359
+ return {
1360
+ ok: false,
1361
+ error: {
1362
+ code: "STIM_NO_PROJECT",
1363
+ message,
1364
+ remedy
1365
+ }
1366
+ };
1367
+ }
1368
+ try {
1369
+ await ensureStorage(root, { note: out });
1370
+ } catch (error) {
1371
+ const code = error?.code || "STIM_WORKSPACE_STATE";
1372
+ const message = `Could not prepare this workspace's Stim state: ${error?.message || error}`;
1373
+ const remedy = "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`).";
1374
+ out(phaseLine("error", chalk.red(`${code}: ${message}`)));
1375
+ out(phaseLine("remedy", remedy));
1376
+ if (json) emit(JSON.stringify({
1377
+ code,
1378
+ message,
1379
+ remedy
1380
+ }));
1381
+ return {
1382
+ ok: false,
1383
+ error: {
1384
+ code,
1385
+ message,
1386
+ remedy
1387
+ }
1388
+ };
1389
+ }
1390
+ const logsDir = workspaceLogsDir(root);
1391
+ const buildLog = join(logsDir, "build-android.ndjson");
1392
+ const writer = createWriter(buildLog, { truncate: true });
1393
+ const record = {
1394
+ fingerprint: null,
1395
+ cacheKey: null,
1396
+ cacheHit: false,
1397
+ appPath: null,
1398
+ bundleId: null,
1399
+ avdName: null,
1400
+ deviceName: null
1401
+ };
1402
+ let buildLock = null;
1403
+ const releaseHeldLock = () => {
1404
+ if (!buildLock) return;
1405
+ const held = buildLock;
1406
+ buildLock = null;
1407
+ try {
1408
+ releaseLock(held);
1409
+ } catch (err) {
1410
+ out(phaseLine("build", chalk.dim(`could not release the build lock at ${held.path}: ${err?.message || err}`)));
1411
+ }
1412
+ };
1413
+ let buildSlot = null;
1414
+ const releaseHeldSlot = () => {
1415
+ if (!buildSlot) return;
1416
+ const held = buildSlot;
1417
+ buildSlot = null;
1418
+ try {
1419
+ releaseSlot(held);
1420
+ } catch (err) {
1421
+ out(phaseLine("build", chalk.dim(`could not release the build slot: ${err?.message || err}`)));
1422
+ }
1423
+ };
1424
+ const phase = (label, text) => out(phaseLine(label, text));
1425
+ const stats = createRunRecorder({
1426
+ platform: PLATFORM,
1427
+ write: recordStats,
1428
+ now,
1429
+ note: (line) => out(phaseLine("stats", chalk.dim(line)))
1430
+ });
1431
+ const recordRun = stats.record;
1432
+ let ccacheActivity = CCACHE_NOT_RUN;
1433
+ const fail = (code, message, remedy, { lastBuildStatus = false, diagnostics = [], lines = [], logPath = null, lease } = {}) => {
1434
+ if (lastBuildStatus) persistLastBuild({
1435
+ writeState,
1436
+ root,
1437
+ record,
1438
+ startedAt,
1439
+ durationMs: now() - started,
1440
+ status: "failed",
1441
+ errorCode: code,
1442
+ out
1443
+ });
1444
+ out(phaseLine("error", chalk.red(`${code}: ${message}`)));
1445
+ for (const diagnostic of diagnostics) out(phaseLine("error", chalk.red(diagnostic)));
1446
+ for (const line of lines) out(phaseLine("", chalk.dim(line)));
1447
+ if (remedy) out(phaseLine("remedy", remedy));
1448
+ if (logPath) out(phaseLine("log", logPath));
1449
+ recordRun({
1450
+ failed: true,
1451
+ durationMs: now() - started
1452
+ });
1453
+ if (json) emit(JSON.stringify({
1454
+ code,
1455
+ message,
1456
+ remedy: remedy ?? null,
1457
+ ...ccacheActivity.status === "not-run" ? {} : { ccache: ccacheActivity },
1458
+ ...lease === void 0 ? {} : { lease }
1459
+ }));
1460
+ writer.close();
1461
+ return {
1462
+ ok: false,
1463
+ error: {
1464
+ code,
1465
+ message,
1466
+ remedy: remedy ?? null
1467
+ }
1468
+ };
1469
+ };
1470
+ const settingsRepoRoot = repoRoot(root);
1471
+ const settingsRoot = root;
1472
+ const settingsContext = {
1473
+ projectPath: root,
1474
+ gitCommonDir: gitCommonDir(root),
1475
+ repoRoot: settingsRepoRoot
1476
+ };
1477
+ const projectKey = statsProjectKey({
1478
+ root,
1479
+ commonDir: settingsContext.gitCommonDir,
1480
+ repoRoot: settingsRepoRoot
1481
+ });
1482
+ stats.setProject(projectKey);
1483
+ let estimatesRead = null;
1484
+ const estimates = () => estimatesRead ??= readEstimates({
1485
+ projectKey,
1486
+ platform: PLATFORM
1487
+ });
1488
+ const settings = resolveSettingsFor(settingsContext);
1489
+ const [shapeError, ...moreShapeErrors] = [parkedMaxSetting("android").error, ...settingShapeErrors(settings)].filter((error) => Boolean(error));
1490
+ if (shapeError) return fail("STIM_BAD_ARG", shapeError, SETTING_SHAPE_REMEDY, { lines: moreShapeErrors });
1491
+ for (const key of unknownSettingKeys(settings)) out(phaseLine("setting", chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`)));
1492
+ let optimizations;
1493
+ try {
1494
+ optimizations = resolveOptimizations(settings);
1495
+ } catch (error) {
1496
+ return fail("STIM_BAD_ARG", `Could not configure Android build: ${error.message}`, SETTING_SHAPE_REMEDY);
1497
+ }
1498
+ const compilerCache = androidCompilerCache({
1499
+ root,
1500
+ optimizations,
1501
+ settingsContext
1502
+ });
1503
+ const cas = compilerCache.cas;
1504
+ optimizations = compilerCache.optimizations;
1505
+ if (compilerCache.warning) out(phaseLine("cache", chalk.yellow(compilerCache.warning)));
1506
+ const buildProfile = optimizationBuildProfile("android", optimizations);
1507
+ const cacheProviderConfig = resolveCacheProvider(settingsContext);
1508
+ const cacheProviderError = cacheProviderSettingError(settings);
1509
+ if (cacheProviderError) out(phaseLine("cache", chalk.yellow(`${cacheProviderError} Using the local cache.`)));
1510
+ const dataPartitionSizeError = androidDataPartitionSizeGbSettingError(settings);
1511
+ if (dataPartitionSizeError) return fail("STIM_BAD_ARG", dataPartitionSizeError, "Set android.dataPartitionSizeGb to a whole number of GiB from 6 through 16384.");
1512
+ const avdConfigError = androidAvdConfigSettingError(settings, settingsRoot);
1513
+ if (avdConfigError) return fail("STIM_BAD_ARG", avdConfigError, "Use only documented android.avdConfig keys, or an android.avdConfigFile fragment contained by the app directory.");
1514
+ const remoteSettingError = remoteDeviceSettingError(settings);
1515
+ if (remoteSettingError) return fail("STIM_BAD_ARG", remoteSettingError, `Set ios.remote and android.remote to one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}.`);
1516
+ const systemImage = resolveSystemImage(systemImageFlag, settings);
1517
+ const variant = resolveVariant(variantFlag, settings);
1518
+ const flavorRefusal = productFlavorRefusal({
1519
+ flavors: readProductFlavors(root),
1520
+ variant
1521
+ });
1522
+ if (flavorRefusal) return fail(flavorRefusal.code, flavorRefusal.reason, flavorRefusal.remedy);
1523
+ const release = isReleaseVariant(variant);
1524
+ const cachePolicy = artifactCachePolicy(optimizations, requestedBuildCache, release);
1525
+ const useBuildCache = cachePolicy.read;
1526
+ const isExpo = detectIsExpo(root);
1527
+ const physical = deviceFlag !== null && deviceFlag !== void 0 && deviceFlag !== false;
1528
+ if (physical && deviceFlag === "") return fail("STIM_BAD_ARG", "--device was given an empty serial.", "Pass `--device` on its own to take the first connected device this workspace can lease, or `--device <serial>` to name one.");
1529
+ if (physical && commandRemoteBackend) return fail("STIM_BAD_ARG", "--device installs on a device connected to this machine, and --remote installs on a remote one.", "Pass only one of --device and --remote.");
1530
+ const noWait = waitFlag === false;
1531
+ const waitFlagged = waitFlag !== void 0;
1532
+ if (waitConflict) return fail("STIM_BAD_ARG", "--wait and --no-wait ask for opposite things.", "Pass `--wait <seconds>` to wait for the lease, or `--no-wait` to install without one.");
1533
+ if (waitFlagged && !physical) return fail("STIM_BAD_ARG", "--wait and --no-wait only apply to a `--device` run.", "This workspace owns its emulator, so nothing contends for it. Drop the flag, or pass `--device`.");
1534
+ const waitParsed = parseDeviceWait(noWait ? void 0 : waitFlag);
1535
+ if ("error" in waitParsed) return fail("STIM_BAD_ARG", waitParsed.error, "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a leased device at once.");
1536
+ const waitSeconds = waitParsed.seconds;
1537
+ const remoteBackend = physical ? null : commandRemoteBackend ?? remoteAndroidSetting(settings);
1538
+ const imageRefusal = systemImageRefusal({
1539
+ flag: systemImageFlag,
1540
+ resolved: systemImage,
1541
+ physical,
1542
+ remoteBackend,
1543
+ listImages: listSystemImages
1544
+ });
1545
+ if (imageRefusal) return fail(imageRefusal.code, imageRefusal.message, imageRefusal.remedy);
1546
+ const requestedSerial = typeof deviceFlag === "string" ? deviceFlag : null;
1547
+ let androidPackage = detectAndroidPackage(root);
1548
+ record.bundleId = androidPackage;
1549
+ const registerProject = () => upsertProject(root, {
1550
+ bundleId: detectBundleId(root) ?? void 0,
1551
+ androidPackage: androidPackage ?? void 0,
1552
+ isExpo
1553
+ });
1554
+ if (remoteBackend !== "eas") registerProject();
1555
+ const project = getProject(root);
1556
+ const label = projectShortcut(root, project);
1557
+ let remoteDevice = null;
1558
+ const reservedPort = project?.metroPort ?? null;
1559
+ let metroPort = null;
1560
+ let phaseFailure = null;
1561
+ async function resolveMetroPort() {
1562
+ if (release) phase("metro", `skipped (${variant}: the JS bundle is embedded, no dev server is used)`);
1563
+ else if (metroCheck) {
1564
+ if (!reservedPort) {
1565
+ phaseFailure = fail(NO_METRO, "No Metro port is reserved for this workspace.", "Run `stim start` first, or pass --no-metro-check.");
1566
+ return false;
1567
+ }
1568
+ const held = await resolveMetroRetrying(resolveMetro, reservedPort, root, { onRetry: ({ delayMs }) => phase("metro", `port ${reservedPort} did not verify yet; retrying in ${Math.round(delayMs / 1e3)}s (Metro may still be indexing)`) });
1569
+ if (!held.metro) {
1570
+ const supervisor = readState(root)?.supervisor ?? null;
1571
+ const supervisorAlive = Boolean(supervisor?.pid && pidAlive(supervisor.pid));
1572
+ phaseFailure = fail(NO_METRO, noMetroMessage({
1573
+ port: reservedPort,
1574
+ resolution: held,
1575
+ supervisor,
1576
+ supervisorAlive
1577
+ }), noMetroRemedy({
1578
+ port: reservedPort,
1579
+ supervisor,
1580
+ supervisorAlive
1581
+ }));
1582
+ return false;
1583
+ }
1584
+ phase("metro", `port ${reservedPort} (pid ${held.metro?.pid})`);
1585
+ } else phase("metro", reservedPort ? `port ${reservedPort} (not checked)` : `no reservation; using ${DEFAULT_METRO_PORT} (not checked)`);
1586
+ metroPort = release ? null : reservedPort ?? 8081;
1587
+ return true;
1588
+ }
1589
+ if (!await resolveMetroPort()) return phaseFailure;
1590
+ if (remoteBackend) {
1591
+ const resolved = await resolveRemoteDeviceContext({
1592
+ root,
1593
+ label,
1594
+ platform: PLATFORM,
1595
+ backend: remoteBackend,
1596
+ easBin: resolveEasBin(root)?.file ?? null
1597
+ });
1598
+ if ("failed" in resolved) return fail(resolved.code ?? "STIM_NO_REMOTE_SESSION", resolved.failed, resolved.remedy);
1599
+ remoteDevice = makeRemoteDeviceDeps(resolved.ctx);
1600
+ if (metroPort !== null) {
1601
+ const reachable = await ensureRemoteMetro({
1602
+ ctx: remoteDevice.ctx,
1603
+ metroPort,
1604
+ isExpo,
1605
+ tunnelMode: tunnelModeSetting(settings) ?? void 0,
1606
+ publicUrl: publicUrlSetting(settings),
1607
+ available: detectRemoteProviders(binOnPath)
1608
+ });
1609
+ if ("failed" in reachable) return fail(reachable.code ?? "STIM_NO_REMOTE_SESSION", reachable.failed, reachable.remedy);
1610
+ }
1611
+ checkCapacity = remoteDevice.checkCapacity;
1612
+ ensureDevice = remoteDevice.ensureDevice;
1613
+ ensureDeviceBooted = remoteDevice.ensureDeviceBooted;
1614
+ install = remoteDevice.install;
1615
+ launch = remoteDevice.launch;
1616
+ }
1617
+ const emuLog = emulatorLogFile(root);
1618
+ const limits = getLimits();
1619
+ let device;
1620
+ let bootDuration = "";
1621
+ let bootPromise;
1622
+ if (physical && !requestedSerial) {
1623
+ const pooled = await pooledAndroidDevice({
1624
+ root,
1625
+ selectPool,
1626
+ listDevices,
1627
+ isEmulatorDevice,
1628
+ deviceModel,
1629
+ waitSeconds,
1630
+ noWait,
1631
+ now,
1632
+ warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
1633
+ });
1634
+ if ("code" in pooled) return fail(pooled.code, pooled.message, pooled.remedy, pooled.extra);
1635
+ device = pooled.device;
1636
+ bootPromise = Promise.resolve({
1637
+ ok: true,
1638
+ serial: pooled.device.serial
1639
+ });
1640
+ } else if (physical) {
1641
+ const resolved = resolvePhysicalDevice(requestedSerial, listDevices(), isEmulatorDevice);
1642
+ if (!resolved.serial) return fail(NO_DEVICE, resolved.error, resolved.remedy);
1643
+ device = {
1644
+ serial: resolved.serial,
1645
+ deviceName: deviceModel(resolved.serial) ?? resolved.serial,
1646
+ owned: false
1647
+ };
1648
+ bootPromise = Promise.resolve({
1649
+ ok: true,
1650
+ serial: resolved.serial
1651
+ });
1652
+ } else {
1653
+ const capacity = checkCapacity({
1654
+ platform: PLATFORM,
1655
+ project,
1656
+ max: limits.maxDevices
1657
+ });
1658
+ if (capacity) return fail(capacity.code, capacity.message, capacity.remedy);
1659
+ const prepare = stepClock(now);
1660
+ try {
1661
+ device = await ensureDevice({
1662
+ platform: PLATFORM,
1663
+ project,
1664
+ projectPath: root,
1665
+ settingsRoot,
1666
+ settings,
1667
+ flags: { systemImage },
1668
+ note: out,
1669
+ out,
1670
+ logFile: emuLog
1671
+ });
1672
+ } catch (err) {
1673
+ const diag = noDeviceDiagnostic({
1674
+ reason: `Could not ensure an owned Android emulator: ${err?.message || err}`,
1675
+ logFile: emuLog,
1676
+ remedy: "Check that JAVA_HOME and ANDROID_HOME are set correctly, and that an arm64 system image is installed (`sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`)."
1677
+ });
1678
+ return fail(NO_DEVICE, diag.message, diag.remedy, {
1679
+ lines: diag.lines,
1680
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
1681
+ });
1682
+ }
1683
+ const prepareMs = prepare();
1684
+ if (device.created || prepareMs >= 2e3) phase("device", `${device.avdName || device.deviceName || label} ${device.created ? "created" : "prepared"} (${formatDuration(prepareMs)})`);
1685
+ const bootTimer = stepTimer(now);
1686
+ const boot = () => Promise.resolve(ensureDeviceBooted({
1687
+ platform: PLATFORM,
1688
+ device,
1689
+ out,
1690
+ logFile: emuLog
1691
+ })).catch((e) => ({
1692
+ failed: true,
1693
+ reason: String(e?.message || e),
1694
+ serial: void 0
1695
+ }));
1696
+ bootPromise = (remoteDevice?.ctx.backend === "eas" ? ensureRemoteOwned({
1697
+ root,
1698
+ platform: PLATFORM,
1699
+ sessionName: ownedSessionName(remoteDevice.ctx.label),
1700
+ startedAt,
1701
+ boot,
1702
+ createdSessionId: remoteDevice.createdSessionId,
1703
+ abandonCreatedSession: remoteDevice.abandonCreatedSession,
1704
+ writeState,
1705
+ register: registerProject
1706
+ }) : boot()).then((result) => {
1707
+ bootDuration = bootTimer();
1708
+ return result;
1709
+ });
1710
+ }
1711
+ if (remoteDevice) {
1712
+ const booted = await bootPromise;
1713
+ if (booted.failed) {
1714
+ if (booted.code) return fail(booted.code, booted.reason ?? "The remote device did not boot.", booted.remedy ?? null);
1715
+ const diag = noDeviceDiagnostic({
1716
+ reason: booted.reason ?? "The remote device did not boot.",
1717
+ logFile: emuLog,
1718
+ remedy: "Run `stim status` to inspect the remote device, then retry the command.",
1719
+ localEmulator: false
1720
+ });
1721
+ return fail(NO_DEVICE, diag.message, diag.remedy, {
1722
+ lines: diag.lines,
1723
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
1724
+ });
1725
+ }
1726
+ }
1727
+ record.avdName = device.avdName ?? null;
1728
+ record.deviceName = device.deviceName ?? device.avdName ?? null;
1729
+ record.systemImage = device.systemImage;
1730
+ const { abi: buildAbi, runOptions: buildRunOptions, remoteRunOptions } = androidBuildOptions({
1731
+ release,
1732
+ physical,
1733
+ device,
1734
+ variant,
1735
+ deviceAbi,
1736
+ compiler: cas?.id,
1737
+ buildProfile,
1738
+ targetAbiOnly: optimizations.android.targetAbiOnly
1739
+ });
1740
+ let hash = "";
1741
+ let providerUpload = null;
1742
+ let providerName = null;
1743
+ let providerLoad = null;
1744
+ const cacheWarn = createWarnOnce((line) => phase("cache", chalk.yellow(line)));
1745
+ const loadTieredProvider = cachePolicy.remote && cacheProviderConfig ? () => providerLoad ??= loadCacheProviderModule({
1746
+ projectRoot: root,
1747
+ config: cacheProviderConfig
1748
+ }) : null;
1749
+ let fingerprintSources = [];
1750
+ let cacheKey = "";
1751
+ let storeHash = "";
1752
+ let storeKey = "";
1753
+ let storeSources = [];
1754
+ let apkPath = null;
1755
+ async function resolveInitialFingerprint() {
1756
+ const fingerprintTimer = stepTimer(now);
1757
+ try {
1758
+ const computed = await fingerprint(root, { platform: PLATFORM });
1759
+ hash = computed?.hash ?? "";
1760
+ fingerprintSources = computed?.sources ?? [];
1761
+ } catch (err) {
1762
+ phaseFailure = fail(NO_FINGERPRINT, `@expo/fingerprint could not fingerprint ${root}: ${err?.message || err}`, "Fix the @expo/fingerprint error above, then retry.");
1763
+ return false;
1764
+ }
1765
+ if (!hash) {
1766
+ phaseFailure = fail(NO_FINGERPRINT, `@expo/fingerprint returned no hash for ${root}, so the build cache cannot be addressed.`, "Check the project native inputs and the @expo/fingerprint error above, then retry.");
1767
+ return false;
1768
+ }
1769
+ record.fingerprint = hash;
1770
+ cacheKey = buildCacheKey(PLATFORM, hash, buildRunOptions);
1771
+ stats.setCacheKey(cacheKey);
1772
+ record.cacheKey = cacheKey;
1773
+ storeHash = hash;
1774
+ storeKey = cacheKey;
1775
+ storeSources = fingerprintSources;
1776
+ const found = await resolveTieredBuild({
1777
+ local: filesystemBuildCapability({
1778
+ resolve: resolveCached,
1779
+ store: storeCached,
1780
+ sources: fingerprintSources
1781
+ }),
1782
+ loadProvider: loadTieredProvider,
1783
+ target: {
1784
+ projectRoot: root,
1785
+ platform: PLATFORM,
1786
+ key: cacheKey
1787
+ },
1788
+ destinationDir: providerDownloadPath(workspaceDir(root)),
1789
+ ensureDestination: prepareProviderDownloadDir,
1790
+ skipRead: !useBuildCache,
1791
+ warn: cacheWarn
1792
+ });
1793
+ const cached = found?.tier === "local" ? found.path : null;
1794
+ record.cacheHit = cached ? "local" : false;
1795
+ record.cacheSkipped = !useBuildCache;
1796
+ let missDiff = "";
1797
+ let missUntracked = null;
1798
+ if (!cached) {
1799
+ const lastBuild = readState(root)?.lastBuild ?? null;
1800
+ const miss = describeFingerprintMiss({
1801
+ platform: PLATFORM,
1802
+ current: {
1803
+ hash,
1804
+ sources: fingerprintSources
1805
+ },
1806
+ lastBuild
1807
+ });
1808
+ if (miss) {
1809
+ missDiff = fingerprintDiffSuffix(miss.changed);
1810
+ writer.write(fingerprintDiffRecord({
1811
+ changed: miss.changed,
1812
+ previousHash: miss.previousHash,
1813
+ hash
1814
+ }));
1815
+ } else if (useBuildCache) missUntracked = untrackedMissLine(untracked({ projectRoot: root }));
1816
+ }
1817
+ phase("fingerprint", `${shortHash(hash)} ${cached ? "hit" : "miss"}${useBuildCache ? "" : !requestedBuildCache ? " (--no-build-cache)" : " (cache reuse off in config)"} ${fingerprintTimer()}${missDiff}`);
1818
+ if (missUntracked) phase("fingerprint", chalk.dim(missUntracked));
1819
+ if (found?.tier === "provider") {
1820
+ record.cacheHit = "remote";
1821
+ providerName = found.providerName ?? null;
1822
+ phase("cache", `provider hit (${providerName})${found.storedLocally ? " -> stored locally" : ""}`);
1823
+ }
1824
+ apkPath = found?.path ?? null;
1825
+ return true;
1826
+ }
1827
+ const runFromFingerprint = async () => {
1828
+ if (!await resolveInitialFingerprint()) return phaseFailure;
1829
+ let remote = null;
1830
+ let abandonedRemote = false;
1831
+ let uploadPending = null;
1832
+ async function resolveRemoteArtifact() {
1833
+ if (buildAbi || cas || buildProfile || !cachePolicy.remote) return;
1834
+ if (!apkPath) {
1835
+ const loaded = await loadProvider(root, { isExpo });
1836
+ if (loaded?.unavailable) phase("cache", chalk.yellow(`provider not usable: ${loaded.unavailable}`));
1837
+ else if (loaded?.provider) remote = loaded;
1838
+ if (remote?.name === "eas") {
1839
+ const auth = easAuth({
1840
+ projectRoot: root,
1841
+ owner: loaded?.owner || null
1842
+ });
1843
+ const authNote = easAuthNote(auth);
1844
+ if (authNote) phase("cache", chalk.yellow(authNote));
1845
+ if (auth?.code === "logged-out") remote = null;
1846
+ }
1847
+ }
1848
+ if (remote && useBuildCache) {
1849
+ const remoteTimer = stepTimer(now);
1850
+ const hit = await resolveRemoteBuild({
1851
+ logWriter: writer,
1852
+ provider: remote.provider,
1853
+ platform: PLATFORM,
1854
+ projectRoot: root,
1855
+ fingerprintHash: hash,
1856
+ runOptions: remoteRunOptions
1857
+ });
1858
+ if (hit?.appPath) {
1859
+ let stored = null;
1860
+ try {
1861
+ stored = storeCached(PLATFORM, cacheKey, hit.appPath, { sources: fingerprintSources });
1862
+ } catch (err) {
1863
+ phase("cache", chalk.yellow(`remote hit could not be stored locally: ${err?.message || err}`));
1864
+ }
1865
+ apkPath = stored || hit.appPath;
1866
+ record.cacheHit = "remote";
1867
+ phase("cache", `remote hit (${remote.name})${stored ? " -> stored locally" : ""} ${remoteTimer()}`);
1868
+ } else if (hit?.timedOut) {
1869
+ abandonedRemote = true;
1870
+ phase("cache", chalk.yellow(`${remote.name} did not answer within ${formatDuration(RESOLVE_TIMEOUT_MS)}; building instead`));
1871
+ } else if (hit?.failed) {
1872
+ const authNote = remote.name === "eas" && isEasAuthFailureText(hit.failed) ? easAuthNote({
1873
+ code: "logged-out",
1874
+ reason: hit.failed
1875
+ }) : null;
1876
+ phase("cache", chalk.yellow(authNote || `${remote.name} could not be used: ${hit.failed}; building instead`));
1877
+ } else phase("cache", `remote miss (${remote.name}) ${remoteTimer()}`);
1878
+ }
1879
+ }
1880
+ await resolveRemoteArtifact();
1881
+ let waitedForBuild = null;
1882
+ let releasedWait = null;
1883
+ async function waitForSharedBuild() {
1884
+ if (!useBuildCache) return true;
1885
+ const waitStarted = now();
1886
+ let failedHolder;
1887
+ while (!apkPath) {
1888
+ let attempt = null;
1889
+ try {
1890
+ attempt = acquireLock({
1891
+ platform: PLATFORM,
1892
+ key: cacheKey,
1893
+ root,
1894
+ logFile: buildLog
1895
+ });
1896
+ } catch (err) {
1897
+ const refusal = claimFailure(err, "stim android");
1898
+ if (refusal) {
1899
+ phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
1900
+ return false;
1901
+ }
1902
+ phase("build", chalk.yellow(`could not take the build lock: ${err?.message || err}; building anyway`));
1903
+ }
1904
+ if (attempt?.acquired) {
1905
+ buildLock = attempt;
1906
+ const previous = attempt.tookOver ?? failedHolder;
1907
+ if (previous) phase("build", chalk.yellow(takeoverLine(previous)));
1908
+ break;
1909
+ } else if (attempt?.held) {
1910
+ releasedWait = null;
1911
+ const holder = attempt.held;
1912
+ const who = holder.projectRoot || "another workspace";
1913
+ phase("build", `${who} is already building ${shortHash(hash)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""}`);
1914
+ let waited = null;
1915
+ try {
1916
+ const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
1917
+ if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${holder.pid}) holds ${attempt.path}.`), {
1918
+ code: "STIM_BUILD_WAIT_TIMEOUT",
1919
+ lockPath: attempt.path
1920
+ });
1921
+ waited = await waitForBuild({
1922
+ platform: PLATFORM,
1923
+ key: cacheKey,
1924
+ out,
1925
+ ceilingMs
1926
+ });
1927
+ } catch (err) {
1928
+ const refusal = claimFailure(err, "stim android");
1929
+ if (refusal) {
1930
+ phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
1931
+ return false;
1932
+ }
1933
+ const wtErr = err;
1934
+ if (wtErr?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
1935
+ phaseFailure = fail("STIM_BUILD_WAIT_TIMEOUT", wtErr.message, `Check pid ${holder.pid}; if it is not really building, remove ${wtErr.lockPath} and run \`stim android\` again.`, { lastBuildStatus: true });
1936
+ return false;
1937
+ }
1938
+ if (waited?.hit) {
1939
+ apkPath = waited.hit ?? null;
1940
+ record.cacheHit = "local";
1941
+ waitedForBuild = {
1942
+ pid: holder.pid,
1943
+ ms: waited.waitedMs
1944
+ };
1945
+ phase("build", `waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache`);
1946
+ } else if (waited?.lockReleased) {
1947
+ failedHolder = void 0;
1948
+ releasedWait = {
1949
+ facts: {
1950
+ pid: holder.pid,
1951
+ ms: waited.waitedMs
1952
+ },
1953
+ who
1954
+ };
1955
+ phase("build", `${who}'s build lock was released; rechecking this workspace before building`);
1956
+ } else {
1957
+ phase("build", chalk.yellow(`${who}'s build ended without an artifact (${waited?.builderFailed}); retrying the build lock`));
1958
+ failedHolder = holder;
1959
+ }
1960
+ } else break;
1961
+ }
1962
+ return true;
1963
+ }
1964
+ if (!await waitForSharedBuild()) return phaseFailure;
1965
+ let swapDir = null;
1966
+ let swapFellBack = false;
1967
+ const installableCachedApk = async (key, cachedPath) => {
1968
+ if (!release) return cachedPath;
1969
+ phase("swap", `regenerating this workspace's JS for the cached ${variant} APK`);
1970
+ const swap = await swapApk({
1971
+ root,
1972
+ isExpo,
1973
+ cachedApkPath: cachedPath,
1974
+ keystore: resolveKeystore(root, settings),
1975
+ logWriter: writer,
1976
+ storedAssets: storedAssets(PLATFORM, key)
1977
+ });
1978
+ if (swap?.ok && swap.apkPath) {
1979
+ if (swap.note) phase("swap", chalk.yellow(swap.note));
1980
+ swapDir = swap.tmpDir ?? null;
1981
+ phase("swap", `${swap.hermes ? "hermes bytecode" : "plain JS"} repacked (store), zipaligned and re-signed (${formatDuration(swap.durationMs)})`);
1982
+ return swap.apkPath;
1983
+ }
1984
+ if (swap?.assetMismatch) phase("swap", chalk.yellow(`${swap.reason} -- building fresh instead` + (swap.assetDiff ? " (an APK cannot be made to carry an asset AAPT did not package)" : "")));
1985
+ else phase("swap", chalk.yellow(`failed at ${swap?.step || "unknown step"}: ${swap?.reason || "unknown reason"} -- building fresh instead (a cached ${variant} APK carries its builder's JS; it is never installed after a failed swap)`));
1986
+ for (const line of swap?.lastLines ?? []) phase("", chalk.dim(line));
1987
+ swapFellBack = true;
1988
+ return null;
1989
+ };
1990
+ async function prepareCachedArtifact() {
1991
+ if (apkPath && record.cacheHit) {
1992
+ const prepared = await installableCachedApk(cacheKey, apkPath);
1993
+ apkPath = prepared;
1994
+ if (!prepared) {
1995
+ record.cacheHit = false;
1996
+ waitedForBuild = null;
1997
+ }
1998
+ }
1999
+ }
2000
+ async function buildArtifact() {
2001
+ if (!apkPath) try {
2002
+ if (limits.maxBuilds) try {
2003
+ buildSlot = await acquireSlot({
2004
+ max: limits.maxBuilds,
2005
+ root,
2006
+ logFile: buildLog,
2007
+ out
2008
+ });
2009
+ } catch (err) {
2010
+ const refusal = claimFailure(err, "stim android");
2011
+ if (refusal) {
2012
+ phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
2013
+ return false;
2014
+ }
2015
+ phase("build", chalk.yellow(`could not take a build slot: ${err?.message || err}; building anyway`));
2016
+ }
2017
+ if (needsPrebuildFor(root, "android", isExpo)) {
2018
+ const pre = await prebuild(root, PLATFORM, writer, { isExpo });
2019
+ if (pre.failed) {
2020
+ phaseFailure = fail(pre.code, pre.reason, pre.remedy, {
2021
+ lastBuildStatus: true,
2022
+ lines: tail(pre.lastLines),
2023
+ logPath: displayPath(root, buildLog)
2024
+ });
2025
+ return false;
2026
+ }
2027
+ phase("prebuild", `android/ generated (${formatDuration(pre.durationMs)})`);
2028
+ androidPackage = androidPackage || detectAndroidPackage(root);
2029
+ record.bundleId = androidPackage;
2030
+ const after = await refingerprintAfterMutation({
2031
+ projectRoot: root,
2032
+ platform: PLATFORM,
2033
+ previousHash: hash,
2034
+ fingerprint
2035
+ });
2036
+ if (after?.moved) {
2037
+ storeHash = after.hash;
2038
+ storeSources = after.sources;
2039
+ storeKey = buildCacheKey(PLATFORM, after.hash, buildRunOptions);
2040
+ record.fingerprint = storeHash;
2041
+ record.cacheKey = storeKey;
2042
+ phase("fingerprint", chalk.dim(`${shortHash(hash)} -> ${shortHash(storeHash)} (after prebuild)`));
2043
+ const late = useBuildCache ? resolveCached(PLATFORM, storeKey) : null;
2044
+ if (late) {
2045
+ const prepared = await installableCachedApk(storeKey, late);
2046
+ if (prepared) {
2047
+ apkPath = prepared;
2048
+ record.cacheHit = "local";
2049
+ phase("cache", `hit ${shortHash(storeHash)} (post-prebuild key)`);
2050
+ if (releasedWait) {
2051
+ waitedForBuild = releasedWait.facts;
2052
+ phase("build", `waited ${formatDuration(waitedForBuild.ms)} for ${releasedWait.who}'s build -> installed from cache`);
2053
+ }
2054
+ }
2055
+ }
2056
+ }
2057
+ }
2058
+ if (!apkPath) {
2059
+ phase("build", `compiling ${variant || "debug"} with Gradle`);
2060
+ const built = await build({
2061
+ root,
2062
+ logWriter: writer,
2063
+ variant,
2064
+ abi: buildAbi
2065
+ }, {
2066
+ estimateMs: estimates().coldBuildMs,
2067
+ ccache: optimizations.android.compilerCache === "ccache" ? ccacheFor({
2068
+ root,
2069
+ onNote: out
2070
+ }) : null,
2071
+ cas,
2072
+ buildCache: optimizations.android.gradleBuildCache,
2073
+ pch: optimizations.android.pch,
2074
+ compilerCacheDisabled: optimizations.android.compilerCache === "none"
2075
+ });
2076
+ ccacheActivity = built.ccache ?? CCACHE_UNAVAILABLE;
2077
+ phase("cache", `compilation cache ${ccacheActivityLine(ccacheActivity)}`);
2078
+ if (built.failed) {
2079
+ const diagnostics = built.diagnostics || [];
2080
+ for (const diag of diagnostics) writer.write({
2081
+ src: "build",
2082
+ level: "error",
2083
+ event: "gradle_diagnostic",
2084
+ msg: formatDiagnostic(diag)
2085
+ });
2086
+ phase("build", chalk.red(`FAILED after ${formatDuration(built.durationMs)}`));
2087
+ const extracted = diagnostics.map(formatDiagnostic);
2088
+ if ((built.truncated ?? 0) > 0) extracted.push(`... and ${built.truncated} more diagnostic(s) in the log`);
2089
+ phaseFailure = fail(built.code, built.reason, diagnostics.find((d) => d.remedy)?.remedy || built.remedy || null, {
2090
+ lastBuildStatus: true,
2091
+ diagnostics: extracted,
2092
+ lines: extracted.length ? [] : tail(built.lastLines),
2093
+ logPath: displayPath(root, buildLog)
2094
+ });
2095
+ return false;
2096
+ }
2097
+ apkPath = built.apkPath ?? null;
2098
+ stats.setBuildMs(built.durationMs ?? 0);
2099
+ phase("build", `ok (${formatDuration(built.durationMs)})`);
2100
+ if (built.apkNote) phase("build", chalk.yellow(built.apkNote));
2101
+ const beforeBuildHash = storeHash;
2102
+ const afterBuild = await refingerprintAfterMutation({
2103
+ projectRoot: root,
2104
+ platform: PLATFORM,
2105
+ previousHash: beforeBuildHash,
2106
+ fingerprint
2107
+ });
2108
+ if (!afterBuild) {
2109
+ record.fingerprint = null;
2110
+ record.cacheKey = null;
2111
+ phase("fingerprint", chalk.yellow("unavailable after Gradle; the build will be installed but not cached"));
2112
+ } else {
2113
+ if (afterBuild.moved) {
2114
+ storeHash = afterBuild.hash;
2115
+ storeSources = afterBuild.sources;
2116
+ storeKey = buildCacheKey(PLATFORM, afterBuild.hash, buildRunOptions);
2117
+ record.fingerprint = storeHash;
2118
+ record.cacheKey = storeKey;
2119
+ phase("fingerprint", chalk.dim(`${shortHash(beforeBuildHash)} -> ${shortHash(storeHash)} (after Gradle)`));
2120
+ }
2121
+ if (cachePolicy.write) {
2122
+ const assetManifest = release ? captureAssets(root, { variant }) : null;
2123
+ try {
2124
+ const stored = await storeTieredBuild({
2125
+ local: filesystemBuildCapability({
2126
+ resolve: resolveCached,
2127
+ store: storeCached,
2128
+ sources: storeSources,
2129
+ assetManifest
2130
+ }),
2131
+ loadProvider: loadTieredProvider,
2132
+ target: {
2133
+ projectRoot: root,
2134
+ platform: PLATFORM,
2135
+ key: storeKey
2136
+ },
2137
+ sourcePath: apkPath,
2138
+ overwrite: !useBuildCache || swapFellBack,
2139
+ warn: cacheWarn
2140
+ });
2141
+ providerUpload = stored.providerUpload;
2142
+ providerName = stored.providerName ?? providerName;
2143
+ } catch (err) {
2144
+ phase("cache", chalk.yellow(`could not store the build: ${err?.message || err}`));
2145
+ }
2146
+ }
2147
+ if (remote) uploadPending = uploadRemoteBuild({
2148
+ logWriter: writer,
2149
+ provider: remote.provider,
2150
+ platform: PLATFORM,
2151
+ projectRoot: root,
2152
+ fingerprintHash: storeHash,
2153
+ buildPath: apkPath,
2154
+ runOptions: remoteRunOptions
2155
+ });
2156
+ }
2157
+ }
2158
+ } finally {
2159
+ releaseHeldLock();
2160
+ releaseHeldSlot();
2161
+ }
2162
+ return true;
2163
+ }
2164
+ await prepareCachedArtifact();
2165
+ if (!await buildArtifact()) return phaseFailure;
2166
+ record.appPath = apkPath;
2167
+ let leaseHandle = null;
2168
+ let stopLeaseSignals = null;
2169
+ const releaseLease = () => {
2170
+ const held = leaseHandle;
2171
+ const stopSignals = stopLeaseSignals;
2172
+ leaseHandle = null;
2173
+ stopLeaseSignals = null;
2174
+ stopSignals?.();
2175
+ try {
2176
+ held?.release();
2177
+ } catch (err) {
2178
+ out(phaseLine("lease", chalk.dim(`could not release this run's lease: ${err?.message || err}`)));
2179
+ }
2180
+ };
2181
+ if (physical) {
2182
+ const acquired = await acquireLease({
2183
+ root,
2184
+ platform: PLATFORM,
2185
+ id: device.serial,
2186
+ deviceName: device.deviceName ?? null,
2187
+ idLabel: "serial",
2188
+ waitSeconds,
2189
+ noWait,
2190
+ installBoundMs: ADB_INSTALL_TIMEOUT_MS,
2191
+ appId: androidPackage,
2192
+ holderAppId: (holder) => getProject(holder)?.androidPackage ?? null,
2193
+ now,
2194
+ warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
2195
+ });
2196
+ if (acquired.status === "refused") return fail(acquired.refusal.code, acquired.refusal.message, acquired.refusal.remedy, { lease: acquired.refusal.lease });
2197
+ leaseHandle = makeRunLease({
2198
+ root,
2199
+ platform: PLATFORM,
2200
+ kind: acquired.status === "leased" ? acquired.kind : null,
2201
+ expiresAt: acquired.status === "leased" ? acquired.expiresAt : null
2202
+ });
2203
+ if (acquired.status === "leased") {
2204
+ stopLeaseSignals = onLeaseSignal(releaseLease);
2205
+ phase("lease", `${acquired.kind} lease on ${device.serial} until ${leaseExpiryText(acquired.expiresAt, now())}`);
2206
+ }
2207
+ }
2208
+ try {
2209
+ return await finishAndroidRun({
2210
+ lease: leaseHandle,
2211
+ releaseLease,
2212
+ root,
2213
+ json,
2214
+ metroCheck,
2215
+ useBuildCache,
2216
+ variant,
2217
+ release,
2218
+ isExpo,
2219
+ metroPort,
2220
+ logsDir,
2221
+ emuLog,
2222
+ device,
2223
+ physical,
2224
+ remoteDevice,
2225
+ bootPromise,
2226
+ bootDuration: () => bootDuration,
2227
+ apkPath,
2228
+ androidPackage,
2229
+ swapDir,
2230
+ record,
2231
+ waitedForBuild,
2232
+ ccache: ccacheActivity,
2233
+ uploadPending,
2234
+ providerUpload,
2235
+ providerName,
2236
+ remote,
2237
+ abandonedRemote,
2238
+ started,
2239
+ startedAt,
2240
+ writer,
2241
+ phase,
2242
+ fail,
2243
+ readApkPackage,
2244
+ install,
2245
+ launch,
2246
+ launchRelease,
2247
+ resolveDevClientScheme,
2248
+ verifyLaunched,
2249
+ verifyReleaseLaunched,
2250
+ spawn,
2251
+ kill,
2252
+ pidAlive,
2253
+ verifyCollector,
2254
+ writeLaunch,
2255
+ writeState,
2256
+ now,
2257
+ out,
2258
+ emit,
2259
+ recordRun
2260
+ });
2261
+ } finally {
2262
+ releaseLease();
2263
+ }
2264
+ };
2265
+ try {
2266
+ return await runFromFingerprint();
2267
+ } catch (error) {
2268
+ recordRun({
2269
+ failed: true,
2270
+ durationMs: now() - started
2271
+ });
2272
+ throw error;
2273
+ }
2274
+ }
2275
+ function tail(lines, n = FALLBACK_LINES) {
2276
+ return (Array.isArray(lines) ? lines : []).slice(-n);
2277
+ }
2278
+ //#endregion
2279
+ export { NO_DEVICE, NO_FINGERPRINT, NO_METRO, androidDevClientScheme, androidFacts, androidSystemImageSetting, androidVariantSetting, apkDevClientFacts, apkPackage, collectorLogFile, androidCommand as default, displayPath, dumpApkManifest, findAapt, formatDuration, isReleaseVariant, killPreviousCollector, lastBuildRecord, noDeviceDiagnostic, parseXmltree, phaseLine, registerAndroid, resolveSystemImage, resolveVariant, runAndroid, shortHash, startCollector };