stim 1.18.0 → 1.20.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 (178) hide show
  1. package/README.md +21 -4
  2. package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
  3. package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
  4. package/dist/android-0vv-OruW.mjs +6 -0
  5. package/dist/android-Brx6EKvJ.mjs +1258 -0
  6. package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
  7. package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
  8. package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
  9. package/dist/android-cas-compiler.mjs +1 -1
  10. package/dist/api-run.mjs +159 -8
  11. package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
  12. package/dist/api.d.mts +104 -4
  13. package/dist/api.mjs +1 -1
  14. package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
  15. package/dist/artifact-CnC0mfov.mjs +807 -0
  16. package/dist/artifact-DClXJNWx.mjs +772 -0
  17. package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
  18. package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
  19. package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
  20. package/dist/build-BixRI6sB.mjs +462 -0
  21. package/dist/build-plan-CP0tckNU.mjs +553 -0
  22. package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/ccache-CjHQ5F8g.mjs +142 -0
  25. package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
  26. package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
  27. package/dist/cli.mjs +1 -1
  28. package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
  29. package/dist/collector-run.d.mts +10 -0
  30. package/dist/collector-run.mjs +7 -7
  31. package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
  32. package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
  33. package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
  34. package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
  35. package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
  36. package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
  37. package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
  38. package/dist/device-DVCwAFlX.mjs +727 -0
  39. package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
  40. package/dist/device-host-worker.mjs +38 -18
  41. package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
  42. package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
  43. package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
  44. package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
  45. package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
  46. package/dist/doctor-C2MSuMMg.mjs +515 -0
  47. package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
  48. package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
  49. package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
  50. package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
  51. package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
  52. package/dist/errors-DzkMYh8Q.mjs +205 -0
  53. package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
  54. package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
  55. package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
  56. package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
  57. package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
  58. package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
  59. package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
  60. package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
  61. package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
  62. package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
  63. package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
  64. package/dist/ios-BZ9dg2Fr.mjs +9 -0
  65. package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
  66. package/dist/ios-CZuv6gRv.mjs +1112 -0
  67. package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
  68. package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
  69. package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
  70. package/dist/ios-project-B9drt_aY.mjs +197 -0
  71. package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
  72. package/dist/js-swap-C8icNYIY.mjs +432 -0
  73. package/dist/launch-DxH0THLP.mjs +745 -0
  74. package/dist/launch-YrbqaIFj.mjs +1530 -0
  75. package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
  76. package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
  77. package/dist/macos-C0iljhsN.mjs +2 -0
  78. package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
  79. package/dist/macos-run.mjs +1 -1
  80. package/dist/maintenance-run.mjs +13 -10
  81. package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
  82. package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
  83. package/dist/miss-reason-BPAzZS0r.mjs +238 -0
  84. package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
  85. package/dist/native-android-lHzaew7r.mjs +226 -0
  86. package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
  87. package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
  88. package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
  89. package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
  90. package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
  91. package/dist/offload-worker.d.mts +12 -6
  92. package/dist/offload-worker.mjs +409 -73
  93. package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
  94. package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
  95. package/dist/plan-BDk2AeY_.mjs +260 -0
  96. package/dist/plan-placement-D_YYlE6N.mjs +73 -0
  97. package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
  98. package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
  99. package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
  100. package/dist/project-B8_duAzW.mjs +1067 -0
  101. package/dist/project-plan-BPwbLwQu.mjs +10 -0
  102. package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
  103. package/dist/pull-requests.mjs +1 -1
  104. package/dist/react-native-android-chm1DMlA.mjs +921 -0
  105. package/dist/react-native-build-oAw6gQeW.mjs +239 -0
  106. package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
  107. package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
  108. package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
  109. package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
  110. package/dist/remote-BU9UyhCJ.mjs +192 -0
  111. package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
  112. package/dist/result-BDoRk1k3.mjs +186 -0
  113. package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
  114. package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
  115. package/dist/server-command-7XIKlWtO.mjs +69 -0
  116. package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
  117. package/dist/server-expo-DV43vep4.mjs +2 -0
  118. package/dist/settings-Bbztm4oV.mjs +1503 -0
  119. package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
  120. package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
  121. package/dist/settings.schema.json +156 -6
  122. package/dist/simslim-ChMfe8rg.mjs +149 -0
  123. package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
  124. package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
  125. package/dist/start-CbQCSnXQ.mjs +2 -0
  126. package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
  127. package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
  128. package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
  129. package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
  130. package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
  131. package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
  132. package/dist/status-watch-DvmI3SmN.mjs +296 -0
  133. package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
  134. package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
  135. package/dist/stop-WQPQHLuy.mjs +72 -0
  136. package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
  137. package/dist/supervisor-run.d.mts +7 -2
  138. package/dist/supervisor-run.mjs +32 -20
  139. package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
  140. package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
  141. package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
  142. package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
  143. package/dist/toolchain-BIupIro1.mjs +2 -0
  144. package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
  145. package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
  146. package/dist/trigger-SHLOWJ4O.mjs +2 -0
  147. package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
  148. package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
  149. package/dist/web-B3eldHFH.mjs +123 -0
  150. package/dist/web-BLXpOe6b.mjs +2 -0
  151. package/dist/web-run.mjs +10 -10
  152. package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
  153. package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
  154. package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
  155. package/dist/worktree-q7_OZNY0.mjs +2 -0
  156. package/package.json +8 -6
  157. package/shim/bundle-response.cjs +2 -1
  158. package/shim/bundle-response.d.cts +1 -1
  159. package/shim/expo-metro-config.cjs +1 -1
  160. package/shim/native-android.gradle +101 -0
  161. package/dist/android-BKA16DTf.mjs +0 -4
  162. package/dist/android-BvwuMUJ9.mjs +0 -3572
  163. package/dist/guide-status-Bhf9SJXc.mjs +0 -125
  164. package/dist/ios-CPdkoKaa.mjs +0 -3953
  165. package/dist/ios-DNpdygiv.mjs +0 -7
  166. package/dist/macos-Ck7OBMNY.mjs +0 -2
  167. package/dist/metro-store-BAHFcJm6.mjs +0 -106
  168. package/dist/plan-placement-CtxkCO54.mjs +0 -1859
  169. package/dist/project-C3ChRLZy.mjs +0 -212
  170. package/dist/projects-DD7dBTCj.mjs +0 -92
  171. package/dist/server-expo-Db9HBUoa.mjs +0 -2
  172. package/dist/settings-CQyMElEP.mjs +0 -701
  173. package/dist/start-NTc1Ifqs.mjs +0 -2
  174. package/dist/stop-BGhKMOuH.mjs +0 -48
  175. package/dist/trigger-DWAq9hkw.mjs +0 -2
  176. package/dist/web-B6_bFDn7.mjs +0 -2
  177. package/dist/worktree-BJfnU6r5.mjs +0 -2
  178. package/dist/worktree-CsKYexJM.mjs +0 -753
@@ -1,1859 +0,0 @@
1
- import { n as ownership_claim_exports } from "./process-identity-Biv3arCO.mjs";
2
- import { t as getExecutor } from "./exec-DWhCZDGm.mjs";
3
- import { r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
4
- import { i as formatDuration, l as phaseLine, m as shortHash, u as plural } from "./command-output-CBtzyIgF.mjs";
5
- import { D as withConfigLock$1, H as projectDeviceSlots, _ as loadConfig$1, a as clearDevice, b as releaseAndroidConsolePort, i as clearAndroidBootPending, k as acquireAvdClaim, p as getProject, t as allConsolePortsAndSerials, w as setDevice, z as deviceSlotPlatforms } from "./config-CgYMahz7.mjs";
6
- import { D as makeTemporaryDirectory, O as removeTemporaryEntry, c as gitCommonDirOnDisk } from "./worktree-CsKYexJM.mjs";
7
- import { u as ownedDeviceLabel } from "./project-C3ChRLZy.mjs";
8
- import { o as pidExists } from "./metro-Dh8T4Ytd.mjs";
9
- import { d as sharedBuildCache } from "./workspace-state-C59dqG-Z.mjs";
10
- import { B as readStats$1, M as estimateBuild, s as peekBudget } from "./budget-DIhL2BKx.mjs";
11
- import { E as teardownOwnedAvd, k as teardownParkedAvd } from "./workspaces-xXItD8Dj.mjs";
12
- import { C as diffFingerprintSources, I as storedSources, w as entryDir, y as artifactIn } from "./device-lease-Co-EwifR.mjs";
13
- import { A as listAvds, B as ownedAvdSystemImage, E as getAvdNameForSerial, F as ownedAvdDeviceProfile, J as resolveOwnedAvdSerial, L as ownedAvdMatchesConfiguration, O as listAdbDevices, P as nextConsolePort, R as ownedAvdName, W as pickDefaultSystemImage, b as createOwnedAvd, f as assertOwnedAvdStopped, ft as parkedMaxSetting, g as avdPoolConfiguration, j as listInstalledSystemImages, n as OWNED_AVD_CONFIG_DEFAULTS, pt as readParked, st as adoptParked, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, ut as isLegacyDeletionClaim, v as bootAndroidEmulator, y as configureNewOwnedAvd } from "./android-DC3ZLuw0.mjs";
14
- import { M as readHostMemoryPressure, d as listAllIosSims } from "./ios-B3SbOWdw.mjs";
15
- import { a as androidAvdConfigSetting, c as androidDataPartitionSizeGbSetting } from "./settings-CQyMElEP.mjs";
16
- import { b as hostingMachines, g as resolveBuildMachine, m as namedBuildMachine } from "./client-DzhapoMB.mjs";
17
- import { t as clearDeviceIdleShutdown } from "./idle-shutdown-jjtxAn5C.mjs";
18
- import { i as countLiveOwnedDevices, n as DeviceAdmissionRefusal, p as withDeviceBootAdmission, s as peekDeviceSlots } from "./device-capacity-EUOOYr8T.mjs";
19
- import { E as tailLines, T as startBuildHeartbeat, g as HEARTBEAT_INTERVAL_MS } from "./toolchain-Bnyd_K3A.mjs";
20
- import { t as cleanLine } from "./server-expo-DoBHhuzK.mjs";
21
- import { a as call, o as connectHost } from "./hosted-client-C-rMtppF.mjs";
22
- import { r as readHostedNative, s as writeHostedNative } from "./ios-state-B2pES6Vs.mjs";
23
- import { n as prepareHostedNative } from "./hosted-native-DeeOG8xZ.mjs";
24
- import { a as easAuthNote } from "./remote-cache-BGg7aGIZ.mjs";
25
- import { t as reconcileSimSlim } from "./simslim-EHBQ2R4G.mjs";
26
- import { i as ensureOwnedIosDevice, r as ensureIosBooted } from "./device-ios-6MewKKnc.mjs";
27
- import { i as staleNativeDirRefusal } from "./prebuild-juCyreaR.mjs";
28
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
29
- import { basename, dirname, join, relative } from "node:path";
30
- import { LAST_BUILD_KEYS, isJsonObject, loadConfig, machineCapacity, parseHostedChoice, parseHostedNativeOffer, readWorkspaceState } from "@stim-cli/core/state";
31
- import chalk from "chalk";
32
- import { isDeepStrictEqual } from "node:util";
33
- import { claimRemoveCommand, clearClaimChild, markClaimChildPending, processGroupAlive, releaseClaim, setClaimChild } from "@stim-cli/core/ownership-claim";
34
- import { captureProcessIdentity } from "@stim-cli/core/process-identity";
35
- import { randomUUID } from "node:crypto";
36
- import * as expoFingerprint from "@expo/fingerprint";
37
- import { resolveTieredBuild } from "@stim-cli/cache";
38
- import { request } from "node:http";
39
- //#region src/device-host/placement.ts
40
- function local$1(code, reason, skipped = []) {
41
- return {
42
- kind: "local",
43
- code,
44
- reason,
45
- skipped
46
- };
47
- }
48
- function decideDevicePlacement(inputs) {
49
- if ("sticky" in inputs) {
50
- if ("machine" in inputs.sticky) {
51
- const reason = `recorded session on ${inputs.sticky.machine}`;
52
- return {
53
- kind: "host",
54
- code: "sticky",
55
- reason,
56
- machines: [{
57
- machine: inputs.sticky.machine,
58
- reason
59
- }],
60
- skipped: []
61
- };
62
- }
63
- return {
64
- kind: "local",
65
- code: "sticky",
66
- reason: "this workspace's device runs here",
67
- skipped: []
68
- };
69
- }
70
- const { platform, here, offers, buildMachine, noWait } = inputs;
71
- const { count, max, queued } = here.devices;
72
- const cannotTake = !(max === 0 || count !== null && count < max) || queued > 0;
73
- const usage = max > 0 ? `${count} of ${max} devices in use` : `${count} devices in use, no device cap`;
74
- const localReason = count === null ? "cannot tell how many devices run here; local boot admission decides" : cannotTake ? `${usage}${queued ? `, ${queued} runs queued ahead` : ""}` : here.budgetRefusal ?? (here.memoryPressure !== "normal" ? `host memory pressure ${here.memoryPressure ?? "unknown"} here` : `load ${here.loadPerCore.toFixed(1)}/core here, ${usage}`);
75
- if (!offers.length) return local$1("no-remote-mac", "no remote Macs configured; " + localReason);
76
- if (count === null) return local$1("device-count-unknown", localReason);
77
- if (!cannotTake && here.memoryPressure === "normal" && !here.budgetRefusal && here.loadPerCore < here.maxLoadPerCore) return local$1("this-mac-free", localReason);
78
- const skipped = [];
79
- const admitted = [];
80
- offers.forEach((probe, index) => {
81
- let reason = null;
82
- let code = "unreachable";
83
- if ("failure" in probe) reason = probe.failure;
84
- else {
85
- const { offer } = probe;
86
- if (offer.declined !== null) [code, reason] = ["declined", `declined: ${offer.declined}`];
87
- else if (offer.platform !== platform || !offer.choice) [code, reason] = ["no-matching-device", `no matching ${platform} device choice`];
88
- else if (offer.resources.memoryPressure !== "normal") [code, reason] = ["memory", "host memory pressure unknown or elevated"];
89
- else if (offer.capacity.available === 0) [code, reason] = ["no-capacity", "no hosted device capacity available"];
90
- else if (!cannotTake && (offer.resources.loadPerCore === null || offer.resources.loadPerCore >= here.loadPerCore)) [code, reason] = ["load", offer.resources.loadPerCore === null ? "host load unknown while this Mac has room" : "host is not less loaded than this Mac"];
91
- else admitted.push({
92
- machine: probe.machine,
93
- offer,
94
- index
95
- });
96
- }
97
- if (reason) skipped.push({
98
- machine: probe.machine,
99
- code,
100
- reason
101
- });
102
- });
103
- admitted.sort((a, b) => Number(b.machine === buildMachine) - Number(a.machine === buildMachine) || (a.offer.resources.loadPerCore ?? Infinity) - (b.offer.resources.loadPerCore ?? Infinity) || b.offer.resources.memoryFreeBytes - a.offer.resources.memoryFreeBytes || a.index - b.index);
104
- if (!admitted.length) return local$1("no-host-admits", `${localReason}; no host admits${cannotTake && !noWait ? "; waiting locally if needed" : ""}`, skipped);
105
- const machines = admitted.map(({ machine, offer }) => ({
106
- machine,
107
- reason: `${localReason}; ${machine}${machine === buildMachine ? " is the explicit remote Mac," : ""} load ${offer.resources.loadPerCore === null ? "unknown" : offer.resources.loadPerCore.toFixed(1) + "/core"}`
108
- }));
109
- return {
110
- kind: "host",
111
- code: "placed",
112
- machines,
113
- reason: machines[0].reason,
114
- skipped
115
- };
116
- }
117
- //#endregion
118
- //#region src/device-host/auto-placement.ts
119
- async function probeHost(machine, platform, selectors) {
120
- let host;
121
- const deadline = Date.now() + 3e3;
122
- try {
123
- host = await connectHost(machine, 3e3, true);
124
- const offer = await call(host, "device-host.offer", {
125
- platform,
126
- ...selectors
127
- }, Math.max(1, deadline - Date.now()));
128
- const resources = isJsonObject(offer.resources) ? offer.resources : {};
129
- const parsed = parseHostedNativeOffer({
130
- ...offer,
131
- resources: {
132
- ...resources,
133
- loadPerCore: resources.loadPerCore ?? 0
134
- }
135
- });
136
- let choice = platform === "ios" ? offer.platform === "ios" ? parseHostedChoice(offer.choice) : null : parsed?.platform === "android" ? parsed.choice : null;
137
- if (choice && ("runtime" in choice ? selectors.deviceType && selectors.deviceType !== choice.deviceType || selectors.runtime && selectors.runtime.replace(/^iOS /, "") !== choice.runtime.replace(/^iOS /, "") : selectors.systemImage && selectors.systemImage !== choice.systemImage || selectors.deviceProfile && selectors.deviceProfile !== choice.deviceProfile)) choice = null;
138
- if (!isJsonObject(offer.capacity) || offer.declined !== null && typeof offer.declined !== "string") throw new Error("unreadable device offer");
139
- return {
140
- probe: {
141
- machine,
142
- offer: {
143
- platform,
144
- choice,
145
- declined: offer.declined,
146
- capacity: { available: typeof offer.capacity.available === "number" ? offer.capacity.available : null },
147
- resources: {
148
- loadPerCore: typeof resources.loadPerCore === "number" && Number.isFinite(resources.loadPerCore) && resources.loadPerCore >= 0 ? resources.loadPerCore : null,
149
- memoryFreeBytes: typeof resources.memoryFreeBytes === "number" ? resources.memoryFreeBytes : 0,
150
- memoryPressure: typeof resources.memoryPressure === "string" ? resources.memoryPressure : null
151
- }
152
- }
153
- },
154
- ...choice ? { target: {
155
- host,
156
- choice,
157
- session: null
158
- } } : {}
159
- };
160
- } catch (error) {
161
- return { probe: {
162
- machine,
163
- failure: error instanceof Error ? error.message : String(error)
164
- } };
165
- } finally {
166
- host?.connection.close();
167
- }
168
- }
169
- function hosted(target, code, reason, skipped = []) {
170
- target.selection = {
171
- selected: "auto",
172
- reason
173
- };
174
- return {
175
- target,
176
- placement: {
177
- decision: "hosted",
178
- machine: target.host.machine,
179
- reason
180
- },
181
- code,
182
- skipped
183
- };
184
- }
185
- function local(code, reason, skipped = []) {
186
- return {
187
- target: null,
188
- placement: {
189
- decision: "local",
190
- reason
191
- },
192
- code,
193
- skipped
194
- };
195
- }
196
- async function automaticDevicePlacement({ root, slot, platform, selectors, buildMachine, noWait }, { machines = hostingMachines, read = readHostedNative, write = writeHostedNative, resume = prepareHostedNative, peek = () => peekDeviceSlots({
197
- platform,
198
- project: getProject(root),
199
- slot
200
- }), capacity = machineCapacity, memory = readHostMemoryPressure, probe = probeHost, budget = peekBudget } = {}) {
201
- const recorded = read(root, platform, slot)[slot];
202
- if (recorded) {
203
- const target = await resume(recorded.machine, selectors, recorded, platform, true);
204
- if (target) return {
205
- target,
206
- placement: {
207
- decision: "hosted",
208
- machine: recorded.machine,
209
- reason: recorded.reason ?? decideDevicePlacement({ sticky: { machine: recorded.machine } }).reason
210
- },
211
- code: "sticky",
212
- skipped: [],
213
- sticky: true
214
- };
215
- write(root, slot, null, platform);
216
- }
217
- const devices = peek();
218
- if (devices.localLive) return {
219
- ...local("sticky", decideDevicePlacement({ sticky: { local: true } }).reason),
220
- sticky: true
221
- };
222
- const entries = machines();
223
- if (entries === null) throw Object.assign(/* @__PURE__ */ new Error("remote.machines is invalid. Run stim guide settings and correct it."), { code: "STIM_HOSTING_REFUSED" });
224
- const preference = resolveBuildMachine(buildMachine, process.env.STIM_REMOTE_BUILD, loadConfig$1()?.remote?.build);
225
- buildMachine = namedBuildMachine(preference) ? preference : void 0;
226
- const load = capacity();
227
- const here = {
228
- loadPerCore: load.loadPerCore,
229
- maxLoadPerCore: load.maxLoadPerCore,
230
- memoryPressure: memory(),
231
- devices,
232
- budgetRefusal: await budget(root)
233
- };
234
- const early = decideDevicePlacement({
235
- platform,
236
- here,
237
- offers: entries.map((machine) => ({
238
- machine,
239
- failure: "not probed"
240
- })),
241
- buildMachine,
242
- noWait
243
- });
244
- if (early.kind === "local" && early.skipped.length === 0) return local(early.code, early.reason);
245
- const probes = await Promise.all(entries.map((machine) => probe(machine, platform, selectors)));
246
- const decision = decideDevicePlacement({
247
- platform,
248
- here,
249
- offers: probes.map((each) => each.probe),
250
- buildMachine,
251
- noWait
252
- });
253
- if (decision.kind === "local") return local(decision.code, decision.reason, decision.skipped);
254
- const target = probes.find((each) => each.probe.machine === decision.machines[0].machine).target;
255
- return hosted(target, decision.code, decision.reason, decision.skipped);
256
- }
257
- function devicePlacementLine(placement, skipped) {
258
- return `${placement.machine ?? "local"}, ${placement.reason}${placement.decision !== "hosted" && skipped.length ? "; " + skipped.map((each) => `${each.machine}: ${each.reason}`).join("; ") : ""}`;
259
- }
260
- //#endregion
261
- //#region src/engine/metro-warmup.ts
262
- const WARMUP_TIMEOUT_MS = 6e4;
263
- async function requestLocal(url, platform, prefetch) {
264
- return new Promise((resolve) => {
265
- const req = request(url, { headers: {
266
- accept: "application/json",
267
- "expo-platform": platform,
268
- ...prefetch ? { "x-stim-metro-warmup": "1" } : {}
269
- } });
270
- const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
271
- timer.unref();
272
- req.on("socket", (socket) => socket.unref());
273
- req.on("error", () => resolve(null));
274
- req.on("close", () => {
275
- clearTimeout(timer);
276
- resolve(null);
277
- });
278
- req.on("response", (res) => {
279
- let body = "";
280
- res.setEncoding("utf8");
281
- res.on("data", (chunk) => {
282
- if (!prefetch) body += chunk;
283
- });
284
- res.on("error", () => resolve(null));
285
- res.on("end", () => resolve(res.statusCode === 200 ? body : null));
286
- });
287
- req.end();
288
- });
289
- }
290
- async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
291
- try {
292
- const origin = `http://127.0.0.1:${port}`;
293
- if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
294
- const url = new URL("/index.bundle", origin);
295
- url.search = new URLSearchParams({
296
- platform,
297
- dev: "true",
298
- lazy: "true",
299
- minify: "false",
300
- ...platform === "ios" ? { inlineSourceMap: "false" } : {},
301
- modulesOnly: "false",
302
- runModule: "true",
303
- excludeSource: "true",
304
- sourcePaths: "url-server",
305
- ...appId ? { app: appId } : {}
306
- }).toString();
307
- if (!bundleUrl && isExpo) {
308
- const body = await requestLocal(new URL(origin), platform, false);
309
- if (body === null) return;
310
- const manifest = JSON.parse(body);
311
- bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
312
- if (typeof bundleUrl !== "string") return;
313
- }
314
- if (bundleUrl) {
315
- const bundle = new URL(bundleUrl, origin);
316
- url.pathname = bundle.pathname;
317
- url.search = bundle.search;
318
- }
319
- await requestLocal(url, platform, true);
320
- } catch {}
321
- }
322
- //#endregion
323
- //#region src/engine/android-avd-setup.ts
324
- var AvdBootError = class extends Error {
325
- remedy;
326
- constructor(message, remedy, cause) {
327
- super(message, { cause });
328
- this.remedy = remedy;
329
- }
330
- };
331
- var AvdRecoveryError = class extends AvdBootError {};
332
- function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
333
- for (const [path, project] of Object.entries(loadConfig$1()?.projects ?? {})) if (projectDeviceSlots(project).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
334
- return null;
335
- }
336
- async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, deviceProfile, configuration, configure, teardown = teardownOwnedAvd }) {
337
- const { claim, reservation, previous, avdName } = withConfigLock$1(() => {
338
- const previous = deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.android;
339
- if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
340
- if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
341
- const avdName = ownedAvdName(label);
342
- const claim = acquireAvdClaim(avdName);
343
- const reservation = {
344
- avdName,
345
- owned: true,
346
- deviceName: avdName,
347
- setupIncomplete: true,
348
- poolConfiguration: configuration
349
- };
350
- try {
351
- setDevice(projectPath, "android", reservation, slot);
352
- return {
353
- claim,
354
- reservation,
355
- previous,
356
- avdName
357
- };
358
- } catch (error) {
359
- releaseClaim(claim);
360
- throw error;
361
- }
362
- });
363
- const currentRecord = () => deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.android;
364
- const assertReservation = () => {
365
- if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
366
- const owner = findOtherOwner(avdName, projectPath, slot);
367
- if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
368
- const current = currentRecord();
369
- if (!isDeepStrictEqual(current, reservation)) {
370
- const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
371
- throw new Error(`AVD ${current?.avdName ?? avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`);
372
- }
373
- };
374
- let prepared;
375
- let child;
376
- const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
377
- let configurationFailure;
378
- try {
379
- try {
380
- const creation = await createOwnedAvd(label, {
381
- systemImage,
382
- deviceProfile,
383
- spawn: (...args) => {
384
- markClaimChildPending(claim);
385
- child = getExecutor().spawn(...args);
386
- if (child.pid !== void 0) {
387
- const captured = captureProcessIdentity(child.pid);
388
- if (captured.ok) try {
389
- setClaimChild(claim, {
390
- pid: child.pid,
391
- processToken: captured.token
392
- });
393
- } catch {}
394
- }
395
- return child;
396
- }
397
- }).then((result) => ({ result }), (error) => ({ error }));
398
- if (creationRunning()) throw new AvdRecoveryError(`AVD ${avdName} creation left its process group running (pid ${child.pid}); its reservation remains incomplete.`, `Wait for process group ${child.pid} to finish before retrying stim android. If its identity could not be recorded, inspect the group and remove the claim only after it stops: ${claimRemoveCommand(claim.path)}`);
399
- clearClaimChild(claim);
400
- if ("error" in creation) throw creation.error;
401
- prepared = {
402
- ...creation.result,
403
- created: true
404
- };
405
- } catch (error) {
406
- const message = String(error?.message || error);
407
- if (!message.includes("already exists")) throw error;
408
- try {
409
- if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", error);
410
- withConfigLock$1(assertReservation);
411
- const resolved = resolveOwnedAvdSerial(avdName);
412
- if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
413
- if (!resolved.serial) assertOwnedAvdStopped(avdName);
414
- prepared = {
415
- avdName,
416
- created: false,
417
- systemImage: ownedAvdSystemImage(avdName),
418
- deviceProfile: ownedAvdDeviceProfile(avdName),
419
- ...resolved.serial ? {
420
- serial: resolved.serial,
421
- consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
422
- } : {}
423
- };
424
- } catch (recoveryError) {
425
- withConfigLock$1(() => {
426
- if (!isDeepStrictEqual(currentRecord(), reservation)) return;
427
- if (previous) setDevice(projectPath, "android", previous, slot);
428
- else clearDevice(projectPath, "android", slot, avdName);
429
- });
430
- if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
431
- throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(recoveryError?.message || recoveryError)}`, "Inspect `npx stim status` and `adb devices`. Wait for any other Stim run using this AVD to finish, then retry `stim android`. Keep the AVD and its process locks while its state is unverified.", recoveryError);
432
- }
433
- }
434
- if (prepared.created) try {
435
- configure(avdName);
436
- } catch (error) {
437
- configurationFailure = { error };
438
- }
439
- if (!configurationFailure) withConfigLock$1(() => {
440
- assertReservation();
441
- setDevice(projectPath, "android", {
442
- avdName,
443
- owned: true,
444
- deviceName: avdName,
445
- ...prepared.created ? {
446
- poolConfiguration: configuration,
447
- bootPending: true
448
- } : {},
449
- ...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
450
- }, slot);
451
- });
452
- } finally {
453
- if (!creationRunning()) releaseClaim(claim);
454
- else {
455
- child?.stdout?.destroy();
456
- child?.stderr?.destroy();
457
- child?.unref();
458
- }
459
- }
460
- if (configurationFailure) {
461
- const { error } = configurationFailure;
462
- const cleanup = teardown(avdName, {
463
- del: true,
464
- owner: {
465
- projectPath,
466
- slot,
467
- expectedRecord: reservation
468
- },
469
- onRemoved: () => {
470
- clearDevice(projectPath, "android", slot, avdName);
471
- }
472
- });
473
- const orphan = cleanup.status === "failed" || cleanup.status === "skipped" ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
474
- throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
475
- }
476
- return prepared;
477
- }
478
- //#endregion
479
- //#region src/engine/device-android.ts
480
- const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
481
- const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
482
- async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
483
- const projectAvdConfig = androidAvdConfigSetting(settings, settingsRoot);
484
- const avdConfig = {
485
- ...OWNED_AVD_CONFIG_DEFAULTS,
486
- ...projectAvdConfig
487
- };
488
- const requestedProfile = flags.deviceProfile || settings.android?.deviceProfile || null;
489
- let replacedProfile = null;
490
- if (record?.setupIncomplete && record.avdName) {
491
- const avdName = record.avdName;
492
- const cleanup = teardownAvd(avdName, {
493
- del: true,
494
- owner: {
495
- projectPath,
496
- slot,
497
- expectedRecord: record
498
- },
499
- onRemoved: () => {
500
- clearDevice(projectPath, "android", slot, avdName);
501
- }
502
- });
503
- if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`, "Wait for any active AVD operation to finish. If the refusal names an unresolved claim, inspect its creator and native processes before removing only that claim. Retry `stim android` to reconcile the incomplete device.");
504
- record = null;
505
- }
506
- const requestedImage = flags.systemImageFlag || null;
507
- const currentImage = requestedImage && record?.owned && record.avdName ? ownedAvdSystemImage(record.avdName) : null;
508
- if (record?.avdName && requestedImage && currentImage && currentImage !== requestedImage) {
509
- const avdName = record.avdName;
510
- if (!record.bootPending) throw new AvdRecoveryError(`this project's emulator ${avdName} uses system image ${currentImage}, but ${requestedImage} was requested. --system-image replaces only an emulator that never finished a boot, and this one has.`, "Run `stim worktree remove` (or `stim gc --delete`) to reap the current emulator, then `stim android` again to create the requested one, or pass `--slot <name>` to create it beside the current one.");
511
- replacedProfile = ownedAvdDeviceProfile(avdName);
512
- const cleanup = teardownAvd(avdName, {
513
- del: true,
514
- owner: {
515
- projectPath,
516
- slot,
517
- expectedRecord: record
518
- },
519
- onRemoved: () => {
520
- clearDevice(projectPath, "android", slot, avdName);
521
- }
522
- });
523
- if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${avdName} never finished a boot, but could not be deleted to create one on ${requestedImage} (${cleanup.reason || cleanup.status}). Stim kept the device record.`, "Fix the cause, then retry, or pass `--slot <name>` to create the requested emulator beside it.");
524
- out(phaseLine("device", `deleted ${avdName}, which never finished a boot, to create one on ${requestedImage}`));
525
- record = null;
526
- }
527
- const deviceProfile = requestedProfile ?? replacedProfile ?? DEFAULT_AVD_DEVICE_PROFILE;
528
- const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig, deviceProfile);
529
- const configurationBeforeDefaults = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), projectAvdConfig, deviceProfile);
530
- if (record?.avdName) {
531
- if (record.owned) {
532
- const resolved = resolveOwnedAvdSerial(record.avdName);
533
- const currentProfile = resolved.notOwned || resolved.missing ? null : ownedAvdDeviceProfile(record.avdName);
534
- if (requestedProfile && currentProfile && currentProfile !== requestedProfile) throw new AvdRecoveryError(`this project's emulator uses device profile ${currentProfile}, but ${requestedProfile} was requested. Stim will not silently boot a different model.`, "Run `stim worktree remove` (or `stim gc --delete`) to reap the current emulator, then `stim android` again to create the requested one, or pass `--slot <name>` to create it beside the current one.");
535
- if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned -- creating a fresh owned AVD instead of reusing it.`));
536
- else if (resolved.serial) {
537
- const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
538
- if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
539
- const updated = {
540
- ...record,
541
- avdName: record.avdName,
542
- consolePort,
543
- owned: true,
544
- deviceName: record.deviceName ?? record.avdName
545
- };
546
- setDevice(projectPath, "android", updated, slot);
547
- return {
548
- ...updated,
549
- systemImage: ownedAvdSystemImage(record.avdName),
550
- deviceProfile: currentProfile
551
- };
552
- } else if (!resolved.missing) {
553
- out(chalk.dim(`Recorded port for owned AVD ${record.avdName} is not currently ours; booting it on a freshly allocated port...`));
554
- return {
555
- ...await bootOwnedAvdOnFreshPort({
556
- deviceSlotWait: flags.deviceSlotWait,
557
- avdName: record.avdName,
558
- metadata: record,
559
- projectPath,
560
- slot,
561
- deviceName: record.deviceName,
562
- out,
563
- logFile,
564
- alive
565
- }),
566
- systemImage: ownedAvdSystemImage(record.avdName),
567
- deviceProfile: currentProfile
568
- };
569
- }
570
- } else if (listAvds().includes(record.avdName)) {
571
- if (!listAdbDevices().emulators.some((e) => e.consolePort === record.consolePort)) {
572
- note(chalk.yellow(`Note: assigned AVD ${record.avdName} (emulator-${record.consolePort}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
573
- note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned AVD."));
574
- }
575
- return record;
576
- }
577
- } else if (record?.serial) {
578
- note(chalk.yellow(`Note: this project has a stored assignment to physical device ${record.serial}, which Stim no longer keeps.`));
579
- note(chalk.dim("Creating an owned emulator instead. Pass `--device` to build for a connected device."));
580
- }
581
- if (parkedMaxSetting("android").max > 0) {
582
- const systemImage = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: flags.systemImage || settings.android?.systemImage })?.pkg;
583
- const candidates = readParked("android").filter((entry) => entry.systemImage === systemImage && (entry.configuration === configuration || entry.configuration === configurationBeforeDefaults)).toSorted((a, b) => a.parkedAt.localeCompare(b.parkedAt));
584
- for (const parked of candidates) {
585
- if (isLegacyDeletionClaim(parked.deletionClaim)) continue;
586
- const resolved = resolveOwnedAvdSerial(parked.name);
587
- if (resolved.missing) {
588
- const result = teardownParkedAvd(parked.name);
589
- if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
590
- continue;
591
- }
592
- if (resolved.notOwned || resolved.serial) continue;
593
- try {
594
- assertOwnedAvdStopped(parked.name);
595
- } catch (error) {
596
- out(phaseLine("device", `kept parked ${parked.name}: ${String(error?.message || error)}`));
597
- continue;
598
- }
599
- if (ownedAvdSystemImage(parked.name) !== systemImage || !ownedAvdMatchesConfiguration(parked.name, parked.configuration)) {
600
- const result = teardownParkedAvd(parked.name);
601
- if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
602
- continue;
603
- }
604
- const adopted = {
605
- avdName: parked.name,
606
- deviceName: parked.name,
607
- owned: true,
608
- poolConfiguration: parked.configuration,
609
- adoptionPending: true
610
- };
611
- if (!adoptParked({
612
- platform: "android",
613
- projectPath,
614
- slot,
615
- udid: parked.udid,
616
- device: adopted
617
- })) continue;
618
- if (parked.configuration !== configuration) try {
619
- configureAvd(parked.name, {
620
- dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
621
- avdConfig
622
- });
623
- adopted.poolConfiguration = configuration;
624
- } catch (error) {
625
- out(phaseLine("device", `${parked.name} keeps its old AVD settings: ${String(error?.message || error)}`));
626
- }
627
- return {
628
- ...await bootOwnedAvdOnFreshPort({
629
- deviceSlotWait: flags.deviceSlotWait,
630
- avdName: parked.name,
631
- projectPath,
632
- slot,
633
- metadata: adopted,
634
- out,
635
- logFile,
636
- alive
637
- }),
638
- adopted: true,
639
- systemImage,
640
- deviceProfile
641
- };
642
- }
643
- }
644
- const created = await prepareOwnedAvd({
645
- projectPath,
646
- slot,
647
- label,
648
- previousAvdName: record?.avdName,
649
- systemImage: flags.systemImage || settings.android?.systemImage || void 0,
650
- deviceProfile,
651
- configuration,
652
- configure: (avdName) => configureAvd(avdName, {
653
- dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
654
- avdConfig
655
- }),
656
- teardown: teardownAvd
657
- });
658
- if (!created.created) {
659
- out(chalk.dim(phaseLine("device", `recovered ${created.avdName} (unrecorded from a prior run)`)));
660
- if (created.serial) return {
661
- ...created,
662
- owned: true,
663
- deviceName: created.avdName
664
- };
665
- }
666
- return {
667
- ...await bootOwnedAvdOnFreshPort({
668
- deviceSlotWait: flags.deviceSlotWait,
669
- avdName: created.avdName,
670
- metadata: created.created ? {
671
- poolConfiguration: configuration,
672
- bootPending: true
673
- } : void 0,
674
- projectPath,
675
- slot,
676
- deviceName: created.avdName,
677
- out,
678
- logFile,
679
- alive
680
- }),
681
- created: created.created,
682
- systemImage: created.systemImage,
683
- deviceProfile: created.deviceProfile
684
- };
685
- }
686
- function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock$1, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
687
- return lock(() => {
688
- const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
689
- const claim = {
690
- ...metadata?.poolConfiguration ? { poolConfiguration: metadata.poolConfiguration } : {},
691
- ...metadata?.adoptionPending ? { adoptionPending: true } : {},
692
- ...metadata?.bootPending ? { bootPending: true } : {},
693
- avdName,
694
- consolePort,
695
- owned: true,
696
- deviceName: deviceName ?? avdName
697
- };
698
- record(projectPath, "android", claim, slot);
699
- return claim;
700
- });
701
- }
702
- function liveAndroidConsolePorts() {
703
- const adbLive = listAdbDevices();
704
- return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
705
- }
706
- async function bootOwnedAvdOnFreshPort({ avdName, deviceSlotWait, metadata, projectPath, slot, deviceName, out, logFile = null, alive = pidExists }) {
707
- return withDeviceBootAdmission({
708
- platform: "android",
709
- key: avdName
710
- }, async () => {
711
- const claim = claimAndroidConsolePort({
712
- projectPath,
713
- slot,
714
- avdName,
715
- deviceName,
716
- livePorts: liveAndroidConsolePorts(),
717
- metadata
718
- });
719
- const serial = `emulator-${claim.consolePort}`;
720
- try {
721
- reportAndroidMemoryPressure(out);
722
- const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
723
- out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
724
- const result = await waitForAndroidBoot({
725
- serial,
726
- timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
727
- pid,
728
- alive,
729
- out
730
- });
731
- if (result.failed) throw new AvdBootError(result.reason, result.remedy);
732
- const running = getAvdNameForSerial(serial);
733
- if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
734
- clearAndroidBootPending(projectPath, avdName, slot);
735
- const { bootPending: _booted, ...booted } = claim;
736
- return {
737
- ...booted,
738
- serial,
739
- setup: true
740
- };
741
- } catch (error) {
742
- releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
743
- throw error;
744
- }
745
- }, {
746
- ...deviceSlotWait,
747
- root: projectPath,
748
- out
749
- });
750
- }
751
- function reportAndroidMemoryPressure(out) {
752
- const pressure = readHostMemoryPressure();
753
- if (pressure === "warning" || pressure === "critical") out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow. Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
754
- }
755
- function androidBootRemedy(pressure) {
756
- let count = null;
757
- try {
758
- const live = countLiveOwnedDevices({
759
- sims: () => process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
760
- adb: () => ({
761
- ...listAdbDevices({ timeoutMs: 2e3 }),
762
- unhealthy: []
763
- }),
764
- booting: []
765
- });
766
- count = typeof live === "number" ? live : null;
767
- } catch {}
768
- const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
769
- const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
770
- if (observation || devices) return `${observation}${devices}Stop an unneeded device with \`stim stop\` only in a workspace you own, then run \`stim android\` again; ask before closing other apps or devices. These observations do not establish the cause of the timeout or an OOM crash.`;
771
- return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
772
- }
773
- async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
774
- const aborted = () => pid !== null && !alive(pid);
775
- const wait = (windowMs) => waitForBoot(serial, windowMs, {
776
- aborted,
777
- commandTimeoutMs: 5e3
778
- });
779
- let result = await wait(timeoutMs);
780
- if (result.ok) return {
781
- ok: true,
782
- serial
783
- };
784
- let pressure = readHostMemoryPressure();
785
- let elapsedMs = timeoutMs;
786
- if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
787
- const extensionMs = 24e4;
788
- out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; retrying the boot wait once for the same live emulator ${serial} (up to ${extensionMs / 1e3}s more).`)));
789
- result = await wait(extensionMs);
790
- if (result.ok) return {
791
- ok: true,
792
- serial
793
- };
794
- elapsedMs += extensionMs;
795
- pressure = readHostMemoryPressure();
796
- }
797
- const exited = result.exited || aborted();
798
- return {
799
- failed: true,
800
- reason: `${exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(elapsedMs / 1e3)}s.`} Diagnostic: ${JSON.stringify(result.diagnostic)}`,
801
- remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
802
- };
803
- }
804
- async function ensureAndroidBooted({ device, deviceSlotWait, projectPath, slot, timeoutMs, out, logFile = null, alive = pidExists }) {
805
- if (!device?.avdName || !projectPath) return {
806
- failed: true,
807
- reason: "No owned Android emulator is recorded for this project."
808
- };
809
- let resolved;
810
- try {
811
- resolved = resolveOwnedAvdSerial(device.avdName);
812
- } catch (e) {
813
- return {
814
- failed: true,
815
- reason: `Could not list AVDs: ${e?.message || e}`
816
- };
817
- }
818
- if (resolved.missing) return {
819
- failed: true,
820
- reason: `AVD ${device.avdName} no longer exists. Run \`stim android\` again to create a fresh owned AVD.`
821
- };
822
- if (resolved.notOwned) return {
823
- failed: true,
824
- reason: `AVD ${device.avdName} is not Stim-owned; refusing to boot it.`
825
- };
826
- const avdName = device.avdName;
827
- const booted = (result) => {
828
- if (!result.failed) clearAndroidBootPending(projectPath, avdName, slot);
829
- return result;
830
- };
831
- if (resolved.serial) return booted(await waitForAndroidBoot({
832
- serial: resolved.serial,
833
- timeoutMs,
834
- out
835
- }));
836
- const freshSerial = `emulator-${device.consolePort}`;
837
- if (device.owned && device.serial === freshSerial) return booted(await waitForAndroidBoot({
838
- serial: freshSerial,
839
- timeoutMs,
840
- out
841
- }));
842
- const boot = async () => {
843
- const claim = claimAndroidConsolePort({
844
- projectPath,
845
- slot,
846
- avdName,
847
- deviceName: device.deviceName,
848
- livePorts: liveAndroidConsolePorts(),
849
- metadata: device
850
- });
851
- const serial = `emulator-${claim.consolePort}`;
852
- reportAndroidMemoryPressure(out);
853
- out(chalk.dim(phaseLine("device", `booting ${avdName} as ${serial}`)));
854
- let pid = null;
855
- try {
856
- pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
857
- } catch (e) {
858
- releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
859
- return {
860
- failed: true,
861
- reason: `Could not start emulator for AVD ${avdName}: ${e?.message || e}`
862
- };
863
- }
864
- const result = await waitForAndroidBoot({
865
- serial,
866
- timeoutMs,
867
- pid,
868
- alive,
869
- out
870
- });
871
- if (result.failed) releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
872
- return booted(result);
873
- };
874
- try {
875
- return await withDeviceBootAdmission({
876
- platform: "android",
877
- key: avdName
878
- }, boot, {
879
- ...deviceSlotWait,
880
- root: projectPath,
881
- out
882
- });
883
- } catch (error) {
884
- const refusal = error instanceof DeviceAdmissionRefusal ? error : (0, ownership_claim_exports.claimFailure)(error, "stim android");
885
- if (!refusal) throw error;
886
- return {
887
- failed: true,
888
- code: refusal.code,
889
- reason: refusal.message,
890
- remedy: refusal.remedy
891
- };
892
- }
893
- }
894
- //#endregion
895
- //#region src/engine/device.ts
896
- /**
897
- * Whether `ensureOwnedDevice` created, adopted or started booting the device, rather than finding it booted.
898
- * Undefined for a remote device, which those functions do not describe.
899
- */
900
- function didSetUpDevice(device, remote) {
901
- return remote ? void 0 : Boolean(device.setup || device.booting);
902
- }
903
- async function ensureOwnedDevice({ platform, project, projectPath, slot, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim, simSnapshot }) {
904
- clearDeviceIdleShutdown(projectPath, platform, slot);
905
- if (slot && slot !== "default") label = `${label}-${slot}`;
906
- const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
907
- if (platform === "ios") return ensureOwnedIosDevice({
908
- record,
909
- projectPath,
910
- slot,
911
- settingsRoot,
912
- label,
913
- settings,
914
- flags,
915
- note,
916
- out,
917
- reconcileIosSimulator,
918
- simSnapshot
919
- });
920
- return ensureOwnedAndroidDevice({
921
- record,
922
- projectPath,
923
- slot,
924
- settingsRoot,
925
- label,
926
- settings,
927
- flags,
928
- note,
929
- out,
930
- logFile,
931
- alive,
932
- configureAvd,
933
- teardownAvd
934
- });
935
- }
936
- const BOOT_POLL_MS = 500;
937
- async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists, projectPath, slot, deviceSlotWait }) {
938
- if (platform === "ios") return ensureIosBooted({
939
- device,
940
- projectPath,
941
- deviceSlotWait,
942
- simulatorApp,
943
- timeoutMs: timeoutMs ?? 6e5,
944
- pollMs,
945
- out
946
- });
947
- if (platform === "android") return ensureAndroidBooted({
948
- device,
949
- deviceSlotWait,
950
- projectPath,
951
- slot,
952
- timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
953
- out,
954
- logFile,
955
- alive
956
- });
957
- return {
958
- failed: true,
959
- reason: `Unknown platform "${platform}".`
960
- };
961
- }
962
- //#endregion
963
- //#region src/engine/js-swap.ts
964
- const JS_BUNDLE_NAME = "main.jsbundle";
965
- const LAST_LINES = 5;
966
- function hermesEnabledFromProperties(text) {
967
- if (typeof text !== "string") return true;
968
- let data;
969
- try {
970
- data = JSON.parse(text);
971
- } catch {
972
- return true;
973
- }
974
- if (!data || typeof data !== "object") return true;
975
- const raw = data.hermesEnabled;
976
- return raw !== "false" && raw !== false;
977
- }
978
- function readHermesEnabled(root) {
979
- try {
980
- return hermesEnabledFromProperties(readFileSync(join(root, "ios", "Podfile.properties.json"), "utf-8"));
981
- } catch {
982
- return true;
983
- }
984
- }
985
- const ENTRY_CANDIDATES = [
986
- "index.js",
987
- "index.ts",
988
- "index.tsx",
989
- "index.jsx"
990
- ];
991
- function pickEntryFile(entries) {
992
- const names = new Set((Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string"));
993
- return ENTRY_CANDIDATES.find((c) => names.has(c)) ?? "index.js";
994
- }
995
- function detectEntryFile(root) {
996
- let entries;
997
- try {
998
- entries = readdirSync(root);
999
- } catch {
1000
- return "index.js";
1001
- }
1002
- return pickEntryFile(entries);
1003
- }
1004
- function bundleCommand({ isExpo, entryFile, bundleOutput, assetsDest, assetCatalogDest = null }) {
1005
- const catalogArgs = assetCatalogDest ? ["--asset-catalog-dest", assetCatalogDest] : [];
1006
- if (isExpo) return {
1007
- file: "npx",
1008
- args: [
1009
- "expo",
1010
- "export:embed",
1011
- "--platform",
1012
- "ios",
1013
- "--dev",
1014
- "false",
1015
- "--bundle-output",
1016
- bundleOutput,
1017
- "--assets-dest",
1018
- assetsDest,
1019
- ...catalogArgs
1020
- ]
1021
- };
1022
- return {
1023
- file: "npx",
1024
- args: [
1025
- "react-native",
1026
- "bundle",
1027
- "--platform",
1028
- "ios",
1029
- "--dev",
1030
- "false",
1031
- "--entry-file",
1032
- entryFile,
1033
- "--bundle-output",
1034
- bundleOutput,
1035
- "--assets-dest",
1036
- assetsDest,
1037
- ...catalogArgs
1038
- ]
1039
- };
1040
- }
1041
- const ASSET_CATALOG_BUNDLE = "RNAssets.bundle";
1042
- /**
1043
- * Where React Native 0.88+ keeps the app's packager images. With the `RCTUseAssetCatalog` Info.plist
1044
- * key, `react-native-xcode.sh` compiles them with actool into RNAssets.bundle and the native image
1045
- * loader reads only that catalog, so a swap has to rebuild it. Null means loose files under assets/.
1046
- */
1047
- function assetCatalogTarget(infoPlist) {
1048
- if (!infoPlist || typeof infoPlist !== "object") return null;
1049
- const plist = infoPlist;
1050
- const flag = plist.RCTUseAssetCatalog;
1051
- if (!(flag === true || flag === 1 || typeof flag === "string" && [
1052
- "true",
1053
- "yes",
1054
- "1"
1055
- ].includes(flag.toLowerCase()))) return null;
1056
- const families = Array.isArray(plist.UIDeviceFamily) ? plist.UIDeviceFamily : [1];
1057
- const targetDevices = [...families.includes(1) ? ["iphone"] : [], ...families.includes(2) ? ["ipad"] : []];
1058
- return {
1059
- platform: typeof plist.DTPlatformName === "string" ? plist.DTPlatformName : "iphoneos",
1060
- minimumDeploymentTarget: typeof plist.MinimumOSVersion === "string" ? plist.MinimumOSVersion : "15.1",
1061
- targetDevices: targetDevices.length ? targetDevices : ["iphone"]
1062
- };
1063
- }
1064
- function readPlistKey(e, plist, key, format) {
1065
- try {
1066
- return e.runFile("plutil", [
1067
- "-extract",
1068
- key,
1069
- format,
1070
- "-o",
1071
- "-",
1072
- plist
1073
- ]);
1074
- } catch (err) {
1075
- if (/No value at that key path/.test(describe(err))) return null;
1076
- throw err;
1077
- }
1078
- }
1079
- function readAssetCatalogTarget(e, plist) {
1080
- const flag = readPlistKey(e, plist, "RCTUseAssetCatalog", "raw");
1081
- if (flag === null) return null;
1082
- const families = readPlistKey(e, plist, "UIDeviceFamily", "json");
1083
- return assetCatalogTarget({
1084
- RCTUseAssetCatalog: flag,
1085
- DTPlatformName: readPlistKey(e, plist, "DTPlatformName", "raw"),
1086
- MinimumOSVersion: readPlistKey(e, plist, "MinimumOSVersion", "raw"),
1087
- UIDeviceFamily: families === null ? void 0 : JSON.parse(families)
1088
- });
1089
- }
1090
- function actoolArgs({ catalog, out, target }) {
1091
- return [
1092
- "actool",
1093
- catalog,
1094
- "--compile",
1095
- out,
1096
- "--output-format",
1097
- "human-readable-text",
1098
- "--errors",
1099
- "--warnings",
1100
- "--notices",
1101
- "--platform",
1102
- target.platform,
1103
- "--minimum-deployment-target",
1104
- target.minimumDeploymentTarget,
1105
- ...target.targetDevices.flatMap((device) => ["--target-device", device])
1106
- ];
1107
- }
1108
- const ASSET_CATALOG_BUNDLE_INFO_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
1109
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1110
- <plist version="1.0">
1111
- <dict>
1112
- <key>CFBundleIdentifier</key>
1113
- <string>org.reactjs.RNAssets</string>
1114
- <key>CFBundleInfoDictionaryVersion</key>
1115
- <string>6.0</string>
1116
- <key>CFBundleName</key>
1117
- <string>RNAssets</string>
1118
- <key>CFBundlePackageType</key>
1119
- <string>BNDL</string>
1120
- </dict>
1121
- </plist>
1122
- `;
1123
- const UPDATES_MANIFEST_NAME = "app.manifest";
1124
- /**
1125
- * expo-updates launches the update with the newest `commitTime` among those built for the binary's
1126
- * runtime version, and the embedded one is only a row in that race. A swapped copy keeps the id and
1127
- * `commitTime` the cached build wrote, so an update downloaded after that build would launch instead
1128
- * of the injected bundle. A full build writes a fresh id and `commitTime`; this does the same.
1129
- */
1130
- function refreshUpdatesManifest(text, { id, commitTime }) {
1131
- const manifest = JSON.parse(text);
1132
- if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw new Error(`${UPDATES_MANIFEST_NAME} is not a JSON object`);
1133
- return JSON.stringify({
1134
- ...manifest,
1135
- id,
1136
- commitTime
1137
- });
1138
- }
1139
- function refreshUpdatesManifestFile(path, commitTime) {
1140
- writeFileSync(path, refreshUpdatesManifest(readFileSync(path, "utf-8"), {
1141
- id: randomUUID(),
1142
- commitTime
1143
- }));
1144
- }
1145
- const IOS_UPDATES_MANIFESTS = [join("EXUpdates.bundle", UPDATES_MANIFEST_NAME), UPDATES_MANIFEST_NAME];
1146
- const IOS_FRAMEWORK_UPDATES_MANIFEST = join("Frameworks", "EXUpdates.framework", "EXUpdates.bundle", UPDATES_MANIFEST_NAME);
1147
- function hermescPath(root, { exists = existsSync } = {}) {
1148
- const candidates = [
1149
- join(root, "node_modules", "hermes-compiler", "hermesc", "osx-bin", "hermesc"),
1150
- join(root, "ios", "Pods", "hermes-engine", "destroot", "bin", "hermesc"),
1151
- join(root, "node_modules", "react-native", "sdks", "hermesc", "osx-bin", "hermesc")
1152
- ];
1153
- return candidates.find(exists) ?? candidates[candidates.length - 1];
1154
- }
1155
- function hermescArgs({ bundle, out }) {
1156
- return [
1157
- "-emit-binary",
1158
- "-out",
1159
- out,
1160
- bundle
1161
- ];
1162
- }
1163
- async function swapJsBundle({ root, isExpo, cachedAppPath, logWriter = null, exec = null, spawnFn = null, mkdtemp = () => makeTemporaryDirectory(cachedAppPath, "stim-js-swap-"), exists = existsSync, hermesEnabled = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, onHeartbeat = (line) => console.error(line) }) {
1164
- const e = exec || getExecutor();
1165
- const startedAt = now();
1166
- const elapsed = () => now() - startedAt;
1167
- let tmp;
1168
- const fail = (step, reason, lastLines = []) => {
1169
- logWriter?.write?.({
1170
- src: "build",
1171
- level: "error",
1172
- msg: `JS swap failed at ${step}: ${reason}`,
1173
- event: "js_swap"
1174
- });
1175
- if (tmp) removeTemporaryEntry(tmp);
1176
- return {
1177
- failed: true,
1178
- step,
1179
- reason,
1180
- lastLines,
1181
- durationMs: elapsed()
1182
- };
1183
- };
1184
- let appCopy;
1185
- try {
1186
- tmp = mkdtemp();
1187
- appCopy = join(tmp, basename(cachedAppPath));
1188
- try {
1189
- e.runFile("cp", [
1190
- "-c",
1191
- "-R",
1192
- cachedAppPath,
1193
- appCopy
1194
- ]);
1195
- } catch {
1196
- removeTemporaryEntry(appCopy);
1197
- e.runFile("cp", [
1198
- "-R",
1199
- cachedAppPath,
1200
- appCopy
1201
- ]);
1202
- }
1203
- } catch (err) {
1204
- return fail("copy", `could not copy ${cachedAppPath} aside: ${describe(err)}`);
1205
- }
1206
- let catalogTarget;
1207
- const infoPlistPath = join(appCopy, "Info.plist");
1208
- try {
1209
- catalogTarget = readAssetCatalogTarget(e, infoPlistPath);
1210
- } catch (err) {
1211
- return fail("catalog", `could not read ${infoPlistPath}: ${describe(err)}`);
1212
- }
1213
- const bundleOutput = join(tmp, JS_BUNDLE_NAME);
1214
- const assetsDest = join(tmp, "assets");
1215
- const catalogStaging = catalogTarget ? join(tmp, "rn-assets") : null;
1216
- const catalogDir = catalogStaging ? join(catalogStaging, "RNAssets.xcassets") : null;
1217
- const command = bundleCommand({
1218
- isExpo,
1219
- entryFile: isExpo ? "index.js" : detectEntryFile(root),
1220
- bundleOutput,
1221
- assetsDest,
1222
- assetCatalogDest: catalogStaging
1223
- });
1224
- try {
1225
- mkdirSync(assetsDest, { recursive: true });
1226
- } catch (err) {
1227
- return fail("bundle", `could not create ${assetsDest}: ${describe(err)}`);
1228
- }
1229
- if (catalogDir) try {
1230
- mkdirSync(catalogDir, { recursive: true });
1231
- } catch (err) {
1232
- return fail("bundle", `could not create ${catalogDir}: ${describe(err)}`);
1233
- }
1234
- logWriter?.write?.({
1235
- src: "build",
1236
- level: "info",
1237
- msg: `${command.file} ${command.args.join(" ")}`,
1238
- event: "js_swap"
1239
- });
1240
- const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
1241
- const transcript = [];
1242
- const push = (line) => {
1243
- const msg = cleanLine(line);
1244
- if (msg.trim() === "") return;
1245
- transcript.push(msg);
1246
- logWriter?.write?.({
1247
- src: "build",
1248
- level: "debug",
1249
- msg,
1250
- raw: true,
1251
- event: "js_swap"
1252
- });
1253
- };
1254
- let child;
1255
- try {
1256
- child = spawn(command.file, command.args, {
1257
- cwd: root,
1258
- stdio: [
1259
- "ignore",
1260
- "pipe",
1261
- "pipe"
1262
- ],
1263
- env: {
1264
- ...process.env,
1265
- FORCE_COLOR: "0"
1266
- }
1267
- });
1268
- } catch (err) {
1269
- return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
1270
- }
1271
- const reader = {
1272
- out: createLineReader(push),
1273
- err: createLineReader(push)
1274
- };
1275
- child.stdout?.setEncoding?.("utf-8");
1276
- child.stderr?.setEncoding?.("utf-8");
1277
- child.stdout?.on("data", (chunk) => reader.out.push(chunk));
1278
- child.stderr?.on("data", (chunk) => reader.err.push(chunk));
1279
- const stopHeartbeat = startBuildHeartbeat({
1280
- intervalMs: heartbeatMs,
1281
- elapsed,
1282
- emit: onHeartbeat,
1283
- label: "swap"
1284
- });
1285
- let outcome;
1286
- try {
1287
- outcome = await waitForChild(child);
1288
- } finally {
1289
- stopHeartbeat();
1290
- }
1291
- reader.out.flush();
1292
- reader.err.flush();
1293
- if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
1294
- if (outcome.code !== 0) {
1295
- const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
1296
- return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
1297
- }
1298
- if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${JS_BUNDLE_NAME} at ${bundleOutput}`);
1299
- let hermes = false;
1300
- let note;
1301
- if (hermesEnabled ?? readHermesEnabled(root)) {
1302
- const hermesc = hermescPath(root);
1303
- if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
1304
- else {
1305
- const hbc = join(tmp, `${JS_BUNDLE_NAME}.hbc`);
1306
- try {
1307
- e.runFile(hermesc, hermescArgs({
1308
- bundle: bundleOutput,
1309
- out: hbc
1310
- }));
1311
- e.runFile("mv", [hbc, bundleOutput]);
1312
- hermes = true;
1313
- } catch (err) {
1314
- return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
1315
- }
1316
- }
1317
- }
1318
- try {
1319
- e.runFile("cp", [bundleOutput, join(appCopy, JS_BUNDLE_NAME)]);
1320
- e.runFile("cp", [
1321
- "-R",
1322
- `${assetsDest}/.`,
1323
- `${appCopy}/`
1324
- ]);
1325
- } catch (err) {
1326
- return fail("replace", `could not replace the JS bundle inside ${appCopy}: ${describe(err)}`);
1327
- }
1328
- if (catalogTarget && catalogDir) {
1329
- const catalogBundle = join(appCopy, ASSET_CATALOG_BUNDLE);
1330
- try {
1331
- rmSync(catalogBundle, {
1332
- recursive: true,
1333
- force: true
1334
- });
1335
- if (readdirSync(catalogDir).some((name) => name.endsWith(".imageset"))) {
1336
- mkdirSync(catalogBundle);
1337
- const output = e.runFile("xcrun", actoolArgs({
1338
- catalog: catalogDir,
1339
- out: catalogBundle,
1340
- target: catalogTarget
1341
- }));
1342
- if (!exists(join(catalogBundle, "Assets.car"))) return fail("actool", `actool wrote no Assets.car into ${catalogBundle}`, tailLines(output.split("\n"), LAST_LINES));
1343
- writeFileSync(join(catalogBundle, "Info.plist"), ASSET_CATALOG_BUNDLE_INFO_PLIST);
1344
- }
1345
- } catch (err) {
1346
- const stdout = String(err?.stdout ?? "");
1347
- return fail("actool", `could not compile the image asset catalog into ${catalogBundle}: ${describe(err)}`, tailLines(stdout.split("\n"), LAST_LINES));
1348
- }
1349
- }
1350
- if (exists(join(appCopy, IOS_FRAMEWORK_UPDATES_MANIFEST))) return fail("updates", `the expo-updates manifest sits inside a nested framework (${IOS_FRAMEWORK_UPDATES_MANIFEST}), where a swap cannot refresh it without re-signing that framework`);
1351
- for (const manifest of IOS_UPDATES_MANIFESTS.map((name) => join(appCopy, name)).filter(exists)) try {
1352
- refreshUpdatesManifestFile(manifest, now());
1353
- } catch (err) {
1354
- return fail("updates", `could not refresh the expo-updates manifest ${manifest}: ${describe(err)}`);
1355
- }
1356
- try {
1357
- e.runFile("codesign", [
1358
- "--force",
1359
- "--sign",
1360
- "-",
1361
- appCopy
1362
- ]);
1363
- } catch (err) {
1364
- return fail("codesign", `codesign --force --sign - ${appCopy} failed: ${describe(err)}`);
1365
- }
1366
- logWriter?.write?.({
1367
- src: "build",
1368
- level: "info",
1369
- msg: `JS swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${appCopy} in ${elapsed()}ms`,
1370
- event: "js_swap"
1371
- });
1372
- const result = {
1373
- ok: true,
1374
- appPath: appCopy,
1375
- tmpDir: tmp,
1376
- hermes,
1377
- durationMs: elapsed()
1378
- };
1379
- if (note) result.note = note;
1380
- return result;
1381
- }
1382
- function describe(err) {
1383
- return String(err?.message || err);
1384
- }
1385
- //#endregion
1386
- //#region src/cache/miss-reason.ts
1387
- const MISS_CHANGE_CAP = 20;
1388
- function recordedBuilds(state, platform) {
1389
- const builds = [];
1390
- for (const value of [state?.[LAST_BUILD_KEYS[platform]], state?.lastBuild]) {
1391
- const record = value;
1392
- if (record?.platform !== platform) continue;
1393
- const startedAt = typeof record.startedAt === "string" ? record.startedAt : "";
1394
- if (typeof record.fingerprint === "string" && typeof record.cacheKey === "string") builds.push({
1395
- fingerprint: record.fingerprint,
1396
- cacheKey: record.cacheKey,
1397
- startedAt
1398
- });
1399
- const carried = record.missReason?.baseline;
1400
- if (typeof carried?.fingerprint === "string" && typeof carried.cacheKey === "string") builds.push({
1401
- fingerprint: carried.fingerprint,
1402
- cacheKey: carried.cacheKey,
1403
- startedAt
1404
- });
1405
- }
1406
- return builds;
1407
- }
1408
- function projectIdentity(root) {
1409
- let real;
1410
- try {
1411
- real = realpathSync.native(root);
1412
- } catch {
1413
- return null;
1414
- }
1415
- for (let dir = real;; dir = dirname(dir)) {
1416
- if (existsSync(join(dir, ".git"))) {
1417
- const common = gitCommonDirOnDisk(dir);
1418
- return common ? `${common}\n${relative(dir, real)}` : null;
1419
- }
1420
- if (dirname(dir) === dir) return null;
1421
- }
1422
- }
1423
- function findMissBaseline(root, platform, { readState = readWorkspaceState, projectRoots = () => Object.keys(loadConfig()?.projects ?? {}), identity = projectIdentity, sourcesOf = (p, key) => storedSources(p, key, sharedBuildCache()) } = {}) {
1424
- for (const build of recordedBuilds(readState(root), platform)) {
1425
- const sources = sourcesOf(platform, build.cacheKey);
1426
- if (sources) return {
1427
- fingerprint: build.fingerprint,
1428
- cacheKey: build.cacheKey,
1429
- sources,
1430
- from: "workspace"
1431
- };
1432
- }
1433
- const project = identity(root);
1434
- if (!project) return null;
1435
- const others = [];
1436
- for (const other of projectRoots()) {
1437
- if (other === root || identity(other) !== project) continue;
1438
- others.push(...recordedBuilds(readState(other), platform));
1439
- }
1440
- for (const build of others.toSorted((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0))) {
1441
- const sources = sourcesOf(platform, build.cacheKey);
1442
- if (sources) return {
1443
- fingerprint: build.fingerprint,
1444
- cacheKey: build.cacheKey,
1445
- sources,
1446
- from: "project"
1447
- };
1448
- }
1449
- return null;
1450
- }
1451
- const CATEGORY_RANK = {
1452
- "native-dependency": 0,
1453
- "app-config": 1,
1454
- "config-plugin": 2,
1455
- package: 3,
1456
- "native-dir": 4,
1457
- file: 5,
1458
- "app-asset": 6,
1459
- other: 7,
1460
- "package-scripts": 8,
1461
- autolinking: 9
1462
- };
1463
- function packageIn(path) {
1464
- return [...path.matchAll(/(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/g)].at(-1)?.[1] ?? null;
1465
- }
1466
- function describeChange({ name, change, reasons }) {
1467
- const pkg = packageIn(name);
1468
- if (name === "expoConfig" || reasons.includes("expoConfig")) return {
1469
- category: "app-config",
1470
- label: "app config changed"
1471
- };
1472
- if (pkg && reasons.some((reason) => /^(expo|rncore)Autolinking(Ios|Android)?$/.test(reason))) return {
1473
- category: "native-dependency",
1474
- label: `native dependency ${change}: ${pkg}`
1475
- };
1476
- if (pkg && reasons.includes("expoConfigPlugins")) return {
1477
- category: "config-plugin",
1478
- label: `config plugin ${change}: ${pkg}`
1479
- };
1480
- if (/^(expoAutolinkingConfig|rncoreAutolinkingConfig)(:|$)/.test(name)) return {
1481
- category: "autolinking",
1482
- label: "autolinking config changed"
1483
- };
1484
- if (name.startsWith("package:")) return {
1485
- category: "package",
1486
- label: `${name.slice(8)} ${change}`
1487
- };
1488
- if (name === "packageJson:scripts") return {
1489
- category: "package-scripts",
1490
- label: "package.json scripts changed"
1491
- };
1492
- if (reasons.includes("expoConfigExternalFile")) return {
1493
- category: "app-asset",
1494
- label: `app asset ${change}: ${name}`
1495
- };
1496
- if (reasons.includes("bareNativeDir")) return {
1497
- category: "native-dir",
1498
- label: `${name}/ ${change}`
1499
- };
1500
- if (reasons.length || name.includes("/") || name.includes(".")) return {
1501
- category: "file",
1502
- label: `${name} ${change}`
1503
- };
1504
- return {
1505
- category: "other",
1506
- label: `${name} ${change}`
1507
- };
1508
- }
1509
- function rekeyPrefix(rekeyedBy) {
1510
- return rekeyedBy.length ? `${rekeyedBy.join(" and ")} changed native inputs; ` : "";
1511
- }
1512
- function missReasonFromChanges({ changes, baseline, rekeyedBy = [] }) {
1513
- const prefix = rekeyPrefix(rekeyedBy);
1514
- if (!baseline) return {
1515
- kind: "no-baseline",
1516
- summary: `${prefix}no earlier build of this project in the cache to compare with`,
1517
- changes: [],
1518
- changeCount: 0,
1519
- baseline: null,
1520
- rekeyedBy
1521
- };
1522
- if (!changes.length) return {
1523
- kind: "same-sources",
1524
- summary: `${prefix}native inputs match the last build; no cached app for this configuration or target`,
1525
- changes: [],
1526
- changeCount: 0,
1527
- baseline,
1528
- rekeyedBy
1529
- };
1530
- const described = changes.map((change, index) => ({
1531
- change,
1532
- index,
1533
- ...describeChange(change)
1534
- })).toSorted((a, b) => CATEGORY_RANK[a.category] - CATEGORY_RANK[b.category] || a.index - b.index);
1535
- const dependencyMoved = described.some((entry) => entry.category === "native-dependency");
1536
- const labels = [...new Set(described.filter((entry) => !(dependencyMoved && entry.category === "autolinking")).map((entry) => entry.label))];
1537
- const more = labels.length > 1 ? ` (+${labels.length - 1} more)` : "";
1538
- return {
1539
- kind: "changed",
1540
- summary: `${prefix}${labels[0]}${more}`,
1541
- changes: described.slice(0, MISS_CHANGE_CAP).map(({ change, category }) => ({
1542
- source: change.name,
1543
- change: change.change,
1544
- category
1545
- })),
1546
- changeCount: changes.length,
1547
- baseline,
1548
- rekeyedBy
1549
- };
1550
- }
1551
- function explainBuildMiss({ root, platform, current, rekeyedBy = [], baselineDeps, differ = expoFingerprint.diffFingerprints }) {
1552
- const baseline = findMissBaseline(root, platform, baselineDeps);
1553
- const changes = baseline ? diffFingerprintSources({
1554
- previous: baseline.sources,
1555
- previousHash: baseline.fingerprint,
1556
- current,
1557
- differ
1558
- }) : [];
1559
- return {
1560
- reason: missReasonFromChanges({
1561
- changes,
1562
- baseline: baseline ? {
1563
- fingerprint: baseline.fingerprint,
1564
- cacheKey: baseline.cacheKey,
1565
- from: baseline.from
1566
- } : null,
1567
- rekeyedBy
1568
- }),
1569
- changedNames: changes.map((change) => change.name),
1570
- previousHash: baseline?.fingerprint ?? null
1571
- };
1572
- }
1573
- function predictBuildMiss({ root, platform, current, prebuild }) {
1574
- const { reason } = explainBuildMiss({
1575
- root,
1576
- platform,
1577
- current
1578
- });
1579
- const baseline = reason.baseline && {
1580
- fingerprint: reason.baseline.fingerprint,
1581
- from: reason.baseline.from
1582
- };
1583
- if (!prebuild || !baseline) return {
1584
- ...reason,
1585
- baseline
1586
- };
1587
- const dir = platform === "ios" ? "ios/" : "android/";
1588
- return {
1589
- ...reason,
1590
- kind: "prebuild-pending",
1591
- summary: `${reason.summary} (before prebuild ${prebuild}s ${dir})`,
1592
- baseline
1593
- };
1594
- }
1595
- function skippedMissReason(summary) {
1596
- return {
1597
- kind: "cache-skipped",
1598
- summary,
1599
- changes: [],
1600
- changeCount: 0,
1601
- baseline: null,
1602
- rekeyedBy: []
1603
- };
1604
- }
1605
- function fingerprintErrorMissReason(message) {
1606
- return {
1607
- kind: "fingerprint-error",
1608
- summary: `fingerprinting failed: ${message}`,
1609
- changes: [],
1610
- changeCount: 0,
1611
- baseline: null,
1612
- rekeyedBy: []
1613
- };
1614
- }
1615
- //#endregion
1616
- //#region src/commands/build-plan.ts
1617
- const DISCARD = {
1618
- file: "",
1619
- written: 0,
1620
- dropped: 0,
1621
- lastError: null,
1622
- write: () => true,
1623
- close: () => ({
1624
- file: "",
1625
- written: 0,
1626
- dropped: 0,
1627
- lastError: null
1628
- })
1629
- };
1630
- async function providerHit({ root, platform, cacheKey, providerConfig }, deps) {
1631
- if (!providerConfig) return null;
1632
- let destinationDir;
1633
- try {
1634
- destinationDir = makeTemporaryDirectory(root, "stim-plan-");
1635
- } catch (error) {
1636
- deps.note(chalk.yellow(phaseLine("cache", `provider not checked: ${error?.message || error}`)));
1637
- return null;
1638
- }
1639
- try {
1640
- const found = await resolveTieredBuild({
1641
- local: {
1642
- resolve: () => null,
1643
- store: () => null
1644
- },
1645
- loadProvider: () => deps.loadCacheProvider({
1646
- projectRoot: root,
1647
- config: providerConfig
1648
- }),
1649
- target: {
1650
- projectRoot: root,
1651
- platform,
1652
- key: cacheKey
1653
- },
1654
- destinationDir,
1655
- warn: (_code, message) => deps.note(chalk.yellow(phaseLine("cache", message)))
1656
- });
1657
- return found ? found.providerName ?? "the cache provider" : null;
1658
- } finally {
1659
- removeTemporaryEntry(destinationDir);
1660
- }
1661
- }
1662
- async function expoRemoteHit(lookup, deps) {
1663
- if (!lookup.expoRemote) return null;
1664
- const { root, platform, isExpo, fingerprint } = lookup;
1665
- const loaded = await deps.loadProjectProvider(root, { isExpo });
1666
- if (loaded?.unavailable) {
1667
- deps.note(chalk.yellow(phaseLine("cache", `provider not usable: ${loaded.unavailable}`)));
1668
- return null;
1669
- }
1670
- if (!loaded?.provider) return null;
1671
- const name = loaded.name ?? "the build cache provider";
1672
- if (name === "eas") {
1673
- const auth = deps.checkEasAuth({
1674
- projectRoot: root,
1675
- owner: loaded.owner || null
1676
- });
1677
- const authNote = easAuthNote(auth);
1678
- if (authNote) deps.note(chalk.yellow(phaseLine("cache", authNote)));
1679
- if (auth?.code === "logged-out") return null;
1680
- }
1681
- const hit = await deps.resolveRemote({
1682
- provider: loaded.provider,
1683
- platform,
1684
- projectRoot: root,
1685
- fingerprintHash: fingerprint,
1686
- runOptions: lookup.expoRemote.runOptions,
1687
- logWriter: DISCARD
1688
- });
1689
- if (hit?.timedOut) deps.note(chalk.yellow(phaseLine("cache", `${name} did not answer; counted as a miss`)));
1690
- else if (hit?.failed) deps.note(chalk.yellow(phaseLine("cache", `${name} could not be used: ${hit.failed}`)));
1691
- return hit?.appPath ? name : null;
1692
- }
1693
- async function planCachedBuild(lookup, deps) {
1694
- const { root, platform, isExpo, fingerprint, sources, cacheKey, cachePolicy } = lookup;
1695
- let cacheHit = false;
1696
- let provider = null;
1697
- if (cachePolicy.read) {
1698
- if (artifactIn(entryDir(platform, cacheKey))) cacheHit = "local";
1699
- else {
1700
- provider = (cachePolicy.remote ? await providerHit(lookup, deps) : null) ?? await expoRemoteHit(lookup, deps);
1701
- if (provider) cacheHit = "remote";
1702
- }
1703
- }
1704
- let prebuild = null;
1705
- let refusal = null;
1706
- let missReason;
1707
- if (!cacheHit) {
1708
- prebuild = deps.planPrebuild(root, platform, {
1709
- isExpo,
1710
- fingerprint,
1711
- sources
1712
- });
1713
- if (prebuild === "refuse") refusal = staleNativeDirRefusal(platform);
1714
- else if (cachePolicy.read) missReason = predictBuildMiss({
1715
- root,
1716
- platform,
1717
- current: {
1718
- hash: fingerprint,
1719
- sources
1720
- },
1721
- prebuild: prebuild === "generate" || prebuild === "regenerate" ? prebuild : null
1722
- });
1723
- }
1724
- return planPayload(lookup, {
1725
- fingerprint,
1726
- cacheKey,
1727
- cacheHit,
1728
- provider,
1729
- cacheSkipped: !cachePolicy.read,
1730
- prebuild,
1731
- ...missReason ? { missReason } : {},
1732
- refusal
1733
- });
1734
- }
1735
- function planPayload({ platform, slot, projectKey }, found) {
1736
- const { refusal, missReason, ...rest } = found;
1737
- const outcome = refusal ? null : found.cacheHit ? "hit" : "cold";
1738
- const estimate = outcome ? estimateBuild(readStats$1().record?.history?.[projectKey], platform, outcome) : {
1739
- expectedMs: null,
1740
- basis: 0
1741
- };
1742
- return {
1743
- platform,
1744
- ...slot === "default" ? {} : { slot },
1745
- ...rest,
1746
- outcome,
1747
- expectedMs: estimate.expectedMs,
1748
- basis: estimate.basis,
1749
- ...missReason ? { missReason } : {},
1750
- ...refusal ? { refusal } : {}
1751
- };
1752
- }
1753
- function cacheLine(plan) {
1754
- if (plan.refusal) return `the build would refuse with ${plan.refusal.code}`;
1755
- if (plan.cacheHit === "local") return "local cache hit";
1756
- if (plan.cacheHit === "remote") return `remote cache hit (${plan.provider})`;
1757
- return `${plan.cacheSkipped ? "cache reads are off" : "cache miss"}: compiles${plan.prebuild === "generate" || plan.prebuild === "regenerate" ? `, ${plan.prebuild}s the native dir` : ""}`;
1758
- }
1759
- function printPlan(plan, json) {
1760
- if (json) {
1761
- console.log(JSON.stringify(plan));
1762
- return;
1763
- }
1764
- const slot = plan.slot ? ` [${plan.slot}]` : "";
1765
- console.log(phaseLine("plan", `${plan.platform}${slot} ${shortHash(plan.fingerprint)} -> ${cacheLine(plan)}`));
1766
- if (plan.placement) console.log(phaseLine("placement", plan.placement));
1767
- if (plan.missReason) console.log(phaseLine("cache", `miss: ${plan.missReason.summary}`));
1768
- if (plan.refusal) {
1769
- console.log(phaseLine("error", plan.refusal.message));
1770
- console.log(phaseLine("remedy", plan.refusal.remedy));
1771
- return;
1772
- }
1773
- console.log(phaseLine("expect", plan.expectedMs === null ? `unknown: no ${plan.outcome} run of this project is recorded yet` : `~${formatDuration(plan.expectedMs)} (median of ${plural(plan.basis, `${plan.outcome} run`)})`));
1774
- }
1775
- function refusePlan({ code, message, remedy }, json, lines = []) {
1776
- console.error(chalk.red(phaseLine("error", message)));
1777
- for (const line of lines) console.error(chalk.dim(phaseLine("", line)));
1778
- console.error(chalk.dim(phaseLine("remedy", remedy)));
1779
- console.error(chalk.red(phaseLine("failed", code)));
1780
- if (json) console.log(JSON.stringify({
1781
- code,
1782
- message,
1783
- remedy
1784
- }));
1785
- process.exitCode = 1;
1786
- }
1787
- const PLAN_ONLY_REMEDY = "Drop the flag to plan the owned simulator or emulator build, or run the command without --plan.";
1788
- function planFlagRefusal(flag) {
1789
- return {
1790
- code: "STIM_BAD_ARG",
1791
- message: `${flag} does not apply to --plan, which predicts the next local build without choosing a device.`,
1792
- remedy: PLAN_ONLY_REMEDY
1793
- };
1794
- }
1795
- //#endregion
1796
- //#region src/device-host/plan-placement.ts
1797
- /**
1798
- * Where `--plan` assumes the next run puts its simulator or emulator, with no session, reservation or state write.
1799
- * A named Mac, or the Mac a recorded session already holds, answers with the device its `device-host.offer` would
1800
- * give; `auto` repeats the placement decision a run makes now and, when that stays on this Mac, notes any Mac
1801
- * whose device would key differently. The result predicts the cache key only; a run reads its own device.
1802
- */
1803
- async function planHostedDevice({ root, slot, platform, machine, selectors, sameKey, deps = {} }) {
1804
- const { automatic = automaticDevicePlacement, probe = probeHost, machines = hostingMachines } = deps;
1805
- const named = async (name) => {
1806
- const result = await probe(name, platform, selectors);
1807
- if ("failure" in result.probe) return {
1808
- kind: "unknown",
1809
- reason: `${name} did not give a ${platform} device (${result.probe.failure})`
1810
- };
1811
- const { choice, declined } = result.probe.offer;
1812
- if (!choice) return {
1813
- kind: "unknown",
1814
- reason: `${name} has no ${platform} device for this selection${declined ? ` (${declined})` : ""}`
1815
- };
1816
- return {
1817
- kind: "hosted",
1818
- choice,
1819
- placement: `on ${name}`
1820
- };
1821
- };
1822
- if (machine !== "auto") return named(machine);
1823
- try {
1824
- const recorded = readHostedNative(root, platform, slot)[slot];
1825
- if (recorded) return await named(recorded.machine);
1826
- const placed = await automatic({
1827
- root,
1828
- slot,
1829
- platform,
1830
- selectors,
1831
- noWait: false
1832
- }, {
1833
- read: () => ({}),
1834
- write: () => {}
1835
- });
1836
- if (placed.target) return {
1837
- kind: "hosted",
1838
- choice: placed.target.choice,
1839
- placement: `on ${placed.target.host.machine}, as auto would now`
1840
- };
1841
- const names = machines() ?? [];
1842
- const differs = (await Promise.all(names.map((name) => probe(name, platform, selectors)))).flatMap(({ probe: each }) => "offer" in each && each.offer.choice && sameKey(each.offer.choice) === false ? [each.machine] : []);
1843
- if (differs.length) return {
1844
- kind: "local",
1845
- placement: `this Mac; auto may use ${differs.join(", ")}, which builds for another architecture, so its key differs`
1846
- };
1847
- return {
1848
- kind: "local",
1849
- placement: names.length ? `this Mac; auto may use ${names.join(", ")}` : "this Mac"
1850
- };
1851
- } catch (error) {
1852
- return {
1853
- kind: "unknown",
1854
- reason: error.message
1855
- };
1856
- }
1857
- }
1858
- //#endregion
1859
- export { AvdBootError as _, printPlan as a, automaticDevicePlacement as b, fingerprintErrorMissReason as c, detectEntryFile as d, refreshUpdatesManifestFile as f, ensureOwnedDevice as g, ensureBooted as h, planPayload as i, skippedMissReason as l, didSetUpDevice as m, planCachedBuild as n, refusePlan as o, swapJsBundle as p, planFlagRefusal as r, explainBuildMiss as s, planHostedDevice as t, UPDATES_MANIFEST_NAME as u, AvdRecoveryError as v, devicePlacementLine as x, warmMetro as y };