stim 1.0.0 → 1.2.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 +29 -10
- package/dist/android-BW8i_YxU.mjs +204 -0
- package/dist/android-Bcmev2s2.mjs +2334 -0
- package/dist/android-K63_0UfF.mjs +727 -0
- package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-NFxUahnH.mjs +1149 -0
- package/dist/build-lock-BsVoLDaq.mjs +645 -0
- package/dist/build-slots-D0b6sJyo.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.d.mts +1 -0
- package/dist/collector-run.mjs +7 -5
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
- package/dist/deps-BLBsTNDS.mjs +435 -0
- package/dist/dev-client-RpvXxCXV.mjs +620 -0
- package/dist/device-CoCPmazS.mjs +250 -0
- package/dist/device-lease-Dpik41RY.mjs +335 -0
- package/dist/device-pool-B5pEVa3t.mjs +408 -0
- package/dist/device-remote-E81KAdXf.mjs +1209 -0
- package/dist/doctor-CqPeMsSm.mjs +1130 -0
- package/dist/doctor-D_CX7ib5.mjs +530 -0
- package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
- package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
- package/dist/gc-B7KkDinF.mjs +1665 -0
- package/dist/guide-BS2Xc24o.mjs +4503 -0
- package/dist/ios-B7KLgIp5.mjs +2733 -0
- package/dist/ios-D1h1Di39.mjs +451 -0
- package/dist/ios-device-D3pzTKQE.mjs +81 -0
- package/dist/ios-device-xNt0Lg6v.mjs +413 -0
- package/dist/logs-DcX9AvRF.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-COslRK_F.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-CGPart6T.mjs +60 -0
- package/dist/ownership-CYOyqxUo.mjs +475 -0
- package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
- package/dist/reclaim-C5Jmuahx.mjs +348 -0
- package/dist/reload-Z8nKifAk.mjs +332 -0
- package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
- package/dist/server-expo-Drd7ksHu.mjs +2 -0
- package/dist/settings-DwU1_U_q.mjs +708 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-BrY4EuIg.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
- package/dist/stats-BCWEVLrM.mjs +307 -0
- package/dist/stats-BsF15Bti.mjs +62 -0
- package/dist/status-DFI1Hfpg.mjs +393 -0
- package/dist/stop-B7DR4awK.mjs +3 -0
- package/dist/stop-Btg770vV.mjs +591 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
- package/dist/worktree-BPTbI-LN.mjs +1322 -0
- package/dist/worktree-C79xx_kM.mjs +543 -0
- package/dist/xcode-DeKCribl.mjs +1832 -0
- package/package.json +5 -5
- package/shim/bundle-response.cjs +5 -0
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,1832 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CyylIdq9.mjs";
|
|
2
|
+
import { C as withConfigLock, E as inspectProcessIdentity, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as captureProcessIdentity, y as saveConfig } from "./config-CMtIaTk9.mjs";
|
|
3
|
+
import { r as register } from "./cache-manifest-Cu4_Znw9.mjs";
|
|
4
|
+
import { E as workspaceId, S as sharedCompilationCache, w as workspaceDerivedData } from "./state-CZ-ER-8d.mjs";
|
|
5
|
+
import { s as ownedDeviceLabel } from "./project-BBTVABUz.mjs";
|
|
6
|
+
import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
|
|
7
|
+
import { r as pidExists } from "./metro-COslRK_F.mjs";
|
|
8
|
+
import { C as selectParked, S as readParked, _ as teardownParkedIosSim, b as dropParked, g as teardownParkedAvd, m as teardownOwnedAvd, x as parkedMaxSetting, y as adoptParked } from "./remote-cache-DDmJ1Roq.mjs";
|
|
9
|
+
import { U as resolveOptimizations, l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DwU1_U_q.mjs";
|
|
10
|
+
import { a as formatElapsed, l as phaseLine } from "./command-output-8wHMEde0.mjs";
|
|
11
|
+
import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-D1h1Di39.mjs";
|
|
12
|
+
import { f as releaseClaim, h as tryAcquireClaim, i as claimRemoveCommand, l as markClaimChildPending, n as ClaimRefusedError, p as setClaimChild, u as processGroupAlive } from "./ownership-claim-7zRxxTfZ.mjs";
|
|
13
|
+
import { A as pickDefaultSystemImage, D as ownedAvdName, E as ownedAvdMatchesConfiguration, L as waitForBoot, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, b as listAvds, c as avdPoolConfiguration, d as configureNewOwnedAvd, f as createOwnedAvd, o as assertOwnedAvdStopped, u as bootAndroidEmulator, v as getAvdNameForSerial, w as nextConsolePort, x as listInstalledSystemImages, y as listAdbDevices } from "./android-K63_0UfF.mjs";
|
|
14
|
+
import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
|
|
15
|
+
import { t as cleanLine } from "./server-expo-C3ktAwUG.mjs";
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
//#region src/engine/simslim.ts
|
|
21
|
+
async function reconcileSimSlim({ udid, profile, previouslyManaged = false, out = () => {}, spawn, timeoutMs = 72e4, cleanupMs = 1e4 }) {
|
|
22
|
+
if (!profile && !previouslyManaged) return {
|
|
23
|
+
managed: false,
|
|
24
|
+
profile: null
|
|
25
|
+
};
|
|
26
|
+
const action = profile ? "on" : "off";
|
|
27
|
+
const args = profile ? [
|
|
28
|
+
"on",
|
|
29
|
+
udid,
|
|
30
|
+
"--profile",
|
|
31
|
+
profile
|
|
32
|
+
] : ["off", udid];
|
|
33
|
+
const lines = [];
|
|
34
|
+
const onLine = (raw) => {
|
|
35
|
+
const line = stripAnsi(raw).trim();
|
|
36
|
+
if (!line) return;
|
|
37
|
+
lines.push(line);
|
|
38
|
+
if (lines.length > 20) lines.shift();
|
|
39
|
+
out(`SimSlim: ${line}`);
|
|
40
|
+
};
|
|
41
|
+
const stdout = createLineReader(onLine);
|
|
42
|
+
const stderr = createLineReader(onLine);
|
|
43
|
+
const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
|
|
44
|
+
const label = `SimSlim for ${udid}`;
|
|
45
|
+
const attempt = tryAcquireClaim({
|
|
46
|
+
root,
|
|
47
|
+
mode: "exclusive",
|
|
48
|
+
label
|
|
49
|
+
});
|
|
50
|
+
if (attempt.pending) releaseClaim(attempt.pending);
|
|
51
|
+
const claim = attempt.acquired;
|
|
52
|
+
if (!claim) throw new ClaimRefusedError({
|
|
53
|
+
root,
|
|
54
|
+
claimPath: attempt.held?.path ?? root,
|
|
55
|
+
label,
|
|
56
|
+
reason: "another SimSlim operation is still using this simulator"
|
|
57
|
+
});
|
|
58
|
+
let child;
|
|
59
|
+
let result;
|
|
60
|
+
let identity;
|
|
61
|
+
let cancellation;
|
|
62
|
+
const stopGroup = () => {
|
|
63
|
+
if (child?.pid !== void 0 && identity && inspectProcessIdentity(identity) === "same") try {
|
|
64
|
+
process.kill(-child.pid, "SIGKILL");
|
|
65
|
+
} catch {}
|
|
66
|
+
};
|
|
67
|
+
const cancel = (signal) => {
|
|
68
|
+
cancellation ??= signal;
|
|
69
|
+
stopGroup();
|
|
70
|
+
};
|
|
71
|
+
const onInterrupt = () => cancel("SIGINT");
|
|
72
|
+
const onTerminate = () => cancel("SIGTERM");
|
|
73
|
+
const groupAlive = () => child?.pid !== void 0 && processGroupAlive(child.pid);
|
|
74
|
+
const settled = () => result !== void 0 && !groupAlive();
|
|
75
|
+
const waitUntil = async (deadline, cancellable = false) => {
|
|
76
|
+
while (!settled() && Date.now() < deadline) {
|
|
77
|
+
if (cancellable && cancellation) break;
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
|
|
79
|
+
}
|
|
80
|
+
return settled();
|
|
81
|
+
};
|
|
82
|
+
process.on("SIGINT", onInterrupt);
|
|
83
|
+
process.on("SIGTERM", onTerminate);
|
|
84
|
+
process.on("exit", stopGroup);
|
|
85
|
+
try {
|
|
86
|
+
markClaimChildPending(claim);
|
|
87
|
+
try {
|
|
88
|
+
child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
|
|
89
|
+
stdio: [
|
|
90
|
+
"ignore",
|
|
91
|
+
"pipe",
|
|
92
|
+
"pipe"
|
|
93
|
+
],
|
|
94
|
+
detached: true
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw simslimLaunchError(error);
|
|
98
|
+
}
|
|
99
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
100
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
101
|
+
waitForChild(child).then((value) => {
|
|
102
|
+
result = value;
|
|
103
|
+
});
|
|
104
|
+
if (child.pid !== void 0) {
|
|
105
|
+
const captured = captureProcessIdentity(child.pid);
|
|
106
|
+
if (captured.ok) {
|
|
107
|
+
identity = {
|
|
108
|
+
pid: child.pid,
|
|
109
|
+
processToken: captured.token
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
setClaimChild(claim, identity);
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const completed = await waitUntil(Date.now() + timeoutMs, true);
|
|
117
|
+
if (cancellation || !completed) {
|
|
118
|
+
stopGroup();
|
|
119
|
+
const stopped = await waitUntil(Date.now() + cleanupMs);
|
|
120
|
+
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
121
|
+
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: ${claimRemoveCommand(claim.path)}`;
|
|
122
|
+
const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
|
|
123
|
+
if (cancellation) out(message);
|
|
124
|
+
throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (!child || settled()) releaseClaim(claim);
|
|
128
|
+
else {
|
|
129
|
+
child.stdout?.destroy?.();
|
|
130
|
+
child.stderr?.destroy?.();
|
|
131
|
+
child.unref();
|
|
132
|
+
}
|
|
133
|
+
process.off("SIGINT", onInterrupt);
|
|
134
|
+
process.off("SIGTERM", onTerminate);
|
|
135
|
+
process.off("exit", stopGroup);
|
|
136
|
+
if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
|
|
137
|
+
}
|
|
138
|
+
stdout.flush();
|
|
139
|
+
stderr.flush();
|
|
140
|
+
if (result?.error) throw simslimLaunchError(result.error);
|
|
141
|
+
if (result?.code !== 0) {
|
|
142
|
+
const detail = lines.length ? ` ${lines.join(" | ")}` : "";
|
|
143
|
+
throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
managed: Boolean(profile),
|
|
147
|
+
profile: profile ?? null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function simslimLaunchError(error) {
|
|
151
|
+
const cause = error;
|
|
152
|
+
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 });
|
|
153
|
+
return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
|
|
154
|
+
}
|
|
155
|
+
function simslimIsOnPath() {
|
|
156
|
+
try {
|
|
157
|
+
return Boolean(getExecutor().runQuiet("command -v simslim", { timeoutMs: 5e3 }));
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/engine/device.ts
|
|
164
|
+
function startIosBoot(udid, configure) {
|
|
165
|
+
const done = (async () => {
|
|
166
|
+
await bootIosSim(udid);
|
|
167
|
+
await configure();
|
|
168
|
+
})();
|
|
169
|
+
done.catch(() => {});
|
|
170
|
+
return {
|
|
171
|
+
udid,
|
|
172
|
+
done
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function ensureOwnedDevice({ platform, project, projectPath, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
|
|
176
|
+
const record = project?.platforms?.[platform] ?? null;
|
|
177
|
+
if (platform === "ios") return ensureOwnedIosDevice({
|
|
178
|
+
record,
|
|
179
|
+
projectPath,
|
|
180
|
+
settingsRoot,
|
|
181
|
+
label,
|
|
182
|
+
settings,
|
|
183
|
+
flags,
|
|
184
|
+
note,
|
|
185
|
+
out,
|
|
186
|
+
reconcileIosSimulator
|
|
187
|
+
});
|
|
188
|
+
return ensureOwnedAndroidDevice({
|
|
189
|
+
record,
|
|
190
|
+
projectPath,
|
|
191
|
+
settingsRoot,
|
|
192
|
+
label,
|
|
193
|
+
settings,
|
|
194
|
+
flags,
|
|
195
|
+
note,
|
|
196
|
+
out,
|
|
197
|
+
logFile,
|
|
198
|
+
alive,
|
|
199
|
+
configureAvd,
|
|
200
|
+
teardownAvd
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
|
|
204
|
+
const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
|
|
205
|
+
if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
|
|
206
|
+
const simslimProfile = iosSimSlimProfileSetting(settings, settingsRoot);
|
|
207
|
+
if (record?.deviceUdid) {
|
|
208
|
+
if (record.owned) {
|
|
209
|
+
const resolved = resolveOwnedIosSim(record.deviceUdid);
|
|
210
|
+
if (resolved.notOwned) note(chalk.yellow(`Note: recorded sim is now named "${resolved.notOwned}", not Stim-owned by name -- creating a fresh owned sim instead of booting it.`));
|
|
211
|
+
else if (resolved.missing) {} else {
|
|
212
|
+
const sim = resolved.sim;
|
|
213
|
+
const wantedType = flags.deviceType || settings?.ios?.deviceType;
|
|
214
|
+
const deviceTypes = listIosDeviceTypes();
|
|
215
|
+
const mismatch = deviceTypeMismatch(sim.deviceTypeIdentifier, wantedType, deviceTypes);
|
|
216
|
+
if (mismatch) throw new Error(`${mismatch}. Stim will not silently boot a different model. Run \`stim worktree remove\` (or \`stim gc --delete\`) to reap the current sim, then \`stim ios\` again to create the requested one.`);
|
|
217
|
+
const model = {
|
|
218
|
+
model: deviceTypes.find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null,
|
|
219
|
+
runtime: parseRuntimeVersion(sim.runtime)
|
|
220
|
+
};
|
|
221
|
+
const name = await withIosDeviceNameLock(() => renameToOwnedName(sim, label, model, projectPath));
|
|
222
|
+
const updated = {
|
|
223
|
+
deviceUdid: sim.udid,
|
|
224
|
+
owned: true,
|
|
225
|
+
deviceName: name,
|
|
226
|
+
...record.simslimManaged ? { simslimManaged: true } : {},
|
|
227
|
+
...record.adopted ? { adopted: true } : {},
|
|
228
|
+
...record.adoptionPending ? { adoptionPending: true } : {},
|
|
229
|
+
...record.parkedCacheKey ? { parkedCacheKey: record.parkedCacheKey } : {}
|
|
230
|
+
};
|
|
231
|
+
const configure = async () => {
|
|
232
|
+
if (record.adoptionPending) resetAdoptedSim(sim.udid, out);
|
|
233
|
+
return configureOwnedIosSim({
|
|
234
|
+
record: updated,
|
|
235
|
+
projectPath,
|
|
236
|
+
profile: simslimProfile,
|
|
237
|
+
out,
|
|
238
|
+
reconcileIosSimulator
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const facts = {
|
|
242
|
+
deviceType: model.model,
|
|
243
|
+
runtime: model.runtime
|
|
244
|
+
};
|
|
245
|
+
if (sim.state !== "Booted") {
|
|
246
|
+
out(chalk.dim(phaseLine("device", `booting ${name} (${sim.udid})`)));
|
|
247
|
+
return {
|
|
248
|
+
...updated,
|
|
249
|
+
booting: startIosBoot(sim.udid, configure),
|
|
250
|
+
...facts
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
...await configure(),
|
|
255
|
+
...facts
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const sim = listAllIosSims().find((s) => s.udid === record.deviceUdid);
|
|
260
|
+
if (sim) {
|
|
261
|
+
if (sim.state !== "Booted") {
|
|
262
|
+
note(chalk.yellow(`Note: assigned sim ${sim.name} (${sim.udid}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
|
|
263
|
+
note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned sim."));
|
|
264
|
+
}
|
|
265
|
+
return record;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const choice = resolveIosCreation({
|
|
270
|
+
deviceType: flags.deviceType || settings.ios?.deviceType,
|
|
271
|
+
runtime: flags.runtime || settings.ios?.runtime
|
|
272
|
+
});
|
|
273
|
+
const adopted = parkedMaxSetting("ios").max > 0 ? await withIosDeviceNameLock(() => takeParkedIosSim({
|
|
274
|
+
projectPath,
|
|
275
|
+
label,
|
|
276
|
+
choice,
|
|
277
|
+
out
|
|
278
|
+
})) : null;
|
|
279
|
+
if (adopted) {
|
|
280
|
+
out(chalk.dim(phaseLine("device", `booting ${adopted.deviceName} (${adopted.deviceUdid})`)));
|
|
281
|
+
const booting = startIosBoot(adopted.deviceUdid, async () => {
|
|
282
|
+
resetAdoptedSim(adopted.deviceUdid, out);
|
|
283
|
+
await configureOwnedIosSim({
|
|
284
|
+
record: adopted,
|
|
285
|
+
projectPath,
|
|
286
|
+
profile: simslimProfile,
|
|
287
|
+
out,
|
|
288
|
+
reconcileIosSimulator
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
...adopted,
|
|
293
|
+
booting,
|
|
294
|
+
deviceType: choice.deviceType,
|
|
295
|
+
runtime: choice.runtime
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const created = await withIosDeviceNameLock(() => {
|
|
299
|
+
const suffix = ownedIosNameSuffix(label, {
|
|
300
|
+
model: choice.deviceType,
|
|
301
|
+
runtime: choice.runtime
|
|
302
|
+
}, projectPath);
|
|
303
|
+
const result = createOwnedIosSim(label, { suffix }, choice);
|
|
304
|
+
setDevice(projectPath, "ios", {
|
|
305
|
+
deviceUdid: result.udid,
|
|
306
|
+
owned: true,
|
|
307
|
+
deviceName: result.name
|
|
308
|
+
});
|
|
309
|
+
return result;
|
|
310
|
+
});
|
|
311
|
+
const newRecord = {
|
|
312
|
+
deviceUdid: created.udid,
|
|
313
|
+
owned: true,
|
|
314
|
+
deviceName: created.name
|
|
315
|
+
};
|
|
316
|
+
const booting = startIosBoot(created.udid, () => configureOwnedIosSim({
|
|
317
|
+
record: newRecord,
|
|
318
|
+
projectPath,
|
|
319
|
+
profile: simslimProfile,
|
|
320
|
+
out,
|
|
321
|
+
reconcileIosSimulator
|
|
322
|
+
}));
|
|
323
|
+
return {
|
|
324
|
+
...newRecord,
|
|
325
|
+
created: true,
|
|
326
|
+
booting,
|
|
327
|
+
deviceType: created.deviceType,
|
|
328
|
+
runtime: created.runtime
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function withIosDeviceNameLock(fn) {
|
|
332
|
+
return withWorkspaceProcessLock(getConfigDir(), "ios-device-names", async () => fn(), { external: true });
|
|
333
|
+
}
|
|
334
|
+
function ownedIosNameSuffix(label, model, projectPath, udid) {
|
|
335
|
+
const names = new Set(listAllIosSims({ includeUnavailable: true }).filter((sim) => sim.udid !== udid).map((sim) => sim.name));
|
|
336
|
+
for (const project of Object.values(loadConfig()?.projects ?? {})) {
|
|
337
|
+
const record = project.platforms?.ios;
|
|
338
|
+
if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
|
|
339
|
+
}
|
|
340
|
+
for (const parked of readParked("ios")) if (parked.udid !== udid) names.add(parked.name);
|
|
341
|
+
let suffix = "";
|
|
342
|
+
for (let attempt = 0; names.has(ownedSimName(label, model, suffix)); attempt++) suffix = ` ${workspaceId(projectPath)}${attempt ? `-${attempt}` : ""}`;
|
|
343
|
+
return suffix;
|
|
344
|
+
}
|
|
345
|
+
function renameToOwnedName(sim, label, model, projectPath) {
|
|
346
|
+
const wanted = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, sim.udid));
|
|
347
|
+
if (sim.name === wanted) return wanted;
|
|
348
|
+
try {
|
|
349
|
+
renameIosSim(sim.udid, wanted);
|
|
350
|
+
return wanted;
|
|
351
|
+
} catch {
|
|
352
|
+
return sim.name;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function resetAdoptedSim(udid, out) {
|
|
356
|
+
out(chalk.dim(phaseLine("device", `resetting privacy grants and the keychain on ${udid}`)));
|
|
357
|
+
resetIosPrivacy(udid);
|
|
358
|
+
resetIosKeychain(udid);
|
|
359
|
+
}
|
|
360
|
+
function takeParkedIosSim({ projectPath, label, choice, out }) {
|
|
361
|
+
const candidates = selectParked(readParked("ios"), {
|
|
362
|
+
deviceTypeIdentifier: choice.deviceTypeId,
|
|
363
|
+
runtimeIdentifier: choice.runtimeId
|
|
364
|
+
});
|
|
365
|
+
if (candidates.length === 0) return null;
|
|
366
|
+
const listed = new Map(listAllIosSims({ includeUnavailable: true }).map((sim) => [sim.udid, sim]));
|
|
367
|
+
for (const parked of candidates) {
|
|
368
|
+
const sim = listed.get(parked.udid);
|
|
369
|
+
if (!sim || !sim.available) {
|
|
370
|
+
const result = sim ? teardownParkedIosSim(parked.udid, { label: parked.name }) : null;
|
|
371
|
+
if (sim ? result?.status === "torn-down" : dropParked("ios", parked.udid)) out(chalk.dim(phaseLine("device", `deleted parked ${parked.name} (${parked.udid}): ${sim ? "unavailable" : "gone"}`)));
|
|
372
|
+
else if (result?.status === "failed") out(chalk.yellow(phaseLine("device", `could not delete unavailable parked ${parked.name} (${parked.udid}): ${result.reason}`)));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (!sim.name.startsWith("stim-")) {
|
|
376
|
+
out(chalk.yellow(phaseLine("device", `kept parked record for ${parked.udid}: simulator is now named ${JSON.stringify(sim.name)} and is not Stim-owned`)));
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const model = {
|
|
380
|
+
model: choice.deviceType,
|
|
381
|
+
runtime: choice.runtime
|
|
382
|
+
};
|
|
383
|
+
const name = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, parked.udid));
|
|
384
|
+
const device = {
|
|
385
|
+
deviceUdid: parked.udid,
|
|
386
|
+
owned: true,
|
|
387
|
+
deviceName: name,
|
|
388
|
+
adopted: true,
|
|
389
|
+
adoptionPending: true,
|
|
390
|
+
...parked.simslimManaged ? { simslimManaged: true } : {},
|
|
391
|
+
...parked.cacheKey ? { parkedCacheKey: parked.cacheKey } : {}
|
|
392
|
+
};
|
|
393
|
+
if (!adoptParked({
|
|
394
|
+
platform: "ios",
|
|
395
|
+
projectPath,
|
|
396
|
+
udid: parked.udid,
|
|
397
|
+
device
|
|
398
|
+
})) continue;
|
|
399
|
+
try {
|
|
400
|
+
renameIosSim(parked.udid, name);
|
|
401
|
+
} catch {}
|
|
402
|
+
return device;
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function clearIosAdoptionPending(projectPath) {
|
|
407
|
+
withConfigLock(() => {
|
|
408
|
+
const cfg = loadConfig();
|
|
409
|
+
const ios = cfg?.projects?.[projectPath]?.platforms?.ios;
|
|
410
|
+
if (!cfg || !ios?.adoptionPending) return;
|
|
411
|
+
delete ios.adopted;
|
|
412
|
+
delete ios.adoptionPending;
|
|
413
|
+
delete ios.parkedCacheKey;
|
|
414
|
+
saveConfig(cfg);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function configureOwnedIosSim({ record, projectPath, profile, out, reconcileIosSimulator }) {
|
|
418
|
+
if (profile) out(chalk.dim(phaseLine("device", `applying the SimSlim profile to ${record.deviceUdid}`)));
|
|
419
|
+
else if (record.simslimManaged) out(chalk.dim(phaseLine("device", `restoring stock simulator services on ${record.deviceUdid}`)));
|
|
420
|
+
const previouslyManaged = Boolean(record.simslimManaged);
|
|
421
|
+
if (profile && !record.simslimManaged) {
|
|
422
|
+
const pending = {
|
|
423
|
+
...record,
|
|
424
|
+
simslimManaged: true
|
|
425
|
+
};
|
|
426
|
+
setDevice(projectPath, "ios", pending);
|
|
427
|
+
record = pending;
|
|
428
|
+
}
|
|
429
|
+
const result = await reconcileIosSimulator({
|
|
430
|
+
udid: record.deviceUdid,
|
|
431
|
+
profile,
|
|
432
|
+
previouslyManaged,
|
|
433
|
+
out
|
|
434
|
+
});
|
|
435
|
+
const updated = { ...record };
|
|
436
|
+
if (result.managed) updated.simslimManaged = true;
|
|
437
|
+
else delete updated.simslimManaged;
|
|
438
|
+
setDevice(projectPath, "ios", updated);
|
|
439
|
+
return updated;
|
|
440
|
+
}
|
|
441
|
+
function findOtherProjectOwningAvd(avdName, projectPath) {
|
|
442
|
+
const cfg = loadConfig();
|
|
443
|
+
for (const [path, proj] of Object.entries(cfg?.projects || {})) {
|
|
444
|
+
if (path === projectPath) continue;
|
|
445
|
+
if (proj?.platforms?.android?.avdName === avdName) return path;
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
var AvdRecoveryError = class extends Error {
|
|
450
|
+
remedy;
|
|
451
|
+
constructor(message, remedy, cause) {
|
|
452
|
+
super(message, { cause });
|
|
453
|
+
this.remedy = remedy;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
|
|
457
|
+
const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
|
|
458
|
+
const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
|
|
459
|
+
if (record?.setupIncomplete && record.avdName) {
|
|
460
|
+
const cleanup = teardownAvd(record.avdName, {
|
|
461
|
+
del: true,
|
|
462
|
+
owner: { projectPath }
|
|
463
|
+
});
|
|
464
|
+
if (cleanup.status === "failed" || cleanup.status === "skipped") throw new Error(`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.`);
|
|
465
|
+
clearDevice(projectPath, "android");
|
|
466
|
+
record = null;
|
|
467
|
+
}
|
|
468
|
+
if (record?.avdName) {
|
|
469
|
+
if (record.owned) {
|
|
470
|
+
const resolved = resolveOwnedAvdSerial(record.avdName);
|
|
471
|
+
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.`));
|
|
472
|
+
else if (resolved.serial) {
|
|
473
|
+
const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
|
|
474
|
+
if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
|
|
475
|
+
const updated = {
|
|
476
|
+
...record,
|
|
477
|
+
avdName: record.avdName,
|
|
478
|
+
consolePort,
|
|
479
|
+
owned: true,
|
|
480
|
+
deviceName: record.deviceName ?? record.avdName
|
|
481
|
+
};
|
|
482
|
+
setDevice(projectPath, "android", updated);
|
|
483
|
+
return {
|
|
484
|
+
...updated,
|
|
485
|
+
systemImage: ownedAvdSystemImage(record.avdName)
|
|
486
|
+
};
|
|
487
|
+
} else if (!resolved.missing) {
|
|
488
|
+
out(chalk.dim(`Recorded port for owned AVD ${record.avdName} is not currently ours; booting it on a freshly allocated port...`));
|
|
489
|
+
return {
|
|
490
|
+
...await bootOwnedAvdOnFreshPort({
|
|
491
|
+
avdName: record.avdName,
|
|
492
|
+
metadata: record,
|
|
493
|
+
projectPath,
|
|
494
|
+
deviceName: record.deviceName,
|
|
495
|
+
out,
|
|
496
|
+
logFile,
|
|
497
|
+
alive
|
|
498
|
+
}),
|
|
499
|
+
systemImage: ownedAvdSystemImage(record.avdName)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
} else if (listAvds().includes(record.avdName)) {
|
|
503
|
+
if (!listAdbDevices().emulators.some((e) => e.consolePort === record.consolePort)) {
|
|
504
|
+
note(chalk.yellow(`Note: assigned AVD ${record.avdName} (emulator-${record.consolePort}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
|
|
505
|
+
note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned AVD."));
|
|
506
|
+
}
|
|
507
|
+
return record;
|
|
508
|
+
}
|
|
509
|
+
} else if (record?.serial) {
|
|
510
|
+
note(chalk.yellow(`Note: this project has a stored assignment to physical device ${record.serial}, which Stim no longer keeps.`));
|
|
511
|
+
note(chalk.dim("Creating an owned emulator instead. Pass `--device` to build for a connected device."));
|
|
512
|
+
}
|
|
513
|
+
if (parkedMaxSetting("android").max > 0) {
|
|
514
|
+
const systemImage = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: flags.systemImage || settings.android?.systemImage })?.pkg;
|
|
515
|
+
const candidates = readParked("android").filter((entry) => entry.systemImage === systemImage && entry.configuration === configuration).toSorted((a, b) => a.parkedAt.localeCompare(b.parkedAt));
|
|
516
|
+
for (const parked of candidates) {
|
|
517
|
+
if (parked.deletionClaim !== void 0) continue;
|
|
518
|
+
const resolved = resolveOwnedAvdSerial(parked.name);
|
|
519
|
+
if (resolved.missing) {
|
|
520
|
+
const result = teardownParkedAvd(parked.name);
|
|
521
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (resolved.notOwned || resolved.serial) continue;
|
|
525
|
+
try {
|
|
526
|
+
assertOwnedAvdStopped(parked.name);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
out(phaseLine("device", `kept parked ${parked.name}: ${String(error?.message || error)}`));
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (ownedAvdSystemImage(parked.name) !== systemImage || !ownedAvdMatchesConfiguration(parked.name, configuration)) {
|
|
532
|
+
const result = teardownParkedAvd(parked.name);
|
|
533
|
+
if (result.status === "failed") out(phaseLine("device", `kept ${parked.name}: ${result.reason}`));
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const adopted = {
|
|
537
|
+
avdName: parked.name,
|
|
538
|
+
deviceName: parked.name,
|
|
539
|
+
owned: true,
|
|
540
|
+
poolConfiguration: configuration,
|
|
541
|
+
adoptionPending: true
|
|
542
|
+
};
|
|
543
|
+
if (!adoptParked({
|
|
544
|
+
platform: "android",
|
|
545
|
+
projectPath,
|
|
546
|
+
udid: parked.udid,
|
|
547
|
+
device: adopted
|
|
548
|
+
})) continue;
|
|
549
|
+
return {
|
|
550
|
+
...await bootOwnedAvdOnFreshPort({
|
|
551
|
+
avdName: parked.name,
|
|
552
|
+
projectPath,
|
|
553
|
+
metadata: adopted,
|
|
554
|
+
out,
|
|
555
|
+
logFile,
|
|
556
|
+
alive
|
|
557
|
+
}),
|
|
558
|
+
adopted: true,
|
|
559
|
+
systemImage
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let created;
|
|
564
|
+
let fresh = false;
|
|
565
|
+
try {
|
|
566
|
+
created = withConfigLock(() => {
|
|
567
|
+
const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
|
|
568
|
+
if (current?.avdName && current.avdName !== record?.avdName) throw new Error(`Another Stim run assigned AVD ${current.avdName} to this workspace. Retry to use it.`);
|
|
569
|
+
if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherProjectOwningAvd(ownedAvdName(label), projectPath)) label = `${label}-${randomUUID().slice(0, 8)}`;
|
|
570
|
+
const result = createOwnedAvd(label, { systemImage: flags.systemImage || settings.android?.systemImage });
|
|
571
|
+
setDevice(projectPath, "android", {
|
|
572
|
+
avdName: result.avdName,
|
|
573
|
+
owned: true,
|
|
574
|
+
deviceName: result.avdName,
|
|
575
|
+
setupIncomplete: true,
|
|
576
|
+
poolConfiguration: configuration
|
|
577
|
+
});
|
|
578
|
+
return result;
|
|
579
|
+
});
|
|
580
|
+
fresh = true;
|
|
581
|
+
} catch (e) {
|
|
582
|
+
const message = String(e?.message || e);
|
|
583
|
+
const avdName = ownedAvdName(label);
|
|
584
|
+
if (message.includes("already exists")) {
|
|
585
|
+
try {
|
|
586
|
+
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.", e);
|
|
587
|
+
created = withConfigLock(() => {
|
|
588
|
+
if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`, { cause: e });
|
|
589
|
+
const owner = findOtherProjectOwningAvd(avdName, projectPath);
|
|
590
|
+
if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`, { cause: e });
|
|
591
|
+
const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
|
|
592
|
+
if (current?.avdName) {
|
|
593
|
+
const state = current.setupIncomplete ? "has incomplete setup" : "was registered";
|
|
594
|
+
throw new Error(`AVD ${current.avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`, { cause: e });
|
|
595
|
+
}
|
|
596
|
+
const resolved = resolveOwnedAvdSerial(avdName);
|
|
597
|
+
if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: e });
|
|
598
|
+
if (!resolved.serial) assertOwnedAvdStopped(avdName);
|
|
599
|
+
const recovered = {
|
|
600
|
+
avdName,
|
|
601
|
+
owned: true,
|
|
602
|
+
deviceName: avdName,
|
|
603
|
+
...resolved.serial ? { consolePort: Number(resolved.serial.replace(/^emulator-/, "")) } : {}
|
|
604
|
+
};
|
|
605
|
+
setDevice(projectPath, "android", recovered);
|
|
606
|
+
return {
|
|
607
|
+
...recovered,
|
|
608
|
+
systemImage: ownedAvdSystemImage(avdName),
|
|
609
|
+
serial: resolved.serial
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (error instanceof AvdRecoveryError) throw error;
|
|
614
|
+
throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(error?.message || error)}`, "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.", error);
|
|
615
|
+
}
|
|
616
|
+
out(chalk.dim(phaseLine("device", `recovered ${avdName} (unrecorded from a prior run)`)));
|
|
617
|
+
if (created.serial) return {
|
|
618
|
+
...created,
|
|
619
|
+
owned: true,
|
|
620
|
+
deviceName: avdName,
|
|
621
|
+
created: false
|
|
622
|
+
};
|
|
623
|
+
} else throw e;
|
|
624
|
+
}
|
|
625
|
+
if (fresh) try {
|
|
626
|
+
configureAvd(created.avdName, {
|
|
627
|
+
dataPartitionSizeGb: androidDataPartitionSizeGbSetting(settings),
|
|
628
|
+
avdConfig
|
|
629
|
+
});
|
|
630
|
+
} catch (error) {
|
|
631
|
+
const cleanup = teardownAvd(created.avdName, {
|
|
632
|
+
del: true,
|
|
633
|
+
owner: { projectPath }
|
|
634
|
+
});
|
|
635
|
+
const kept = cleanup.status === "failed" || cleanup.status === "skipped";
|
|
636
|
+
if (!kept) clearDevice(projectPath, "android");
|
|
637
|
+
const orphan = kept ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
|
|
638
|
+
throw new Error(`Created owned AVD ${created.avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
...await bootOwnedAvdOnFreshPort({
|
|
642
|
+
avdName: created.avdName,
|
|
643
|
+
metadata: fresh ? { poolConfiguration: configuration } : void 0,
|
|
644
|
+
projectPath,
|
|
645
|
+
deviceName: created.avdName,
|
|
646
|
+
out,
|
|
647
|
+
logFile,
|
|
648
|
+
alive
|
|
649
|
+
}),
|
|
650
|
+
created: fresh,
|
|
651
|
+
systemImage: created.systemImage
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function claimAndroidConsolePort({ projectPath, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
|
|
655
|
+
return lock(() => {
|
|
656
|
+
const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
|
|
657
|
+
const claim = {
|
|
658
|
+
...metadata?.poolConfiguration ? { poolConfiguration: metadata.poolConfiguration } : {},
|
|
659
|
+
...metadata?.adoptionPending ? { adoptionPending: true } : {},
|
|
660
|
+
avdName,
|
|
661
|
+
consolePort,
|
|
662
|
+
owned: true,
|
|
663
|
+
deviceName: deviceName ?? avdName
|
|
664
|
+
};
|
|
665
|
+
record(projectPath, "android", claim);
|
|
666
|
+
return claim;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function liveAndroidConsolePorts() {
|
|
670
|
+
const adbLive = listAdbDevices();
|
|
671
|
+
return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
|
|
672
|
+
}
|
|
673
|
+
async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, deviceName, out, logFile = null, alive = pidExists }) {
|
|
674
|
+
const claim = claimAndroidConsolePort({
|
|
675
|
+
projectPath,
|
|
676
|
+
avdName,
|
|
677
|
+
deviceName,
|
|
678
|
+
livePorts: liveAndroidConsolePorts(),
|
|
679
|
+
metadata
|
|
680
|
+
});
|
|
681
|
+
const serial = `emulator-${claim.consolePort}`;
|
|
682
|
+
try {
|
|
683
|
+
const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
|
|
684
|
+
out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
|
|
685
|
+
const result = await waitForBoot(serial, 12e4, { aborted: emulatorGone(pid, alive) });
|
|
686
|
+
if (!result.ok) throw new Error(`${bootFailurePrefix(serial, result.exited, 12e4)} Diagnostic: ${JSON.stringify(result.diagnostic)}`);
|
|
687
|
+
const running = getAvdNameForSerial(serial);
|
|
688
|
+
if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
|
|
689
|
+
return {
|
|
690
|
+
...claim,
|
|
691
|
+
serial
|
|
692
|
+
};
|
|
693
|
+
} catch (error) {
|
|
694
|
+
releaseAndroidConsolePort(projectPath, claim.consolePort);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function emulatorGone(pid, alive) {
|
|
699
|
+
if (!pid) return () => false;
|
|
700
|
+
return () => !alive(pid);
|
|
701
|
+
}
|
|
702
|
+
function bootFailurePrefix(serial, exited, timeoutMs) {
|
|
703
|
+
return exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(timeoutMs / 1e3)}s.`;
|
|
704
|
+
}
|
|
705
|
+
function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
|
|
706
|
+
let count = 0;
|
|
707
|
+
for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
|
|
708
|
+
const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
|
|
709
|
+
for (const proj of Object.values(config?.projects || {})) {
|
|
710
|
+
const android = proj?.platforms?.android;
|
|
711
|
+
if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
|
|
712
|
+
}
|
|
713
|
+
return count;
|
|
714
|
+
}
|
|
715
|
+
function workspaceHasLiveDevice({ platform, project, sims = [], adbEmulators = [] } = {}) {
|
|
716
|
+
if (!platform) return false;
|
|
717
|
+
const record = project?.platforms?.[platform];
|
|
718
|
+
if (!record) return false;
|
|
719
|
+
if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
|
|
720
|
+
return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
|
|
721
|
+
}
|
|
722
|
+
function deviceCapacityRefusal({ platform, project, max, sims = [], adb = null, config = null } = {}) {
|
|
723
|
+
if (!max || max <= 0) return null;
|
|
724
|
+
const adbEmulators = adb?.emulators || [];
|
|
725
|
+
if (workspaceHasLiveDevice({
|
|
726
|
+
platform,
|
|
727
|
+
project,
|
|
728
|
+
sims,
|
|
729
|
+
adbEmulators
|
|
730
|
+
})) return null;
|
|
731
|
+
const count = liveOwnedDeviceCount({
|
|
732
|
+
sims,
|
|
733
|
+
adbEmulators,
|
|
734
|
+
config
|
|
735
|
+
});
|
|
736
|
+
if (count < max) return null;
|
|
737
|
+
return {
|
|
738
|
+
code: "STIM_AT_CAPACITY",
|
|
739
|
+
message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
|
|
740
|
+
remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function checkDeviceCapacity({ platform, project, max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
|
|
744
|
+
if (!max || max <= 0) return null;
|
|
745
|
+
let simList = [];
|
|
746
|
+
let adbRes = {
|
|
747
|
+
emulators: [],
|
|
748
|
+
physical: [],
|
|
749
|
+
unhealthy: []
|
|
750
|
+
};
|
|
751
|
+
try {
|
|
752
|
+
simList = typeof sims === "function" ? sims() || [] : sims || [];
|
|
753
|
+
} catch {}
|
|
754
|
+
try {
|
|
755
|
+
adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
|
|
756
|
+
} catch {}
|
|
757
|
+
let cfg = null;
|
|
758
|
+
try {
|
|
759
|
+
cfg = typeof config === "function" ? config() : config ?? null;
|
|
760
|
+
} catch {}
|
|
761
|
+
return deviceCapacityRefusal({
|
|
762
|
+
platform,
|
|
763
|
+
project,
|
|
764
|
+
max,
|
|
765
|
+
sims: simList,
|
|
766
|
+
adb: adbRes,
|
|
767
|
+
config: cfg
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
|
|
771
|
+
if (!requestedName || !recordedTypeId) return null;
|
|
772
|
+
const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
|
|
773
|
+
if (!wanted) return null;
|
|
774
|
+
if (wanted.identifier === recordedTypeId) return null;
|
|
775
|
+
const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
|
|
776
|
+
return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
|
|
777
|
+
}
|
|
778
|
+
function installedNames(names) {
|
|
779
|
+
const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
|
|
780
|
+
return unique.length > 0 ? unique.join(", ") : "none";
|
|
781
|
+
}
|
|
782
|
+
function unknownIosRuntimeRefusal(requested, runtimes) {
|
|
783
|
+
if (!requested) return null;
|
|
784
|
+
if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
|
|
785
|
+
return {
|
|
786
|
+
message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
|
|
787
|
+
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."
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function creatableIosDeviceTypeNames(runtimes, runtime) {
|
|
791
|
+
return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
|
|
792
|
+
}
|
|
793
|
+
function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
|
|
794
|
+
if (!requested) return null;
|
|
795
|
+
const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
|
|
796
|
+
if (creatable.includes(requested)) return null;
|
|
797
|
+
return {
|
|
798
|
+
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)}.`,
|
|
799
|
+
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."
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function unknownAndroidSystemImageRefusal(requested, images) {
|
|
803
|
+
if (!requested) return null;
|
|
804
|
+
if ((images || []).some((i) => i.pkg === requested)) return null;
|
|
805
|
+
return {
|
|
806
|
+
message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
|
|
807
|
+
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\"`."
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const BOOT_POLL_MS = 500;
|
|
811
|
+
async function ensureBooted({ platform, device, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists } = {}) {
|
|
812
|
+
if (platform === "ios") return ensureIosBooted({
|
|
813
|
+
device,
|
|
814
|
+
timeoutMs: timeoutMs ?? 6e5,
|
|
815
|
+
pollMs,
|
|
816
|
+
out
|
|
817
|
+
});
|
|
818
|
+
if (platform === "android") return ensureAndroidBooted({
|
|
819
|
+
device,
|
|
820
|
+
timeoutMs: timeoutMs ?? 24e4,
|
|
821
|
+
out,
|
|
822
|
+
logFile,
|
|
823
|
+
alive
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
failed: true,
|
|
827
|
+
reason: `Unknown platform "${platform}".`
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async function ensureIosBooted({ device, timeoutMs, pollMs, out }) {
|
|
831
|
+
const udid = device?.deviceUdid;
|
|
832
|
+
if (!udid) return {
|
|
833
|
+
failed: true,
|
|
834
|
+
reason: "No iOS simulator is recorded for this project."
|
|
835
|
+
};
|
|
836
|
+
const ready = () => {
|
|
837
|
+
try {
|
|
838
|
+
getExecutor().runFile("xcrun", [
|
|
839
|
+
"simctl",
|
|
840
|
+
"spawn",
|
|
841
|
+
udid,
|
|
842
|
+
"/usr/bin/true"
|
|
843
|
+
], { timeoutMs: 3e4 });
|
|
844
|
+
return {
|
|
845
|
+
ok: true,
|
|
846
|
+
udid
|
|
847
|
+
};
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return {
|
|
850
|
+
failed: true,
|
|
851
|
+
reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
const booting = device?.booting;
|
|
856
|
+
if (booting?.udid === udid) {
|
|
857
|
+
try {
|
|
858
|
+
await booting.done;
|
|
859
|
+
} catch (e) {
|
|
860
|
+
return {
|
|
861
|
+
failed: true,
|
|
862
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return ready();
|
|
866
|
+
}
|
|
867
|
+
let resolved;
|
|
868
|
+
try {
|
|
869
|
+
resolved = resolveOwnedIosSim(udid);
|
|
870
|
+
} catch (e) {
|
|
871
|
+
return {
|
|
872
|
+
failed: true,
|
|
873
|
+
reason: `Could not list simulators: ${e?.message || e}`
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (resolved.missing) return {
|
|
877
|
+
failed: true,
|
|
878
|
+
reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
|
|
879
|
+
};
|
|
880
|
+
if (resolved.notOwned) return {
|
|
881
|
+
failed: true,
|
|
882
|
+
reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
|
|
883
|
+
};
|
|
884
|
+
const sim = resolved.sim;
|
|
885
|
+
if (sim.state === "Booted") return ready();
|
|
886
|
+
out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
|
|
887
|
+
const bootDeadline = Date.now() + timeoutMs;
|
|
888
|
+
try {
|
|
889
|
+
await bootIosSim(udid, { timeoutMs });
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return {
|
|
892
|
+
failed: true,
|
|
893
|
+
reason: `Could not boot simulator ${udid}: ${e?.message || e}`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
|
|
897
|
+
while (Date.now() < deadline) {
|
|
898
|
+
await sleep(pollMs);
|
|
899
|
+
let state = null;
|
|
900
|
+
try {
|
|
901
|
+
state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
|
|
902
|
+
} catch {}
|
|
903
|
+
if (state === "Booted") return ready();
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
failed: true,
|
|
907
|
+
reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, alive = pidExists }) {
|
|
911
|
+
if (!device?.avdName) return {
|
|
912
|
+
failed: true,
|
|
913
|
+
reason: "No owned Android emulator is recorded for this project."
|
|
914
|
+
};
|
|
915
|
+
let resolved;
|
|
916
|
+
try {
|
|
917
|
+
resolved = resolveOwnedAvdSerial(device.avdName);
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return {
|
|
920
|
+
failed: true,
|
|
921
|
+
reason: `Could not list AVDs: ${e?.message || e}`
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
if (resolved.missing) return {
|
|
925
|
+
failed: true,
|
|
926
|
+
reason: `AVD ${device.avdName} no longer exists. Run \`stim android\` again to create a fresh owned AVD.`
|
|
927
|
+
};
|
|
928
|
+
if (resolved.notOwned) return {
|
|
929
|
+
failed: true,
|
|
930
|
+
reason: `AVD ${device.avdName} is not Stim-owned by name; refusing to boot it.`
|
|
931
|
+
};
|
|
932
|
+
if (resolved.serial) {
|
|
933
|
+
const ready = await waitForBoot(resolved.serial, timeoutMs);
|
|
934
|
+
if (!ready.ok) return {
|
|
935
|
+
failed: true,
|
|
936
|
+
reason: `Emulator ${resolved.serial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
|
|
937
|
+
};
|
|
938
|
+
return {
|
|
939
|
+
ok: true,
|
|
940
|
+
serial: resolved.serial
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const freshSerial = `emulator-${device.consolePort}`;
|
|
944
|
+
if (device.owned && device.serial === freshSerial) {
|
|
945
|
+
const ready = await waitForBoot(freshSerial, timeoutMs);
|
|
946
|
+
if (ready.ok) return {
|
|
947
|
+
ok: true,
|
|
948
|
+
serial: freshSerial
|
|
949
|
+
};
|
|
950
|
+
return {
|
|
951
|
+
failed: true,
|
|
952
|
+
reason: `Emulator ${freshSerial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const serial = `emulator-${pickConsolePort(device.consolePort)}`;
|
|
956
|
+
out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
|
|
957
|
+
let pid = null;
|
|
958
|
+
try {
|
|
959
|
+
pid = bootAndroidEmulator(device.avdName, Number(serial.replace(/^emulator-/, "")), { logFile });
|
|
960
|
+
} catch (e) {
|
|
961
|
+
return {
|
|
962
|
+
failed: true,
|
|
963
|
+
reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const ready = await waitForBoot(serial, timeoutMs, { aborted: emulatorGone(pid, alive) });
|
|
967
|
+
if (!ready.ok) return {
|
|
968
|
+
failed: true,
|
|
969
|
+
reason: `${bootFailurePrefix(serial, ready.exited, timeoutMs)} Diagnostic: ${JSON.stringify(ready.diagnostic)}`
|
|
970
|
+
};
|
|
971
|
+
return {
|
|
972
|
+
ok: true,
|
|
973
|
+
serial
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function pickConsolePort(recorded) {
|
|
977
|
+
const live = liveAndroidConsolePorts();
|
|
978
|
+
if (recorded && !live.includes(Number(recorded))) return Number(recorded);
|
|
979
|
+
return nextConsolePort([...allConsolePortsAndSerials().androidConsolePorts, ...live]);
|
|
980
|
+
}
|
|
981
|
+
function sleep(ms) {
|
|
982
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
983
|
+
}
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/engine/bundler.ts
|
|
986
|
+
const COCOAPODS_SPEC = /^ {4}cocoapods \(/m;
|
|
987
|
+
function bundlerPin(root) {
|
|
988
|
+
const gemfile = join(root, "Gemfile");
|
|
989
|
+
const lockfile = join(root, "Gemfile.lock");
|
|
990
|
+
if (!existsSync(gemfile)) return null;
|
|
991
|
+
let lock;
|
|
992
|
+
try {
|
|
993
|
+
lock = readFileSync(lockfile, "utf-8");
|
|
994
|
+
} catch {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
if (!COCOAPODS_SPEC.test(lock)) return null;
|
|
998
|
+
return {
|
|
999
|
+
gemfile,
|
|
1000
|
+
lockfile
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function podInstallCommand(root, ...args) {
|
|
1004
|
+
const pod = [
|
|
1005
|
+
"pod",
|
|
1006
|
+
"install",
|
|
1007
|
+
...args
|
|
1008
|
+
].join(" ");
|
|
1009
|
+
return bundlerPin(root) ? `bundle exec ${pod}` : pod;
|
|
1010
|
+
}
|
|
1011
|
+
const BUILD_FAILED = "** BUILD FAILED **";
|
|
1012
|
+
const POSITIONED = /^(\S[^\t]*?):(\d+):(?:(\d+):)?\s*(?:fatal\s+)?error:\s+(.+)$/;
|
|
1013
|
+
const UNPOSITIONED = /^(\S[^\t]*?)?\berror:\s+(.+)$/;
|
|
1014
|
+
const LD_ERROR = /^ld:\s+(?!warning:)(.+)$/;
|
|
1015
|
+
const UNDEFINED_HEADER = /^(?:ld:\s+)?Undefined symbols?(?:\s+for architecture\s+\S+)?:\s*$/;
|
|
1016
|
+
const UNDEFINED_SYMBOL = /^\s+"?([^",]+)"?,\s+referenced from:\s*$/;
|
|
1017
|
+
function undefinedSymbolMessage(symbol) {
|
|
1018
|
+
return `Undefined symbol: ${symbol}`;
|
|
1019
|
+
}
|
|
1020
|
+
const PODS_OUT_OF_SYNC = /The sandbox is not in sync with the Podfile\.lock/;
|
|
1021
|
+
const SIGNING = [
|
|
1022
|
+
/No profiles for '.*' were found/,
|
|
1023
|
+
/requires a development team/i,
|
|
1024
|
+
/[Cc]ode ?[Ss]igning [Ee]rror/,
|
|
1025
|
+
/No signing certificate/i,
|
|
1026
|
+
/code signing is required/i,
|
|
1027
|
+
/errSecInternalComponent/,
|
|
1028
|
+
/Provisioning profile .* doesn't (?:include|match)/i
|
|
1029
|
+
];
|
|
1030
|
+
const NO_SUCH_SCHEME = /does not contain a scheme named/;
|
|
1031
|
+
const CAS_CORRUPT = /not a IncludeTreeRoot node kind/;
|
|
1032
|
+
const DEVICE_SIGNING_REMEDY = "Set a team and a Development profile for the target's configuration in Xcode > Signing & Capabilities, then build once from Xcode to install the profile. Stim never passes -allowProvisioningUpdates, because registering a device or minting a profile changes your Apple Developer account.";
|
|
1033
|
+
const SIMULATOR_SIGNING_REMEDY = "Stim builds for the simulator here, which needs no signing. Check CODE_SIGNING_REQUIRED / DEVELOPMENT_TEAM in the target's configuration, or build for a phone with `stim ios --device`.";
|
|
1034
|
+
function remedyFor(message, root, sdk) {
|
|
1035
|
+
if (CAS_CORRUPT.test(message)) return "The compilation cache holds a damaged object. Run `stim gc --delete --cache \"compilation cache\"` to empty that cache, then build again. The next build is a cold one.";
|
|
1036
|
+
if (PODS_OUT_OF_SYNC.test(message)) return `Run \`${root ? podInstallCommand(root) : "pod install"}\` in ios/ (stim ios does this when Podfile.lock and Pods/Manifest.lock disagree), then build again.`;
|
|
1037
|
+
if (NO_SUCH_SCHEME.test(message)) return "Run `xcodebuild -list` in ios/ to see the schemes this project defines, and share the app scheme so it is visible to the build.";
|
|
1038
|
+
for (const pattern of SIGNING) if (pattern.test(message)) return sdk.startsWith("iphoneos") ? DEVICE_SIGNING_REMEDY : SIMULATOR_SIGNING_REMEDY;
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
function fileFromPrefix(prefix) {
|
|
1042
|
+
const trimmed = String(prefix).trim().replace(/:$/, "");
|
|
1043
|
+
if (!trimmed) return null;
|
|
1044
|
+
const head = (trimmed.split(": ")[0] ?? "").trim();
|
|
1045
|
+
if (!head.includes("/")) return null;
|
|
1046
|
+
return head;
|
|
1047
|
+
}
|
|
1048
|
+
function makeDiagnostic({ file = null, line = null, column = null, message }, root, sdk) {
|
|
1049
|
+
const text = String(message).trim();
|
|
1050
|
+
const out = { message: text };
|
|
1051
|
+
if (file) out.file = file;
|
|
1052
|
+
if (line !== null && line !== void 0) out.line = line;
|
|
1053
|
+
if (column !== null && column !== void 0) out.column = column;
|
|
1054
|
+
out.message = text;
|
|
1055
|
+
const remedy = remedyFor(text, root, sdk);
|
|
1056
|
+
if (remedy) out.remedy = remedy;
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
function dedupeKey(d) {
|
|
1060
|
+
return `${d.file || ""}|${d.line || ""}|${d.column || ""}|${d.message}`;
|
|
1061
|
+
}
|
|
1062
|
+
function extractXcodeDiagnostics(transcript, root = null, sdk = "iphonesimulator") {
|
|
1063
|
+
if (typeof transcript !== "string" || transcript === "") return [];
|
|
1064
|
+
const lines = transcript.split("\n");
|
|
1065
|
+
const out = [];
|
|
1066
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1067
|
+
const push = (d) => {
|
|
1068
|
+
const key = dedupeKey(d);
|
|
1069
|
+
if (seen.has(key)) return;
|
|
1070
|
+
seen.add(key);
|
|
1071
|
+
out.push(d);
|
|
1072
|
+
};
|
|
1073
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1074
|
+
const rawLine = lines[i];
|
|
1075
|
+
if (rawLine === void 0) continue;
|
|
1076
|
+
const raw = rawLine.replace(/\r$/, "");
|
|
1077
|
+
if (raw.includes(BUILD_FAILED)) break;
|
|
1078
|
+
if (UNDEFINED_HEADER.test(raw)) {
|
|
1079
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
1080
|
+
const symLine = lines[j];
|
|
1081
|
+
if (symLine === void 0) continue;
|
|
1082
|
+
const sym = UNDEFINED_SYMBOL.exec(symLine.replace(/\r$/, ""));
|
|
1083
|
+
if (sym) {
|
|
1084
|
+
const symbol = sym[1];
|
|
1085
|
+
if (symbol !== void 0) push(makeDiagnostic({ message: undefinedSymbolMessage(symbol) }, root, sdk));
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (/^\s+\S/.test(symLine) && !/^\S/.test(symLine)) continue;
|
|
1089
|
+
i = j - 1;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const ld = LD_ERROR.exec(raw);
|
|
1095
|
+
if (ld) {
|
|
1096
|
+
push(makeDiagnostic({ message: `ld: ${ld[1]}` }, root, sdk));
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const positioned = POSITIONED.exec(raw);
|
|
1100
|
+
if (positioned) {
|
|
1101
|
+
const posMsg = positioned[4];
|
|
1102
|
+
if (posMsg === void 0) continue;
|
|
1103
|
+
push(makeDiagnostic({
|
|
1104
|
+
file: positioned[1],
|
|
1105
|
+
line: Number(positioned[2]),
|
|
1106
|
+
column: positioned[3] === void 0 ? null : Number(positioned[3]),
|
|
1107
|
+
message: posMsg
|
|
1108
|
+
}, root, sdk));
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const plain = UNPOSITIONED.exec(raw);
|
|
1112
|
+
if (plain) {
|
|
1113
|
+
const plainMsg = plain[2];
|
|
1114
|
+
if (plainMsg === void 0) continue;
|
|
1115
|
+
push(makeDiagnostic({
|
|
1116
|
+
file: fileFromPrefix(plain[1] || ""),
|
|
1117
|
+
message: plainMsg
|
|
1118
|
+
}, root, sdk));
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return out;
|
|
1122
|
+
}
|
|
1123
|
+
function capDiagnostics(diagnostics, max = 10) {
|
|
1124
|
+
const list = Array.isArray(diagnostics) ? diagnostics : [];
|
|
1125
|
+
if (list.length <= max) return {
|
|
1126
|
+
diagnostics: list.slice(),
|
|
1127
|
+
truncated: 0
|
|
1128
|
+
};
|
|
1129
|
+
return {
|
|
1130
|
+
diagnostics: list.slice(0, max),
|
|
1131
|
+
truncated: list.length - max
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function describeDiagnostic(diagnostic) {
|
|
1135
|
+
if (!diagnostic || typeof diagnostic !== "object") return "";
|
|
1136
|
+
const { file, line, column, message } = diagnostic;
|
|
1137
|
+
if (!file) return String(message ?? "");
|
|
1138
|
+
return `${file}${line ? column ? `:${line}:${column}` : `:${line}` : ""}: ${message}`;
|
|
1139
|
+
}
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/engine/xcode.ts
|
|
1142
|
+
const IOS_DIR = "ios";
|
|
1143
|
+
const PREBUILD_REMEDY = "Generate it with `npx expo prebuild -p ios` (stim ios does this automatically for an Expo project with no ios/ directory), or commit the native project.";
|
|
1144
|
+
function buildFailure(message, remedy) {
|
|
1145
|
+
return { error: {
|
|
1146
|
+
code: "STIM_BUILD_FAILED",
|
|
1147
|
+
message,
|
|
1148
|
+
remedy
|
|
1149
|
+
} };
|
|
1150
|
+
}
|
|
1151
|
+
function pickXcodeProject(entries) {
|
|
1152
|
+
const names = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string");
|
|
1153
|
+
const workspaces = names.filter((e) => e.endsWith(".xcworkspace")).toSorted();
|
|
1154
|
+
const projects = names.filter((e) => e.endsWith(".xcodeproj")).toSorted();
|
|
1155
|
+
if (workspaces.length > 0) {
|
|
1156
|
+
const projectNames = new Set(projects.map((p) => basename(p, ".xcodeproj")));
|
|
1157
|
+
const file = workspaces.find((w) => projectNames.has(basename(w, ".xcworkspace"))) || workspaces[0];
|
|
1158
|
+
if (file === void 0) return null;
|
|
1159
|
+
return {
|
|
1160
|
+
kind: "workspace",
|
|
1161
|
+
flag: "-workspace",
|
|
1162
|
+
file,
|
|
1163
|
+
name: basename(file, ".xcworkspace")
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (projects.length > 0) {
|
|
1167
|
+
const file = projects[0];
|
|
1168
|
+
if (file === void 0) return null;
|
|
1169
|
+
return {
|
|
1170
|
+
kind: "project",
|
|
1171
|
+
flag: "-project",
|
|
1172
|
+
file,
|
|
1173
|
+
name: basename(file, ".xcodeproj")
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
function discoverXcodeProject(root) {
|
|
1179
|
+
const dir = join(root, IOS_DIR);
|
|
1180
|
+
let entries;
|
|
1181
|
+
try {
|
|
1182
|
+
entries = readdirSync(dir);
|
|
1183
|
+
} catch {
|
|
1184
|
+
return buildFailure(`No ${IOS_DIR}/ directory in ${root}.`, PREBUILD_REMEDY);
|
|
1185
|
+
}
|
|
1186
|
+
const picked = pickXcodeProject(entries);
|
|
1187
|
+
if (!picked) return buildFailure(`${dir} contains no .xcworkspace and no .xcodeproj.`, PREBUILD_REMEDY);
|
|
1188
|
+
return {
|
|
1189
|
+
...picked,
|
|
1190
|
+
dir,
|
|
1191
|
+
path: join(dir, picked.file)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function parseSchemeList(text) {
|
|
1195
|
+
const empty = {
|
|
1196
|
+
name: null,
|
|
1197
|
+
schemes: []
|
|
1198
|
+
};
|
|
1199
|
+
if (typeof text !== "string") return empty;
|
|
1200
|
+
const start = text.indexOf("{");
|
|
1201
|
+
const end = text.lastIndexOf("}");
|
|
1202
|
+
if (start < 0 || end <= start) return empty;
|
|
1203
|
+
let data;
|
|
1204
|
+
try {
|
|
1205
|
+
data = JSON.parse(text.slice(start, end + 1));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return empty;
|
|
1208
|
+
}
|
|
1209
|
+
if (!data || typeof data !== "object") return empty;
|
|
1210
|
+
const record = data;
|
|
1211
|
+
const container = record.workspace || record.project;
|
|
1212
|
+
if (!container || typeof container !== "object") return empty;
|
|
1213
|
+
const info = container;
|
|
1214
|
+
const schemes = Array.isArray(info.schemes) ? info.schemes.filter((s) => typeof s === "string" && s.trim() !== "") : [];
|
|
1215
|
+
return {
|
|
1216
|
+
name: typeof info.name === "string" ? info.name : null,
|
|
1217
|
+
schemes
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const TEST_SCHEME = /(?:UI)?Tests$/;
|
|
1221
|
+
function pickScheme(schemes, containerName) {
|
|
1222
|
+
const list = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "");
|
|
1223
|
+
if (list.length === 0) return null;
|
|
1224
|
+
const name = typeof containerName === "string" ? containerName.trim() : "";
|
|
1225
|
+
if (name) {
|
|
1226
|
+
const exact = list.find((s) => s === name);
|
|
1227
|
+
if (exact) return exact;
|
|
1228
|
+
const insensitive = list.find((s) => s.toLowerCase() === name.toLowerCase());
|
|
1229
|
+
if (insensitive) return insensitive;
|
|
1230
|
+
}
|
|
1231
|
+
const app = list.filter((s) => !TEST_SCHEME.test(s));
|
|
1232
|
+
return app.length === 1 ? app[0] : null;
|
|
1233
|
+
}
|
|
1234
|
+
const LIST_TIMEOUT_MS = 18e4;
|
|
1235
|
+
function listSchemes(project, { exec = null } = {}) {
|
|
1236
|
+
const executor = exec || getExecutor();
|
|
1237
|
+
try {
|
|
1238
|
+
return parseSchemeList(executor.runFile("xcodebuild", [
|
|
1239
|
+
project.flag,
|
|
1240
|
+
project.path,
|
|
1241
|
+
"-list",
|
|
1242
|
+
"-json"
|
|
1243
|
+
], { timeoutMs: LIST_TIMEOUT_MS }));
|
|
1244
|
+
} catch {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function resolveScheme(project, { exec = null, scheme: requested = null } = {}) {
|
|
1249
|
+
const listing = listSchemes(project, { exec });
|
|
1250
|
+
if (listing === null) return { error: {
|
|
1251
|
+
code: "STIM_NO_SCHEME",
|
|
1252
|
+
message: `Could not list schemes for ${project.path}.`,
|
|
1253
|
+
remedy: `Run \`xcodebuild ${project.flag} ${project.path} -list\` to see what it reports.`
|
|
1254
|
+
} };
|
|
1255
|
+
if (requested !== null) {
|
|
1256
|
+
if (listing.schemes.includes(requested)) return {
|
|
1257
|
+
scheme: requested,
|
|
1258
|
+
schemes: listing.schemes
|
|
1259
|
+
};
|
|
1260
|
+
return { error: {
|
|
1261
|
+
code: "STIM_NO_SCHEME",
|
|
1262
|
+
message: `No shared Xcode scheme named ${JSON.stringify(requested)} in ${project.path}. Available schemes: ${listing.schemes.join(", ") || "none"}.`,
|
|
1263
|
+
remedy: "Pass an exact available name with --scheme, or share the intended app scheme in Xcode."
|
|
1264
|
+
} };
|
|
1265
|
+
}
|
|
1266
|
+
let scheme = pickScheme(listing.schemes, listing.name || project.name);
|
|
1267
|
+
if (!scheme && project.dir) try {
|
|
1268
|
+
const app = JSON.parse(readFileSync(join(project.dir, "..", "app.json"), "utf8"));
|
|
1269
|
+
scheme = pickScheme(listing.schemes, app?.name);
|
|
1270
|
+
} catch {}
|
|
1271
|
+
if (!scheme) {
|
|
1272
|
+
const found = listing.schemes.length ? listing.schemes.join(", ") : "none";
|
|
1273
|
+
return { error: {
|
|
1274
|
+
code: "STIM_NO_SCHEME",
|
|
1275
|
+
message: `Could not select an app scheme in ${project.path} (schemes: ${found}).`,
|
|
1276
|
+
remedy: listing.schemes.length > 0 ? "Pass --scheme <name> to select the intended shared app scheme. Stim does not guess between unmatched schemes." : "Share the app scheme in Xcode (Product > Scheme > Manage Schemes, tick Shared) so xcodebuild can see it."
|
|
1277
|
+
} };
|
|
1278
|
+
}
|
|
1279
|
+
return {
|
|
1280
|
+
scheme,
|
|
1281
|
+
schemes: listing.schemes
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const PREFIX_MAP_TARGET = "/^src";
|
|
1285
|
+
function prefixMapping(workspaceRoot) {
|
|
1286
|
+
return `${String(workspaceRoot).replace(/\/+$/, "")}=${PREFIX_MAP_TARGET}`;
|
|
1287
|
+
}
|
|
1288
|
+
function compilationPrefixMappings(workspaceRoot, derivedDataPath) {
|
|
1289
|
+
const source = resolve(workspaceRoot);
|
|
1290
|
+
const derived = resolve(derivedDataPath);
|
|
1291
|
+
const mappings = [prefixMapping(source)];
|
|
1292
|
+
const rel = relative(source, derived);
|
|
1293
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) mappings.push(`${derived}=/^derived-data`);
|
|
1294
|
+
return mappings.join(" ");
|
|
1295
|
+
}
|
|
1296
|
+
function ccacheEnabled(podfileProperties) {
|
|
1297
|
+
if (!podfileProperties || typeof podfileProperties !== "object") return false;
|
|
1298
|
+
return podfileProperties["apple.ccacheEnabled"] === "true";
|
|
1299
|
+
}
|
|
1300
|
+
function readPodfileProperties(root) {
|
|
1301
|
+
try {
|
|
1302
|
+
const parsed = JSON.parse(readFileSync(join(root, IOS_DIR, "Podfile.properties.json"), "utf-8"));
|
|
1303
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function compilationCacheSettings({ workspaceRoot, derivedDataPath, casPath, xcodeMajor, ccache = false, optimizations = resolveOptimizations({}, {}).ios }) {
|
|
1309
|
+
if (xcodeMajor === null || xcodeMajor === void 0) return [];
|
|
1310
|
+
if (xcodeMajor < 26) return [];
|
|
1311
|
+
if (ccache) return [];
|
|
1312
|
+
return [
|
|
1313
|
+
`COMPILATION_CACHE_ENABLE_CACHING=${optimizations.compilationCache ? "YES" : "NO"}`,
|
|
1314
|
+
`COMPILATION_CACHE_CAS_PATH=${casPath}`,
|
|
1315
|
+
`SWIFT_ENABLE_COMPILE_CACHE=${optimizations.compilationCache && optimizations.swiftCompilationCache ? "YES" : "NO"}`,
|
|
1316
|
+
`CLANG_ENABLE_PREFIX_MAPPING=${optimizations.prefixMapping ? "YES" : "NO"}`,
|
|
1317
|
+
`CLANG_OTHER_PREFIX_MAPPINGS=${optimizations.prefixMapping ? compilationPrefixMappings(workspaceRoot, derivedDataPath) : ""}`
|
|
1318
|
+
];
|
|
1319
|
+
}
|
|
1320
|
+
function parseXcodeMajor(output) {
|
|
1321
|
+
const m = /^Xcode\s+(\d+)/m.exec(String(output || ""));
|
|
1322
|
+
if (!m) return null;
|
|
1323
|
+
const digits = m[1];
|
|
1324
|
+
if (digits === void 0) return null;
|
|
1325
|
+
const major = parseInt(digits, 10);
|
|
1326
|
+
return Number.isFinite(major) ? major : null;
|
|
1327
|
+
}
|
|
1328
|
+
function detectXcodeMajor(exec = null) {
|
|
1329
|
+
return parseXcodeMajor((exec || getExecutor()).runQuiet("xcodebuild -version", { timeoutMs: 1e4 }));
|
|
1330
|
+
}
|
|
1331
|
+
function resolveCompilationCacheSettings({ root, derivedDataPath, exec = null, casPath = sharedCompilationCache(), optimizations = resolveOptimizations({}, {}).ios, onNote = (line) => console.error(line) }) {
|
|
1332
|
+
const settings = compilationCacheSettings({
|
|
1333
|
+
workspaceRoot: root,
|
|
1334
|
+
derivedDataPath,
|
|
1335
|
+
casPath,
|
|
1336
|
+
xcodeMajor: detectXcodeMajor(exec),
|
|
1337
|
+
ccache: ccacheEnabled(readPodfileProperties(root)),
|
|
1338
|
+
optimizations
|
|
1339
|
+
});
|
|
1340
|
+
if (settings.length > 0 && optimizations.compilationCache) {
|
|
1341
|
+
register({
|
|
1342
|
+
dir: casPath,
|
|
1343
|
+
name: "Xcode compilation cache",
|
|
1344
|
+
prune: "atomic",
|
|
1345
|
+
note: "shared Xcode compilation cache"
|
|
1346
|
+
});
|
|
1347
|
+
onNote(chalk.dim(phaseLine("cache", `compilation cache on (CAS at ${casPath})`)));
|
|
1348
|
+
}
|
|
1349
|
+
return settings;
|
|
1350
|
+
}
|
|
1351
|
+
function xcodebuildArgs({ project, scheme, udid = null, destination = null, configuration = "Debug", sdk = "iphonesimulator", derivedDataPath, extraArgs = [], buildSettings = [] }) {
|
|
1352
|
+
return [
|
|
1353
|
+
project.flag,
|
|
1354
|
+
project.path,
|
|
1355
|
+
"-scheme",
|
|
1356
|
+
scheme,
|
|
1357
|
+
"-configuration",
|
|
1358
|
+
configuration,
|
|
1359
|
+
"-sdk",
|
|
1360
|
+
sdk,
|
|
1361
|
+
"-destination",
|
|
1362
|
+
destination || `id=${udid}`,
|
|
1363
|
+
"-derivedDataPath",
|
|
1364
|
+
derivedDataPath,
|
|
1365
|
+
...extraArgs,
|
|
1366
|
+
"build",
|
|
1367
|
+
...buildSettings
|
|
1368
|
+
];
|
|
1369
|
+
}
|
|
1370
|
+
function productsDir(derivedDataPath, { configuration = "Debug", sdk = "iphonesimulator" } = {}) {
|
|
1371
|
+
return join(derivedDataPath, "Build", "Products", `${configuration}-${sdk}`);
|
|
1372
|
+
}
|
|
1373
|
+
function pickAppBundle(entries, preferredName = null) {
|
|
1374
|
+
const apps = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string" && e.endsWith(".app")).toSorted();
|
|
1375
|
+
if (apps.length === 0) return null;
|
|
1376
|
+
if (preferredName) {
|
|
1377
|
+
const wanted = `${preferredName}.app`;
|
|
1378
|
+
const exact = apps.find((a) => a === wanted);
|
|
1379
|
+
if (exact) return exact;
|
|
1380
|
+
}
|
|
1381
|
+
return apps[0];
|
|
1382
|
+
}
|
|
1383
|
+
function findAppBundle(dir, preferredName = null) {
|
|
1384
|
+
let entries;
|
|
1385
|
+
try {
|
|
1386
|
+
entries = readdirSync(dir);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
const app = pickAppBundle(entries, preferredName);
|
|
1391
|
+
return app ? join(dir, app) : null;
|
|
1392
|
+
}
|
|
1393
|
+
function parseBundleId(plistJson) {
|
|
1394
|
+
if (typeof plistJson !== "string") return null;
|
|
1395
|
+
let data;
|
|
1396
|
+
try {
|
|
1397
|
+
data = JSON.parse(plistJson);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
if (!data || typeof data !== "object") return null;
|
|
1402
|
+
const id = data.CFBundleIdentifier;
|
|
1403
|
+
return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
|
|
1404
|
+
}
|
|
1405
|
+
function readBundleId(appPath, { exec = null } = {}) {
|
|
1406
|
+
const executor = exec || getExecutor();
|
|
1407
|
+
try {
|
|
1408
|
+
const id = parseBundleId(executor.runFile("plutil", [
|
|
1409
|
+
"-convert",
|
|
1410
|
+
"json",
|
|
1411
|
+
"-o",
|
|
1412
|
+
"-",
|
|
1413
|
+
join(appPath, "Info.plist")
|
|
1414
|
+
]));
|
|
1415
|
+
if (id) return id;
|
|
1416
|
+
} catch {}
|
|
1417
|
+
try {
|
|
1418
|
+
const value = executor.runFile("defaults", [
|
|
1419
|
+
"read",
|
|
1420
|
+
join(appPath, "Info"),
|
|
1421
|
+
"CFBundleIdentifier"
|
|
1422
|
+
]);
|
|
1423
|
+
const trimmed = String(value).trim();
|
|
1424
|
+
return trimmed === "" ? null : trimmed;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function parseBundleExecutable(plistJson) {
|
|
1430
|
+
if (typeof plistJson !== "string") return null;
|
|
1431
|
+
let data;
|
|
1432
|
+
try {
|
|
1433
|
+
data = JSON.parse(plistJson);
|
|
1434
|
+
} catch {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
if (!data || typeof data !== "object") return null;
|
|
1438
|
+
const name = data.CFBundleExecutable;
|
|
1439
|
+
return typeof name === "string" && name.trim() !== "" ? name.trim() : null;
|
|
1440
|
+
}
|
|
1441
|
+
function readBundleExecutable(appPath, { exec = null } = {}) {
|
|
1442
|
+
const executor = exec || getExecutor();
|
|
1443
|
+
try {
|
|
1444
|
+
const name = parseBundleExecutable(executor.runFile("plutil", [
|
|
1445
|
+
"-convert",
|
|
1446
|
+
"json",
|
|
1447
|
+
"-o",
|
|
1448
|
+
"-",
|
|
1449
|
+
join(appPath, "Info.plist")
|
|
1450
|
+
]));
|
|
1451
|
+
if (name) return name;
|
|
1452
|
+
} catch {}
|
|
1453
|
+
try {
|
|
1454
|
+
const value = executor.runFile("defaults", [
|
|
1455
|
+
"read",
|
|
1456
|
+
join(appPath, "Info"),
|
|
1457
|
+
"CFBundleExecutable"
|
|
1458
|
+
]);
|
|
1459
|
+
const trimmed = String(value).trim();
|
|
1460
|
+
return trimmed === "" ? null : trimmed;
|
|
1461
|
+
} catch {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function tailLines(lines, count = 5) {
|
|
1466
|
+
return (Array.isArray(lines) ? lines : []).filter((l) => typeof l === "string" && l.trim() !== "").slice(-count);
|
|
1467
|
+
}
|
|
1468
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1469
|
+
const HEARTBEAT_ACTIVITY = {
|
|
1470
|
+
build: "compiling",
|
|
1471
|
+
deps: "installing",
|
|
1472
|
+
pods: "installing"
|
|
1473
|
+
};
|
|
1474
|
+
function heartbeatLine(elapsedMs, label = "build", estimateMs = null) {
|
|
1475
|
+
const activity = HEARTBEAT_ACTIVITY[label] ?? "running";
|
|
1476
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
1477
|
+
const inside = estimateMs && estimateMs > 0 ? elapsedMs > estimateMs ? `${elapsed}, usually ~${formatElapsed(estimateMs)}` : `${elapsed} of ~${formatElapsed(estimateMs)}` : elapsed;
|
|
1478
|
+
return phaseLine(label, `still ${activity} (${inside})`);
|
|
1479
|
+
}
|
|
1480
|
+
const defaultSchedule = (run, delayMs) => {
|
|
1481
|
+
const timer = setTimeout(run, delayMs);
|
|
1482
|
+
timer.unref?.();
|
|
1483
|
+
return () => clearTimeout(timer);
|
|
1484
|
+
};
|
|
1485
|
+
function startBuildHeartbeat({ intervalMs, elapsed, emit, label = "build", estimateMs = null, schedule = defaultSchedule }) {
|
|
1486
|
+
if (!(intervalMs > 0)) return () => {};
|
|
1487
|
+
let stopped = false;
|
|
1488
|
+
let lastBeat = 0;
|
|
1489
|
+
let cancel = null;
|
|
1490
|
+
function tick() {
|
|
1491
|
+
if (stopped) return;
|
|
1492
|
+
const ms = elapsed();
|
|
1493
|
+
const beat = Math.floor(ms / intervalMs) * intervalMs;
|
|
1494
|
+
if (beat > lastBeat) {
|
|
1495
|
+
lastBeat = beat;
|
|
1496
|
+
emit(heartbeatLine(beat, label, estimateMs));
|
|
1497
|
+
}
|
|
1498
|
+
cancel = schedule(tick, intervalMs - ms % intervalMs);
|
|
1499
|
+
}
|
|
1500
|
+
cancel = schedule(tick, intervalMs);
|
|
1501
|
+
return () => {
|
|
1502
|
+
stopped = true;
|
|
1503
|
+
cancel?.();
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function failedResult({ code, diagnostics, durationMs, exitCode = null, transcriptLines = 0, tail = [], compilationCache = COMPILATION_CACHE_UNAVAILABLE }) {
|
|
1507
|
+
const capped = capDiagnostics(diagnostics);
|
|
1508
|
+
return {
|
|
1509
|
+
failed: true,
|
|
1510
|
+
code,
|
|
1511
|
+
diagnostics: capped.diagnostics,
|
|
1512
|
+
truncated: capped.truncated,
|
|
1513
|
+
durationMs,
|
|
1514
|
+
exitCode,
|
|
1515
|
+
transcriptLines,
|
|
1516
|
+
tail,
|
|
1517
|
+
compilationCache
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const COMPILATION_CACHE_UNAVAILABLE = {
|
|
1521
|
+
status: "unavailable",
|
|
1522
|
+
hits: null,
|
|
1523
|
+
cacheableTasks: null,
|
|
1524
|
+
hitRatePercent: null
|
|
1525
|
+
};
|
|
1526
|
+
const COMPILATION_CACHE_NOT_RUN = {
|
|
1527
|
+
status: "not-run",
|
|
1528
|
+
hits: null,
|
|
1529
|
+
cacheableTasks: null,
|
|
1530
|
+
hitRatePercent: null
|
|
1531
|
+
};
|
|
1532
|
+
function parseCompilationCacheActivity(line) {
|
|
1533
|
+
if (typeof line !== "string") return null;
|
|
1534
|
+
const match = line.match(/(?:note:\s*)?(\d+) hits \/ (\d+) cacheable tasks \((\d+(?:\.\d+)?)%\)/);
|
|
1535
|
+
if (!match) return null;
|
|
1536
|
+
return {
|
|
1537
|
+
status: "reported",
|
|
1538
|
+
hits: Number(match[1]),
|
|
1539
|
+
cacheableTasks: Number(match[2]),
|
|
1540
|
+
hitRatePercent: Number(match[3])
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function compilationCacheActivityLine(activity) {
|
|
1544
|
+
if (activity.status === "reported") return `${activity.hits}/${activity.cacheableTasks} hits (${activity.hitRatePercent}%)`;
|
|
1545
|
+
if (activity.status === "not-run") return "not run; artifact cache supplied the app";
|
|
1546
|
+
return "unavailable; Xcode did not report reliable statistics";
|
|
1547
|
+
}
|
|
1548
|
+
async function buildIos({ root, udid = null, logWriter, project = null, scheme = null, configuration = "Debug", sdk = "iphonesimulator", destination = null, derivedDataPath = null, extraArgs = [], compilationCache = void 0, optimizations = resolveOptimizations({}, {}).ios, now = () => Date.now(), exec = null, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line), onNote = (line) => console.error(line) }) {
|
|
1549
|
+
if (!root || typeof root !== "string") throw new TypeError("buildIos requires {root}");
|
|
1550
|
+
if (!logWriter || typeof logWriter.write !== "function") throw new TypeError("buildIos requires {logWriter} with a write() method");
|
|
1551
|
+
if (!udid && !destination) throw new TypeError("buildIos requires {udid} (or an explicit {destination})");
|
|
1552
|
+
const executor = exec || getExecutor();
|
|
1553
|
+
const dd = derivedDataPath || (scheme ? join(workspaceDerivedData(root), `scheme-${createHash("sha256").update(scheme).digest("hex")}`) : workspaceDerivedData(root));
|
|
1554
|
+
const startedAt = now();
|
|
1555
|
+
const elapsed = () => now() - startedAt;
|
|
1556
|
+
const reportError = (message, remedy) => {
|
|
1557
|
+
logWriter.write({
|
|
1558
|
+
src: "build",
|
|
1559
|
+
level: "error",
|
|
1560
|
+
msg: message,
|
|
1561
|
+
event: "build_diagnostic",
|
|
1562
|
+
...remedy ? { remedy } : {}
|
|
1563
|
+
});
|
|
1564
|
+
};
|
|
1565
|
+
let target = project;
|
|
1566
|
+
if (!target) {
|
|
1567
|
+
const discovered = discoverXcodeProject(root);
|
|
1568
|
+
if (discovered.error) {
|
|
1569
|
+
reportError(discovered.error.message, discovered.error.remedy);
|
|
1570
|
+
return failedResult({
|
|
1571
|
+
code: discovered.error.code,
|
|
1572
|
+
diagnostics: [{
|
|
1573
|
+
message: discovered.error.message,
|
|
1574
|
+
remedy: discovered.error.remedy ?? void 0
|
|
1575
|
+
}],
|
|
1576
|
+
durationMs: elapsed()
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
target = discovered;
|
|
1580
|
+
}
|
|
1581
|
+
const resolvedTarget = target;
|
|
1582
|
+
const selected = resolveScheme(resolvedTarget, {
|
|
1583
|
+
exec: executor,
|
|
1584
|
+
scheme
|
|
1585
|
+
});
|
|
1586
|
+
if (selected.error) {
|
|
1587
|
+
reportError(selected.error.message, selected.error.remedy);
|
|
1588
|
+
return failedResult({
|
|
1589
|
+
code: selected.error.code,
|
|
1590
|
+
diagnostics: [{
|
|
1591
|
+
message: selected.error.message,
|
|
1592
|
+
remedy: selected.error.remedy
|
|
1593
|
+
}],
|
|
1594
|
+
durationMs: elapsed()
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const buildScheme = selected.scheme;
|
|
1598
|
+
const buildSettings = compilationCache === void 0 ? resolveCompilationCacheSettings({
|
|
1599
|
+
root,
|
|
1600
|
+
derivedDataPath: dd,
|
|
1601
|
+
exec: executor,
|
|
1602
|
+
onNote,
|
|
1603
|
+
optimizations
|
|
1604
|
+
}) : compilationCache || [];
|
|
1605
|
+
const args = xcodebuildArgs({
|
|
1606
|
+
project: resolvedTarget,
|
|
1607
|
+
scheme: buildScheme,
|
|
1608
|
+
udid,
|
|
1609
|
+
destination,
|
|
1610
|
+
configuration,
|
|
1611
|
+
sdk,
|
|
1612
|
+
derivedDataPath: dd,
|
|
1613
|
+
extraArgs,
|
|
1614
|
+
buildSettings
|
|
1615
|
+
});
|
|
1616
|
+
logWriter.write({
|
|
1617
|
+
src: "build",
|
|
1618
|
+
level: "info",
|
|
1619
|
+
msg: `xcodebuild ${args.join(" ")}`,
|
|
1620
|
+
event: "build_start"
|
|
1621
|
+
});
|
|
1622
|
+
const transcript = [];
|
|
1623
|
+
let compilationCacheActivity = COMPILATION_CACHE_UNAVAILABLE;
|
|
1624
|
+
const onLine = (line) => {
|
|
1625
|
+
const msg = cleanLine(line);
|
|
1626
|
+
transcript.push(msg);
|
|
1627
|
+
if (msg.trim() === "") return;
|
|
1628
|
+
logWriter.write({
|
|
1629
|
+
src: "build",
|
|
1630
|
+
level: "debug",
|
|
1631
|
+
msg
|
|
1632
|
+
});
|
|
1633
|
+
const activity = parseCompilationCacheActivity(msg);
|
|
1634
|
+
if (activity) {
|
|
1635
|
+
compilationCacheActivity = activity;
|
|
1636
|
+
logWriter.write({
|
|
1637
|
+
src: "build",
|
|
1638
|
+
level: "info",
|
|
1639
|
+
msg: compilationCacheActivityLine(activity),
|
|
1640
|
+
event: "compilation_cache",
|
|
1641
|
+
hits: activity.hits,
|
|
1642
|
+
cacheableTasks: activity.cacheableTasks,
|
|
1643
|
+
hitRatePercent: activity.hitRatePercent
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
let child;
|
|
1648
|
+
try {
|
|
1649
|
+
child = executor.spawn("xcodebuild", args, {
|
|
1650
|
+
cwd: resolvedTarget.dir || root,
|
|
1651
|
+
stdio: [
|
|
1652
|
+
"ignore",
|
|
1653
|
+
"pipe",
|
|
1654
|
+
"pipe"
|
|
1655
|
+
],
|
|
1656
|
+
detached: false,
|
|
1657
|
+
env: {
|
|
1658
|
+
...process.env,
|
|
1659
|
+
NSUnbufferedIO: "YES",
|
|
1660
|
+
FORCE_COLOR: "0"
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
const message = `Could not run xcodebuild: ${err.message}`;
|
|
1665
|
+
const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
|
|
1666
|
+
reportError(message, remedy);
|
|
1667
|
+
return failedResult({
|
|
1668
|
+
code: "STIM_BUILD_FAILED",
|
|
1669
|
+
diagnostics: [{
|
|
1670
|
+
message,
|
|
1671
|
+
remedy
|
|
1672
|
+
}],
|
|
1673
|
+
durationMs: elapsed()
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
const outReader = createLineReader(onLine);
|
|
1677
|
+
const errReader = createLineReader(onLine);
|
|
1678
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1679
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1680
|
+
child.stdout?.on("data", (chunk) => outReader.push(chunk));
|
|
1681
|
+
child.stderr?.on("data", (chunk) => errReader.push(chunk));
|
|
1682
|
+
const stopHeartbeat = startBuildHeartbeat({
|
|
1683
|
+
intervalMs: heartbeatMs,
|
|
1684
|
+
elapsed,
|
|
1685
|
+
emit: onHeartbeat,
|
|
1686
|
+
estimateMs
|
|
1687
|
+
});
|
|
1688
|
+
const outcome = await new Promise((settlePromise) => {
|
|
1689
|
+
let settled = false;
|
|
1690
|
+
const finish = (value) => {
|
|
1691
|
+
if (settled) return;
|
|
1692
|
+
settled = true;
|
|
1693
|
+
outReader.flush();
|
|
1694
|
+
errReader.flush();
|
|
1695
|
+
settlePromise(value);
|
|
1696
|
+
};
|
|
1697
|
+
child.on("close", (code, signal) => finish({
|
|
1698
|
+
code,
|
|
1699
|
+
signal
|
|
1700
|
+
}));
|
|
1701
|
+
child.on("error", (error) => finish({
|
|
1702
|
+
code: null,
|
|
1703
|
+
signal: null,
|
|
1704
|
+
error
|
|
1705
|
+
}));
|
|
1706
|
+
});
|
|
1707
|
+
stopHeartbeat();
|
|
1708
|
+
const durationMs = elapsed();
|
|
1709
|
+
const text = transcript.join("\n");
|
|
1710
|
+
if (outcome.error) {
|
|
1711
|
+
const message = `Could not run xcodebuild: ${outcome.error.message}`;
|
|
1712
|
+
const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
|
|
1713
|
+
reportError(message, remedy);
|
|
1714
|
+
return failedResult({
|
|
1715
|
+
code: "STIM_BUILD_FAILED",
|
|
1716
|
+
diagnostics: [{
|
|
1717
|
+
message,
|
|
1718
|
+
remedy
|
|
1719
|
+
}],
|
|
1720
|
+
durationMs,
|
|
1721
|
+
transcriptLines: transcript.length,
|
|
1722
|
+
tail: tailLines(transcript),
|
|
1723
|
+
compilationCache: compilationCacheActivity
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
if (outcome.code !== 0) {
|
|
1727
|
+
const diagnostics = extractXcodeDiagnostics(text, root, sdk);
|
|
1728
|
+
const capped = capDiagnostics(diagnostics);
|
|
1729
|
+
for (const d of capped.diagnostics) reportError(describeDiagnostic(d), d.remedy);
|
|
1730
|
+
if (capped.diagnostics.length === 0) reportError(`xcodebuild exited ${outcome.code ?? `on ${outcome.signal}`} with no recognizable diagnostic; see the transcript above.`, null);
|
|
1731
|
+
return failedResult({
|
|
1732
|
+
code: "STIM_BUILD_FAILED",
|
|
1733
|
+
diagnostics,
|
|
1734
|
+
durationMs,
|
|
1735
|
+
exitCode: outcome.code,
|
|
1736
|
+
transcriptLines: transcript.length,
|
|
1737
|
+
tail: tailLines(transcript),
|
|
1738
|
+
compilationCache: compilationCacheActivity
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
const products = productsDir(dd, {
|
|
1742
|
+
configuration,
|
|
1743
|
+
sdk
|
|
1744
|
+
});
|
|
1745
|
+
let appPath = null;
|
|
1746
|
+
if (scheme) try {
|
|
1747
|
+
const targets = JSON.parse(executor.runFile("xcodebuild", [
|
|
1748
|
+
resolvedTarget.flag,
|
|
1749
|
+
resolvedTarget.path,
|
|
1750
|
+
"-scheme",
|
|
1751
|
+
buildScheme,
|
|
1752
|
+
"-configuration",
|
|
1753
|
+
configuration,
|
|
1754
|
+
"-sdk",
|
|
1755
|
+
sdk,
|
|
1756
|
+
"-destination",
|
|
1757
|
+
destination || `id=${udid}`,
|
|
1758
|
+
"-derivedDataPath",
|
|
1759
|
+
dd,
|
|
1760
|
+
...extraArgs,
|
|
1761
|
+
...buildSettings,
|
|
1762
|
+
"-showBuildSettings",
|
|
1763
|
+
"-json"
|
|
1764
|
+
], { timeoutMs: 3e4 }));
|
|
1765
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1766
|
+
for (const item of Array.isArray(targets) ? targets : []) {
|
|
1767
|
+
const settings = item?.buildSettings;
|
|
1768
|
+
if (settings?.PRODUCT_TYPE !== "com.apple.product-type.application" || settings.PLATFORM_NAME !== sdk) continue;
|
|
1769
|
+
const dir = settings.TARGET_BUILD_DIR;
|
|
1770
|
+
const name = settings.FULL_PRODUCT_NAME;
|
|
1771
|
+
if (typeof dir !== "string" || !isAbsolute(dir) || typeof name !== "string" || basename(name) !== name || !name.endsWith(".app")) continue;
|
|
1772
|
+
const path = join(dir, name);
|
|
1773
|
+
if (statSync(path).isDirectory()) paths.add(path);
|
|
1774
|
+
}
|
|
1775
|
+
if (paths.size === 1) appPath = [...paths][0];
|
|
1776
|
+
} catch {}
|
|
1777
|
+
else appPath = findAppBundle(products, buildScheme);
|
|
1778
|
+
if (!appPath) {
|
|
1779
|
+
const message = scheme ? `xcodebuild succeeded, but the built app for scheme ${JSON.stringify(buildScheme)} could not be identified unambiguously from its build settings.` : `xcodebuild reported success but no .app is in ${products}.`;
|
|
1780
|
+
const remedy = "Check that the scheme builds one application target for this platform, not a library, test bundle, or several apps.";
|
|
1781
|
+
reportError(message, remedy);
|
|
1782
|
+
return failedResult({
|
|
1783
|
+
code: "STIM_BUILD_FAILED",
|
|
1784
|
+
diagnostics: [{
|
|
1785
|
+
message,
|
|
1786
|
+
remedy
|
|
1787
|
+
}],
|
|
1788
|
+
durationMs,
|
|
1789
|
+
exitCode: 0,
|
|
1790
|
+
transcriptLines: transcript.length,
|
|
1791
|
+
tail: tailLines(transcript),
|
|
1792
|
+
compilationCache: compilationCacheActivity
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const bundleId = readBundleId(appPath, { exec: executor });
|
|
1796
|
+
if (!bundleId) {
|
|
1797
|
+
const message = `No readable CFBundleIdentifier in ${join(appPath, "Info.plist")}.`;
|
|
1798
|
+
const remedy = "Check PRODUCT_BUNDLE_IDENTIFIER in the target's Debug configuration.";
|
|
1799
|
+
reportError(message, remedy);
|
|
1800
|
+
return failedResult({
|
|
1801
|
+
code: "STIM_BUILD_FAILED",
|
|
1802
|
+
diagnostics: [{
|
|
1803
|
+
message,
|
|
1804
|
+
remedy
|
|
1805
|
+
}],
|
|
1806
|
+
durationMs,
|
|
1807
|
+
exitCode: 0,
|
|
1808
|
+
transcriptLines: transcript.length,
|
|
1809
|
+
tail: tailLines(transcript),
|
|
1810
|
+
compilationCache: compilationCacheActivity
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
logWriter.write({
|
|
1814
|
+
src: "build",
|
|
1815
|
+
level: "info",
|
|
1816
|
+
msg: `BUILD SUCCEEDED ${appPath} (${bundleId}) in ${durationMs}ms`,
|
|
1817
|
+
event: "build_done"
|
|
1818
|
+
});
|
|
1819
|
+
return {
|
|
1820
|
+
appPath,
|
|
1821
|
+
bundleId,
|
|
1822
|
+
durationMs,
|
|
1823
|
+
scheme: buildScheme,
|
|
1824
|
+
project: resolvedTarget,
|
|
1825
|
+
derivedDataPath: dd,
|
|
1826
|
+
productsDir: products,
|
|
1827
|
+
transcriptLines: transcript.length,
|
|
1828
|
+
compilationCache: compilationCacheActivity
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
//#endregion
|
|
1832
|
+
export { unknownAndroidSystemImageRefusal as C, simslimIsOnPath as E, liveOwnedDeviceCount as S, unknownIosRuntimeRefusal as T, AvdRecoveryError as _, ccacheEnabled as a, ensureBooted as b, discoverXcodeProject as c, resolveScheme as d, startBuildHeartbeat as f, podInstallCommand as g, bundlerPin as h, buildIos as i, readBundleExecutable as l, describeDiagnostic as m, COMPILATION_CACHE_UNAVAILABLE as n, compilationCacheActivityLine as o, tailLines as p, HEARTBEAT_INTERVAL_MS as r, detectXcodeMajor as s, COMPILATION_CACHE_NOT_RUN as t, readBundleId as u, checkDeviceCapacity as v, unknownIosDeviceTypeRefusal as w, ensureOwnedDevice as x, clearIosAdoptionPending as y };
|