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