stim 1.8.0 → 1.9.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 +23 -155
- package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
- package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
- package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
- package/dist/app-install-Uf3a83Tv.mjs +819 -0
- package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
- package/dist/build-progress-CVqkfPC6.mjs +188 -0
- package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +22 -19
- package/dist/collector-run.d.mts +2 -0
- package/dist/collector-run.mjs +10 -9
- package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
- package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
- package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
- package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
- package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
- package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
- package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
- package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
- package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
- package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
- package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
- package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
- package/dist/exec-jIAlgilk.mjs +132 -0
- package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
- package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
- package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
- package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
- package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
- package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
- package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
- package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
- package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
- package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
- package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
- package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
- package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
- package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
- package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
- package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
- package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
- package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
- package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
- package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
- package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
- package/dist/server-expo-CeILEu86.mjs +2 -0
- package/dist/settings-BKNh5rl4.mjs +376 -0
- package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
- package/dist/settings.schema.json +989 -0
- package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
- package/dist/simslim-B_LsGLuJ.mjs +259 -0
- package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
- package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
- package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
- package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
- package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
- package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
- package/dist/status-S6ehL4Ui.mjs +932 -0
- package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
- package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
- package/dist/supervisor-run.mjs +52 -34
- package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
- package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
- package/dist/worktree-DKhSF0UX.mjs +2 -0
- package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
- package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
- package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
- package/package.json +5 -6
- package/skill/SKILL.md +2 -0
- package/dist/app-install-BDbsSgCl.mjs +0 -1175
- package/dist/build-slots-C-oAHRT1.mjs +0 -151
- package/dist/dev-client-BmT-ioFI.mjs +0 -797
- package/dist/exec-B29XNuhP.mjs +0 -77
- package/dist/reclaim-KJ-0uWEF.mjs +0 -361
- package/dist/server-expo-YDDG3eP7.mjs +0 -2
- package/dist/status-DdkLki47.mjs +0 -426
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
|
@@ -1,318 +1,330 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { l as
|
|
10
|
-
import {
|
|
11
|
-
import { t as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
1
|
+
import { K as projectDeviceSlots, N as readJsonObject, O as acquireAvdClaim, S as setDevice, T as withConfigLock, U as deviceSlotPlatforms, g as loadConfig, i as clearDevice, j as isJsonObject, k as ownership_claim_exports, t as allConsolePortsAndSerials, u as getConfigDir, v as releaseAndroidConsolePort, x as saveConfig } from "./config-Cp9lM3wX.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-jIAlgilk.mjs";
|
|
3
|
+
import { u as ensureWorkspaceStorage, v as workspaceId } from "./workspace-state-DF2gCFhx.mjs";
|
|
4
|
+
import { E as removeTemporaryEntry, T as makeTemporaryDirectory } from "./worktree-R_cUlE7s.mjs";
|
|
5
|
+
import { a as detectIsExpo, c as ownedDeviceLabel, s as isPackageResolvable } from "./project-DS112DYB.mjs";
|
|
6
|
+
import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
7
|
+
import { a as pidExists, t as NOT_OURS_FOREIGN_CWD } from "./metro-DrLHaMn2.mjs";
|
|
8
|
+
import { _ as iosSimSlimProfileSetting, a as androidAvdConfigSetting, c as androidDataPartitionSizeGbSetting } from "./settings-D030k0t4.mjs";
|
|
9
|
+
import { A as ownedAvdMatchesConfiguration, C as listAdbDevices, F as pickDefaultSystemImage, H as waitForBoot, N as ownedAvdSystemImage, O as nextConsolePort, R as resolveOwnedAvdSerial, T as listInstalledSystemImages, d as avdPoolConfiguration, h as createOwnedAvd, j as ownedAvdName, l as assertOwnedAvdStopped, m as configureNewOwnedAvd, p as bootAndroidEmulator, w as listAvds, x as getAvdNameForSerial } from "./android-_y4Ohi5z.mjs";
|
|
10
|
+
import { l as phaseLine } from "./command-output-gByVmIjy.mjs";
|
|
11
|
+
import { r as releaseBuildLock, t as acquireBuildLock } from "./build-lock-DPXPLgTc.mjs";
|
|
12
|
+
import { n as spawnDeclared } from "./spawn-claims-Dn3C_N6w.mjs";
|
|
13
|
+
import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-B_lctBR3.mjs";
|
|
14
|
+
import { b as resolveEasCliBin, i as isLegacyDeletionClaim, l as selectParked, n as adoptParked, o as parkedMaxSetting, r as dropParked, s as readParked } from "./sim-pool-KaIsp2Ft.mjs";
|
|
15
|
+
import { t as withWorkspaceProcessLock } from "./workspace-process-lock-DWaiRD0e.mjs";
|
|
16
|
+
import { r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING } from "./launch-verify-Q_OtW1Rf.mjs";
|
|
17
|
+
import { A as readHostMemoryPressure, C as resolveOwnedIosSim, D as configuredIosSimulatorViewer, S as resolveIosCreation, _ as parseRuntimeVersion, b as resetIosKeychain, f as listIosDeviceTypes, h as ownedSimName, i as createOwnedIosSim, k as hostMemoryPressureAdvice, l as iosSimulatorFailureAdvice, n as bootIosSim, u as listAllIosSims, x as resetIosPrivacy, y as renameIosSim } from "./ios-BL2NFckr.mjs";
|
|
18
|
+
import { d as startBuildHeartbeat, f as tailLines, n as HEARTBEAT_INTERVAL_MS } from "./xcode-BZqyxh3e.mjs";
|
|
19
|
+
import { a as liveOwnedDeviceCount, i as deviceTypeMismatch, t as reconcileSimSlim } from "./simslim-B_LsGLuJ.mjs";
|
|
20
|
+
import { D as teardownParkedAvd, O as teardownParkedIosSim, T as teardownOwnedAvd } from "./device-remote-AQ5rQPqr.mjs";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
22
|
+
import { basename, join } from "node:path";
|
|
15
23
|
import { claimRemoveCommand, clearClaimChild, markClaimChildPending, processGroupAlive, releaseClaim, setClaimChild } from "@stim-cli/core/ownership-claim";
|
|
16
24
|
import { captureProcessIdentity } from "@stim-cli/core/process-identity";
|
|
25
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
26
|
import { isDeepStrictEqual } from "node:util";
|
|
18
27
|
import chalk from "chalk";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"on",
|
|
28
|
-
udid,
|
|
29
|
-
"--profile",
|
|
30
|
-
profile
|
|
31
|
-
] : ["off", udid];
|
|
32
|
-
const lines = [];
|
|
33
|
-
const onLine = (raw) => {
|
|
34
|
-
const line = stripAnsi(raw).trim();
|
|
35
|
-
if (!line) return;
|
|
36
|
-
lines.push(line);
|
|
37
|
-
if (lines.length > 20) lines.shift();
|
|
38
|
-
out(`SimSlim: ${line}`);
|
|
39
|
-
};
|
|
40
|
-
const stdout = createLineReader(onLine);
|
|
41
|
-
const stderr = createLineReader(onLine);
|
|
42
|
-
const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
|
|
43
|
-
const label = `SimSlim for ${udid}`;
|
|
44
|
-
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
45
|
-
root,
|
|
46
|
-
mode: "exclusive",
|
|
47
|
-
label
|
|
48
|
-
});
|
|
49
|
-
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
50
|
-
const claim = attempt.acquired;
|
|
51
|
-
if (!claim) throw new ownership_claim_exports.ClaimRefusedError({
|
|
52
|
-
root,
|
|
53
|
-
claimPath: attempt.held?.path ?? root,
|
|
54
|
-
label,
|
|
55
|
-
reason: "another SimSlim operation is still using this simulator"
|
|
56
|
-
});
|
|
57
|
-
let child;
|
|
58
|
-
let result;
|
|
59
|
-
let identity;
|
|
60
|
-
let cancellation;
|
|
61
|
-
const stopGroup = () => {
|
|
62
|
-
if (child?.pid !== void 0 && identity && (0, process_identity_exports.inspectProcessIdentity)(identity) === "same") try {
|
|
63
|
-
process.kill(-child.pid, "SIGKILL");
|
|
64
|
-
} catch {}
|
|
65
|
-
};
|
|
66
|
-
const cancel = (signal) => {
|
|
67
|
-
cancellation ??= signal;
|
|
68
|
-
stopGroup();
|
|
69
|
-
};
|
|
70
|
-
const onInterrupt = () => cancel("SIGINT");
|
|
71
|
-
const onTerminate = () => cancel("SIGTERM");
|
|
72
|
-
const groupAlive = () => child?.pid !== void 0 && (0, ownership_claim_exports.processGroupAlive)(child.pid);
|
|
73
|
-
const settled = () => result !== void 0 && !groupAlive();
|
|
74
|
-
const waitUntil = async (deadline, cancellable = false) => {
|
|
75
|
-
while (!settled() && Date.now() < deadline) {
|
|
76
|
-
if (cancellable && cancellation) break;
|
|
77
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
78
|
-
}
|
|
79
|
-
return settled();
|
|
80
|
-
};
|
|
81
|
-
process.on("SIGINT", onInterrupt);
|
|
82
|
-
process.on("SIGTERM", onTerminate);
|
|
83
|
-
process.on("exit", stopGroup);
|
|
84
|
-
try {
|
|
85
|
-
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
86
|
-
try {
|
|
87
|
-
child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
|
|
88
|
-
stdio: [
|
|
89
|
-
"ignore",
|
|
90
|
-
"pipe",
|
|
91
|
-
"pipe"
|
|
92
|
-
],
|
|
93
|
-
detached: true
|
|
94
|
-
});
|
|
95
|
-
} catch (error) {
|
|
96
|
-
throw simslimLaunchError(error);
|
|
97
|
-
}
|
|
98
|
-
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
99
|
-
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
100
|
-
waitForChild(child).then((value) => {
|
|
101
|
-
result = value;
|
|
102
|
-
});
|
|
103
|
-
if (child.pid !== void 0) {
|
|
104
|
-
const captured = (0, process_identity_exports.captureProcessIdentity)(child.pid);
|
|
105
|
-
if (captured.ok) {
|
|
106
|
-
identity = {
|
|
107
|
-
pid: child.pid,
|
|
108
|
-
processToken: captured.token
|
|
109
|
-
};
|
|
110
|
-
try {
|
|
111
|
-
(0, ownership_claim_exports.setClaimChild)(claim, identity);
|
|
112
|
-
} catch {}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const completed = await waitUntil(Date.now() + timeoutMs, true);
|
|
116
|
-
if (cancellation || !completed) {
|
|
117
|
-
stopGroup();
|
|
118
|
-
const stopped = await waitUntil(Date.now() + cleanupMs);
|
|
119
|
-
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
120
|
-
const recovery = stopped ? "Its process group has stopped. Retry stim ios to reconcile the retained simulator settings." : `Its process group could not be confirmed stopped. The claim remains at ${claim.path}. Inspect the SimSlim process group ${child.pid ?? "unknown"} before retrying. Only when nothing is using it, remove the claim: ${(0, ownership_claim_exports.claimRemoveCommand)(claim.path)}`;
|
|
121
|
-
const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
|
|
122
|
-
if (cancellation) out(message);
|
|
123
|
-
throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
|
|
124
|
-
}
|
|
125
|
-
} finally {
|
|
126
|
-
if (!child || settled()) (0, ownership_claim_exports.releaseClaim)(claim);
|
|
127
|
-
else {
|
|
128
|
-
child.stdout?.destroy?.();
|
|
129
|
-
child.stderr?.destroy?.();
|
|
130
|
-
child.unref();
|
|
131
|
-
}
|
|
132
|
-
process.off("SIGINT", onInterrupt);
|
|
133
|
-
process.off("SIGTERM", onTerminate);
|
|
134
|
-
process.off("exit", stopGroup);
|
|
135
|
-
if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
|
|
136
|
-
}
|
|
137
|
-
stdout.flush();
|
|
138
|
-
stderr.flush();
|
|
139
|
-
if (result?.error) throw simslimLaunchError(result.error);
|
|
140
|
-
if (result?.code !== 0) {
|
|
141
|
-
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
142
|
-
throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
|
|
143
|
-
}
|
|
28
|
+
import { request } from "node:http";
|
|
29
|
+
//#region src/engine/eas-build.ts
|
|
30
|
+
function isEasBuildFailure(result) {
|
|
31
|
+
return result?.ok === false;
|
|
32
|
+
}
|
|
33
|
+
function easOptionRefusal({ profile, isExpo, buildSelector, buildCache }) {
|
|
34
|
+
if (profile === void 0) return null;
|
|
35
|
+
if (profile.trim() && isExpo && buildSelector === void 0 && buildCache !== false) return null;
|
|
144
36
|
return {
|
|
145
|
-
|
|
146
|
-
|
|
37
|
+
code: "STIM_BAD_ARG",
|
|
38
|
+
message: "--eas-profile requires an Expo project and a non-empty development profile.",
|
|
39
|
+
remedy: "Use --eas-profile <name> without --scheme, --configuration, --variant, or --no-build-cache."
|
|
147
40
|
};
|
|
148
41
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
if (cause?.code === "ENOENT") return new Error("SimSlim is configured but the `simslim` command is not installed. Run `brew install mobai-app/tap/simslim`.", { cause });
|
|
152
|
-
return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
|
|
42
|
+
function object(value) {
|
|
43
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
153
44
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
return Boolean(getExecutor().findExecutable("simslim"));
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
45
|
+
function shellArg(value) {
|
|
46
|
+
return /^[a-zA-Z0-9_.-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
160
47
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
48
|
+
function easDeviceBuildRemedy(profile) {
|
|
49
|
+
return `If the device is not registered, run npx eas-cli device:create. With approval for cloud build costs and signing changes, run npx eas-cli build --platform ios --profile ${shellArg(profile)} to produce a build with a valid profile for this device, then retry the same Stim command.`;
|
|
50
|
+
}
|
|
51
|
+
var EasFailure = class extends Error {
|
|
52
|
+
code;
|
|
164
53
|
remedy;
|
|
165
|
-
constructor(message, remedy
|
|
166
|
-
super(message
|
|
54
|
+
constructor(code, message, remedy) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
167
57
|
this.remedy = remedy;
|
|
168
58
|
}
|
|
169
59
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const
|
|
60
|
+
function selectEasBuild(builds, { platform, profile, fingerprint, projectId, physical = false }) {
|
|
61
|
+
if (!Array.isArray(builds)) throw new Error("EAS build:list did not return an array.");
|
|
62
|
+
for (const value of builds) {
|
|
63
|
+
const build = object(value);
|
|
64
|
+
if (typeof build.id === "string" && build.id.length > 0 && build.status === "FINISHED" && build.platform === platform.toUpperCase() && build.buildProfile === profile && object(build.fingerprint).hash === fingerprint && (object(build.app).id ?? object(build.project).id) === projectId && (platform !== "ios" || build.isForIosSimulator === !physical) && build.distribution === "INTERNAL" && typeof object(build.artifacts).applicationArchiveUrl === "string") return build.id;
|
|
65
|
+
}
|
|
173
66
|
return null;
|
|
174
67
|
}
|
|
175
|
-
async function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const reservation = {
|
|
183
|
-
avdName,
|
|
184
|
-
owned: true,
|
|
185
|
-
deviceName: avdName,
|
|
186
|
-
setupIncomplete: true,
|
|
187
|
-
poolConfiguration: configuration
|
|
188
|
-
};
|
|
189
|
-
try {
|
|
190
|
-
setDevice(projectPath, "android", reservation, slot);
|
|
191
|
-
return {
|
|
192
|
-
claim,
|
|
193
|
-
reservation,
|
|
194
|
-
previous,
|
|
195
|
-
avdName
|
|
196
|
-
};
|
|
197
|
-
} catch (error) {
|
|
198
|
-
releaseClaim(claim);
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
68
|
+
async function resolveEasDevelopmentBuild({ root, platform, profile, note, isExpo = true, physical = false, selectors = [], buildCache }) {
|
|
69
|
+
if (profile === void 0) return null;
|
|
70
|
+
const refusal = easOptionRefusal({
|
|
71
|
+
profile,
|
|
72
|
+
isExpo,
|
|
73
|
+
buildSelector: selectors.find((value) => value != null) ?? void 0,
|
|
74
|
+
buildCache
|
|
201
75
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const owner = findOtherOwner(avdName, projectPath, slot);
|
|
206
|
-
if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
|
|
207
|
-
const current = currentRecord();
|
|
208
|
-
if (!isDeepStrictEqual(current, reservation)) {
|
|
209
|
-
const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
|
|
210
|
-
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.`);
|
|
211
|
-
}
|
|
76
|
+
if (refusal) return {
|
|
77
|
+
ok: false,
|
|
78
|
+
...refusal
|
|
212
79
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let configurationFailure;
|
|
80
|
+
const retry = "the same Stim command";
|
|
81
|
+
const buildCommand = `npx eas-cli build --platform ${platform} --profile ${shellArg(profile)}`;
|
|
82
|
+
let lock = null;
|
|
217
83
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
markClaimChildPending(claim);
|
|
223
|
-
child = getExecutor().spawn(...args);
|
|
224
|
-
if (child.pid !== void 0) {
|
|
225
|
-
const captured = captureProcessIdentity(child.pid);
|
|
226
|
-
if (captured.ok) try {
|
|
227
|
-
setClaimChild(claim, {
|
|
228
|
-
pid: child.pid,
|
|
229
|
-
processToken: captured.token
|
|
230
|
-
});
|
|
231
|
-
} catch {}
|
|
232
|
-
}
|
|
233
|
-
return child;
|
|
234
|
-
}
|
|
235
|
-
}).then((result) => ({ result }), (error) => ({ error }));
|
|
236
|
-
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)}`);
|
|
237
|
-
clearClaimChild(claim);
|
|
238
|
-
if ("error" in creation) throw creation.error;
|
|
239
|
-
prepared = {
|
|
240
|
-
...creation.result,
|
|
241
|
-
created: true
|
|
242
|
-
};
|
|
243
|
-
} catch (error) {
|
|
244
|
-
const message = String(error?.message || error);
|
|
245
|
-
if (!message.includes("already exists")) throw error;
|
|
84
|
+
const cli = resolveEasCliBin(root);
|
|
85
|
+
if (!cli) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS CLI is not installed.", "Install eas-cli, run eas login, then retry.");
|
|
86
|
+
let profileEnv = {};
|
|
87
|
+
const run = (args, timeoutMs = 12e4) => {
|
|
246
88
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
|
|
259
|
-
} : {}
|
|
260
|
-
};
|
|
261
|
-
} catch (recoveryError) {
|
|
262
|
-
withConfigLock(() => {
|
|
263
|
-
if (!isDeepStrictEqual(currentRecord(), reservation)) return;
|
|
264
|
-
if (previous) setDevice(projectPath, "android", previous, slot);
|
|
265
|
-
else clearDevice(projectPath, "android", slot, avdName);
|
|
266
|
-
});
|
|
267
|
-
if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
|
|
268
|
-
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);
|
|
89
|
+
return JSON.parse(getExecutor().runFile(cli.file, [
|
|
90
|
+
...args,
|
|
91
|
+
"--json",
|
|
92
|
+
"--non-interactive"
|
|
93
|
+
], {
|
|
94
|
+
cwd: root,
|
|
95
|
+
timeoutMs,
|
|
96
|
+
env: profileEnv
|
|
97
|
+
}));
|
|
98
|
+
} catch {
|
|
99
|
+
throw new EasFailure("STIM_EAS_UNAVAILABLE", `EAS ${args[0]} failed or returned invalid JSON.`, `Run npx eas-cli ${args.map(shellArg).join(" ")} to inspect the error, then retry ${retry}.`);
|
|
269
100
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
101
|
+
};
|
|
102
|
+
note(`eas resolving profile ${profile}`);
|
|
103
|
+
const config = object(run([
|
|
104
|
+
"config",
|
|
105
|
+
"--platform",
|
|
106
|
+
platform,
|
|
107
|
+
"--profile",
|
|
108
|
+
profile
|
|
109
|
+
]));
|
|
110
|
+
const buildProfile = object(config.buildProfile);
|
|
111
|
+
profileEnv = object(buildProfile.env);
|
|
112
|
+
const projectId = object(object(object(config.appConfig).extra).eas).projectId;
|
|
113
|
+
if (buildProfile.developmentClient !== true || buildProfile.distribution !== "internal" || platform === "ios" && buildProfile.simulator === true !== !physical) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} is not an internal development build for this ${platform} target.`, `Set developmentClient: true and distribution: internal${platform === "ios" ? `, with ios.simulator: ${!physical}` : ""} in that eas.json profile.`);
|
|
114
|
+
if (typeof projectId !== "string" || !projectId) throw new EasFailure("STIM_BAD_ARG", "The Expo app is not linked to an EAS project.", "Link the intended EAS project with eas init, then retry.");
|
|
115
|
+
if (platform === "ios" ? buildProfile.buildConfiguration !== void 0 && buildProfile.buildConfiguration !== "Debug" : buildProfile.gradleCommand !== void 0 && (typeof buildProfile.gradleCommand !== "string" || !/^:app:assemble\w*Debug$/.test(buildProfile.gradleCommand))) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} overrides the development build configuration.`, "Use ios.buildConfiguration: Debug or an android.gradleCommand that assembles a single Debug APK, or remove the override.");
|
|
116
|
+
note(`eas fingerprinting ${platform} with profile ${profile}`);
|
|
117
|
+
const fingerprint = object(run([
|
|
118
|
+
"fingerprint:generate",
|
|
119
|
+
"--platform",
|
|
120
|
+
platform,
|
|
121
|
+
"--build-profile",
|
|
122
|
+
profile
|
|
123
|
+
])).hash;
|
|
124
|
+
if (typeof fingerprint !== "string" || !/^[a-f0-9]{40,64}$/.test(fingerprint)) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS returned no valid native fingerprint.", `Run npx eas-cli fingerprint:generate --platform ${platform} --build-profile ${shellArg(profile)} to inspect the result.`);
|
|
125
|
+
note(`eas looking for ${fingerprint.slice(0, 8)}`);
|
|
126
|
+
const buildId = selectEasBuild(run([
|
|
127
|
+
"build:list",
|
|
128
|
+
"--platform",
|
|
129
|
+
platform,
|
|
130
|
+
"--build-profile",
|
|
131
|
+
profile,
|
|
132
|
+
"--fingerprint-hash",
|
|
133
|
+
fingerprint,
|
|
134
|
+
"--status",
|
|
135
|
+
"finished",
|
|
136
|
+
...platform === "ios" && !physical ? ["--simulator"] : ["--distribution", "internal"],
|
|
137
|
+
"--limit",
|
|
138
|
+
"1"
|
|
139
|
+
]), {
|
|
140
|
+
platform,
|
|
141
|
+
profile,
|
|
142
|
+
fingerprint,
|
|
143
|
+
projectId,
|
|
144
|
+
physical
|
|
285
145
|
});
|
|
146
|
+
if (!buildId) throw new EasFailure("STIM_EAS_BUILD_MISSING", `No compatible EAS ${platform} build exists for profile ${profile} and fingerprint ${fingerprint}.`, `A cloud build may incur charges. With approval to run it, use ${buildCommand}, then retry ${retry}.`);
|
|
147
|
+
const cacheKey = `eas-${createHash("sha256").update(JSON.stringify({
|
|
148
|
+
projectId,
|
|
149
|
+
buildId
|
|
150
|
+
})).digest("hex")}-debug-${physical && platform === "ios" ? "device" : "sim"}`;
|
|
151
|
+
lock = acquireBuildLock({
|
|
152
|
+
platform,
|
|
153
|
+
key: cacheKey,
|
|
154
|
+
root
|
|
155
|
+
});
|
|
156
|
+
if (!lock.acquired) throw new EasFailure("STIM_EAS_UNAVAILABLE", `Another run holds the EAS artifact claim at ${lock.path}.`, `Wait for that run to finish, then retry ${retry}.`);
|
|
157
|
+
note(`eas resolving artifact for build ${buildId}`);
|
|
158
|
+
const result = object(run([
|
|
159
|
+
"build:download",
|
|
160
|
+
"--build-id",
|
|
161
|
+
buildId
|
|
162
|
+
], 6e5));
|
|
163
|
+
if (typeof result.path !== "string" || !existsSync(result.path)) throw new Error("EAS returned no downloaded artifact.");
|
|
164
|
+
const path = realpathSync(result.path);
|
|
165
|
+
if (platform === "ios" ? !path.endsWith(".app") || !statSync(path).isDirectory() : !path.endsWith(".apk") || !statSync(path).isFile()) throw new Error("EAS returned an unexpected artifact path or type.");
|
|
166
|
+
note(`eas artifact ready ${fingerprint.slice(0, 8)}`);
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
path,
|
|
170
|
+
fingerprint,
|
|
171
|
+
cacheKey,
|
|
172
|
+
cacheHit: "remote"
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const claim = (0, ownership_claim_exports.claimFailure)(error, null);
|
|
176
|
+
if (claim) return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: claim.code,
|
|
179
|
+
message: claim.message,
|
|
180
|
+
remedy: claim.remedy
|
|
181
|
+
};
|
|
182
|
+
if (error instanceof EasFailure) return {
|
|
183
|
+
ok: false,
|
|
184
|
+
code: error.code,
|
|
185
|
+
message: error.message,
|
|
186
|
+
remedy: error.remedy
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
code: "STIM_EAS_UNAVAILABLE",
|
|
191
|
+
message: `Could not resolve the EAS development build: ${error.message}`,
|
|
192
|
+
remedy: `Retry ${retry}. No cloud build was started.`
|
|
193
|
+
};
|
|
286
194
|
} finally {
|
|
287
|
-
|
|
288
|
-
else {
|
|
289
|
-
child?.stdout?.destroy();
|
|
290
|
-
child?.stderr?.destroy();
|
|
291
|
-
child?.unref();
|
|
292
|
-
}
|
|
195
|
+
releaseBuildLock(lock);
|
|
293
196
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/engine/metro-warmup.ts
|
|
200
|
+
const WARMUP_TIMEOUT_MS = 6e4;
|
|
201
|
+
async function requestLocal(url, platform, prefetch) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const req = request(url, { headers: {
|
|
204
|
+
accept: "application/json",
|
|
205
|
+
"expo-platform": platform,
|
|
206
|
+
...prefetch ? { "x-stim-metro-warmup": "1" } : {}
|
|
207
|
+
} });
|
|
208
|
+
const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
|
|
209
|
+
timer.unref();
|
|
210
|
+
req.on("socket", (socket) => socket.unref());
|
|
211
|
+
req.on("error", () => resolve(null));
|
|
212
|
+
req.on("close", () => {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
resolve(null);
|
|
306
215
|
});
|
|
307
|
-
|
|
308
|
-
|
|
216
|
+
req.on("response", (res) => {
|
|
217
|
+
let body = "";
|
|
218
|
+
res.setEncoding("utf8");
|
|
219
|
+
res.on("data", (chunk) => {
|
|
220
|
+
if (!prefetch) body += chunk;
|
|
221
|
+
});
|
|
222
|
+
res.on("error", () => resolve(null));
|
|
223
|
+
res.on("end", () => resolve(res.statusCode === 200 ? body : null));
|
|
224
|
+
});
|
|
225
|
+
req.end();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
|
|
229
|
+
try {
|
|
230
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
231
|
+
if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
|
|
232
|
+
const url = new URL("/index.bundle", origin);
|
|
233
|
+
url.search = new URLSearchParams({
|
|
234
|
+
platform,
|
|
235
|
+
dev: "true",
|
|
236
|
+
lazy: "true",
|
|
237
|
+
minify: "false",
|
|
238
|
+
...platform === "ios" ? { inlineSourceMap: "false" } : {},
|
|
239
|
+
modulesOnly: "false",
|
|
240
|
+
runModule: "true",
|
|
241
|
+
excludeSource: "true",
|
|
242
|
+
sourcePaths: "url-server",
|
|
243
|
+
...appId ? { app: appId } : {}
|
|
244
|
+
}).toString();
|
|
245
|
+
if (!bundleUrl && isExpo) {
|
|
246
|
+
const body = await requestLocal(new URL(origin), platform, false);
|
|
247
|
+
if (body === null) return;
|
|
248
|
+
const manifest = JSON.parse(body);
|
|
249
|
+
bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
|
|
250
|
+
if (typeof bundleUrl !== "string") return;
|
|
251
|
+
}
|
|
252
|
+
if (bundleUrl) {
|
|
253
|
+
const bundle = new URL(bundleUrl, origin);
|
|
254
|
+
url.pathname = bundle.pathname;
|
|
255
|
+
url.search = bundle.search;
|
|
256
|
+
}
|
|
257
|
+
await requestLocal(url, platform, true);
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/commands/native-runtime.ts
|
|
262
|
+
const sleep$1 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
263
|
+
const GATE_RETRY_DELAYS_MS = [
|
|
264
|
+
3e3,
|
|
265
|
+
7e3,
|
|
266
|
+
1e4
|
|
267
|
+
];
|
|
268
|
+
function gateShouldRetry(resolution) {
|
|
269
|
+
if (resolution?.metro) return false;
|
|
270
|
+
return resolution?.kind !== NOT_OURS_FOREIGN_CWD;
|
|
271
|
+
}
|
|
272
|
+
async function resolveMetroWithRetry(resolve, port, root, { delays = GATE_RETRY_DELAYS_MS, sleep: wait = sleep$1, onRetry = (_info) => {} } = {}) {
|
|
273
|
+
let resolution = await resolve(port, root);
|
|
274
|
+
for (let i = 0; i < delays.length && gateShouldRetry(resolution); i++) {
|
|
275
|
+
const delayMs = delays[i];
|
|
276
|
+
if (delayMs === void 0) break;
|
|
277
|
+
onRetry({
|
|
278
|
+
attempt: i + 1,
|
|
279
|
+
delayMs,
|
|
280
|
+
resolution
|
|
281
|
+
});
|
|
282
|
+
await wait(delayMs);
|
|
283
|
+
resolution = await resolve(port, root);
|
|
309
284
|
}
|
|
310
|
-
return
|
|
285
|
+
return resolution;
|
|
286
|
+
}
|
|
287
|
+
function noMetroMessage({ port, resolution, supervisor, supervisorAlive }) {
|
|
288
|
+
const foreign = resolution?.notOurs;
|
|
289
|
+
if (supervisor && supervisor.port === port && supervisorAlive) {
|
|
290
|
+
const mode = supervisor.mode ? `${supervisor.mode} ` : "";
|
|
291
|
+
return `A supervisor record exists for port ${port} (pid ${supervisor.pid}, ${mode}dev server) but it did not verify as this workspace's Metro${foreign ? `: ${foreign}` : " -- nothing answered /status"}. Metro may still be indexing this project (a monorepo file-map crawl blocks its event loop for ~20s after the port opens).`;
|
|
292
|
+
}
|
|
293
|
+
if (foreign) return `Port ${port} is in use but is NOT this workspace's dev server: ${foreign}.`;
|
|
294
|
+
return `Nothing is serving this workspace's dev server on port ${port}.`;
|
|
295
|
+
}
|
|
296
|
+
function noMetroRemedy({ port, supervisor, supervisorAlive }) {
|
|
297
|
+
if (supervisor && supervisor.port === port && supervisorAlive) return "Re-run `stim ios` in a few seconds, or give the dev server longer to verify with `stim start --wait <seconds>`.";
|
|
298
|
+
return "Run `stim start` first, or pass --no-metro-check.";
|
|
299
|
+
}
|
|
300
|
+
async function ensureWorkspaceStorageSafely(root, { note = (_line) => {} } = {}) {
|
|
301
|
+
try {
|
|
302
|
+
return ensureWorkspaceStorage(root);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
note(chalk.dim(`Could not prepare this workspace's Stim state: ${err?.message || err}`));
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function launchOutcomeRecord({ launchState, release, bundleId, configuration, metroPort }) {
|
|
309
|
+
const unverified = launchState === LAUNCH_UNVERIFIED;
|
|
310
|
+
const bundling = launchState === LAUNCH_BUNDLING;
|
|
311
|
+
let msg;
|
|
312
|
+
if (release) msg = unverified ? `${bundleId} could not be verified as running after its ${configuration} launch` : `${bundleId} is running its embedded ${configuration} bundle`;
|
|
313
|
+
else if (unverified) msg = `no bundle request from ${bundleId} reached this workspace's Metro on port ${metroPort}`;
|
|
314
|
+
else if (bundling) msg = `${bundleId} requested a bundle from this workspace's Metro on port ${metroPort}; it was still being built when the launch check ended`;
|
|
315
|
+
else msg = `${bundleId} fetched a bundle from this workspace's Metro on port ${metroPort}`;
|
|
316
|
+
return {
|
|
317
|
+
src: "build",
|
|
318
|
+
level: unverified ? "warn" : "info",
|
|
319
|
+
event: unverified ? "launch_unverified" : bundling ? "launch_bundling" : "launch_verified",
|
|
320
|
+
msg
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function isPhysicalDeviceRequest(flag) {
|
|
324
|
+
return flag !== null && flag !== void 0 && flag !== false;
|
|
311
325
|
}
|
|
312
326
|
//#endregion
|
|
313
|
-
//#region src/engine/device.ts
|
|
314
|
-
const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
|
|
315
|
-
const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
|
|
327
|
+
//#region src/engine/device-ios.ts
|
|
316
328
|
function startIosBoot(udid, configure, label, out, simulatorApp) {
|
|
317
329
|
const done = (async () => {
|
|
318
330
|
await bootIosSim(udid, {
|
|
@@ -328,37 +340,6 @@ function startIosBoot(udid, configure, label, out, simulatorApp) {
|
|
|
328
340
|
done
|
|
329
341
|
};
|
|
330
342
|
}
|
|
331
|
-
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 }) {
|
|
332
|
-
if (slot && slot !== "default") label = `${label}-${slot}`;
|
|
333
|
-
const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
|
|
334
|
-
if (platform === "ios") return ensureOwnedIosDevice({
|
|
335
|
-
record,
|
|
336
|
-
projectPath,
|
|
337
|
-
slot,
|
|
338
|
-
settingsRoot,
|
|
339
|
-
label,
|
|
340
|
-
settings,
|
|
341
|
-
flags,
|
|
342
|
-
note,
|
|
343
|
-
out,
|
|
344
|
-
reconcileIosSimulator
|
|
345
|
-
});
|
|
346
|
-
return ensureOwnedAndroidDevice({
|
|
347
|
-
record,
|
|
348
|
-
projectPath,
|
|
349
|
-
slot,
|
|
350
|
-
settingsRoot,
|
|
351
|
-
label,
|
|
352
|
-
settings,
|
|
353
|
-
flags,
|
|
354
|
-
note,
|
|
355
|
-
out,
|
|
356
|
-
logFile,
|
|
357
|
-
alive,
|
|
358
|
-
configureAvd,
|
|
359
|
-
teardownAvd
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
343
|
async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
|
|
363
344
|
const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
|
|
364
345
|
if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
|
|
@@ -608,26 +589,274 @@ async function configureOwnedIosSim({ record, projectPath, slot, profile, out, r
|
|
|
608
589
|
setDevice(projectPath, "ios", updated, slot);
|
|
609
590
|
return updated;
|
|
610
591
|
}
|
|
611
|
-
async function
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
592
|
+
async function ensureIosBooted({ device, simulatorApp, timeoutMs, pollMs, out }) {
|
|
593
|
+
const udid = device?.deviceUdid;
|
|
594
|
+
if (!udid) return {
|
|
595
|
+
failed: true,
|
|
596
|
+
reason: "No iOS simulator is recorded for this project."
|
|
597
|
+
};
|
|
598
|
+
const ready = () => {
|
|
599
|
+
try {
|
|
600
|
+
getExecutor().runFile("xcrun", [
|
|
601
|
+
"simctl",
|
|
602
|
+
"spawn",
|
|
603
|
+
udid,
|
|
604
|
+
"launchctl",
|
|
605
|
+
"list"
|
|
606
|
+
], { timeoutMs: 3e4 });
|
|
607
|
+
return {
|
|
608
|
+
ok: true,
|
|
609
|
+
udid
|
|
610
|
+
};
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return {
|
|
613
|
+
failed: true,
|
|
614
|
+
reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const booting = device?.booting;
|
|
619
|
+
if (booting?.udid === udid) {
|
|
620
|
+
try {
|
|
621
|
+
await booting.done;
|
|
622
|
+
} catch (e) {
|
|
623
|
+
return {
|
|
624
|
+
failed: true,
|
|
625
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return ready();
|
|
629
|
+
}
|
|
630
|
+
let resolved;
|
|
631
|
+
try {
|
|
632
|
+
resolved = resolveOwnedIosSim(udid);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
return {
|
|
635
|
+
failed: true,
|
|
636
|
+
reason: `Could not list simulators: ${e?.message || e}`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (resolved.missing) return {
|
|
640
|
+
failed: true,
|
|
641
|
+
reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
|
|
642
|
+
};
|
|
643
|
+
if (resolved.notOwned) return {
|
|
644
|
+
failed: true,
|
|
645
|
+
reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
|
|
646
|
+
};
|
|
647
|
+
const sim = resolved.sim;
|
|
648
|
+
if (sim.state === "Booted") {
|
|
649
|
+
const result = ready();
|
|
650
|
+
if (result.ok && simulatorApp !== void 0) configuredIosSimulatorViewer(simulatorApp).open(udid);
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
|
|
654
|
+
const bootDeadline = Date.now() + timeoutMs;
|
|
655
|
+
try {
|
|
656
|
+
await bootIosSim(udid, {
|
|
657
|
+
timeoutMs,
|
|
658
|
+
label: sim.name,
|
|
659
|
+
out,
|
|
660
|
+
simulatorApp
|
|
661
|
+
});
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return {
|
|
664
|
+
failed: true,
|
|
665
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
|
|
669
|
+
while (Date.now() < deadline) {
|
|
670
|
+
await sleep(pollMs);
|
|
671
|
+
let state = null;
|
|
672
|
+
try {
|
|
673
|
+
state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
|
|
674
|
+
} catch {}
|
|
675
|
+
if (state === "Booted") return ready();
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
failed: true,
|
|
679
|
+
reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function sleep(ms) {
|
|
683
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/engine/android-avd-setup.ts
|
|
687
|
+
var AvdBootError = class extends Error {
|
|
688
|
+
remedy;
|
|
689
|
+
constructor(message, remedy, cause) {
|
|
690
|
+
super(message, { cause });
|
|
691
|
+
this.remedy = remedy;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
var AvdRecoveryError = class extends AvdBootError {};
|
|
695
|
+
function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
|
|
696
|
+
for (const [path, project] of Object.entries(loadConfig()?.projects ?? {})) if (projectDeviceSlots(project).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, configuration, configure, teardown = teardownOwnedAvd }) {
|
|
700
|
+
const { claim, reservation, previous, avdName } = withConfigLock(() => {
|
|
701
|
+
const previous = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
702
|
+
if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
|
|
703
|
+
if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
|
|
704
|
+
const avdName = ownedAvdName(label);
|
|
705
|
+
const claim = acquireAvdClaim(avdName);
|
|
706
|
+
const reservation = {
|
|
707
|
+
avdName,
|
|
708
|
+
owned: true,
|
|
709
|
+
deviceName: avdName,
|
|
710
|
+
setupIncomplete: true,
|
|
711
|
+
poolConfiguration: configuration
|
|
712
|
+
};
|
|
713
|
+
try {
|
|
714
|
+
setDevice(projectPath, "android", reservation, slot);
|
|
715
|
+
return {
|
|
716
|
+
claim,
|
|
717
|
+
reservation,
|
|
718
|
+
previous,
|
|
719
|
+
avdName
|
|
720
|
+
};
|
|
721
|
+
} catch (error) {
|
|
722
|
+
releaseClaim(claim);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
const currentRecord = () => deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
|
|
727
|
+
const assertReservation = () => {
|
|
728
|
+
if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
|
|
729
|
+
const owner = findOtherOwner(avdName, projectPath, slot);
|
|
730
|
+
if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
|
|
731
|
+
const current = currentRecord();
|
|
732
|
+
if (!isDeepStrictEqual(current, reservation)) {
|
|
733
|
+
const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
|
|
734
|
+
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.`);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
let prepared;
|
|
738
|
+
let child;
|
|
739
|
+
const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
|
|
740
|
+
let configurationFailure;
|
|
741
|
+
try {
|
|
742
|
+
try {
|
|
743
|
+
const creation = await createOwnedAvd(label, {
|
|
744
|
+
systemImage,
|
|
745
|
+
spawn: (...args) => {
|
|
746
|
+
markClaimChildPending(claim);
|
|
747
|
+
child = getExecutor().spawn(...args);
|
|
748
|
+
if (child.pid !== void 0) {
|
|
749
|
+
const captured = captureProcessIdentity(child.pid);
|
|
750
|
+
if (captured.ok) try {
|
|
751
|
+
setClaimChild(claim, {
|
|
752
|
+
pid: child.pid,
|
|
753
|
+
processToken: captured.token
|
|
754
|
+
});
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
return child;
|
|
758
|
+
}
|
|
759
|
+
}).then((result) => ({ result }), (error) => ({ error }));
|
|
760
|
+
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)}`);
|
|
761
|
+
clearClaimChild(claim);
|
|
762
|
+
if ("error" in creation) throw creation.error;
|
|
763
|
+
prepared = {
|
|
764
|
+
...creation.result,
|
|
765
|
+
created: true
|
|
766
|
+
};
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const message = String(error?.message || error);
|
|
769
|
+
if (!message.includes("already exists")) throw error;
|
|
770
|
+
try {
|
|
771
|
+
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);
|
|
772
|
+
withConfigLock(assertReservation);
|
|
773
|
+
const resolved = resolveOwnedAvdSerial(avdName);
|
|
774
|
+
if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
|
|
775
|
+
if (!resolved.serial) assertOwnedAvdStopped(avdName);
|
|
776
|
+
prepared = {
|
|
777
|
+
avdName,
|
|
778
|
+
created: false,
|
|
779
|
+
systemImage: ownedAvdSystemImage(avdName),
|
|
780
|
+
...resolved.serial ? {
|
|
781
|
+
serial: resolved.serial,
|
|
782
|
+
consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
|
|
783
|
+
} : {}
|
|
784
|
+
};
|
|
785
|
+
} catch (recoveryError) {
|
|
786
|
+
withConfigLock(() => {
|
|
787
|
+
if (!isDeepStrictEqual(currentRecord(), reservation)) return;
|
|
788
|
+
if (previous) setDevice(projectPath, "android", previous, slot);
|
|
789
|
+
else clearDevice(projectPath, "android", slot, avdName);
|
|
790
|
+
});
|
|
791
|
+
if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
|
|
792
|
+
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);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (prepared.created) try {
|
|
796
|
+
configure(avdName);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
configurationFailure = { error };
|
|
799
|
+
}
|
|
800
|
+
if (!configurationFailure) withConfigLock(() => {
|
|
801
|
+
assertReservation();
|
|
802
|
+
setDevice(projectPath, "android", {
|
|
803
|
+
avdName,
|
|
804
|
+
owned: true,
|
|
805
|
+
deviceName: avdName,
|
|
806
|
+
...prepared.created ? { poolConfiguration: configuration } : {},
|
|
807
|
+
...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
|
|
808
|
+
}, slot);
|
|
809
|
+
});
|
|
810
|
+
} finally {
|
|
811
|
+
if (!creationRunning()) releaseClaim(claim);
|
|
812
|
+
else {
|
|
813
|
+
child?.stdout?.destroy();
|
|
814
|
+
child?.stderr?.destroy();
|
|
815
|
+
child?.unref();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (configurationFailure) {
|
|
819
|
+
const { error } = configurationFailure;
|
|
820
|
+
const cleanup = teardown(avdName, {
|
|
821
|
+
del: true,
|
|
822
|
+
owner: {
|
|
823
|
+
projectPath,
|
|
824
|
+
slot,
|
|
825
|
+
expectedRecord: reservation
|
|
826
|
+
},
|
|
827
|
+
onRemoved: () => {
|
|
828
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
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\`.` : "";
|
|
832
|
+
throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
|
|
833
|
+
}
|
|
834
|
+
return prepared;
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/engine/device-android.ts
|
|
838
|
+
const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
|
|
839
|
+
const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
|
|
840
|
+
async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
|
|
841
|
+
const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
|
|
842
|
+
const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
|
|
843
|
+
if (record?.setupIncomplete && record.avdName) {
|
|
844
|
+
const avdName = record.avdName;
|
|
845
|
+
const cleanup = teardownAvd(avdName, {
|
|
846
|
+
del: true,
|
|
847
|
+
owner: {
|
|
848
|
+
projectPath,
|
|
849
|
+
slot,
|
|
850
|
+
expectedRecord: record
|
|
851
|
+
},
|
|
852
|
+
onRemoved: () => {
|
|
853
|
+
clearDevice(projectPath, "android", slot, avdName);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
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.");
|
|
857
|
+
record = null;
|
|
858
|
+
}
|
|
859
|
+
if (record?.avdName) {
|
|
631
860
|
if (record.owned) {
|
|
632
861
|
const resolved = resolveOwnedAvdSerial(record.avdName);
|
|
633
862
|
if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned by name -- creating a fresh owned AVD instead of reusing it.`));
|
|
@@ -786,302 +1015,84 @@ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, slot, d
|
|
|
786
1015
|
slot,
|
|
787
1016
|
avdName,
|
|
788
1017
|
deviceName,
|
|
789
|
-
livePorts: liveAndroidConsolePorts(),
|
|
790
|
-
metadata
|
|
791
|
-
});
|
|
792
|
-
const serial = `emulator-${claim.consolePort}`;
|
|
793
|
-
try {
|
|
794
|
-
reportAndroidMemoryPressure(out);
|
|
795
|
-
const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
796
|
-
out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
|
|
797
|
-
const result = await waitForAndroidBoot({
|
|
798
|
-
serial,
|
|
799
|
-
timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
|
|
800
|
-
pid,
|
|
801
|
-
alive,
|
|
802
|
-
out
|
|
803
|
-
});
|
|
804
|
-
if (result.failed) throw new AvdBootError(result.reason, result.remedy);
|
|
805
|
-
const running = getAvdNameForSerial(serial);
|
|
806
|
-
if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
|
|
807
|
-
return {
|
|
808
|
-
...claim,
|
|
809
|
-
serial
|
|
810
|
-
};
|
|
811
|
-
} catch (error) {
|
|
812
|
-
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
813
|
-
throw error;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function reportAndroidMemoryPressure(out) {
|
|
817
|
-
const pressure = readHostMemoryPressure();
|
|
818
|
-
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.`)));
|
|
819
|
-
}
|
|
820
|
-
function androidBootRemedy(pressure) {
|
|
821
|
-
let count = null;
|
|
822
|
-
try {
|
|
823
|
-
count = liveOwnedDeviceCount({
|
|
824
|
-
sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
|
|
825
|
-
adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
|
|
826
|
-
config: loadConfig()
|
|
827
|
-
});
|
|
828
|
-
} catch {}
|
|
829
|
-
const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
|
|
830
|
-
const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
|
|
831
|
-
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.`;
|
|
832
|
-
return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
|
|
833
|
-
}
|
|
834
|
-
async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
|
|
835
|
-
const aborted = () => pid !== null && !alive(pid);
|
|
836
|
-
const wait = (windowMs) => waitForBoot(serial, windowMs, {
|
|
837
|
-
aborted,
|
|
838
|
-
commandTimeoutMs: 5e3
|
|
839
|
-
});
|
|
840
|
-
let result = await wait(timeoutMs);
|
|
841
|
-
if (result.ok) return {
|
|
842
|
-
ok: true,
|
|
843
|
-
serial
|
|
844
|
-
};
|
|
845
|
-
let pressure = readHostMemoryPressure();
|
|
846
|
-
let elapsedMs = timeoutMs;
|
|
847
|
-
if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
|
|
848
|
-
const extensionMs = 24e4;
|
|
849
|
-
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).`)));
|
|
850
|
-
result = await wait(extensionMs);
|
|
851
|
-
if (result.ok) return {
|
|
852
|
-
ok: true,
|
|
853
|
-
serial
|
|
854
|
-
};
|
|
855
|
-
elapsedMs += extensionMs;
|
|
856
|
-
pressure = readHostMemoryPressure();
|
|
857
|
-
}
|
|
858
|
-
const exited = result.exited || aborted();
|
|
859
|
-
return {
|
|
860
|
-
failed: true,
|
|
861
|
-
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)}`,
|
|
862
|
-
remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
|
|
866
|
-
let count = 0;
|
|
867
|
-
for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
|
|
868
|
-
const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
|
|
869
|
-
for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
|
|
870
|
-
const android = platforms.android;
|
|
871
|
-
if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
|
|
872
|
-
}
|
|
873
|
-
return count;
|
|
874
|
-
}
|
|
875
|
-
function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
|
|
876
|
-
if (!platform) return false;
|
|
877
|
-
const record = deviceSlotPlatforms(project, slot)?.[platform];
|
|
878
|
-
if (!record) return false;
|
|
879
|
-
if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
|
|
880
|
-
return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
|
|
881
|
-
}
|
|
882
|
-
function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
|
|
883
|
-
if (!max || max <= 0) return null;
|
|
884
|
-
const adbEmulators = adb?.emulators || [];
|
|
885
|
-
if (workspaceHasLiveDevice({
|
|
886
|
-
platform,
|
|
887
|
-
project,
|
|
888
|
-
slot,
|
|
889
|
-
sims,
|
|
890
|
-
adbEmulators
|
|
891
|
-
})) return null;
|
|
892
|
-
const count = liveOwnedDeviceCount({
|
|
893
|
-
sims,
|
|
894
|
-
adbEmulators,
|
|
895
|
-
config
|
|
896
|
-
});
|
|
897
|
-
if (count < max) return null;
|
|
898
|
-
return {
|
|
899
|
-
code: "STIM_AT_CAPACITY",
|
|
900
|
-
message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
|
|
901
|
-
remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
|
|
905
|
-
if (!max || max <= 0) return null;
|
|
906
|
-
let simList = [];
|
|
907
|
-
let adbRes = {
|
|
908
|
-
emulators: [],
|
|
909
|
-
physical: [],
|
|
910
|
-
unhealthy: []
|
|
911
|
-
};
|
|
912
|
-
try {
|
|
913
|
-
simList = typeof sims === "function" ? sims() || [] : sims || [];
|
|
914
|
-
} catch {}
|
|
915
|
-
try {
|
|
916
|
-
adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
|
|
917
|
-
} catch {}
|
|
918
|
-
let cfg = null;
|
|
919
|
-
try {
|
|
920
|
-
cfg = typeof config === "function" ? config() : config ?? null;
|
|
921
|
-
} catch {}
|
|
922
|
-
return deviceCapacityRefusal({
|
|
923
|
-
platform,
|
|
924
|
-
project,
|
|
925
|
-
slot,
|
|
926
|
-
max,
|
|
927
|
-
sims: simList,
|
|
928
|
-
adb: adbRes,
|
|
929
|
-
config: cfg
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
|
|
933
|
-
if (!requestedName || !recordedTypeId) return null;
|
|
934
|
-
const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
|
|
935
|
-
if (!wanted) return null;
|
|
936
|
-
if (wanted.identifier === recordedTypeId) return null;
|
|
937
|
-
const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
|
|
938
|
-
return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
|
|
939
|
-
}
|
|
940
|
-
function installedNames(names) {
|
|
941
|
-
const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
|
|
942
|
-
return unique.length > 0 ? unique.join(", ") : "none";
|
|
943
|
-
}
|
|
944
|
-
function unknownIosRuntimeRefusal(requested, runtimes) {
|
|
945
|
-
if (!requested) return null;
|
|
946
|
-
if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
|
|
947
|
-
return {
|
|
948
|
-
message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
|
|
949
|
-
remedy: "Pass `--runtime` (or set ios.runtime) a version printed above (\"26.5\") or a runtime's full name (\"iOS 26.5\"); nothing else matches. Install more runtimes through Xcode."
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
function creatableIosDeviceTypeNames(runtimes, runtime) {
|
|
953
|
-
return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
|
|
954
|
-
}
|
|
955
|
-
function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
|
|
956
|
-
if (!requested) return null;
|
|
957
|
-
const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
|
|
958
|
-
if (creatable.includes(requested)) return null;
|
|
959
|
-
return {
|
|
960
|
-
message: `No device type named "${requested}" can be created on ${runtime ? `runtime ${runtime}` : "any installed simulator runtime"}. ${runtime ? `Device types runtime ${runtime} supports` : "Device types the installed runtimes support"}: ${installedNames(creatable)}.`,
|
|
961
|
-
remedy: "Pass `--device-type` (or set ios.deviceType) one of the names printed above, exactly as `xcrun simctl list devicetypes` spells it. `xcrun simctl list devicetypes` also lists watchOS, tvOS and visionOS models, which no iOS runtime can create. Install more models through Xcode."
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
function unknownAndroidSystemImageRefusal(requested, images) {
|
|
965
|
-
if (!requested) return null;
|
|
966
|
-
if ((images || []).some((i) => i.pkg === requested)) return null;
|
|
967
|
-
return {
|
|
968
|
-
message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
|
|
969
|
-
remedy: "Pass `--system-image` (or set android.systemImage) to one of the package ids printed above. Install more with `sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`."
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
const BOOT_POLL_MS = 500;
|
|
973
|
-
async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists } = {}) {
|
|
974
|
-
if (platform === "ios") return ensureIosBooted({
|
|
975
|
-
device,
|
|
976
|
-
simulatorApp,
|
|
977
|
-
timeoutMs: timeoutMs ?? 6e5,
|
|
978
|
-
pollMs,
|
|
979
|
-
out
|
|
980
|
-
});
|
|
981
|
-
if (platform === "android") return ensureAndroidBooted({
|
|
982
|
-
device,
|
|
983
|
-
timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
|
|
984
|
-
out,
|
|
985
|
-
logFile,
|
|
986
|
-
alive
|
|
987
|
-
});
|
|
988
|
-
return {
|
|
989
|
-
failed: true,
|
|
990
|
-
reason: `Unknown platform "${platform}".`
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
async function ensureIosBooted({ device, simulatorApp, timeoutMs, pollMs, out }) {
|
|
994
|
-
const udid = device?.deviceUdid;
|
|
995
|
-
if (!udid) return {
|
|
996
|
-
failed: true,
|
|
997
|
-
reason: "No iOS simulator is recorded for this project."
|
|
998
|
-
};
|
|
999
|
-
const ready = () => {
|
|
1000
|
-
try {
|
|
1001
|
-
getExecutor().runFile("xcrun", [
|
|
1002
|
-
"simctl",
|
|
1003
|
-
"spawn",
|
|
1004
|
-
udid,
|
|
1005
|
-
"launchctl",
|
|
1006
|
-
"list"
|
|
1007
|
-
], { timeoutMs: 3e4 });
|
|
1008
|
-
return {
|
|
1009
|
-
ok: true,
|
|
1010
|
-
udid
|
|
1011
|
-
};
|
|
1012
|
-
} catch (error) {
|
|
1013
|
-
return {
|
|
1014
|
-
failed: true,
|
|
1015
|
-
reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
const booting = device?.booting;
|
|
1020
|
-
if (booting?.udid === udid) {
|
|
1021
|
-
try {
|
|
1022
|
-
await booting.done;
|
|
1023
|
-
} catch (e) {
|
|
1024
|
-
return {
|
|
1025
|
-
failed: true,
|
|
1026
|
-
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
return ready();
|
|
1030
|
-
}
|
|
1031
|
-
let resolved;
|
|
1032
|
-
try {
|
|
1033
|
-
resolved = resolveOwnedIosSim(udid);
|
|
1034
|
-
} catch (e) {
|
|
1035
|
-
return {
|
|
1036
|
-
failed: true,
|
|
1037
|
-
reason: `Could not list simulators: ${e?.message || e}`
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
if (resolved.missing) return {
|
|
1041
|
-
failed: true,
|
|
1042
|
-
reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
|
|
1043
|
-
};
|
|
1044
|
-
if (resolved.notOwned) return {
|
|
1045
|
-
failed: true,
|
|
1046
|
-
reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
|
|
1047
|
-
};
|
|
1048
|
-
const sim = resolved.sim;
|
|
1049
|
-
if (sim.state === "Booted") {
|
|
1050
|
-
const result = ready();
|
|
1051
|
-
if (result.ok && simulatorApp !== void 0) configuredIosSimulatorViewer(simulatorApp).open(udid);
|
|
1052
|
-
return result;
|
|
1053
|
-
}
|
|
1054
|
-
out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
|
|
1055
|
-
const bootDeadline = Date.now() + timeoutMs;
|
|
1018
|
+
livePorts: liveAndroidConsolePorts(),
|
|
1019
|
+
metadata
|
|
1020
|
+
});
|
|
1021
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
1056
1022
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1023
|
+
reportAndroidMemoryPressure(out);
|
|
1024
|
+
const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
1025
|
+
out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
|
|
1026
|
+
const result = await waitForAndroidBoot({
|
|
1027
|
+
serial,
|
|
1028
|
+
timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
|
|
1029
|
+
pid,
|
|
1030
|
+
alive,
|
|
1031
|
+
out
|
|
1062
1032
|
});
|
|
1063
|
-
|
|
1033
|
+
if (result.failed) throw new AvdBootError(result.reason, result.remedy);
|
|
1034
|
+
const running = getAvdNameForSerial(serial);
|
|
1035
|
+
if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
|
|
1064
1036
|
return {
|
|
1065
|
-
|
|
1066
|
-
|
|
1037
|
+
...claim,
|
|
1038
|
+
serial
|
|
1067
1039
|
};
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
1042
|
+
throw error;
|
|
1068
1043
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1044
|
+
}
|
|
1045
|
+
function reportAndroidMemoryPressure(out) {
|
|
1046
|
+
const pressure = readHostMemoryPressure();
|
|
1047
|
+
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.`)));
|
|
1048
|
+
}
|
|
1049
|
+
function androidBootRemedy(pressure) {
|
|
1050
|
+
let count = null;
|
|
1051
|
+
try {
|
|
1052
|
+
count = liveOwnedDeviceCount({
|
|
1053
|
+
sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
|
|
1054
|
+
adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
|
|
1055
|
+
config: loadConfig()
|
|
1056
|
+
});
|
|
1057
|
+
} catch {}
|
|
1058
|
+
const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
|
|
1059
|
+
const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
|
|
1060
|
+
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.`;
|
|
1061
|
+
return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
|
|
1062
|
+
}
|
|
1063
|
+
async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
|
|
1064
|
+
const aborted = () => pid !== null && !alive(pid);
|
|
1065
|
+
const wait = (windowMs) => waitForBoot(serial, windowMs, {
|
|
1066
|
+
aborted,
|
|
1067
|
+
commandTimeoutMs: 5e3
|
|
1068
|
+
});
|
|
1069
|
+
let result = await wait(timeoutMs);
|
|
1070
|
+
if (result.ok) return {
|
|
1071
|
+
ok: true,
|
|
1072
|
+
serial
|
|
1073
|
+
};
|
|
1074
|
+
let pressure = readHostMemoryPressure();
|
|
1075
|
+
let elapsedMs = timeoutMs;
|
|
1076
|
+
if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
|
|
1077
|
+
const extensionMs = 24e4;
|
|
1078
|
+
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).`)));
|
|
1079
|
+
result = await wait(extensionMs);
|
|
1080
|
+
if (result.ok) return {
|
|
1081
|
+
ok: true,
|
|
1082
|
+
serial
|
|
1083
|
+
};
|
|
1084
|
+
elapsedMs += extensionMs;
|
|
1085
|
+
pressure = readHostMemoryPressure();
|
|
1077
1086
|
}
|
|
1087
|
+
const exited = result.exited || aborted();
|
|
1078
1088
|
return {
|
|
1079
1089
|
failed: true,
|
|
1080
|
-
reason: `
|
|
1090
|
+
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)}`,
|
|
1091
|
+
remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
|
|
1081
1092
|
};
|
|
1082
1093
|
}
|
|
1083
|
-
async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, alive = pidExists }) {
|
|
1084
|
-
if (!device?.avdName) return {
|
|
1094
|
+
async function ensureAndroidBooted({ device, projectPath, slot, timeoutMs, out, logFile = null, alive = pidExists }) {
|
|
1095
|
+
if (!device?.avdName || !projectPath) return {
|
|
1085
1096
|
failed: true,
|
|
1086
1097
|
reason: "No owned Android emulator is recorded for this project."
|
|
1087
1098
|
};
|
|
@@ -1113,33 +1124,565 @@ async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, ali
|
|
|
1113
1124
|
timeoutMs,
|
|
1114
1125
|
out
|
|
1115
1126
|
});
|
|
1116
|
-
const
|
|
1127
|
+
const claim = claimAndroidConsolePort({
|
|
1128
|
+
projectPath,
|
|
1129
|
+
slot,
|
|
1130
|
+
avdName: device.avdName,
|
|
1131
|
+
deviceName: device.deviceName,
|
|
1132
|
+
livePorts: liveAndroidConsolePorts(),
|
|
1133
|
+
metadata: device
|
|
1134
|
+
});
|
|
1135
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
1117
1136
|
reportAndroidMemoryPressure(out);
|
|
1118
1137
|
out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
|
|
1119
1138
|
let pid = null;
|
|
1120
1139
|
try {
|
|
1121
|
-
pid = bootAndroidEmulator(device.avdName,
|
|
1140
|
+
pid = bootAndroidEmulator(device.avdName, claim.consolePort, { logFile });
|
|
1122
1141
|
} catch (e) {
|
|
1142
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
1123
1143
|
return {
|
|
1124
1144
|
failed: true,
|
|
1125
1145
|
reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
|
|
1126
1146
|
};
|
|
1127
1147
|
}
|
|
1128
|
-
|
|
1148
|
+
const result = await waitForAndroidBoot({
|
|
1129
1149
|
serial,
|
|
1130
1150
|
timeoutMs,
|
|
1131
1151
|
pid,
|
|
1132
1152
|
alive,
|
|
1133
1153
|
out
|
|
1134
1154
|
});
|
|
1155
|
+
if (result.failed) releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
|
|
1156
|
+
return result;
|
|
1135
1157
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/engine/device.ts
|
|
1160
|
+
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 }) {
|
|
1161
|
+
if (slot && slot !== "default") label = `${label}-${slot}`;
|
|
1162
|
+
const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
|
|
1163
|
+
if (platform === "ios") return ensureOwnedIosDevice({
|
|
1164
|
+
record,
|
|
1165
|
+
projectPath,
|
|
1166
|
+
slot,
|
|
1167
|
+
settingsRoot,
|
|
1168
|
+
label,
|
|
1169
|
+
settings,
|
|
1170
|
+
flags,
|
|
1171
|
+
note,
|
|
1172
|
+
out,
|
|
1173
|
+
reconcileIosSimulator
|
|
1174
|
+
});
|
|
1175
|
+
return ensureOwnedAndroidDevice({
|
|
1176
|
+
record,
|
|
1177
|
+
projectPath,
|
|
1178
|
+
slot,
|
|
1179
|
+
settingsRoot,
|
|
1180
|
+
label,
|
|
1181
|
+
settings,
|
|
1182
|
+
flags,
|
|
1183
|
+
note,
|
|
1184
|
+
out,
|
|
1185
|
+
logFile,
|
|
1186
|
+
alive,
|
|
1187
|
+
configureAvd,
|
|
1188
|
+
teardownAvd
|
|
1189
|
+
});
|
|
1140
1190
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1191
|
+
const BOOT_POLL_MS = 500;
|
|
1192
|
+
async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists, projectPath, slot } = {}) {
|
|
1193
|
+
if (platform === "ios") return ensureIosBooted({
|
|
1194
|
+
device,
|
|
1195
|
+
simulatorApp,
|
|
1196
|
+
timeoutMs: timeoutMs ?? 6e5,
|
|
1197
|
+
pollMs,
|
|
1198
|
+
out
|
|
1199
|
+
});
|
|
1200
|
+
if (platform === "android") return ensureAndroidBooted({
|
|
1201
|
+
device,
|
|
1202
|
+
projectPath,
|
|
1203
|
+
slot,
|
|
1204
|
+
timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
|
|
1205
|
+
out,
|
|
1206
|
+
logFile,
|
|
1207
|
+
alive
|
|
1208
|
+
});
|
|
1209
|
+
return {
|
|
1210
|
+
failed: true,
|
|
1211
|
+
reason: `Unknown platform "${platform}".`
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/engine/prebuild.ts
|
|
1216
|
+
const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
|
|
1217
|
+
const LAST_LINES$1 = 20;
|
|
1218
|
+
function nativeDirName(platform) {
|
|
1219
|
+
return platform === "android" ? "android" : "ios";
|
|
1220
|
+
}
|
|
1221
|
+
function nativeDir(root, platform) {
|
|
1222
|
+
return join(root, nativeDirName(platform));
|
|
1223
|
+
}
|
|
1224
|
+
function shouldPrebuild({ isExpo, nativeDirExists }) {
|
|
1225
|
+
return Boolean(isExpo) && !nativeDirExists;
|
|
1226
|
+
}
|
|
1227
|
+
function needsPrebuild(root, platform, isExpo) {
|
|
1228
|
+
return shouldPrebuild({
|
|
1229
|
+
isExpo,
|
|
1230
|
+
nativeDirExists: existsSync(nativeDir(root, platform))
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
function prebuildRefusal({ isExpo, platform, nativeDirExists }) {
|
|
1234
|
+
if (nativeDirExists || isExpo) return null;
|
|
1235
|
+
const dir = nativeDirName(platform);
|
|
1236
|
+
return {
|
|
1237
|
+
code: PREBUILD_ERROR,
|
|
1238
|
+
message: `This project has no ${dir}/ directory and is not an Expo (CNG) project, so Stim cannot generate one.`,
|
|
1239
|
+
remedy: `Check out or generate the native project (\`npx @react-native-community/cli init\` produced one originally), or add \`expo\` to the project so \`expo prebuild\` can create ${dir}/.`
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function prebuildFailure(logWriter, result) {
|
|
1243
|
+
logWriter?.write?.({
|
|
1244
|
+
src: "build",
|
|
1245
|
+
level: "error",
|
|
1246
|
+
msg: result.reason || "Expo prebuild failed.",
|
|
1247
|
+
event: "prebuild_failed"
|
|
1248
|
+
});
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
async function runPrebuild(root, platform, logWriter, { spawnFn = null, now = Date.now, isExpo = null } = {}) {
|
|
1252
|
+
const dirExists = existsSync(nativeDir(root, platform));
|
|
1253
|
+
const refusal = prebuildRefusal({
|
|
1254
|
+
isExpo: isExpo === null ? detectIsExpo(root) : isExpo,
|
|
1255
|
+
platform,
|
|
1256
|
+
nativeDirExists: dirExists
|
|
1257
|
+
});
|
|
1258
|
+
if (refusal) return prebuildFailure(logWriter, {
|
|
1259
|
+
failed: true,
|
|
1260
|
+
code: refusal.code,
|
|
1261
|
+
reason: refusal.message,
|
|
1262
|
+
remedy: refusal.remedy,
|
|
1263
|
+
error: refusal,
|
|
1264
|
+
lastLines: []
|
|
1265
|
+
});
|
|
1266
|
+
const bin = expoBinPath(root);
|
|
1267
|
+
if (!bin) {
|
|
1268
|
+
const binRefusal = expoBinRefusal(root, "prebuild");
|
|
1269
|
+
return prebuildFailure(logWriter, {
|
|
1270
|
+
failed: true,
|
|
1271
|
+
code: PREBUILD_ERROR,
|
|
1272
|
+
reason: binRefusal.message,
|
|
1273
|
+
remedy: binRefusal.remedy,
|
|
1274
|
+
lastLines: []
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
|
|
1278
|
+
const startedAt = now();
|
|
1279
|
+
const tail = [];
|
|
1280
|
+
const push = (line) => {
|
|
1281
|
+
const msg = stripAnsi(String(line)).trimEnd();
|
|
1282
|
+
if (!msg.trim()) return;
|
|
1283
|
+
tail.push(msg);
|
|
1284
|
+
if (tail.length > LAST_LINES$1) tail.shift();
|
|
1285
|
+
logWriter?.write?.({
|
|
1286
|
+
src: "build",
|
|
1287
|
+
level: "debug",
|
|
1288
|
+
msg,
|
|
1289
|
+
raw: true,
|
|
1290
|
+
event: "prebuild"
|
|
1291
|
+
});
|
|
1292
|
+
};
|
|
1293
|
+
let child;
|
|
1294
|
+
try {
|
|
1295
|
+
child = spawnDeclared(() => spawn(bin, [
|
|
1296
|
+
"prebuild",
|
|
1297
|
+
"-p",
|
|
1298
|
+
platform,
|
|
1299
|
+
"--no-install"
|
|
1300
|
+
], {
|
|
1301
|
+
cwd: root,
|
|
1302
|
+
stdio: [
|
|
1303
|
+
"ignore",
|
|
1304
|
+
"pipe",
|
|
1305
|
+
"pipe"
|
|
1306
|
+
],
|
|
1307
|
+
env: {
|
|
1308
|
+
...process.env,
|
|
1309
|
+
FORCE_COLOR: "0",
|
|
1310
|
+
CI: "1"
|
|
1311
|
+
}
|
|
1312
|
+
}));
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
return prebuildFailure(logWriter, {
|
|
1315
|
+
failed: true,
|
|
1316
|
+
code: PREBUILD_ERROR,
|
|
1317
|
+
reason: `Could not run \`expo prebuild\`: ${err?.message || err}`,
|
|
1318
|
+
lastLines: []
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const outReader = createLineReader(push);
|
|
1322
|
+
const errReader = createLineReader(push);
|
|
1323
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1324
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1325
|
+
child.stdout?.on("data", (chunk) => outReader.push(chunk));
|
|
1326
|
+
child.stderr?.on("data", (chunk) => errReader.push(chunk));
|
|
1327
|
+
const result = await waitForChild(child);
|
|
1328
|
+
outReader.flush();
|
|
1329
|
+
errReader.flush();
|
|
1330
|
+
const durationMs = now() - startedAt;
|
|
1331
|
+
if (result.error) return prebuildFailure(logWriter, {
|
|
1332
|
+
failed: true,
|
|
1333
|
+
code: PREBUILD_ERROR,
|
|
1334
|
+
reason: `Could not run \`expo prebuild\`: ${result.error?.message || result.error}`,
|
|
1335
|
+
lastLines: tail.slice(),
|
|
1336
|
+
durationMs
|
|
1337
|
+
});
|
|
1338
|
+
if (result.code !== 0) {
|
|
1339
|
+
const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
|
|
1340
|
+
return prebuildFailure(logWriter, {
|
|
1341
|
+
failed: true,
|
|
1342
|
+
code: PREBUILD_ERROR,
|
|
1343
|
+
reason: `\`expo prebuild -p ${platform}\` failed (${how}).`,
|
|
1344
|
+
lastLines: tail.slice(),
|
|
1345
|
+
durationMs
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
if (!existsSync(nativeDir(root, platform))) return prebuildFailure(logWriter, {
|
|
1349
|
+
failed: true,
|
|
1350
|
+
code: PREBUILD_ERROR,
|
|
1351
|
+
reason: `\`expo prebuild -p ${platform}\` succeeded but did not create ${nativeDirName(platform)}/.`,
|
|
1352
|
+
lastLines: tail.slice(),
|
|
1353
|
+
durationMs
|
|
1354
|
+
});
|
|
1355
|
+
return {
|
|
1356
|
+
ok: true,
|
|
1357
|
+
durationMs,
|
|
1358
|
+
nativeDir: nativeDir(root, platform)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/engine/js-swap.ts
|
|
1363
|
+
const JS_BUNDLE_NAME = "main.jsbundle";
|
|
1364
|
+
const LAST_LINES = 5;
|
|
1365
|
+
function hermesEnabledFromProperties(text) {
|
|
1366
|
+
if (typeof text !== "string") return true;
|
|
1367
|
+
let data;
|
|
1368
|
+
try {
|
|
1369
|
+
data = JSON.parse(text);
|
|
1370
|
+
} catch {
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
if (!data || typeof data !== "object") return true;
|
|
1374
|
+
const raw = data.hermesEnabled;
|
|
1375
|
+
return raw !== "false" && raw !== false;
|
|
1376
|
+
}
|
|
1377
|
+
function readHermesEnabled(root) {
|
|
1378
|
+
try {
|
|
1379
|
+
return hermesEnabledFromProperties(readFileSync(join(root, "ios", "Podfile.properties.json"), "utf-8"));
|
|
1380
|
+
} catch {
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const ENTRY_CANDIDATES = [
|
|
1385
|
+
"index.js",
|
|
1386
|
+
"index.ts",
|
|
1387
|
+
"index.tsx",
|
|
1388
|
+
"index.jsx"
|
|
1389
|
+
];
|
|
1390
|
+
function pickEntryFile(entries) {
|
|
1391
|
+
const names = new Set((Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string"));
|
|
1392
|
+
return ENTRY_CANDIDATES.find((c) => names.has(c)) ?? "index.js";
|
|
1393
|
+
}
|
|
1394
|
+
function detectEntryFile(root) {
|
|
1395
|
+
let entries;
|
|
1396
|
+
try {
|
|
1397
|
+
entries = readdirSync(root);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return "index.js";
|
|
1400
|
+
}
|
|
1401
|
+
return pickEntryFile(entries);
|
|
1402
|
+
}
|
|
1403
|
+
function bundleCommand({ isExpo, entryFile, bundleOutput, assetsDest }) {
|
|
1404
|
+
if (isExpo) return {
|
|
1405
|
+
file: "npx",
|
|
1406
|
+
args: [
|
|
1407
|
+
"expo",
|
|
1408
|
+
"export:embed",
|
|
1409
|
+
"--platform",
|
|
1410
|
+
"ios",
|
|
1411
|
+
"--dev",
|
|
1412
|
+
"false",
|
|
1413
|
+
"--bundle-output",
|
|
1414
|
+
bundleOutput,
|
|
1415
|
+
"--assets-dest",
|
|
1416
|
+
assetsDest
|
|
1417
|
+
]
|
|
1418
|
+
};
|
|
1419
|
+
return {
|
|
1420
|
+
file: "npx",
|
|
1421
|
+
args: [
|
|
1422
|
+
"react-native",
|
|
1423
|
+
"bundle",
|
|
1424
|
+
"--platform",
|
|
1425
|
+
"ios",
|
|
1426
|
+
"--dev",
|
|
1427
|
+
"false",
|
|
1428
|
+
"--entry-file",
|
|
1429
|
+
entryFile,
|
|
1430
|
+
"--bundle-output",
|
|
1431
|
+
bundleOutput,
|
|
1432
|
+
"--assets-dest",
|
|
1433
|
+
assetsDest
|
|
1434
|
+
]
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function hermescPath(root, { exists = existsSync } = {}) {
|
|
1438
|
+
const candidates = [
|
|
1439
|
+
join(root, "node_modules", "hermes-compiler", "hermesc", "osx-bin", "hermesc"),
|
|
1440
|
+
join(root, "ios", "Pods", "hermes-engine", "destroot", "bin", "hermesc"),
|
|
1441
|
+
join(root, "node_modules", "react-native", "sdks", "hermesc", "osx-bin", "hermesc")
|
|
1442
|
+
];
|
|
1443
|
+
return candidates.find(exists) ?? candidates[candidates.length - 1];
|
|
1444
|
+
}
|
|
1445
|
+
function hermescArgs({ bundle, out }) {
|
|
1446
|
+
return [
|
|
1447
|
+
"-emit-binary",
|
|
1448
|
+
"-out",
|
|
1449
|
+
out,
|
|
1450
|
+
bundle
|
|
1451
|
+
];
|
|
1452
|
+
}
|
|
1453
|
+
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) }) {
|
|
1454
|
+
const e = exec || getExecutor();
|
|
1455
|
+
const startedAt = now();
|
|
1456
|
+
const elapsed = () => now() - startedAt;
|
|
1457
|
+
let tmp;
|
|
1458
|
+
const fail = (step, reason, lastLines = []) => {
|
|
1459
|
+
logWriter?.write?.({
|
|
1460
|
+
src: "build",
|
|
1461
|
+
level: "error",
|
|
1462
|
+
msg: `JS swap failed at ${step}: ${reason}`,
|
|
1463
|
+
event: "js_swap"
|
|
1464
|
+
});
|
|
1465
|
+
if (tmp) removeTemporaryEntry(tmp);
|
|
1466
|
+
return {
|
|
1467
|
+
failed: true,
|
|
1468
|
+
step,
|
|
1469
|
+
reason,
|
|
1470
|
+
lastLines,
|
|
1471
|
+
durationMs: elapsed()
|
|
1472
|
+
};
|
|
1473
|
+
};
|
|
1474
|
+
let appCopy;
|
|
1475
|
+
try {
|
|
1476
|
+
tmp = mkdtemp();
|
|
1477
|
+
appCopy = join(tmp, basename(cachedAppPath));
|
|
1478
|
+
try {
|
|
1479
|
+
e.runFile("cp", [
|
|
1480
|
+
"-c",
|
|
1481
|
+
"-R",
|
|
1482
|
+
cachedAppPath,
|
|
1483
|
+
appCopy
|
|
1484
|
+
]);
|
|
1485
|
+
} catch {
|
|
1486
|
+
removeTemporaryEntry(appCopy);
|
|
1487
|
+
e.runFile("cp", [
|
|
1488
|
+
"-R",
|
|
1489
|
+
cachedAppPath,
|
|
1490
|
+
appCopy
|
|
1491
|
+
]);
|
|
1492
|
+
}
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
return fail("copy", `could not copy ${cachedAppPath} aside: ${describe(err)}`);
|
|
1495
|
+
}
|
|
1496
|
+
const bundleOutput = join(tmp, JS_BUNDLE_NAME);
|
|
1497
|
+
const assetsDest = join(tmp, "assets");
|
|
1498
|
+
const command = bundleCommand({
|
|
1499
|
+
isExpo,
|
|
1500
|
+
entryFile: isExpo ? "index.js" : detectEntryFile(root),
|
|
1501
|
+
bundleOutput,
|
|
1502
|
+
assetsDest
|
|
1503
|
+
});
|
|
1504
|
+
try {
|
|
1505
|
+
mkdirSync(assetsDest, { recursive: true });
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
return fail("bundle", `could not create ${assetsDest}: ${describe(err)}`);
|
|
1508
|
+
}
|
|
1509
|
+
logWriter?.write?.({
|
|
1510
|
+
src: "build",
|
|
1511
|
+
level: "info",
|
|
1512
|
+
msg: `${command.file} ${command.args.join(" ")}`,
|
|
1513
|
+
event: "js_swap"
|
|
1514
|
+
});
|
|
1515
|
+
const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
|
|
1516
|
+
const transcript = [];
|
|
1517
|
+
const push = (line) => {
|
|
1518
|
+
const msg = cleanLine(line);
|
|
1519
|
+
if (msg.trim() === "") return;
|
|
1520
|
+
transcript.push(msg);
|
|
1521
|
+
logWriter?.write?.({
|
|
1522
|
+
src: "build",
|
|
1523
|
+
level: "debug",
|
|
1524
|
+
msg,
|
|
1525
|
+
raw: true,
|
|
1526
|
+
event: "js_swap"
|
|
1527
|
+
});
|
|
1528
|
+
};
|
|
1529
|
+
let child;
|
|
1530
|
+
try {
|
|
1531
|
+
child = spawn(command.file, command.args, {
|
|
1532
|
+
cwd: root,
|
|
1533
|
+
stdio: [
|
|
1534
|
+
"ignore",
|
|
1535
|
+
"pipe",
|
|
1536
|
+
"pipe"
|
|
1537
|
+
],
|
|
1538
|
+
env: {
|
|
1539
|
+
...process.env,
|
|
1540
|
+
FORCE_COLOR: "0"
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
|
|
1545
|
+
}
|
|
1546
|
+
const reader = {
|
|
1547
|
+
out: createLineReader(push),
|
|
1548
|
+
err: createLineReader(push)
|
|
1549
|
+
};
|
|
1550
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1551
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1552
|
+
child.stdout?.on("data", (chunk) => reader.out.push(chunk));
|
|
1553
|
+
child.stderr?.on("data", (chunk) => reader.err.push(chunk));
|
|
1554
|
+
const stopHeartbeat = startBuildHeartbeat({
|
|
1555
|
+
intervalMs: heartbeatMs,
|
|
1556
|
+
elapsed,
|
|
1557
|
+
emit: onHeartbeat,
|
|
1558
|
+
label: "swap"
|
|
1559
|
+
});
|
|
1560
|
+
let outcome;
|
|
1561
|
+
try {
|
|
1562
|
+
outcome = await waitForChild(child);
|
|
1563
|
+
} finally {
|
|
1564
|
+
stopHeartbeat();
|
|
1565
|
+
}
|
|
1566
|
+
reader.out.flush();
|
|
1567
|
+
reader.err.flush();
|
|
1568
|
+
if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
|
|
1569
|
+
if (outcome.code !== 0) {
|
|
1570
|
+
const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
|
|
1571
|
+
return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
|
|
1572
|
+
}
|
|
1573
|
+
if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${JS_BUNDLE_NAME} at ${bundleOutput}`);
|
|
1574
|
+
let hermes = false;
|
|
1575
|
+
let note;
|
|
1576
|
+
if (hermesEnabled ?? readHermesEnabled(root)) {
|
|
1577
|
+
const hermesc = hermescPath(root);
|
|
1578
|
+
if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
|
|
1579
|
+
else {
|
|
1580
|
+
const hbc = join(tmp, `${JS_BUNDLE_NAME}.hbc`);
|
|
1581
|
+
try {
|
|
1582
|
+
e.runFile(hermesc, hermescArgs({
|
|
1583
|
+
bundle: bundleOutput,
|
|
1584
|
+
out: hbc
|
|
1585
|
+
}));
|
|
1586
|
+
e.runFile("mv", [hbc, bundleOutput]);
|
|
1587
|
+
hermes = true;
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
e.runFile("cp", [bundleOutput, join(appCopy, JS_BUNDLE_NAME)]);
|
|
1595
|
+
e.runFile("cp", [
|
|
1596
|
+
"-R",
|
|
1597
|
+
`${assetsDest}/.`,
|
|
1598
|
+
`${appCopy}/`
|
|
1599
|
+
]);
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
return fail("replace", `could not replace the JS bundle inside ${appCopy}: ${describe(err)}`);
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
e.runFile("codesign", [
|
|
1605
|
+
"--force",
|
|
1606
|
+
"--sign",
|
|
1607
|
+
"-",
|
|
1608
|
+
appCopy
|
|
1609
|
+
]);
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
return fail("codesign", `codesign --force --sign - ${appCopy} failed: ${describe(err)}`);
|
|
1612
|
+
}
|
|
1613
|
+
logWriter?.write?.({
|
|
1614
|
+
src: "build",
|
|
1615
|
+
level: "info",
|
|
1616
|
+
msg: `JS swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${appCopy} in ${elapsed()}ms`,
|
|
1617
|
+
event: "js_swap"
|
|
1618
|
+
});
|
|
1619
|
+
const result = {
|
|
1620
|
+
ok: true,
|
|
1621
|
+
appPath: appCopy,
|
|
1622
|
+
tmpDir: tmp,
|
|
1623
|
+
hermes,
|
|
1624
|
+
durationMs: elapsed()
|
|
1625
|
+
};
|
|
1626
|
+
if (note) result.note = note;
|
|
1627
|
+
return result;
|
|
1628
|
+
}
|
|
1629
|
+
function describe(err) {
|
|
1630
|
+
return String(err?.message || err);
|
|
1631
|
+
}
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/commands/dev-client.ts
|
|
1634
|
+
function devClientScheme(root, appPath = null, { exec = null } = {}) {
|
|
1635
|
+
if (!hasDevClient(root)) return void 0;
|
|
1636
|
+
const fromBundle = pickDevClientScheme(readBundleSchemes(appPath, { exec }));
|
|
1637
|
+
if (fromBundle) return fromBundle;
|
|
1638
|
+
const app = readJsonObject(join(root, "app.json"));
|
|
1639
|
+
const expo = app?.expo;
|
|
1640
|
+
const raw = (isJsonObject(expo) ? expo.scheme : void 0) ?? app?.scheme ?? null;
|
|
1641
|
+
const scheme = Array.isArray(raw) ? raw.find((s) => typeof s === "string" && s.trim() !== "") : raw;
|
|
1642
|
+
if (typeof scheme !== "string" || scheme.trim() === "") return void 0;
|
|
1643
|
+
return scheme.trim();
|
|
1644
|
+
}
|
|
1645
|
+
function schemesFromInfoPlist(plist) {
|
|
1646
|
+
const types = plist?.CFBundleURLTypes;
|
|
1647
|
+
if (!Array.isArray(types)) return [];
|
|
1648
|
+
const out = [];
|
|
1649
|
+
for (const type of types) {
|
|
1650
|
+
const schemes = type?.CFBundleURLSchemes;
|
|
1651
|
+
if (Array.isArray(schemes)) out.push(...schemes.filter((s) => typeof s === "string" && s.trim() !== ""));
|
|
1652
|
+
}
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
function readBundleSchemes(appPath, { exec = null } = {}) {
|
|
1656
|
+
if (typeof appPath !== "string" || appPath.trim() === "") return [];
|
|
1657
|
+
const e = exec || getExecutor();
|
|
1658
|
+
let out;
|
|
1659
|
+
try {
|
|
1660
|
+
out = e.runFile("plutil", [
|
|
1661
|
+
"-convert",
|
|
1662
|
+
"json",
|
|
1663
|
+
"-o",
|
|
1664
|
+
"-",
|
|
1665
|
+
join(appPath, "Info.plist")
|
|
1666
|
+
]);
|
|
1667
|
+
} catch {
|
|
1668
|
+
return [];
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
return schemesFromInfoPlist(JSON.parse(String(out)));
|
|
1672
|
+
} catch {
|
|
1673
|
+
return [];
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const THIRD_PARTY_SCHEME = /^(?:fb\d+|com\.googleusercontent\.apps\.|msauth\.|msauthv2|twitterkit-|db-[a-z0-9]+$|spotify|snapchat|com\.facebook)/i;
|
|
1677
|
+
function pickDevClientScheme(schemes) {
|
|
1678
|
+
const all = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim()).filter((s) => !/^(?:https?|mailto|tel|sms|itms(?:-apps)?)$/i.test(s));
|
|
1679
|
+
const expo = all.filter((s) => s.startsWith("exp+"));
|
|
1680
|
+
return (expo.length ? expo : all.filter((s) => !THIRD_PARTY_SCHEME.test(s))).toSorted((a, b) => b.length - a.length)[0] ?? null;
|
|
1681
|
+
}
|
|
1682
|
+
function hasDevClient(root) {
|
|
1683
|
+
const pkg = readJsonObject(join(root, "package.json"));
|
|
1684
|
+
if ([pkg?.dependencies, pkg?.devDependencies].some((deps) => isJsonObject(deps) && "expo-dev-client" in deps)) return true;
|
|
1685
|
+
return isPackageResolvable(root, "expo-dev-client");
|
|
1143
1686
|
}
|
|
1144
1687
|
//#endregion
|
|
1145
|
-
export {
|
|
1688
|
+
export { isEasBuildFailure as C, easDeviceBuildRemedy as S, noMetroMessage as _, swapJsBundle as a, sleep$1 as b, ensureBooted as c, AvdRecoveryError as d, clearIosAdoptionPending as f, launchOutcomeRecord as g, isPhysicalDeviceRequest as h, detectEntryFile as i, ensureOwnedDevice as l, gateShouldRetry as m, pickDevClientScheme as n, needsPrebuild as o, ensureWorkspaceStorageSafely as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, AvdBootError as u, noMetroRemedy as v, resolveEasDevelopmentBuild as w, warmMetro as x, resolveMetroWithRetry as y };
|