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