stim 1.0.0 → 1.1.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 +22 -10
- package/dist/android-CC5bUer7.mjs +204 -0
- package/dist/android-DnZlT3aU.mjs +2279 -0
- package/dist/android-cas-DGlTStWu.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/android-xmyGBNy5.mjs +665 -0
- package/dist/app-install-oZIQRbdD.mjs +1141 -0
- package/dist/build-lock-vE45ZzJZ.mjs +645 -0
- package/dist/build-slots-CgacA9zQ.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-oeH18ERr.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.mjs +6 -4
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-D7uu8Gbq.mjs} +43 -12
- package/dist/deps-64wVnpZc.mjs +435 -0
- package/dist/dev-client-Bvky1hya.mjs +557 -0
- package/dist/device-DJKv5Qej.mjs +250 -0
- package/dist/device-lease-D-G3SgVe.mjs +335 -0
- package/dist/device-pool-BlHXnYYs.mjs +408 -0
- package/dist/device-remote-CCgkG82J.mjs +1209 -0
- package/dist/doctor-BRF9aAJ3.mjs +530 -0
- package/dist/doctor-DlVxTtMq.mjs +1137 -0
- package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
- package/dist/gc-DhXdgSdt.mjs +1651 -0
- package/dist/guide-DrhrDBo5.mjs +4319 -0
- package/dist/ios-BZxKPuqP.mjs +382 -0
- package/dist/ios-CK1Vrk6Q.mjs +2721 -0
- package/dist/ios-device-CYbuRNXR.mjs +81 -0
- package/dist/ios-device-CdSm0GDw.mjs +413 -0
- package/dist/logs-Dxoy16Qv.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-B01H3EuD.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-B9QIdQOw.mjs +475 -0
- package/dist/ownership-D8HmSv2E.mjs +60 -0
- package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-COj6nzzh.mjs} +12 -3
- package/dist/reclaim-BPvN2HlP.mjs +347 -0
- package/dist/reload-DYZEBbZy.mjs +332 -0
- package/dist/remote-cache-DOi5-zLj.mjs +934 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-BYHqEeyw.mjs} +3 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-BEMr_m6p.mjs} +5 -4
- package/dist/server-expo-rb0k-0w7.mjs +2 -0
- package/dist/settings-BSwrY4FZ.mjs +687 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-ByCylYtB.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-IYEY-zi8.mjs} +21 -22
- package/dist/stats-DZ4Q5J-7.mjs +62 -0
- package/dist/stats-wqxPm7Tv.mjs +307 -0
- package/dist/status-Dia0XTqG.mjs +363 -0
- package/dist/stop-BApyaF21.mjs +591 -0
- package/dist/stop-D6o5zAdm.mjs +3 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
- package/dist/worktree-Dx3ZC5lS.mjs +539 -0
- package/dist/worktree-DxEmUI_n.mjs +1322 -0
- package/dist/xcode-CDB6fytw.mjs +1705 -0
- package/package.json +5 -5
- 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,2279 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { C as withConfigLock, S as upsertProject, f as getProject, g as loadConfig, l as getConcurrencyLimits, y as saveConfig } from "./config-D7uu8Gbq.mjs";
|
|
3
|
+
import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
|
|
4
|
+
import { D as workspaceLogsDir, T as workspaceDir, _ as writeWorkspaceState, g as writeWorkspaceLaunch, n as MODE_EXPO, p as readWorkspaceState, t as MODE_BARE, v as emulatorLogFile } from "./state-IYEY-zi8.mjs";
|
|
5
|
+
import { C as removeTemporaryEntry, S as makeTemporaryDirectory, g as repoRoot, s as gitCommonDir } from "./worktree-Dx3ZC5lS.mjs";
|
|
6
|
+
import { a as findProjectRoot, c as projectShortcut, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-COj6nzzh.mjs";
|
|
7
|
+
import { r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
|
|
8
|
+
import { i as resolveProjectMetro, r as pidExists } from "./metro-B01H3EuD.mjs";
|
|
9
|
+
import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-D8HmSv2E.mjs";
|
|
10
|
+
import { a as easAuthNote, c as loadProjectProvider, d as resolveEasCliBin, f as resolveRemote, i as checkEasAuth, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as uploadRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS, x as parkedMaxSetting } from "./remote-cache-DOi5-zLj.mjs";
|
|
11
|
+
import { A as settingFile, B as compilerCacheFallbackMessage, E as remoteDeviceSettingError, F as unknownSettingKeys, H as resolveOptimizations, L as detectProviders, M as settingShapeErrors, O as resolveCacheProviderConfig, P as tunnelModeSetting, T as remoteAndroidSetting, V as optimizationBuildProfile, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, k as resolveSettings, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, w as publicUrlSetting, z as artifactCachePolicy } from "./settings-BSwrY4FZ.mjs";
|
|
12
|
+
import { _ as launchErrorPreview, c as launchErrorReport, g as stepTimer, h as stepClock, i as formatDuration, l as phaseLine, n as appReadinessMessage, p as shortHash } from "./command-output-8wHMEde0.mjs";
|
|
13
|
+
import { D as verifyAndroidReleaseLaunch, E as unverifiedLaunchLines, O as verifyLaunch, S as launchAndroidReleaseApp, _ as installConflictKind, c as RELEASE_VERIFY_WAIT_MS, d as androidDevClientUrl, g as installAndroidApp, h as deviceShellArg, i as LAUNCH_BUNDLING, n as DEFAULT_METRO_PORT, o as LAUNCH_FATAL, s as LAUNCH_UNVERIFIED, t as ADB_INSTALL_TIMEOUT_MS, u as androidAppProcess, x as launchAndroidApp } from "./app-install-oZIQRbdD.mjs";
|
|
14
|
+
import { C as untrackedNativeFiles, D as readAssetManifest, E as compareAssetManifests, S as untrackedMissLine, T as captureAssetManifest, _ as providerUploadOutcome, b as storeBuild, c as buildCacheKey, d as filesystemBuildCapability, f as fingerprintDiffRecord, g as providerDownloadPath, h as prepareProviderDownloadDir, i as releaseBuildLock, l as describeFingerprintMiss, m as fingerprintProject, n as acquireBuildLock, o as takeoverLine, p as fingerprintDiffSuffix, s as waitForBuild, t as WAIT_CEILING_MS, v as refingerprintAfterMutation, w as assetDiffReason, x as storedAssetManifest, y as resolveBuild } from "./build-lock-vE45ZzJZ.mjs";
|
|
15
|
+
import { r as claimFailure } from "./ownership-claim-CeJLEpjD.mjs";
|
|
16
|
+
import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-BlHXnYYs.mjs";
|
|
17
|
+
import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-CgacA9zQ.mjs";
|
|
18
|
+
import { c as ensureWorkspaceStorageSafely, d as noMetroMessage, f as noMetroRemedy, i as detectEntryFile, n as pickDevClientScheme, o as needsPrebuild, p as resolveMetroWithRetry, s as runPrebuild, t as devClientScheme, u as launchOutcomeRecord } from "./dev-client-Bvky1hya.mjs";
|
|
19
|
+
import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-wqxPm7Tv.mjs";
|
|
20
|
+
import { E as physicalDeviceModel, O as probeEmulatorSerial, _ as listAdbDevices, a as androidSystemImageAbi, b as memoizeEmulatorProbe, f as emulatorDiskSpaceRemedy, h as findBuildTool, i as androidPoolNoCandidatesRefusal, j as resolvePhysicalDevice, k as resetAdoptedAvd, m as extractEmulatorFailure, n as androidHome, p as emulatorFailureRemedy, r as androidPoolCandidates, t as androidDeviceAbi, y as listInstalledSystemImages } from "./android-xmyGBNy5.mjs";
|
|
21
|
+
import { S as unknownAndroidSystemImageRefusal, _ as checkDeviceCapacity, b as ensureOwnedDevice, f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS, y as ensureBooted } from "./xcode-CDB6fytw.mjs";
|
|
22
|
+
import { a as ensureMetroReachable, b as ownedSessionName, l as resolveRemoteContext, n as binOnPath, o as ensureRemoteBootOwned, s as remoteAndroidDeps } from "./device-remote-CCgkG82J.mjs";
|
|
23
|
+
import { t as cleanLine } from "./server-expo-BEMr_m6p.mjs";
|
|
24
|
+
import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-DGlTStWu.mjs";
|
|
25
|
+
import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
|
|
26
|
+
import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-B5IJwv3-.mjs";
|
|
27
|
+
import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
28
|
+
import { InvalidArgumentError } from "commander";
|
|
29
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
30
|
+
import chalk from "chalk";
|
|
31
|
+
import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
|
|
32
|
+
//#region src/engine/apk-swap.ts
|
|
33
|
+
const ANDROID_BUNDLE_NAME = "index.android.bundle";
|
|
34
|
+
const ANDROID_BUNDLE_ENTRY = `assets/${ANDROID_BUNDLE_NAME}`;
|
|
35
|
+
const LAST_LINES = 5;
|
|
36
|
+
function hermesEnabledFromGradleProperties(text) {
|
|
37
|
+
if (typeof text !== "string") return true;
|
|
38
|
+
let value = null;
|
|
39
|
+
for (const raw of text.split("\n")) {
|
|
40
|
+
const line = raw.trim();
|
|
41
|
+
if (line === "" || line.startsWith("#") || line.startsWith("!")) continue;
|
|
42
|
+
const match = /^hermesEnabled\s*[=:]\s*(.*)$/.exec(line);
|
|
43
|
+
if (match) value = match[1].trim();
|
|
44
|
+
}
|
|
45
|
+
return value === null || value.toLowerCase() !== "false";
|
|
46
|
+
}
|
|
47
|
+
function readAndroidHermesEnabled(root) {
|
|
48
|
+
try {
|
|
49
|
+
return hermesEnabledFromGradleProperties(readFileSync(join(root, "android", "gradle.properties"), "utf-8"));
|
|
50
|
+
} catch {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function hermescBinDir(platform = process.platform) {
|
|
55
|
+
return platform === "darwin" ? "osx-bin" : "linux64-bin";
|
|
56
|
+
}
|
|
57
|
+
function hermescCandidates(root, { platform = process.platform, reactNativePath = null } = {}) {
|
|
58
|
+
const bin = hermescBinDir(platform);
|
|
59
|
+
const rn = reactNativePath ?? join(root, "node_modules", "react-native");
|
|
60
|
+
return [.../* @__PURE__ */ new Set([
|
|
61
|
+
join(dirname(rn), "hermes-compiler", "hermesc", bin, "hermesc"),
|
|
62
|
+
join(root, "node_modules", "hermes-compiler", "hermesc", bin, "hermesc"),
|
|
63
|
+
join(root, "node_modules", "react-native", "sdks", "hermesc", bin, "hermesc"),
|
|
64
|
+
join(root, "node_modules", "react-native", "sdks", "hermes", "build", "bin", "hermesc")
|
|
65
|
+
])];
|
|
66
|
+
}
|
|
67
|
+
function androidHermescPath(root, { exists = existsSync, platform = process.platform, reactNativePath = null } = {}) {
|
|
68
|
+
const candidates = hermescCandidates(root, {
|
|
69
|
+
platform,
|
|
70
|
+
reactNativePath
|
|
71
|
+
});
|
|
72
|
+
return candidates.find(exists) ?? candidates[candidates.length - 1];
|
|
73
|
+
}
|
|
74
|
+
function androidHermescArgs({ bundle, out }) {
|
|
75
|
+
return [
|
|
76
|
+
"-emit-binary",
|
|
77
|
+
"-O",
|
|
78
|
+
"-w",
|
|
79
|
+
"-out",
|
|
80
|
+
out,
|
|
81
|
+
bundle
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
function androidBundleCommand({ isExpo, entryFile, bundleOutput, assetsDest }) {
|
|
85
|
+
if (isExpo) return {
|
|
86
|
+
file: "npx",
|
|
87
|
+
args: [
|
|
88
|
+
"expo",
|
|
89
|
+
"export:embed",
|
|
90
|
+
"--platform",
|
|
91
|
+
"android",
|
|
92
|
+
"--dev",
|
|
93
|
+
"false",
|
|
94
|
+
"--bundle-output",
|
|
95
|
+
bundleOutput,
|
|
96
|
+
"--assets-dest",
|
|
97
|
+
assetsDest
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
file: "npx",
|
|
102
|
+
args: [
|
|
103
|
+
"react-native",
|
|
104
|
+
"bundle",
|
|
105
|
+
"--platform",
|
|
106
|
+
"android",
|
|
107
|
+
"--dev",
|
|
108
|
+
"false",
|
|
109
|
+
"--entry-file",
|
|
110
|
+
entryFile,
|
|
111
|
+
"--bundle-output",
|
|
112
|
+
bundleOutput,
|
|
113
|
+
"--assets-dest",
|
|
114
|
+
assetsDest
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function isNothingToDelete(text) {
|
|
119
|
+
return /nothing to do|name not matched|no matches found/i.test(String(text ?? ""));
|
|
120
|
+
}
|
|
121
|
+
function zipalignArgs({ buildToolsMajor, input, output }) {
|
|
122
|
+
return [
|
|
123
|
+
...buildToolsMajor >= 35 ? ["-P", "16"] : ["-p"],
|
|
124
|
+
"-f",
|
|
125
|
+
"-v",
|
|
126
|
+
"4",
|
|
127
|
+
input,
|
|
128
|
+
output
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function keystorePassArg(value) {
|
|
132
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
133
|
+
if (text === "") return "pass:android";
|
|
134
|
+
if (/^(?:pass|file|env):/.test(text) || text === "stdin") return text;
|
|
135
|
+
return `pass:${text}`;
|
|
136
|
+
}
|
|
137
|
+
function resolveKeystore(root, settings) {
|
|
138
|
+
const android = settings?.["android"];
|
|
139
|
+
const bag = android && typeof android === "object" && !Array.isArray(android) ? android : {};
|
|
140
|
+
const configured = bag["keystore"];
|
|
141
|
+
return {
|
|
142
|
+
path: typeof configured === "string" && configured.trim() !== "" ? configured.trim().startsWith("/") ? configured.trim() : join(root, configured.trim()) : join(root, "android", "app", "debug.keystore"),
|
|
143
|
+
pass: keystorePassArg(bag["keystorePassword"])
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function apksignerArgs({ keystore, apkPath }) {
|
|
147
|
+
return [
|
|
148
|
+
"sign",
|
|
149
|
+
"--ks",
|
|
150
|
+
keystore.path,
|
|
151
|
+
"--ks-pass",
|
|
152
|
+
keystore.pass,
|
|
153
|
+
apkPath
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
async function swapApkBundle({ root, isExpo, cachedApkPath, keystore, logWriter = null, exec = null, spawnFn = null, mkdtemp = () => makeTemporaryDirectory(cachedApkPath, "stim-apk-swap-"), exists = existsSync, hermesEnabled = null, buildTools = null, findTool = findBuildTool, storedAssets = null, readManifest = readAssetManifest, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, onHeartbeat = (line) => console.error(line) }) {
|
|
157
|
+
const e = exec || getExecutor();
|
|
158
|
+
const startedAt = now();
|
|
159
|
+
const elapsed = () => now() - startedAt;
|
|
160
|
+
let tmp;
|
|
161
|
+
const fail = (step, reason, lastLines = []) => {
|
|
162
|
+
logWriter?.write?.({
|
|
163
|
+
src: "build",
|
|
164
|
+
level: "error",
|
|
165
|
+
msg: `APK swap failed at ${step}: ${reason}`,
|
|
166
|
+
event: "apk_swap"
|
|
167
|
+
});
|
|
168
|
+
if (tmp) removeTemporaryEntry(tmp);
|
|
169
|
+
return {
|
|
170
|
+
failed: true,
|
|
171
|
+
step,
|
|
172
|
+
reason,
|
|
173
|
+
lastLines,
|
|
174
|
+
durationMs: elapsed()
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
let work;
|
|
178
|
+
let final;
|
|
179
|
+
try {
|
|
180
|
+
tmp = mkdtemp();
|
|
181
|
+
const base = basename(cachedApkPath);
|
|
182
|
+
work = join(tmp, `unaligned-${base}`);
|
|
183
|
+
final = join(tmp, base);
|
|
184
|
+
try {
|
|
185
|
+
e.runFile("cp", [
|
|
186
|
+
"-c",
|
|
187
|
+
cachedApkPath,
|
|
188
|
+
work
|
|
189
|
+
]);
|
|
190
|
+
} catch {
|
|
191
|
+
e.runFile("cp", [cachedApkPath, work]);
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return fail("copy", `could not copy ${cachedApkPath} aside: ${describe(err)}`);
|
|
195
|
+
}
|
|
196
|
+
const stage = join(tmp, "stage");
|
|
197
|
+
const bundleOutput = join(stage, "assets", ANDROID_BUNDLE_NAME);
|
|
198
|
+
const assetsDest = join(stage, "res");
|
|
199
|
+
const command = androidBundleCommand({
|
|
200
|
+
isExpo,
|
|
201
|
+
entryFile: isExpo ? "index.js" : detectEntryFile(root),
|
|
202
|
+
bundleOutput,
|
|
203
|
+
assetsDest
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
mkdirSync(join(stage, "assets"), { recursive: true });
|
|
207
|
+
mkdirSync(assetsDest, { recursive: true });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return fail("bundle", `could not create ${stage}: ${describe(err)}`);
|
|
210
|
+
}
|
|
211
|
+
logWriter?.write?.({
|
|
212
|
+
src: "build",
|
|
213
|
+
level: "info",
|
|
214
|
+
msg: `${command.file} ${command.args.join(" ")}`,
|
|
215
|
+
event: "apk_swap"
|
|
216
|
+
});
|
|
217
|
+
const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
|
|
218
|
+
const transcript = [];
|
|
219
|
+
const push = (line) => {
|
|
220
|
+
const msg = cleanLine(line);
|
|
221
|
+
if (msg.trim() === "") return;
|
|
222
|
+
transcript.push(msg);
|
|
223
|
+
logWriter?.write?.({
|
|
224
|
+
src: "build",
|
|
225
|
+
level: "debug",
|
|
226
|
+
msg,
|
|
227
|
+
raw: true,
|
|
228
|
+
event: "apk_swap"
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
let child;
|
|
232
|
+
try {
|
|
233
|
+
child = spawn(command.file, command.args, {
|
|
234
|
+
cwd: root,
|
|
235
|
+
stdio: [
|
|
236
|
+
"ignore",
|
|
237
|
+
"pipe",
|
|
238
|
+
"pipe"
|
|
239
|
+
],
|
|
240
|
+
env: {
|
|
241
|
+
...process.env,
|
|
242
|
+
FORCE_COLOR: "0"
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
|
|
247
|
+
}
|
|
248
|
+
const reader = {
|
|
249
|
+
out: createLineReader(push),
|
|
250
|
+
err: createLineReader(push)
|
|
251
|
+
};
|
|
252
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
253
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
254
|
+
child.stdout?.on("data", (chunk) => reader.out.push(chunk));
|
|
255
|
+
child.stderr?.on("data", (chunk) => reader.err.push(chunk));
|
|
256
|
+
const stopHeartbeat = startBuildHeartbeat({
|
|
257
|
+
intervalMs: heartbeatMs,
|
|
258
|
+
elapsed,
|
|
259
|
+
emit: onHeartbeat,
|
|
260
|
+
label: "swap"
|
|
261
|
+
});
|
|
262
|
+
let outcome;
|
|
263
|
+
try {
|
|
264
|
+
outcome = await waitForChild(child);
|
|
265
|
+
} finally {
|
|
266
|
+
stopHeartbeat();
|
|
267
|
+
}
|
|
268
|
+
reader.out.flush();
|
|
269
|
+
reader.err.flush();
|
|
270
|
+
if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
|
|
271
|
+
if (outcome.code !== 0) {
|
|
272
|
+
const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
|
|
273
|
+
return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
|
|
274
|
+
}
|
|
275
|
+
if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${ANDROID_BUNDLE_NAME} at ${bundleOutput}`);
|
|
276
|
+
let hermes = false;
|
|
277
|
+
let note;
|
|
278
|
+
if (hermesEnabled ?? readAndroidHermesEnabled(root)) {
|
|
279
|
+
const hermesc = androidHermescPath(root);
|
|
280
|
+
if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
|
|
281
|
+
else {
|
|
282
|
+
const hbc = `${bundleOutput}.hbc`;
|
|
283
|
+
try {
|
|
284
|
+
e.runFile(hermesc, androidHermescArgs({
|
|
285
|
+
bundle: bundleOutput,
|
|
286
|
+
out: hbc
|
|
287
|
+
}));
|
|
288
|
+
e.runFile("mv", [hbc, bundleOutput]);
|
|
289
|
+
hermes = true;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const refuse = (reason, assetDiff) => {
|
|
296
|
+
logWriter?.write?.({
|
|
297
|
+
src: "build",
|
|
298
|
+
level: "warn",
|
|
299
|
+
msg: `APK swap refused: ${reason}`,
|
|
300
|
+
event: "apk_swap"
|
|
301
|
+
});
|
|
302
|
+
removeTemporaryEntry(tmp);
|
|
303
|
+
const result = {
|
|
304
|
+
assetMismatch: true,
|
|
305
|
+
reason,
|
|
306
|
+
durationMs: elapsed()
|
|
307
|
+
};
|
|
308
|
+
if (assetDiff) result.assetDiff = assetDiff;
|
|
309
|
+
return result;
|
|
310
|
+
};
|
|
311
|
+
if (!storedAssets) return refuse("this cache entry predates asset tracking (no assets-manifest.json beside the artifact), so its asset set cannot be proven to match this one");
|
|
312
|
+
const fresh = readManifest(assetsDest);
|
|
313
|
+
if (!fresh) return fail("assets", `could not hash the assets emitted into ${assetsDest}, so the asset set cannot be verified`);
|
|
314
|
+
const diff = compareAssetManifests(fresh, storedAssets);
|
|
315
|
+
if (!diff.same) return refuse(assetDiffReason(diff), diff);
|
|
316
|
+
try {
|
|
317
|
+
e.runFile("zip", [
|
|
318
|
+
"-d",
|
|
319
|
+
work,
|
|
320
|
+
ANDROID_BUNDLE_ENTRY
|
|
321
|
+
]);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (!isNothingToDelete(describe(err))) return fail("zip", `zip -d ${ANDROID_BUNDLE_ENTRY} failed on ${work}: ${describe(err)}`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
e.runFile("zip", [
|
|
327
|
+
"-0",
|
|
328
|
+
"-r",
|
|
329
|
+
work,
|
|
330
|
+
"assets"
|
|
331
|
+
], { cwd: stage });
|
|
332
|
+
} catch (err) {
|
|
333
|
+
return fail("zip", `zip -0 -r ${work} assets failed: ${describe(err)}`);
|
|
334
|
+
}
|
|
335
|
+
const tools = buildTools ?? findTool(["zipalign"]);
|
|
336
|
+
if (!tools) return fail("zipalign", "no zipalign found under the Android SDK build-tools; install one with `sdkmanager \"build-tools;36.0.0\"`");
|
|
337
|
+
try {
|
|
338
|
+
e.runFile(tools.path, zipalignArgs({
|
|
339
|
+
buildToolsMajor: tools.major,
|
|
340
|
+
input: work,
|
|
341
|
+
output: final
|
|
342
|
+
}));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return fail("zipalign", `zipalign failed on ${work}: ${describe(err)}`);
|
|
345
|
+
}
|
|
346
|
+
const signer = buildTools ? {
|
|
347
|
+
...buildTools,
|
|
348
|
+
path: join(dirname(buildTools.path), "apksigner")
|
|
349
|
+
} : findTool(["apksigner"]);
|
|
350
|
+
if (!signer) return fail("apksigner", "no apksigner found under the Android SDK build-tools; install one with `sdkmanager \"build-tools;36.0.0\"`");
|
|
351
|
+
try {
|
|
352
|
+
e.runFile(signer.path, apksignerArgs({
|
|
353
|
+
keystore,
|
|
354
|
+
apkPath: final
|
|
355
|
+
}));
|
|
356
|
+
} catch (err) {
|
|
357
|
+
return fail("apksigner", `apksigner sign failed on ${final} with ${keystore.path}: ${describe(err)}`);
|
|
358
|
+
}
|
|
359
|
+
logWriter?.write?.({
|
|
360
|
+
src: "build",
|
|
361
|
+
level: "info",
|
|
362
|
+
msg: `APK swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${final} in ${elapsed()}ms`,
|
|
363
|
+
event: "apk_swap"
|
|
364
|
+
});
|
|
365
|
+
const result = {
|
|
366
|
+
ok: true,
|
|
367
|
+
apkPath: final,
|
|
368
|
+
tmpDir: tmp,
|
|
369
|
+
hermes,
|
|
370
|
+
durationMs: elapsed()
|
|
371
|
+
};
|
|
372
|
+
if (note) result.note = note;
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
function describe(err) {
|
|
376
|
+
const e = err;
|
|
377
|
+
const streams = [e?.stdout, e?.stderr].map((s) => s ? String(s).trim() : "").filter(Boolean).join("\n");
|
|
378
|
+
const message = e?.message ? String(e.message).trim() : String(err);
|
|
379
|
+
return streams ? `${message}: ${streams}` : message;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/commands/android/support.ts
|
|
383
|
+
const PLATFORM = "android";
|
|
384
|
+
function androidVariantSetting(settings) {
|
|
385
|
+
const android = settings?.["android"];
|
|
386
|
+
if (!android || typeof android !== "object" || Array.isArray(android)) return null;
|
|
387
|
+
const raw = android["variant"];
|
|
388
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
389
|
+
}
|
|
390
|
+
function resolveVariant(flag, settings) {
|
|
391
|
+
return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || androidVariantSetting(settings);
|
|
392
|
+
}
|
|
393
|
+
function androidSystemImageSetting(settings) {
|
|
394
|
+
const android = settings?.["android"];
|
|
395
|
+
if (!android || typeof android !== "object" || Array.isArray(android)) return null;
|
|
396
|
+
const raw = android["systemImage"];
|
|
397
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
398
|
+
}
|
|
399
|
+
function resolveSystemImage(flag, settings) {
|
|
400
|
+
return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || androidSystemImageSetting(settings);
|
|
401
|
+
}
|
|
402
|
+
function systemImageRefusal({ flag, resolved, physical, remoteBackend, listImages }) {
|
|
403
|
+
if (typeof flag === "string" && flag.trim() === "") return {
|
|
404
|
+
code: "STIM_BAD_ARG",
|
|
405
|
+
message: "--system-image was given an empty id.",
|
|
406
|
+
remedy: "Pass `--system-image <id>` with an sdkmanager package id, e.g. \"system-images;android-36;google_apis;arm64-v8a\"."
|
|
407
|
+
};
|
|
408
|
+
if (physical || remoteBackend) return null;
|
|
409
|
+
if (!resolved) return null;
|
|
410
|
+
let images;
|
|
411
|
+
try {
|
|
412
|
+
images = listImages();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
return {
|
|
415
|
+
code: NO_DEVICE,
|
|
416
|
+
message: `Could not read the installed Android system images: ${error?.message || error}`,
|
|
417
|
+
remedy: "Check that ANDROID_HOME points at a readable SDK, then try again."
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const refusal = unknownAndroidSystemImageRefusal(resolved, images);
|
|
421
|
+
return refusal ? {
|
|
422
|
+
code: "STIM_BAD_ARG",
|
|
423
|
+
message: refusal.message,
|
|
424
|
+
remedy: refusal.remedy
|
|
425
|
+
} : null;
|
|
426
|
+
}
|
|
427
|
+
function isReleaseVariant(variant) {
|
|
428
|
+
return typeof variant === "string" && /release$/i.test(variant.trim());
|
|
429
|
+
}
|
|
430
|
+
const NO_METRO = "STIM_NO_METRO";
|
|
431
|
+
const NO_FINGERPRINT = "STIM_NO_FINGERPRINT";
|
|
432
|
+
const NO_DEVICE = "STIM_NO_DEVICE";
|
|
433
|
+
const LAUNCH_FAILED = "STIM_LAUNCH_FAILED";
|
|
434
|
+
function findAapt(home = androidHome(), { readDir = readdirSync, exists = existsSync } = {}) {
|
|
435
|
+
const found = findBuildTool(["aapt", "aapt2"], {
|
|
436
|
+
home,
|
|
437
|
+
readDir,
|
|
438
|
+
exists
|
|
439
|
+
});
|
|
440
|
+
return found ? {
|
|
441
|
+
path: found.path,
|
|
442
|
+
tool: found.tool,
|
|
443
|
+
version: found.version
|
|
444
|
+
} : null;
|
|
445
|
+
}
|
|
446
|
+
function dumpApkManifest(apkPath, { exec = null, aapt = null } = {}) {
|
|
447
|
+
if (typeof apkPath !== "string" || apkPath.trim() === "") return null;
|
|
448
|
+
const tool = aapt || findAapt();
|
|
449
|
+
if (!tool) return null;
|
|
450
|
+
const e = exec || getExecutor();
|
|
451
|
+
const args = tool.tool === "aapt2" ? [
|
|
452
|
+
"dump",
|
|
453
|
+
"xmltree",
|
|
454
|
+
"--file",
|
|
455
|
+
"AndroidManifest.xml",
|
|
456
|
+
apkPath
|
|
457
|
+
] : [
|
|
458
|
+
"dump",
|
|
459
|
+
"xmltree",
|
|
460
|
+
apkPath,
|
|
461
|
+
"AndroidManifest.xml"
|
|
462
|
+
];
|
|
463
|
+
try {
|
|
464
|
+
const out = e.runFile(tool.path, args);
|
|
465
|
+
return typeof out === "string" && out.includes("E: manifest") ? out : null;
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function parseXmltree(text) {
|
|
471
|
+
const root = {
|
|
472
|
+
tag: "#root",
|
|
473
|
+
attrs: {},
|
|
474
|
+
children: [],
|
|
475
|
+
indent: -1
|
|
476
|
+
};
|
|
477
|
+
const stack = [root];
|
|
478
|
+
for (const raw of String(text ?? "").split("\n")) {
|
|
479
|
+
const indent = raw.search(/\S/);
|
|
480
|
+
if (indent < 0) continue;
|
|
481
|
+
const line = raw.trim();
|
|
482
|
+
const element = /^E: ([\w.:-]+)/.exec(line);
|
|
483
|
+
if (element) {
|
|
484
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
485
|
+
const node = {
|
|
486
|
+
tag: element[1],
|
|
487
|
+
attrs: {},
|
|
488
|
+
children: [],
|
|
489
|
+
indent
|
|
490
|
+
};
|
|
491
|
+
stack[stack.length - 1].children.push(node);
|
|
492
|
+
stack.push(node);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const attr = /^A: ([^(=]+?)(?:\(0x[0-9a-f]+\))?=(.*)$/.exec(line);
|
|
496
|
+
if (attr && stack.length > 1) {
|
|
497
|
+
const name = attr[1].replace(/^http:\/\/schemas\.android\.com\/apk\/res\/android:/, "android:");
|
|
498
|
+
const value = /^"((?:[^"\\]|\\.)*)"/.exec(attr[2]);
|
|
499
|
+
stack[stack.length - 1].attrs[name] = value ? value[1] : null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return root;
|
|
503
|
+
}
|
|
504
|
+
function eachNode(node, fn) {
|
|
505
|
+
fn(node);
|
|
506
|
+
for (const child of node.children) eachNode(child, fn);
|
|
507
|
+
}
|
|
508
|
+
function apkPackage(text) {
|
|
509
|
+
const pkg = parseXmltree(text).children.find((c) => c.tag === "manifest")?.attrs["package"];
|
|
510
|
+
return typeof pkg === "string" && pkg.trim() ? pkg.trim() : null;
|
|
511
|
+
}
|
|
512
|
+
function apkDevClientFacts(text) {
|
|
513
|
+
const root = parseXmltree(text);
|
|
514
|
+
const facts = {
|
|
515
|
+
devClient: false,
|
|
516
|
+
schemes: []
|
|
517
|
+
};
|
|
518
|
+
let launchable = null;
|
|
519
|
+
eachNode(root, (node) => {
|
|
520
|
+
const name = node.attrs["android:name"];
|
|
521
|
+
if (typeof name === "string" && name.startsWith("expo.modules.devlauncher")) facts.devClient = true;
|
|
522
|
+
if (node.tag !== "activity" && node.tag !== "activity-alias") return;
|
|
523
|
+
const filters = node.children.filter((c) => c.tag === "intent-filter");
|
|
524
|
+
if (!filters.some((f) => f.children.some((c) => c.tag === "action" && c.attrs["android:name"] === "android.intent.action.MAIN")) || launchable) return;
|
|
525
|
+
launchable = node;
|
|
526
|
+
for (const filter of filters) for (const data of filter.children) {
|
|
527
|
+
if (data.tag !== "data") continue;
|
|
528
|
+
const scheme = data.attrs["android:scheme"];
|
|
529
|
+
if (typeof scheme === "string" && scheme.trim()) facts.schemes.push(scheme.trim());
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
return facts;
|
|
533
|
+
}
|
|
534
|
+
function androidDevClientScheme(root, apkPath, { exec = null, dump = dumpApkManifest, aapt = null } = {}) {
|
|
535
|
+
const text = dump(apkPath, {
|
|
536
|
+
exec,
|
|
537
|
+
aapt
|
|
538
|
+
});
|
|
539
|
+
if (text) {
|
|
540
|
+
const facts = apkDevClientFacts(text);
|
|
541
|
+
if (!facts.devClient) return void 0;
|
|
542
|
+
const scheme = pickDevClientScheme(facts.schemes);
|
|
543
|
+
if (scheme) return scheme;
|
|
544
|
+
}
|
|
545
|
+
return devClientScheme(root, null);
|
|
546
|
+
}
|
|
547
|
+
function noDeviceDiagnostic({ reason, logFile, remedy, localEmulator = true, readLog = readEmulatorLogTail }) {
|
|
548
|
+
const text = localEmulator ? readLog(logFile) : "";
|
|
549
|
+
const logPath = text.trim() ? logFile : null;
|
|
550
|
+
const found = extractEmulatorFailure(text);
|
|
551
|
+
const diskRemedy = localEmulator ? emulatorDiskSpaceRemedy([reason, ...found]) : null;
|
|
552
|
+
if (found.length === 0) return {
|
|
553
|
+
message: reason,
|
|
554
|
+
remedy: diskRemedy ?? remedy,
|
|
555
|
+
lines: [],
|
|
556
|
+
logPath
|
|
557
|
+
};
|
|
558
|
+
return {
|
|
559
|
+
message: `${reason} The emulator reported: ${found[0]}`,
|
|
560
|
+
remedy: diskRemedy ?? emulatorFailureRemedy(found),
|
|
561
|
+
lines: found.slice(1),
|
|
562
|
+
logPath
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function readEmulatorLogTail(file) {
|
|
566
|
+
try {
|
|
567
|
+
return readFileSync(file, "utf-8").split("\n").slice(-400).join("\n");
|
|
568
|
+
} catch {
|
|
569
|
+
return "";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function displayPath(root, path) {
|
|
573
|
+
const rel = relative(root, path);
|
|
574
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
575
|
+
}
|
|
576
|
+
async function pooledAndroidDevice({ root, selectPool, listDevices, isEmulatorDevice, deviceModel, waitSeconds, noWait, now, warn }) {
|
|
577
|
+
const isEmulator = memoizeEmulatorProbe(isEmulatorDevice);
|
|
578
|
+
const pooled = await selectPool({
|
|
579
|
+
root,
|
|
580
|
+
platform: PLATFORM,
|
|
581
|
+
idLabel: "serial",
|
|
582
|
+
list: () => androidPoolCandidates(listDevices(), isEmulator).map((entry) => ({ id: entry.serial })),
|
|
583
|
+
noCandidates: () => {
|
|
584
|
+
const resolved = androidPoolNoCandidatesRefusal(listDevices(), isEmulator);
|
|
585
|
+
return {
|
|
586
|
+
message: resolved.error,
|
|
587
|
+
remedy: resolved.remedy
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
waitSeconds,
|
|
591
|
+
noWait,
|
|
592
|
+
now,
|
|
593
|
+
warn
|
|
594
|
+
});
|
|
595
|
+
if (pooled.status === "refused") {
|
|
596
|
+
const { code, message, remedy, lease } = pooled.refusal;
|
|
597
|
+
return {
|
|
598
|
+
code,
|
|
599
|
+
message,
|
|
600
|
+
remedy,
|
|
601
|
+
extra: lease === null ? {} : { lease }
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return { device: {
|
|
605
|
+
serial: pooled.candidate.id,
|
|
606
|
+
deviceName: deviceModel(pooled.candidate.id) ?? pooled.candidate.id,
|
|
607
|
+
owned: false
|
|
608
|
+
} };
|
|
609
|
+
}
|
|
610
|
+
function androidBuildOptions({ release, physical, device, variant, deviceAbi, compiler, buildProfile, targetAbiOnly = true }) {
|
|
611
|
+
let abi = null;
|
|
612
|
+
if (targetAbiOnly && !release && physical && device.serial) abi = deviceAbi(device.serial);
|
|
613
|
+
if (targetAbiOnly && !release && !physical) abi = androidSystemImageAbi(device.systemImage);
|
|
614
|
+
const runOptions = {};
|
|
615
|
+
if (variant) runOptions.variant = variant;
|
|
616
|
+
if (abi) runOptions.abi = abi;
|
|
617
|
+
if (compiler) runOptions.compiler = compiler;
|
|
618
|
+
if (buildProfile) runOptions.buildProfile = buildProfile;
|
|
619
|
+
return {
|
|
620
|
+
abi,
|
|
621
|
+
runOptions,
|
|
622
|
+
remoteRunOptions: Object.keys(runOptions).length === 0 ? null : runOptions
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/commands/android/result.ts
|
|
627
|
+
function androidFacts({ serial, avdName = null, deviceName = null, systemImage = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease }) {
|
|
628
|
+
return {
|
|
629
|
+
platform: PLATFORM,
|
|
630
|
+
serial: serial ?? null,
|
|
631
|
+
avdName: avdName ?? null,
|
|
632
|
+
deviceName: deviceName ?? avdName ?? null,
|
|
633
|
+
systemImage: systemImage ?? null,
|
|
634
|
+
fingerprint: fingerprint ?? null,
|
|
635
|
+
cacheKey: cacheKey ?? null,
|
|
636
|
+
variant: variant ?? null,
|
|
637
|
+
metroPort: metroPort ?? null,
|
|
638
|
+
cacheHit: cacheLevel(cacheHit),
|
|
639
|
+
cacheSkipped: Boolean(cacheSkipped),
|
|
640
|
+
waitedForBuild: waitedForBuild ? {
|
|
641
|
+
pid: waitedForBuild.pid ?? null,
|
|
642
|
+
ms: waitedForBuild.ms ?? 0
|
|
643
|
+
} : null,
|
|
644
|
+
appPath: appPath ?? null,
|
|
645
|
+
bundleId: bundleId ?? null,
|
|
646
|
+
installSkipped: Boolean(installSkipped),
|
|
647
|
+
launched: launched === "unverified" || launched === "bundling" ? launched : Boolean(launched),
|
|
648
|
+
ccache,
|
|
649
|
+
debugHttpHost: debugHttpHost ?? null,
|
|
650
|
+
debugHttpHostNote: debugHttpHostNote ?? null,
|
|
651
|
+
devClientUrl: devClientUrl ?? null,
|
|
652
|
+
logs: logs ?? null,
|
|
653
|
+
durationMs: typeof durationMs === "number" && Number.isFinite(durationMs) ? durationMs : null,
|
|
654
|
+
...lease === void 0 ? {} : { lease }
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null }) {
|
|
658
|
+
const record = {
|
|
659
|
+
platform: PLATFORM,
|
|
660
|
+
avdName: avdName ?? null,
|
|
661
|
+
deviceName: deviceName ?? avdName ?? null,
|
|
662
|
+
fingerprint: fingerprint ?? null,
|
|
663
|
+
cacheKey: cacheKey ?? null,
|
|
664
|
+
cacheHit: cacheLevel(cacheHit),
|
|
665
|
+
cacheSkipped: Boolean(cacheSkipped),
|
|
666
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : null,
|
|
667
|
+
appPath: appPath ?? null,
|
|
668
|
+
bundleId: bundleId ?? null,
|
|
669
|
+
startedAt,
|
|
670
|
+
status
|
|
671
|
+
};
|
|
672
|
+
if (errorCode) record.errorCode = errorCode;
|
|
673
|
+
return record;
|
|
674
|
+
}
|
|
675
|
+
async function finishAndroidUpload(uploadPending, remote, phase) {
|
|
676
|
+
if (!uploadPending) return false;
|
|
677
|
+
const upload = await uploadPending;
|
|
678
|
+
if (upload?.uploaded) phase("cache", `uploaded (${remote?.name})`);
|
|
679
|
+
else if (upload?.timedOut) {
|
|
680
|
+
phase("cache", chalk.yellow(`${remote?.name} upload still running after ${formatDuration(UPLOAD_TIMEOUT_MS)}; not waiting`));
|
|
681
|
+
return true;
|
|
682
|
+
} else if (upload?.failed) {
|
|
683
|
+
const authNote = remote?.name === "eas" && isEasAuthFailureText(upload.failed) ? easAuthNote({
|
|
684
|
+
code: "logged-out",
|
|
685
|
+
reason: upload.failed,
|
|
686
|
+
phase: "upload"
|
|
687
|
+
}) : null;
|
|
688
|
+
phase("cache", chalk.yellow(authNote || `${remote?.name} upload failed: ${upload.failed}`));
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
function reportAndroidResult({ json, useBuildCache, variant, release, metroCheck, metroPort, logsDir, serial, apkPath, androidPackage, installSkipped, record, waitedForBuild, remote, providerName, launchState, launchWarning, launched, ccache, durationMs, writer, emit, lease, recordRun }) {
|
|
693
|
+
recordRun({
|
|
694
|
+
failed: false,
|
|
695
|
+
cacheHit: cacheLevel(record.cacheHit),
|
|
696
|
+
waited: waitedForBuild,
|
|
697
|
+
durationMs
|
|
698
|
+
});
|
|
699
|
+
const facts = androidFacts({
|
|
700
|
+
serial,
|
|
701
|
+
avdName: record.avdName,
|
|
702
|
+
deviceName: record.deviceName,
|
|
703
|
+
systemImage: record.systemImage,
|
|
704
|
+
debugHttpHost: launched.debugHttpHost ?? null,
|
|
705
|
+
debugHttpHostNote: launched.debugHttpHostNote ?? null,
|
|
706
|
+
devClientUrl: launched.devClientUrl ?? null,
|
|
707
|
+
fingerprint: record.fingerprint,
|
|
708
|
+
cacheKey: record.cacheKey,
|
|
709
|
+
variant,
|
|
710
|
+
metroPort,
|
|
711
|
+
cacheHit: record.cacheHit,
|
|
712
|
+
cacheSkipped: !useBuildCache,
|
|
713
|
+
waitedForBuild,
|
|
714
|
+
appPath: apkPath,
|
|
715
|
+
bundleId: androidPackage,
|
|
716
|
+
installSkipped,
|
|
717
|
+
launched: launchState,
|
|
718
|
+
ccache,
|
|
719
|
+
logs: logsDir,
|
|
720
|
+
durationMs,
|
|
721
|
+
lease
|
|
722
|
+
});
|
|
723
|
+
writer.close();
|
|
724
|
+
if (json) emit(JSON.stringify(facts));
|
|
725
|
+
else {
|
|
726
|
+
const summary = `${launchWarning ? "WARNING" : "OK"}: ${androidPackage} launched on ${serial}, ${release ? `${variant} (embedded JS, no Metro)` : `Metro port ${metroPort}`} (${cacheOutcome(record.cacheHit, remote?.name ?? providerName)})`;
|
|
727
|
+
const outcome = launchWarning ? chalk.yellow(`${summary} -- ${launchWarning}`) : launchState === "unverified" ? chalk.yellow(`${summary} -- launch UNVERIFIED`) : launchState === "bundling" ? chalk.green(`${summary} -- bundle requested, still building`) : chalk.green(summary);
|
|
728
|
+
const deviceName = record.avdName || record.deviceName || serial;
|
|
729
|
+
const cacheResult = useBuildCache ? cacheOutcome(record.cacheHit, remote?.name ?? providerName) : "bypassed; built";
|
|
730
|
+
const metroResult = release ? `embedded (${variant})` : !metroCheck ? `check skipped on port ${metroPort}` : launchState === "unverified" ? `state unverified on port ${metroPort}` : `running on port ${metroPort}`;
|
|
731
|
+
emit([
|
|
732
|
+
outcome,
|
|
733
|
+
phaseLine("device", `${deviceName} (${serial})`),
|
|
734
|
+
phaseLine("app", androidPackage),
|
|
735
|
+
phaseLine("metro", metroResult),
|
|
736
|
+
phaseLine("cache", cacheResult),
|
|
737
|
+
...ccache.status === "not-run" ? [phaseLine("compilation cache", ccacheActivityLine(ccache))] : [],
|
|
738
|
+
phaseLine("logs", logsDir || "unavailable (remote device)")
|
|
739
|
+
].join("\n"));
|
|
740
|
+
}
|
|
741
|
+
return facts;
|
|
742
|
+
}
|
|
743
|
+
function cacheOutcome(cacheHit, providerName = null) {
|
|
744
|
+
if (cacheHit === "remote") return `cache hit from ${providerName || "the remote cache"}`;
|
|
745
|
+
if (cacheHit === "local") return "cache hit";
|
|
746
|
+
return "built";
|
|
747
|
+
}
|
|
748
|
+
function persistLastBuild({ writeState, root, record, startedAt, durationMs, status, errorCode = null, out }) {
|
|
749
|
+
const lastBuild = lastBuildRecord({
|
|
750
|
+
...record,
|
|
751
|
+
startedAt,
|
|
752
|
+
durationMs,
|
|
753
|
+
status,
|
|
754
|
+
errorCode
|
|
755
|
+
});
|
|
756
|
+
try {
|
|
757
|
+
writeState(root, { lastBuild });
|
|
758
|
+
} catch (err) {
|
|
759
|
+
out(phaseLine("state", chalk.yellow(`could not record lastBuild: ${err?.message || err}`)));
|
|
760
|
+
}
|
|
761
|
+
return lastBuild;
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/commands/android/collector.ts
|
|
765
|
+
function collectorEntry() {
|
|
766
|
+
return spawnEntry("collector-run");
|
|
767
|
+
}
|
|
768
|
+
function collectorLogFile(root) {
|
|
769
|
+
return join(workspaceLogsDir(root), `collector-${PLATFORM}.log`);
|
|
770
|
+
}
|
|
771
|
+
const COLLECTOR_EXIT_WAIT_MS = 2e3;
|
|
772
|
+
const COLLECTOR_POLL_MS = 25;
|
|
773
|
+
function killPreviousCollector(root, { platform = PLATFORM, kill = (pid, signal) => process.kill(pid, signal), collectors = null, verify = verifyCollectorOwnership, isAlive = pidExists, note = (_line) => {} } = {}) {
|
|
774
|
+
const record = (collectors ?? readCollectors(root))?.[platform];
|
|
775
|
+
const pid = Number(record?.pid);
|
|
776
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return null;
|
|
777
|
+
const ownership = verify({
|
|
778
|
+
pid,
|
|
779
|
+
platform,
|
|
780
|
+
root,
|
|
781
|
+
isAlive,
|
|
782
|
+
expected: record
|
|
783
|
+
});
|
|
784
|
+
if (ownership.status === "gone") return null;
|
|
785
|
+
if (ownership.status === "unverified") {
|
|
786
|
+
note(chalk.dim(`Previous ${platform} log collector (pid ${pid}): ${ownership.reason}, so it was not signalled -- starting a replacement anyway`));
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
kill(pid, "SIGTERM");
|
|
791
|
+
return pid;
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function startCollector({ root, serial, packageName, spawn, kill, alive = pidExists, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), out }) {
|
|
797
|
+
const previousPid = killPreviousCollector(root, {
|
|
798
|
+
kill,
|
|
799
|
+
isAlive: alive,
|
|
800
|
+
verify,
|
|
801
|
+
note: (line) => out(phaseLine("logs", line))
|
|
802
|
+
});
|
|
803
|
+
if (previousPid) {
|
|
804
|
+
const deadline = Date.now() + waitMs;
|
|
805
|
+
while (Date.now() < deadline && alive(previousPid)) await sleep(COLLECTOR_POLL_MS);
|
|
806
|
+
}
|
|
807
|
+
let stdio = "ignore";
|
|
808
|
+
try {
|
|
809
|
+
mkdirSync(workspaceLogsDir(root), { recursive: true });
|
|
810
|
+
const fd = openSync(collectorLogFile(root), "a");
|
|
811
|
+
stdio = [
|
|
812
|
+
"ignore",
|
|
813
|
+
fd,
|
|
814
|
+
fd
|
|
815
|
+
];
|
|
816
|
+
} catch {}
|
|
817
|
+
try {
|
|
818
|
+
const child = spawn(process.execPath, [
|
|
819
|
+
collectorEntry(),
|
|
820
|
+
"--platform",
|
|
821
|
+
PLATFORM,
|
|
822
|
+
"--root",
|
|
823
|
+
root,
|
|
824
|
+
"--serial",
|
|
825
|
+
serial,
|
|
826
|
+
"--package",
|
|
827
|
+
packageName
|
|
828
|
+
], {
|
|
829
|
+
cwd: root,
|
|
830
|
+
detached: true,
|
|
831
|
+
stdio,
|
|
832
|
+
env: process.env
|
|
833
|
+
});
|
|
834
|
+
child?.unref?.();
|
|
835
|
+
return child?.pid ?? null;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
out(phaseLine("logs", chalk.yellow(`could not start the device log collector: ${err?.message || err}`)));
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/commands/android/launch.ts
|
|
843
|
+
async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, verifyReleaseLaunched, verifyLaunched, serial, androidPackage, variant, metroCheck, logsDir, launchedAt, metroPort, isExpo, physical, scheme, component = null, phase }) {
|
|
844
|
+
const readNativeCrashes = () => remoteDevice ? [] : captureNativeCrashes({
|
|
845
|
+
root,
|
|
846
|
+
platform: "android",
|
|
847
|
+
deviceId: serial,
|
|
848
|
+
appId: androidPackage,
|
|
849
|
+
since: launchedAt
|
|
850
|
+
}, logsDir);
|
|
851
|
+
if (remoteRelease) {
|
|
852
|
+
phase("verify", chalk.yellow("UNVERIFIED: remote adapter launch accepted; process verification is unavailable"));
|
|
853
|
+
return { state: LAUNCH_UNVERIFIED };
|
|
854
|
+
}
|
|
855
|
+
if (release) {
|
|
856
|
+
const processCheck = await verifyReleaseLaunched({
|
|
857
|
+
serial,
|
|
858
|
+
packageName: androidPackage
|
|
859
|
+
});
|
|
860
|
+
const crashes = readNativeCrashes();
|
|
861
|
+
if (processCheck?.verified && !crashes.length) {
|
|
862
|
+
phase("verify", `process alive ${formatDuration(processCheck.waitedMs ?? 0)} after launch (${variant}: no bundle fetch to observe)`);
|
|
863
|
+
return { state: true };
|
|
864
|
+
}
|
|
865
|
+
if (processCheck?.reason === "probe-failed" && !crashes.length) {
|
|
866
|
+
phase("verify", chalk.yellow("UNVERIFIED: the app process check failed"));
|
|
867
|
+
return { state: LAUNCH_UNVERIFIED };
|
|
868
|
+
}
|
|
869
|
+
for (const line of launchErrorPreview(crashes, root)) phase("launch", chalk.red(line));
|
|
870
|
+
if (!crashes.length) phase("logs", "Process missing; no attributable native crash report captured. Read `stim logs --source device` for available output.");
|
|
871
|
+
phase("verify", chalk.yellow(crashes.length ? "FATAL: the app reported a native crash" : `FATAL: no ${androidPackage} process on ${serial} ${formatDuration(processCheck?.waitedMs ?? 0)} after launch`));
|
|
872
|
+
phase("", chalk.yellow("A release process exited before readiness. Run `stim logs --errors` for captured crash reports, or `stim logs --source device` for the full device output."));
|
|
873
|
+
return { state: LAUNCH_FATAL };
|
|
874
|
+
}
|
|
875
|
+
const verification = metroCheck ? await verifyLaunched({
|
|
876
|
+
onReadinessPending: () => phase("readiness", "waiting for app readiness (up to 30s after bundle load)"),
|
|
877
|
+
logsDir,
|
|
878
|
+
since: launchedAt,
|
|
879
|
+
metroPort,
|
|
880
|
+
platform: "android",
|
|
881
|
+
mode: isExpo ? MODE_EXPO : MODE_BARE,
|
|
882
|
+
readNativeCrashes,
|
|
883
|
+
processAlive: () => {
|
|
884
|
+
const pid = androidAppProcess(serial, androidPackage);
|
|
885
|
+
return pid === void 0 ? null : pid !== null;
|
|
886
|
+
}
|
|
887
|
+
}) : {
|
|
888
|
+
verified: false,
|
|
889
|
+
skipped: true
|
|
890
|
+
};
|
|
891
|
+
const nativeCrashes = verification.errors?.some((record) => record.event === "native_crash") ? [] : readNativeCrashes();
|
|
892
|
+
if (nativeCrashes.length) verification.fatal = true;
|
|
893
|
+
verification.errors = await errorDiagnostics([...verification.errors ?? [], ...nativeCrashes], {
|
|
894
|
+
root,
|
|
895
|
+
logsDir,
|
|
896
|
+
port: metroPort,
|
|
897
|
+
allowRequest: true
|
|
898
|
+
});
|
|
899
|
+
if (verification.readiness) phase("readiness", appReadinessMessage(verification.readiness, verification.waitedMs ?? 0));
|
|
900
|
+
if (verification?.fatal) {
|
|
901
|
+
const nativeFatal = verification.errors?.some((record) => record.event === "native_crash");
|
|
902
|
+
const deliveryFailed = verification.record?.event === "bundle_response_failed";
|
|
903
|
+
const reason = nativeFatal ? "the app reported a native crash" : verification.processAlive === false ? "the app process exited" : deliveryFailed ? "Metro bundle delivery failed" : "Metro could not build the bundle";
|
|
904
|
+
phase("verify", chalk.red(`FATAL after ${formatDuration(verification.waitedMs ?? 0)}: ${reason}`));
|
|
905
|
+
for (const line of launchErrorPreview(verification.errors ?? [], root)) phase("", chalk.red(line));
|
|
906
|
+
if (verification.processAlive === false && !verification.errors?.some((record) => record.event === "native_crash")) phase("logs", "No attributable native crash report captured yet. Run `stim logs --errors` again for delayed reports, or `stim logs --source device` for available output.");
|
|
907
|
+
if (nativeFatal || verification.processAlive === false) phase("remedy", chalk.yellow(`Fix the crash, then restart the app: \`adb -s ${deviceShellArg(serial)} shell am force-stop ${deviceShellArg(androidPackage)}\`, then run \`stim android\` again. A crashed Android process can remain alive behind the system crash dialog; Metro reload cannot recover it.`));
|
|
908
|
+
else if (verification.processAlive === true && metroPort !== null) {
|
|
909
|
+
const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
|
|
910
|
+
phase("remedy", chalk.yellow(`The native app is still running. ${deliveryFailed ? "Check the Metro logs and device connection, then" : "Fix the JavaScript or TypeScript error, then"} ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
|
|
911
|
+
}
|
|
912
|
+
return { state: LAUNCH_FATAL };
|
|
913
|
+
}
|
|
914
|
+
if (verification?.verified) {
|
|
915
|
+
phase("verify", `bundle loaded` + (verification.processAlive === true ? ", process alive" : "") + (verification.readiness ? "" : ", stable for 3s -- the first screen may still be rendering") + ` (${formatDuration(verification.waitedMs ?? 0)} total)`);
|
|
916
|
+
const report = launchErrorReport(verification.errors ?? [], root);
|
|
917
|
+
if (report.summary) phase("launch", chalk.dim(report.summary));
|
|
918
|
+
for (const line of report.lines) phase("launch", chalk.yellow(line));
|
|
919
|
+
if (report.lines.length > 0 && verification.processAlive === true && metroPort !== null) {
|
|
920
|
+
const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
|
|
921
|
+
phase("remedy", chalk.yellow(`The native app is still running. Fix the JavaScript or TypeScript error; Fast Refresh should apply the edit. If the error screen remains, ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
state: true,
|
|
925
|
+
warning: report.lines.length > 0 ? "app errors detected; inspect the error above" : verification.readiness === "timed-out" || verification.readiness === "error" ? "app readiness not confirmed; inspect the UI and logs" : void 0
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (verification?.skipped) {
|
|
929
|
+
phase("verify", "skipped (--no-metro-check): the launch is reported as unverified");
|
|
930
|
+
return { state: LAUNCH_UNVERIFIED };
|
|
931
|
+
}
|
|
932
|
+
if (verification?.requested) {
|
|
933
|
+
phase("verify", `BUNDLING: the app asked port ${metroPort} for its bundle; build, delivery, or JavaScript loading was still pending after ${formatDuration(verification.waitedMs ?? 0)} (a cold bundle on a large graph outlasts this window)`);
|
|
934
|
+
phase("", chalk.dim("Nothing to do: `stim logs --source metro` shows the build finishing, usually within a minute."));
|
|
935
|
+
return { state: LAUNCH_BUNDLING };
|
|
936
|
+
}
|
|
937
|
+
phase("verify", chalk.yellow("UNVERIFIED: no bundle request reached this workspace's Metro"));
|
|
938
|
+
for (const line of unverifiedLaunchLines({
|
|
939
|
+
platform: PLATFORM,
|
|
940
|
+
metroPort: metroPort ?? 8081,
|
|
941
|
+
waitedMs: verification?.waitedMs,
|
|
942
|
+
bundleId: androidPackage,
|
|
943
|
+
serial,
|
|
944
|
+
component,
|
|
945
|
+
devClientUrl: scheme ? androidDevClientUrl(scheme, metroPort ?? 8081, physical) : null,
|
|
946
|
+
mode: isExpo ? MODE_EXPO : MODE_BARE
|
|
947
|
+
})) phase("", chalk.yellow(line));
|
|
948
|
+
return { state: LAUNCH_UNVERIFIED };
|
|
949
|
+
}
|
|
950
|
+
async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, useBuildCache, variant, release, isExpo, metroPort, logsDir, emuLog, device, physical, remoteDevice, bootPromise, bootDuration, apkPath, androidPackage: initialPackage, swapDir, record, waitedForBuild, ccache, uploadPending, providerUpload, providerName, remote, abandonedRemote: remoteWasAbandoned, started, startedAt, writer, phase, fail, readApkPackage, install, launch, launchRelease, resolveDevClientScheme, verifyLaunched, verifyReleaseLaunched, spawn, kill, pidAlive, verifyCollector, writeLaunch, writeState, now, out, emit, recordRun }) {
|
|
951
|
+
let androidPackage = initialPackage;
|
|
952
|
+
let leaseWarned = false;
|
|
953
|
+
const raiseLeaseFor = (boundMs, beforeInstall) => {
|
|
954
|
+
const step = lease?.raise(boundMs);
|
|
955
|
+
if (!step || step.ok) return null;
|
|
956
|
+
if (beforeInstall) {
|
|
957
|
+
const refusal = lostRefusal(step.holder, step.expiresAt, now());
|
|
958
|
+
return fail(refusal.code, refusal.message, refusal.remedy, { lease: refusal.lease });
|
|
959
|
+
}
|
|
960
|
+
if (!leaseWarned) {
|
|
961
|
+
leaseWarned = true;
|
|
962
|
+
phase("lease", chalk.yellow(lostLine(step.holder, step.expiresAt, now())));
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
};
|
|
966
|
+
const booted = await bootPromise;
|
|
967
|
+
if (booted.failed) {
|
|
968
|
+
const diag = noDeviceDiagnostic({
|
|
969
|
+
reason: booted.reason ?? "The emulator did not boot.",
|
|
970
|
+
logFile: emuLog,
|
|
971
|
+
remedy: "Run `stim status` to see what Stim thinks it owns; re-running `stim android` creates a fresh owned AVD.",
|
|
972
|
+
localEmulator: !physical
|
|
973
|
+
});
|
|
974
|
+
return fail(NO_DEVICE, diag.message, diag.remedy, {
|
|
975
|
+
lines: diag.lines,
|
|
976
|
+
logPath: diag.logPath ? displayPath(root, diag.logPath) : null
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const serial = booted.serial;
|
|
980
|
+
phase("device", physical ? `${device.deviceName || serial} (${serial}) connected, not owned by Stim` : `${device.avdName || serial} (${serial}) ${device.adopted || device.adoptionPending ? "adopted" : "booted"} ${bootDuration()}`);
|
|
981
|
+
const packageFromApk = readApkPackage(apkPath);
|
|
982
|
+
if (packageFromApk && androidPackage && packageFromApk !== androidPackage) phase("install", chalk.dim(`applicationId ${packageFromApk} (from the APK; project files say ${androidPackage})`));
|
|
983
|
+
androidPackage = packageFromApk || androidPackage || detectAndroidPackage(root);
|
|
984
|
+
const adopting = !physical && !remoteDevice && Boolean(device.adoptionPending);
|
|
985
|
+
if (adopting) {
|
|
986
|
+
if (!androidPackage || !device.avdName) return fail(LAUNCH_FAILED, "Cannot clean an adopted emulator without its app package and AVD name.", "Retry once the APK applicationId can be read.");
|
|
987
|
+
try {
|
|
988
|
+
await resetAdoptedAvd(device.avdName, serial, androidPackage);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
return fail(LAUNCH_FAILED, `Could not clean adopted emulator ${device.avdName}: ${String(error?.message || error)}`, "Fix the device cleanup error and retry; adoption remains pending and the app was not launched.");
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const lostBeforeInstall = physical ? raiseLeaseFor(ADB_INSTALL_TIMEOUT_MS, true) : null;
|
|
994
|
+
if (lostBeforeInstall) return lostBeforeInstall;
|
|
995
|
+
const installTimer = stepTimer(now);
|
|
996
|
+
const installed = install({
|
|
997
|
+
serial,
|
|
998
|
+
apkPath,
|
|
999
|
+
packageName: androidPackage,
|
|
1000
|
+
allowUninstall: release || adopting
|
|
1001
|
+
});
|
|
1002
|
+
if (installed.failed) {
|
|
1003
|
+
const conflict = installConflictKind(installed.reason);
|
|
1004
|
+
const rerun = useBuildCache ? " Then run this command again; it installs the APK from cache without building it again." : " Then run this command again.";
|
|
1005
|
+
const installRemedy = conflict ? `${androidPackage} is already installed on ${serial} ` + (conflict === "signature" ? "with a different signer" : "at a higher versionCode") + `. Uninstall it first (\`adb -s ${serial} uninstall ${androidPackage}\`); its data goes with it.` + rerun : `Check that ${serial} is still connected (\`adb devices\`) and has room for the APK.`;
|
|
1006
|
+
return fail(installed.code || "STIM_INSTALL_FAILED", installed.reason, installRemedy, { lastBuildStatus: true });
|
|
1007
|
+
}
|
|
1008
|
+
if (adopting) try {
|
|
1009
|
+
withConfigLock(() => {
|
|
1010
|
+
const config = loadConfig();
|
|
1011
|
+
const current = config?.projects?.[root]?.platforms?.android;
|
|
1012
|
+
if (!config || !current || current.avdName !== device.avdName) throw new Error("The adopted emulator assignment changed during installation.");
|
|
1013
|
+
delete current.adoptionPending;
|
|
1014
|
+
saveConfig(config);
|
|
1015
|
+
});
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
return fail(LAUNCH_FAILED, `Could not finish adopting emulator ${device.avdName}: ${String(error?.message || error)}`, "Retry to reconcile the emulator assignment; the app was not launched.");
|
|
1018
|
+
}
|
|
1019
|
+
const installSkipped = Boolean(installed.skipped);
|
|
1020
|
+
phase("install", installSkipped ? `unchanged (${serial} already has this build) ${installTimer()}` : `${basename(apkPath)} -> ${serial} ${installTimer()}`);
|
|
1021
|
+
if (installed.note) {
|
|
1022
|
+
phase("install", chalk.yellow(installed.note));
|
|
1023
|
+
writer.write({
|
|
1024
|
+
src: "build",
|
|
1025
|
+
level: "warn",
|
|
1026
|
+
event: "install_uninstalled_first",
|
|
1027
|
+
msg: installed.note
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (swapDir) try {
|
|
1031
|
+
rmSync(swapDir, {
|
|
1032
|
+
recursive: true,
|
|
1033
|
+
force: true
|
|
1034
|
+
});
|
|
1035
|
+
} catch {}
|
|
1036
|
+
if (androidPackage) upsertProject(root, { androidPackage });
|
|
1037
|
+
record.bundleId = androidPackage;
|
|
1038
|
+
if (!androidPackage) return fail(LAUNCH_FAILED, "Could not determine this app's Android package name, so there is nothing to launch.", "Set `expo.android.package` in app.json / app.config.js, or `namespace` in android/app/build.gradle.", { lastBuildStatus: true });
|
|
1039
|
+
if (physical) raiseLeaseFor(0, false);
|
|
1040
|
+
const scheme = release ? void 0 : resolveDevClientScheme(root, apkPath);
|
|
1041
|
+
const launchTimer = stepTimer(now);
|
|
1042
|
+
const launchedAt = now();
|
|
1043
|
+
writer.write({
|
|
1044
|
+
src: "build",
|
|
1045
|
+
level: "info",
|
|
1046
|
+
marker: true,
|
|
1047
|
+
event: "launch_attempt",
|
|
1048
|
+
ts: launchedAt,
|
|
1049
|
+
platform: "android",
|
|
1050
|
+
appId: androidPackage,
|
|
1051
|
+
deviceId: serial,
|
|
1052
|
+
remote: Boolean(remoteDevice),
|
|
1053
|
+
physical,
|
|
1054
|
+
msg: `launching ${androidPackage} on ${serial}`
|
|
1055
|
+
});
|
|
1056
|
+
const launched = release ? remoteDevice ? remoteDevice.launch({
|
|
1057
|
+
serial,
|
|
1058
|
+
packageName: androidPackage,
|
|
1059
|
+
metroPort: null
|
|
1060
|
+
}) : launchRelease({
|
|
1061
|
+
serial,
|
|
1062
|
+
packageName: androidPackage
|
|
1063
|
+
}) : launch({
|
|
1064
|
+
serial,
|
|
1065
|
+
packageName: androidPackage,
|
|
1066
|
+
metroPort: metroPort ?? 8081,
|
|
1067
|
+
devClientScheme: scheme,
|
|
1068
|
+
physical
|
|
1069
|
+
});
|
|
1070
|
+
if (launched.failed) {
|
|
1071
|
+
printNativeCrashReport({
|
|
1072
|
+
root,
|
|
1073
|
+
platform: "android",
|
|
1074
|
+
deviceId: serial,
|
|
1075
|
+
appId: androidPackage,
|
|
1076
|
+
since: launchedAt
|
|
1077
|
+
}, logsDir, (line) => phase("launch", chalk.red(line)), Boolean(remoteDevice));
|
|
1078
|
+
return fail(launched.code || "STIM_LAUNCH_FAILED", launched.reason, `Check the app installed correctly (\`adb -s ${serial} shell pm list packages ${androidPackage}\`).`, { lastBuildStatus: true });
|
|
1079
|
+
}
|
|
1080
|
+
writer.write({
|
|
1081
|
+
src: "build",
|
|
1082
|
+
level: "info",
|
|
1083
|
+
event: "app_launched",
|
|
1084
|
+
msg: release ? `launched ${androidPackage} on ${serial} (${variant}, embedded JS bundle, no Metro)` : `launched ${androidPackage} on ${serial} against Metro port ${metroPort}`
|
|
1085
|
+
});
|
|
1086
|
+
phase("launch", `${androidPackage} ${launchTimer()}`);
|
|
1087
|
+
if (!physical && !remoteDevice) try {
|
|
1088
|
+
writeLaunch(root, "android", {
|
|
1089
|
+
appId: androidPackage,
|
|
1090
|
+
deviceId: serial,
|
|
1091
|
+
metroPort,
|
|
1092
|
+
release,
|
|
1093
|
+
launchedAt: new Date(launchedAt).toISOString()
|
|
1094
|
+
});
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
out(phaseLine("state", chalk.yellow(`could not record Android launch: ${error?.message || error}`)));
|
|
1097
|
+
}
|
|
1098
|
+
const reversedSummary = (launched.reversed ?? []).join(", ") || `tcp:${metroPort}->tcp:${metroPort}`;
|
|
1099
|
+
if (!release && launched.debugHttpHost) phase("metro", `debug_http_host ${launched.debugHttpHost} + adb reverse ${reversedSummary}`);
|
|
1100
|
+
else if (!release) {
|
|
1101
|
+
phase("metro", chalk.yellow(`adb reverse ${reversedSummary}; ${launched.debugHttpHostNote || "debug_http_host not written"}`));
|
|
1102
|
+
writer.write({
|
|
1103
|
+
src: "build",
|
|
1104
|
+
level: "warn",
|
|
1105
|
+
event: "debug_http_host_failed",
|
|
1106
|
+
msg: `debug_http_host was not written for ${androidPackage} on ${serial}: ${launched.debugHttpHostNote || "unknown reason"}`
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (launched.devClientNote) {
|
|
1110
|
+
phase("metro", chalk.yellow(launched.devClientNote));
|
|
1111
|
+
writer.write({
|
|
1112
|
+
src: "build",
|
|
1113
|
+
level: "warn",
|
|
1114
|
+
event: "dev_client_link_failed",
|
|
1115
|
+
msg: launched.devClientNote
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
const uploadWasAbandoned = await finishAndroidUpload(uploadPending, remote, phase);
|
|
1119
|
+
const providerOutcome = providerUploadOutcome(providerUpload ? await providerUpload : null, providerName);
|
|
1120
|
+
if (providerOutcome) phase("cache", providerOutcome.warn ? chalk.yellow(providerOutcome.line) : providerOutcome.line);
|
|
1121
|
+
persistLastBuild({
|
|
1122
|
+
writeState,
|
|
1123
|
+
root,
|
|
1124
|
+
record,
|
|
1125
|
+
startedAt,
|
|
1126
|
+
durationMs: now() - started,
|
|
1127
|
+
status: "ok",
|
|
1128
|
+
out
|
|
1129
|
+
});
|
|
1130
|
+
const remoteRelease = Boolean(remoteDevice && release);
|
|
1131
|
+
if (!remoteRelease) {
|
|
1132
|
+
if (physical) raiseLeaseFor(0, false);
|
|
1133
|
+
const collectorPid = await startCollector({
|
|
1134
|
+
root,
|
|
1135
|
+
serial,
|
|
1136
|
+
packageName: androidPackage,
|
|
1137
|
+
spawn,
|
|
1138
|
+
kill,
|
|
1139
|
+
alive: pidAlive,
|
|
1140
|
+
verify: verifyCollector,
|
|
1141
|
+
out
|
|
1142
|
+
});
|
|
1143
|
+
phase("logs", `${displayPath(root, logsDir)}${collectorPid ? ` (collector pid ${collectorPid})` : ""}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (physical) raiseLeaseFor(release ? RELEASE_VERIFY_WAIT_MS : DEBUG_VERIFY_STEP_MS, false);
|
|
1146
|
+
const { state: launchState, warning: launchWarning } = await verifyAndroidRun({
|
|
1147
|
+
root,
|
|
1148
|
+
release,
|
|
1149
|
+
remoteRelease,
|
|
1150
|
+
remoteDevice: Boolean(remoteDevice),
|
|
1151
|
+
verifyReleaseLaunched,
|
|
1152
|
+
verifyLaunched,
|
|
1153
|
+
serial,
|
|
1154
|
+
androidPackage,
|
|
1155
|
+
variant,
|
|
1156
|
+
metroCheck,
|
|
1157
|
+
logsDir,
|
|
1158
|
+
launchedAt,
|
|
1159
|
+
metroPort,
|
|
1160
|
+
isExpo,
|
|
1161
|
+
physical,
|
|
1162
|
+
scheme,
|
|
1163
|
+
component: launched.component ?? null,
|
|
1164
|
+
phase
|
|
1165
|
+
});
|
|
1166
|
+
if (launchState === "fatal") return fail(LAUNCH_FAILED, "The app failed its launch readiness check.", `Read the launch error above or run \`stim logs --errors\`. The full timeline is in ${logsDir}.`, {
|
|
1167
|
+
lastBuildStatus: true,
|
|
1168
|
+
logPath: displayPath(root, logsDir)
|
|
1169
|
+
});
|
|
1170
|
+
writer.write(launchOutcomeRecord({
|
|
1171
|
+
launchState,
|
|
1172
|
+
release,
|
|
1173
|
+
bundleId: androidPackage,
|
|
1174
|
+
configuration: variant,
|
|
1175
|
+
metroPort
|
|
1176
|
+
}));
|
|
1177
|
+
const leaseFacts = lease?.facts() ?? null;
|
|
1178
|
+
releaseLease();
|
|
1179
|
+
const facts = reportAndroidResult({
|
|
1180
|
+
json,
|
|
1181
|
+
useBuildCache,
|
|
1182
|
+
variant,
|
|
1183
|
+
release,
|
|
1184
|
+
metroCheck,
|
|
1185
|
+
metroPort,
|
|
1186
|
+
logsDir: remoteRelease ? null : logsDir,
|
|
1187
|
+
serial,
|
|
1188
|
+
apkPath,
|
|
1189
|
+
androidPackage,
|
|
1190
|
+
installSkipped,
|
|
1191
|
+
record,
|
|
1192
|
+
waitedForBuild,
|
|
1193
|
+
remote,
|
|
1194
|
+
providerName,
|
|
1195
|
+
launchState,
|
|
1196
|
+
launchWarning,
|
|
1197
|
+
launched,
|
|
1198
|
+
ccache,
|
|
1199
|
+
durationMs: now() - started,
|
|
1200
|
+
writer,
|
|
1201
|
+
emit,
|
|
1202
|
+
lease: physical ? leaseFacts : void 0,
|
|
1203
|
+
recordRun
|
|
1204
|
+
});
|
|
1205
|
+
if (remoteWasAbandoned || uploadWasAbandoned) exitAfterFlush(0);
|
|
1206
|
+
return {
|
|
1207
|
+
ok: true,
|
|
1208
|
+
facts
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1212
|
+
//#region src/commands/android.ts
|
|
1213
|
+
const FALLBACK_LINES = 5;
|
|
1214
|
+
function androidCompilerCache({ root, optimizations, settingsContext }) {
|
|
1215
|
+
const { cas, optimizations: resolved } = resolveAndroidCompilerCache({
|
|
1216
|
+
optimizations,
|
|
1217
|
+
use: (manifest) => resolveAndroidCas(root, {
|
|
1218
|
+
...process.env,
|
|
1219
|
+
STIM_ANDROID_CAS_TOOLCHAIN: manifest
|
|
1220
|
+
})
|
|
1221
|
+
});
|
|
1222
|
+
const fallback = resolved.android.compilerCacheFallback;
|
|
1223
|
+
if (!fallback) return {
|
|
1224
|
+
cas,
|
|
1225
|
+
optimizations: resolved,
|
|
1226
|
+
warning: null
|
|
1227
|
+
};
|
|
1228
|
+
return {
|
|
1229
|
+
cas,
|
|
1230
|
+
optimizations: resolved,
|
|
1231
|
+
warning: `Warning: ${compilerCacheFallbackMessage({
|
|
1232
|
+
fallback,
|
|
1233
|
+
compilerCache: resolved.android.compilerCache === "none" ? "none" : "ccache",
|
|
1234
|
+
file: settingFile(settingsContext, fallback.key)
|
|
1235
|
+
})}`
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function androidCommand(program) {
|
|
1239
|
+
registerAndroid(program);
|
|
1240
|
+
}
|
|
1241
|
+
function registerAndroid(program) {
|
|
1242
|
+
program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. Never starts the bundler -- run `stim start` first.").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--no-metro-check", "Skip the reserved-port Metro health check (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. The build still happens here.", (value) => {
|
|
1243
|
+
if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
|
|
1244
|
+
throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
|
|
1245
|
+
}).option("--wait <seconds>", "How long to wait for another workspace to release the device it leases, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once). Only with --device.").option("--no-wait", "Install on a device another workspace leases instead of waiting: this run takes no lease and, when both workspaces build the same app id, the install terminates the holder's running app. Only with --device.").action(async (opts) => {
|
|
1246
|
+
const root = findProjectRoot(process.cwd());
|
|
1247
|
+
if (!root) {
|
|
1248
|
+
console.error(chalk.red("Not in a React Native project (no package.json found)."));
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (!(await runAndroid({
|
|
1253
|
+
root,
|
|
1254
|
+
json: Boolean(opts.json),
|
|
1255
|
+
metroCheck: opts.metroCheck !== false,
|
|
1256
|
+
useBuildCache: opts.buildCache !== false,
|
|
1257
|
+
variant: opts.variant ?? null,
|
|
1258
|
+
systemImage: opts.systemImage ?? null,
|
|
1259
|
+
remoteDevice: opts.remote ?? null,
|
|
1260
|
+
device: opts.device ?? null,
|
|
1261
|
+
wait: opts.wait,
|
|
1262
|
+
waitConflict: waitFlagConflict(process.argv)
|
|
1263
|
+
})).ok) process.exit(1);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, variant: variantFlag = null, systemImage: systemImageFlag = null, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, ensureDeviceBooted = ensureBooted, resolveMetro = resolveProjectMetro, resolveMetroRetrying = resolveMetroWithRetry, readState = readWorkspaceState, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, needsPrebuildFor = needsPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
|
|
1267
|
+
return {
|
|
1268
|
+
root,
|
|
1269
|
+
json,
|
|
1270
|
+
commandRemoteBackend,
|
|
1271
|
+
resolveSettingsFor,
|
|
1272
|
+
resolveRemoteDeviceContext,
|
|
1273
|
+
makeRemoteDeviceDeps,
|
|
1274
|
+
resolveEasBin,
|
|
1275
|
+
ensureRemoteMetro,
|
|
1276
|
+
ensureRemoteOwned,
|
|
1277
|
+
detectRemoteProviders,
|
|
1278
|
+
metroCheck,
|
|
1279
|
+
useBuildCache,
|
|
1280
|
+
variantFlag,
|
|
1281
|
+
systemImageFlag,
|
|
1282
|
+
deviceFlag,
|
|
1283
|
+
waitFlag,
|
|
1284
|
+
waitConflict,
|
|
1285
|
+
acquireLease,
|
|
1286
|
+
makeRunLease,
|
|
1287
|
+
selectPool,
|
|
1288
|
+
onLeaseSignal,
|
|
1289
|
+
listDevices,
|
|
1290
|
+
deviceModel,
|
|
1291
|
+
deviceAbi,
|
|
1292
|
+
isEmulatorDevice,
|
|
1293
|
+
readApkPackage,
|
|
1294
|
+
getLimits,
|
|
1295
|
+
checkCapacity,
|
|
1296
|
+
acquireSlot,
|
|
1297
|
+
releaseSlot,
|
|
1298
|
+
ensureDevice,
|
|
1299
|
+
listSystemImages,
|
|
1300
|
+
ensureDeviceBooted,
|
|
1301
|
+
resolveMetro,
|
|
1302
|
+
resolveMetroRetrying,
|
|
1303
|
+
readState,
|
|
1304
|
+
pidAlive,
|
|
1305
|
+
verifyCollector,
|
|
1306
|
+
verifyLaunched,
|
|
1307
|
+
ensureStorage,
|
|
1308
|
+
fingerprint,
|
|
1309
|
+
untracked,
|
|
1310
|
+
resolveCached,
|
|
1311
|
+
storeCached,
|
|
1312
|
+
storedAssets,
|
|
1313
|
+
captureAssets,
|
|
1314
|
+
acquireLock,
|
|
1315
|
+
releaseLock,
|
|
1316
|
+
waitForBuild: waitForBuild$1,
|
|
1317
|
+
loadProvider,
|
|
1318
|
+
easAuth,
|
|
1319
|
+
resolveRemoteBuild,
|
|
1320
|
+
uploadRemoteBuild,
|
|
1321
|
+
resolveCacheProvider,
|
|
1322
|
+
loadCacheProviderModule,
|
|
1323
|
+
needsPrebuildFor,
|
|
1324
|
+
prebuild,
|
|
1325
|
+
build,
|
|
1326
|
+
ccacheFor,
|
|
1327
|
+
install,
|
|
1328
|
+
launch,
|
|
1329
|
+
launchRelease,
|
|
1330
|
+
verifyReleaseLaunched,
|
|
1331
|
+
swapApk,
|
|
1332
|
+
resolveDevClientScheme,
|
|
1333
|
+
spawn,
|
|
1334
|
+
kill,
|
|
1335
|
+
createWriter,
|
|
1336
|
+
writeLaunch,
|
|
1337
|
+
writeState,
|
|
1338
|
+
recordStats,
|
|
1339
|
+
readEstimates,
|
|
1340
|
+
now,
|
|
1341
|
+
out,
|
|
1342
|
+
emit
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async function runAndroid(options = {}) {
|
|
1346
|
+
let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, variantFlag, systemImageFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, ensureDeviceBooted, resolveMetro, resolveMetroRetrying, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, needsPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = resolveRunAndroidOptions(options);
|
|
1347
|
+
const started = now();
|
|
1348
|
+
const startedAt = new Date(started).toISOString();
|
|
1349
|
+
const projectProblem = appProjectProblem(root);
|
|
1350
|
+
if (projectProblem) {
|
|
1351
|
+
const { message, remedy } = projectProblem;
|
|
1352
|
+
out(phaseLine("error", chalk.red(`STIM_NO_PROJECT: ${message}`)));
|
|
1353
|
+
out(phaseLine("remedy", remedy));
|
|
1354
|
+
if (json) emit(JSON.stringify({
|
|
1355
|
+
code: "STIM_NO_PROJECT",
|
|
1356
|
+
message,
|
|
1357
|
+
remedy
|
|
1358
|
+
}));
|
|
1359
|
+
return {
|
|
1360
|
+
ok: false,
|
|
1361
|
+
error: {
|
|
1362
|
+
code: "STIM_NO_PROJECT",
|
|
1363
|
+
message,
|
|
1364
|
+
remedy
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
await ensureStorage(root, { note: out });
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
const code = error?.code || "STIM_WORKSPACE_STATE";
|
|
1372
|
+
const message = `Could not prepare this workspace's Stim state: ${error?.message || error}`;
|
|
1373
|
+
const remedy = "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`).";
|
|
1374
|
+
out(phaseLine("error", chalk.red(`${code}: ${message}`)));
|
|
1375
|
+
out(phaseLine("remedy", remedy));
|
|
1376
|
+
if (json) emit(JSON.stringify({
|
|
1377
|
+
code,
|
|
1378
|
+
message,
|
|
1379
|
+
remedy
|
|
1380
|
+
}));
|
|
1381
|
+
return {
|
|
1382
|
+
ok: false,
|
|
1383
|
+
error: {
|
|
1384
|
+
code,
|
|
1385
|
+
message,
|
|
1386
|
+
remedy
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const logsDir = workspaceLogsDir(root);
|
|
1391
|
+
const buildLog = join(logsDir, "build-android.ndjson");
|
|
1392
|
+
const writer = createWriter(buildLog, { truncate: true });
|
|
1393
|
+
const record = {
|
|
1394
|
+
fingerprint: null,
|
|
1395
|
+
cacheKey: null,
|
|
1396
|
+
cacheHit: false,
|
|
1397
|
+
appPath: null,
|
|
1398
|
+
bundleId: null,
|
|
1399
|
+
avdName: null,
|
|
1400
|
+
deviceName: null
|
|
1401
|
+
};
|
|
1402
|
+
let buildLock = null;
|
|
1403
|
+
const releaseHeldLock = () => {
|
|
1404
|
+
if (!buildLock) return;
|
|
1405
|
+
const held = buildLock;
|
|
1406
|
+
buildLock = null;
|
|
1407
|
+
try {
|
|
1408
|
+
releaseLock(held);
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
out(phaseLine("build", chalk.dim(`could not release the build lock at ${held.path}: ${err?.message || err}`)));
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
let buildSlot = null;
|
|
1414
|
+
const releaseHeldSlot = () => {
|
|
1415
|
+
if (!buildSlot) return;
|
|
1416
|
+
const held = buildSlot;
|
|
1417
|
+
buildSlot = null;
|
|
1418
|
+
try {
|
|
1419
|
+
releaseSlot(held);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
out(phaseLine("build", chalk.dim(`could not release the build slot: ${err?.message || err}`)));
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
const phase = (label, text) => out(phaseLine(label, text));
|
|
1425
|
+
const stats = createRunRecorder({
|
|
1426
|
+
platform: PLATFORM,
|
|
1427
|
+
write: recordStats,
|
|
1428
|
+
now,
|
|
1429
|
+
note: (line) => out(phaseLine("stats", chalk.dim(line)))
|
|
1430
|
+
});
|
|
1431
|
+
const recordRun = stats.record;
|
|
1432
|
+
let ccacheActivity = CCACHE_NOT_RUN;
|
|
1433
|
+
const fail = (code, message, remedy, { lastBuildStatus = false, diagnostics = [], lines = [], logPath = null, lease } = {}) => {
|
|
1434
|
+
if (lastBuildStatus) persistLastBuild({
|
|
1435
|
+
writeState,
|
|
1436
|
+
root,
|
|
1437
|
+
record,
|
|
1438
|
+
startedAt,
|
|
1439
|
+
durationMs: now() - started,
|
|
1440
|
+
status: "failed",
|
|
1441
|
+
errorCode: code,
|
|
1442
|
+
out
|
|
1443
|
+
});
|
|
1444
|
+
out(phaseLine("error", chalk.red(`${code}: ${message}`)));
|
|
1445
|
+
for (const diagnostic of diagnostics) out(phaseLine("error", chalk.red(diagnostic)));
|
|
1446
|
+
for (const line of lines) out(phaseLine("", chalk.dim(line)));
|
|
1447
|
+
if (remedy) out(phaseLine("remedy", remedy));
|
|
1448
|
+
if (logPath) out(phaseLine("log", logPath));
|
|
1449
|
+
recordRun({
|
|
1450
|
+
failed: true,
|
|
1451
|
+
durationMs: now() - started
|
|
1452
|
+
});
|
|
1453
|
+
if (json) emit(JSON.stringify({
|
|
1454
|
+
code,
|
|
1455
|
+
message,
|
|
1456
|
+
remedy: remedy ?? null,
|
|
1457
|
+
...ccacheActivity.status === "not-run" ? {} : { ccache: ccacheActivity },
|
|
1458
|
+
...lease === void 0 ? {} : { lease }
|
|
1459
|
+
}));
|
|
1460
|
+
writer.close();
|
|
1461
|
+
return {
|
|
1462
|
+
ok: false,
|
|
1463
|
+
error: {
|
|
1464
|
+
code,
|
|
1465
|
+
message,
|
|
1466
|
+
remedy: remedy ?? null
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
};
|
|
1470
|
+
const settingsRepoRoot = repoRoot(root);
|
|
1471
|
+
const settingsRoot = root;
|
|
1472
|
+
const settingsContext = {
|
|
1473
|
+
projectPath: root,
|
|
1474
|
+
gitCommonDir: gitCommonDir(root),
|
|
1475
|
+
repoRoot: settingsRepoRoot
|
|
1476
|
+
};
|
|
1477
|
+
const projectKey = statsProjectKey({
|
|
1478
|
+
root,
|
|
1479
|
+
commonDir: settingsContext.gitCommonDir,
|
|
1480
|
+
repoRoot: settingsRepoRoot
|
|
1481
|
+
});
|
|
1482
|
+
stats.setProject(projectKey);
|
|
1483
|
+
let estimatesRead = null;
|
|
1484
|
+
const estimates = () => estimatesRead ??= readEstimates({
|
|
1485
|
+
projectKey,
|
|
1486
|
+
platform: PLATFORM
|
|
1487
|
+
});
|
|
1488
|
+
const settings = resolveSettingsFor(settingsContext);
|
|
1489
|
+
const [shapeError, ...moreShapeErrors] = [parkedMaxSetting("android").error, ...settingShapeErrors(settings)].filter((error) => Boolean(error));
|
|
1490
|
+
if (shapeError) return fail("STIM_BAD_ARG", shapeError, SETTING_SHAPE_REMEDY, { lines: moreShapeErrors });
|
|
1491
|
+
for (const key of unknownSettingKeys(settings)) out(phaseLine("setting", chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`)));
|
|
1492
|
+
let optimizations;
|
|
1493
|
+
try {
|
|
1494
|
+
optimizations = resolveOptimizations(settings);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
return fail("STIM_BAD_ARG", `Could not configure Android build: ${error.message}`, SETTING_SHAPE_REMEDY);
|
|
1497
|
+
}
|
|
1498
|
+
const compilerCache = androidCompilerCache({
|
|
1499
|
+
root,
|
|
1500
|
+
optimizations,
|
|
1501
|
+
settingsContext
|
|
1502
|
+
});
|
|
1503
|
+
const cas = compilerCache.cas;
|
|
1504
|
+
optimizations = compilerCache.optimizations;
|
|
1505
|
+
if (compilerCache.warning) out(phaseLine("cache", chalk.yellow(compilerCache.warning)));
|
|
1506
|
+
const buildProfile = optimizationBuildProfile("android", optimizations);
|
|
1507
|
+
const cacheProviderConfig = resolveCacheProvider(settingsContext);
|
|
1508
|
+
const cacheProviderError = cacheProviderSettingError(settings);
|
|
1509
|
+
if (cacheProviderError) out(phaseLine("cache", chalk.yellow(`${cacheProviderError} Using the local cache.`)));
|
|
1510
|
+
const dataPartitionSizeError = androidDataPartitionSizeGbSettingError(settings);
|
|
1511
|
+
if (dataPartitionSizeError) return fail("STIM_BAD_ARG", dataPartitionSizeError, "Set android.dataPartitionSizeGb to a whole number of GiB from 6 through 16384.");
|
|
1512
|
+
const avdConfigError = androidAvdConfigSettingError(settings, settingsRoot);
|
|
1513
|
+
if (avdConfigError) return fail("STIM_BAD_ARG", avdConfigError, "Use only documented android.avdConfig keys, or an android.avdConfigFile fragment contained by the app directory.");
|
|
1514
|
+
const remoteSettingError = remoteDeviceSettingError(settings);
|
|
1515
|
+
if (remoteSettingError) return fail("STIM_BAD_ARG", remoteSettingError, `Set ios.remote and android.remote to one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}.`);
|
|
1516
|
+
const systemImage = resolveSystemImage(systemImageFlag, settings);
|
|
1517
|
+
const variant = resolveVariant(variantFlag, settings);
|
|
1518
|
+
const flavorRefusal = productFlavorRefusal({
|
|
1519
|
+
flavors: readProductFlavors(root),
|
|
1520
|
+
variant
|
|
1521
|
+
});
|
|
1522
|
+
if (flavorRefusal) return fail(flavorRefusal.code, flavorRefusal.reason, flavorRefusal.remedy);
|
|
1523
|
+
const release = isReleaseVariant(variant);
|
|
1524
|
+
const cachePolicy = artifactCachePolicy(optimizations, requestedBuildCache, release);
|
|
1525
|
+
const useBuildCache = cachePolicy.read;
|
|
1526
|
+
const isExpo = detectIsExpo(root);
|
|
1527
|
+
const physical = deviceFlag !== null && deviceFlag !== void 0 && deviceFlag !== false;
|
|
1528
|
+
if (physical && deviceFlag === "") return fail("STIM_BAD_ARG", "--device was given an empty serial.", "Pass `--device` on its own to take the first connected device this workspace can lease, or `--device <serial>` to name one.");
|
|
1529
|
+
if (physical && commandRemoteBackend) return fail("STIM_BAD_ARG", "--device installs on a device connected to this machine, and --remote installs on a remote one.", "Pass only one of --device and --remote.");
|
|
1530
|
+
const noWait = waitFlag === false;
|
|
1531
|
+
const waitFlagged = waitFlag !== void 0;
|
|
1532
|
+
if (waitConflict) return fail("STIM_BAD_ARG", "--wait and --no-wait ask for opposite things.", "Pass `--wait <seconds>` to wait for the lease, or `--no-wait` to install without one.");
|
|
1533
|
+
if (waitFlagged && !physical) return fail("STIM_BAD_ARG", "--wait and --no-wait only apply to a `--device` run.", "This workspace owns its emulator, so nothing contends for it. Drop the flag, or pass `--device`.");
|
|
1534
|
+
const waitParsed = parseDeviceWait(noWait ? void 0 : waitFlag);
|
|
1535
|
+
if ("error" in waitParsed) return fail("STIM_BAD_ARG", waitParsed.error, "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a leased device at once.");
|
|
1536
|
+
const waitSeconds = waitParsed.seconds;
|
|
1537
|
+
const remoteBackend = physical ? null : commandRemoteBackend ?? remoteAndroidSetting(settings);
|
|
1538
|
+
const imageRefusal = systemImageRefusal({
|
|
1539
|
+
flag: systemImageFlag,
|
|
1540
|
+
resolved: systemImage,
|
|
1541
|
+
physical,
|
|
1542
|
+
remoteBackend,
|
|
1543
|
+
listImages: listSystemImages
|
|
1544
|
+
});
|
|
1545
|
+
if (imageRefusal) return fail(imageRefusal.code, imageRefusal.message, imageRefusal.remedy);
|
|
1546
|
+
const requestedSerial = typeof deviceFlag === "string" ? deviceFlag : null;
|
|
1547
|
+
let androidPackage = detectAndroidPackage(root);
|
|
1548
|
+
record.bundleId = androidPackage;
|
|
1549
|
+
const registerProject = () => upsertProject(root, {
|
|
1550
|
+
bundleId: detectBundleId(root) ?? void 0,
|
|
1551
|
+
androidPackage: androidPackage ?? void 0,
|
|
1552
|
+
isExpo
|
|
1553
|
+
});
|
|
1554
|
+
if (remoteBackend !== "eas") registerProject();
|
|
1555
|
+
const project = getProject(root);
|
|
1556
|
+
const label = projectShortcut(root, project);
|
|
1557
|
+
let remoteDevice = null;
|
|
1558
|
+
const reservedPort = project?.metroPort ?? null;
|
|
1559
|
+
let metroPort = null;
|
|
1560
|
+
let phaseFailure = null;
|
|
1561
|
+
async function resolveMetroPort() {
|
|
1562
|
+
if (release) phase("metro", `skipped (${variant}: the JS bundle is embedded, no dev server is used)`);
|
|
1563
|
+
else if (metroCheck) {
|
|
1564
|
+
if (!reservedPort) {
|
|
1565
|
+
phaseFailure = fail(NO_METRO, "No Metro port is reserved for this workspace.", "Run `stim start` first, or pass --no-metro-check.");
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
const held = await resolveMetroRetrying(resolveMetro, reservedPort, root, { onRetry: ({ delayMs }) => phase("metro", `port ${reservedPort} did not verify yet; retrying in ${Math.round(delayMs / 1e3)}s (Metro may still be indexing)`) });
|
|
1569
|
+
if (!held.metro) {
|
|
1570
|
+
const supervisor = readState(root)?.supervisor ?? null;
|
|
1571
|
+
const supervisorAlive = Boolean(supervisor?.pid && pidAlive(supervisor.pid));
|
|
1572
|
+
phaseFailure = fail(NO_METRO, noMetroMessage({
|
|
1573
|
+
port: reservedPort,
|
|
1574
|
+
resolution: held,
|
|
1575
|
+
supervisor,
|
|
1576
|
+
supervisorAlive
|
|
1577
|
+
}), noMetroRemedy({
|
|
1578
|
+
port: reservedPort,
|
|
1579
|
+
supervisor,
|
|
1580
|
+
supervisorAlive
|
|
1581
|
+
}));
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
phase("metro", `port ${reservedPort} (pid ${held.metro?.pid})`);
|
|
1585
|
+
} else phase("metro", reservedPort ? `port ${reservedPort} (not checked)` : `no reservation; using ${DEFAULT_METRO_PORT} (not checked)`);
|
|
1586
|
+
metroPort = release ? null : reservedPort ?? 8081;
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
if (!await resolveMetroPort()) return phaseFailure;
|
|
1590
|
+
if (remoteBackend) {
|
|
1591
|
+
const resolved = await resolveRemoteDeviceContext({
|
|
1592
|
+
root,
|
|
1593
|
+
label,
|
|
1594
|
+
platform: PLATFORM,
|
|
1595
|
+
backend: remoteBackend,
|
|
1596
|
+
easBin: resolveEasBin(root)?.file ?? null
|
|
1597
|
+
});
|
|
1598
|
+
if ("failed" in resolved) return fail(resolved.code ?? "STIM_NO_REMOTE_SESSION", resolved.failed, resolved.remedy);
|
|
1599
|
+
remoteDevice = makeRemoteDeviceDeps(resolved.ctx);
|
|
1600
|
+
if (metroPort !== null) {
|
|
1601
|
+
const reachable = await ensureRemoteMetro({
|
|
1602
|
+
ctx: remoteDevice.ctx,
|
|
1603
|
+
metroPort,
|
|
1604
|
+
isExpo,
|
|
1605
|
+
tunnelMode: tunnelModeSetting(settings) ?? void 0,
|
|
1606
|
+
publicUrl: publicUrlSetting(settings),
|
|
1607
|
+
available: detectRemoteProviders(binOnPath)
|
|
1608
|
+
});
|
|
1609
|
+
if ("failed" in reachable) return fail(reachable.code ?? "STIM_NO_REMOTE_SESSION", reachable.failed, reachable.remedy);
|
|
1610
|
+
}
|
|
1611
|
+
checkCapacity = remoteDevice.checkCapacity;
|
|
1612
|
+
ensureDevice = remoteDevice.ensureDevice;
|
|
1613
|
+
ensureDeviceBooted = remoteDevice.ensureDeviceBooted;
|
|
1614
|
+
install = remoteDevice.install;
|
|
1615
|
+
launch = remoteDevice.launch;
|
|
1616
|
+
}
|
|
1617
|
+
const emuLog = emulatorLogFile(root);
|
|
1618
|
+
const limits = getLimits();
|
|
1619
|
+
let device;
|
|
1620
|
+
let bootDuration = "";
|
|
1621
|
+
let bootPromise;
|
|
1622
|
+
if (physical && !requestedSerial) {
|
|
1623
|
+
const pooled = await pooledAndroidDevice({
|
|
1624
|
+
root,
|
|
1625
|
+
selectPool,
|
|
1626
|
+
listDevices,
|
|
1627
|
+
isEmulatorDevice,
|
|
1628
|
+
deviceModel,
|
|
1629
|
+
waitSeconds,
|
|
1630
|
+
noWait,
|
|
1631
|
+
now,
|
|
1632
|
+
warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
|
|
1633
|
+
});
|
|
1634
|
+
if ("code" in pooled) return fail(pooled.code, pooled.message, pooled.remedy, pooled.extra);
|
|
1635
|
+
device = pooled.device;
|
|
1636
|
+
bootPromise = Promise.resolve({
|
|
1637
|
+
ok: true,
|
|
1638
|
+
serial: pooled.device.serial
|
|
1639
|
+
});
|
|
1640
|
+
} else if (physical) {
|
|
1641
|
+
const resolved = resolvePhysicalDevice(requestedSerial, listDevices(), isEmulatorDevice);
|
|
1642
|
+
if (!resolved.serial) return fail(NO_DEVICE, resolved.error, resolved.remedy);
|
|
1643
|
+
device = {
|
|
1644
|
+
serial: resolved.serial,
|
|
1645
|
+
deviceName: deviceModel(resolved.serial) ?? resolved.serial,
|
|
1646
|
+
owned: false
|
|
1647
|
+
};
|
|
1648
|
+
bootPromise = Promise.resolve({
|
|
1649
|
+
ok: true,
|
|
1650
|
+
serial: resolved.serial
|
|
1651
|
+
});
|
|
1652
|
+
} else {
|
|
1653
|
+
const capacity = checkCapacity({
|
|
1654
|
+
platform: PLATFORM,
|
|
1655
|
+
project,
|
|
1656
|
+
max: limits.maxDevices
|
|
1657
|
+
});
|
|
1658
|
+
if (capacity) return fail(capacity.code, capacity.message, capacity.remedy);
|
|
1659
|
+
const prepare = stepClock(now);
|
|
1660
|
+
try {
|
|
1661
|
+
device = await ensureDevice({
|
|
1662
|
+
platform: PLATFORM,
|
|
1663
|
+
project,
|
|
1664
|
+
projectPath: root,
|
|
1665
|
+
settingsRoot,
|
|
1666
|
+
settings,
|
|
1667
|
+
flags: { systemImage },
|
|
1668
|
+
note: out,
|
|
1669
|
+
out,
|
|
1670
|
+
logFile: emuLog
|
|
1671
|
+
});
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
const diag = noDeviceDiagnostic({
|
|
1674
|
+
reason: `Could not ensure an owned Android emulator: ${err?.message || err}`,
|
|
1675
|
+
logFile: emuLog,
|
|
1676
|
+
remedy: "Check that JAVA_HOME and ANDROID_HOME are set correctly, and that an arm64 system image is installed (`sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`)."
|
|
1677
|
+
});
|
|
1678
|
+
return fail(NO_DEVICE, diag.message, diag.remedy, {
|
|
1679
|
+
lines: diag.lines,
|
|
1680
|
+
logPath: diag.logPath ? displayPath(root, diag.logPath) : null
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
const prepareMs = prepare();
|
|
1684
|
+
if (device.created || prepareMs >= 2e3) phase("device", `${device.avdName || device.deviceName || label} ${device.created ? "created" : "prepared"} (${formatDuration(prepareMs)})`);
|
|
1685
|
+
const bootTimer = stepTimer(now);
|
|
1686
|
+
const boot = () => Promise.resolve(ensureDeviceBooted({
|
|
1687
|
+
platform: PLATFORM,
|
|
1688
|
+
device,
|
|
1689
|
+
out,
|
|
1690
|
+
logFile: emuLog
|
|
1691
|
+
})).catch((e) => ({
|
|
1692
|
+
failed: true,
|
|
1693
|
+
reason: String(e?.message || e),
|
|
1694
|
+
serial: void 0
|
|
1695
|
+
}));
|
|
1696
|
+
bootPromise = (remoteDevice?.ctx.backend === "eas" ? ensureRemoteOwned({
|
|
1697
|
+
root,
|
|
1698
|
+
platform: PLATFORM,
|
|
1699
|
+
sessionName: ownedSessionName(remoteDevice.ctx.label),
|
|
1700
|
+
startedAt,
|
|
1701
|
+
boot,
|
|
1702
|
+
createdSessionId: remoteDevice.createdSessionId,
|
|
1703
|
+
abandonCreatedSession: remoteDevice.abandonCreatedSession,
|
|
1704
|
+
writeState,
|
|
1705
|
+
register: registerProject
|
|
1706
|
+
}) : boot()).then((result) => {
|
|
1707
|
+
bootDuration = bootTimer();
|
|
1708
|
+
return result;
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
if (remoteDevice) {
|
|
1712
|
+
const booted = await bootPromise;
|
|
1713
|
+
if (booted.failed) {
|
|
1714
|
+
if (booted.code) return fail(booted.code, booted.reason ?? "The remote device did not boot.", booted.remedy ?? null);
|
|
1715
|
+
const diag = noDeviceDiagnostic({
|
|
1716
|
+
reason: booted.reason ?? "The remote device did not boot.",
|
|
1717
|
+
logFile: emuLog,
|
|
1718
|
+
remedy: "Run `stim status` to inspect the remote device, then retry the command.",
|
|
1719
|
+
localEmulator: false
|
|
1720
|
+
});
|
|
1721
|
+
return fail(NO_DEVICE, diag.message, diag.remedy, {
|
|
1722
|
+
lines: diag.lines,
|
|
1723
|
+
logPath: diag.logPath ? displayPath(root, diag.logPath) : null
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
record.avdName = device.avdName ?? null;
|
|
1728
|
+
record.deviceName = device.deviceName ?? device.avdName ?? null;
|
|
1729
|
+
record.systemImage = device.systemImage;
|
|
1730
|
+
const { abi: buildAbi, runOptions: buildRunOptions, remoteRunOptions } = androidBuildOptions({
|
|
1731
|
+
release,
|
|
1732
|
+
physical,
|
|
1733
|
+
device,
|
|
1734
|
+
variant,
|
|
1735
|
+
deviceAbi,
|
|
1736
|
+
compiler: cas?.id,
|
|
1737
|
+
buildProfile,
|
|
1738
|
+
targetAbiOnly: optimizations.android.targetAbiOnly
|
|
1739
|
+
});
|
|
1740
|
+
let hash = "";
|
|
1741
|
+
let providerUpload = null;
|
|
1742
|
+
let providerName = null;
|
|
1743
|
+
let providerLoad = null;
|
|
1744
|
+
const cacheWarn = createWarnOnce((line) => phase("cache", chalk.yellow(line)));
|
|
1745
|
+
const loadTieredProvider = cachePolicy.remote && cacheProviderConfig ? () => providerLoad ??= loadCacheProviderModule({
|
|
1746
|
+
projectRoot: root,
|
|
1747
|
+
config: cacheProviderConfig
|
|
1748
|
+
}) : null;
|
|
1749
|
+
let fingerprintSources = [];
|
|
1750
|
+
let cacheKey = "";
|
|
1751
|
+
let storeHash = "";
|
|
1752
|
+
let storeKey = "";
|
|
1753
|
+
let storeSources = [];
|
|
1754
|
+
let apkPath = null;
|
|
1755
|
+
async function resolveInitialFingerprint() {
|
|
1756
|
+
const fingerprintTimer = stepTimer(now);
|
|
1757
|
+
try {
|
|
1758
|
+
const computed = await fingerprint(root, { platform: PLATFORM });
|
|
1759
|
+
hash = computed?.hash ?? "";
|
|
1760
|
+
fingerprintSources = computed?.sources ?? [];
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
phaseFailure = fail(NO_FINGERPRINT, `@expo/fingerprint could not fingerprint ${root}: ${err?.message || err}`, "Fix the @expo/fingerprint error above, then retry.");
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
if (!hash) {
|
|
1766
|
+
phaseFailure = fail(NO_FINGERPRINT, `@expo/fingerprint returned no hash for ${root}, so the build cache cannot be addressed.`, "Check the project native inputs and the @expo/fingerprint error above, then retry.");
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
record.fingerprint = hash;
|
|
1770
|
+
cacheKey = buildCacheKey(PLATFORM, hash, buildRunOptions);
|
|
1771
|
+
stats.setCacheKey(cacheKey);
|
|
1772
|
+
record.cacheKey = cacheKey;
|
|
1773
|
+
storeHash = hash;
|
|
1774
|
+
storeKey = cacheKey;
|
|
1775
|
+
storeSources = fingerprintSources;
|
|
1776
|
+
const found = await resolveTieredBuild({
|
|
1777
|
+
local: filesystemBuildCapability({
|
|
1778
|
+
resolve: resolveCached,
|
|
1779
|
+
store: storeCached,
|
|
1780
|
+
sources: fingerprintSources
|
|
1781
|
+
}),
|
|
1782
|
+
loadProvider: loadTieredProvider,
|
|
1783
|
+
target: {
|
|
1784
|
+
projectRoot: root,
|
|
1785
|
+
platform: PLATFORM,
|
|
1786
|
+
key: cacheKey
|
|
1787
|
+
},
|
|
1788
|
+
destinationDir: providerDownloadPath(workspaceDir(root)),
|
|
1789
|
+
ensureDestination: prepareProviderDownloadDir,
|
|
1790
|
+
skipRead: !useBuildCache,
|
|
1791
|
+
warn: cacheWarn
|
|
1792
|
+
});
|
|
1793
|
+
const cached = found?.tier === "local" ? found.path : null;
|
|
1794
|
+
record.cacheHit = cached ? "local" : false;
|
|
1795
|
+
record.cacheSkipped = !useBuildCache;
|
|
1796
|
+
let missDiff = "";
|
|
1797
|
+
let missUntracked = null;
|
|
1798
|
+
if (!cached) {
|
|
1799
|
+
const lastBuild = readState(root)?.lastBuild ?? null;
|
|
1800
|
+
const miss = describeFingerprintMiss({
|
|
1801
|
+
platform: PLATFORM,
|
|
1802
|
+
current: {
|
|
1803
|
+
hash,
|
|
1804
|
+
sources: fingerprintSources
|
|
1805
|
+
},
|
|
1806
|
+
lastBuild
|
|
1807
|
+
});
|
|
1808
|
+
if (miss) {
|
|
1809
|
+
missDiff = fingerprintDiffSuffix(miss.changed);
|
|
1810
|
+
writer.write(fingerprintDiffRecord({
|
|
1811
|
+
changed: miss.changed,
|
|
1812
|
+
previousHash: miss.previousHash,
|
|
1813
|
+
hash
|
|
1814
|
+
}));
|
|
1815
|
+
} else if (useBuildCache) missUntracked = untrackedMissLine(untracked({ projectRoot: root }));
|
|
1816
|
+
}
|
|
1817
|
+
phase("fingerprint", `${shortHash(hash)} ${cached ? "hit" : "miss"}${useBuildCache ? "" : !requestedBuildCache ? " (--no-build-cache)" : " (cache reuse off in config)"} ${fingerprintTimer()}${missDiff}`);
|
|
1818
|
+
if (missUntracked) phase("fingerprint", chalk.dim(missUntracked));
|
|
1819
|
+
if (found?.tier === "provider") {
|
|
1820
|
+
record.cacheHit = "remote";
|
|
1821
|
+
providerName = found.providerName ?? null;
|
|
1822
|
+
phase("cache", `provider hit (${providerName})${found.storedLocally ? " -> stored locally" : ""}`);
|
|
1823
|
+
}
|
|
1824
|
+
apkPath = found?.path ?? null;
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
const runFromFingerprint = async () => {
|
|
1828
|
+
if (!await resolveInitialFingerprint()) return phaseFailure;
|
|
1829
|
+
let remote = null;
|
|
1830
|
+
let abandonedRemote = false;
|
|
1831
|
+
let uploadPending = null;
|
|
1832
|
+
async function resolveRemoteArtifact() {
|
|
1833
|
+
if (buildAbi || cas || buildProfile || !cachePolicy.remote) return;
|
|
1834
|
+
if (!apkPath) {
|
|
1835
|
+
const loaded = await loadProvider(root, { isExpo });
|
|
1836
|
+
if (loaded?.unavailable) phase("cache", chalk.yellow(`provider not usable: ${loaded.unavailable}`));
|
|
1837
|
+
else if (loaded?.provider) remote = loaded;
|
|
1838
|
+
if (remote?.name === "eas") {
|
|
1839
|
+
const auth = easAuth({
|
|
1840
|
+
projectRoot: root,
|
|
1841
|
+
owner: loaded?.owner || null
|
|
1842
|
+
});
|
|
1843
|
+
const authNote = easAuthNote(auth);
|
|
1844
|
+
if (authNote) phase("cache", chalk.yellow(authNote));
|
|
1845
|
+
if (auth?.code === "logged-out") remote = null;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (remote && useBuildCache) {
|
|
1849
|
+
const remoteTimer = stepTimer(now);
|
|
1850
|
+
const hit = await resolveRemoteBuild({
|
|
1851
|
+
logWriter: writer,
|
|
1852
|
+
provider: remote.provider,
|
|
1853
|
+
platform: PLATFORM,
|
|
1854
|
+
projectRoot: root,
|
|
1855
|
+
fingerprintHash: hash,
|
|
1856
|
+
runOptions: remoteRunOptions
|
|
1857
|
+
});
|
|
1858
|
+
if (hit?.appPath) {
|
|
1859
|
+
let stored = null;
|
|
1860
|
+
try {
|
|
1861
|
+
stored = storeCached(PLATFORM, cacheKey, hit.appPath, { sources: fingerprintSources });
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
phase("cache", chalk.yellow(`remote hit could not be stored locally: ${err?.message || err}`));
|
|
1864
|
+
}
|
|
1865
|
+
apkPath = stored || hit.appPath;
|
|
1866
|
+
record.cacheHit = "remote";
|
|
1867
|
+
phase("cache", `remote hit (${remote.name})${stored ? " -> stored locally" : ""} ${remoteTimer()}`);
|
|
1868
|
+
} else if (hit?.timedOut) {
|
|
1869
|
+
abandonedRemote = true;
|
|
1870
|
+
phase("cache", chalk.yellow(`${remote.name} did not answer within ${formatDuration(RESOLVE_TIMEOUT_MS)}; building instead`));
|
|
1871
|
+
} else if (hit?.failed) {
|
|
1872
|
+
const authNote = remote.name === "eas" && isEasAuthFailureText(hit.failed) ? easAuthNote({
|
|
1873
|
+
code: "logged-out",
|
|
1874
|
+
reason: hit.failed
|
|
1875
|
+
}) : null;
|
|
1876
|
+
phase("cache", chalk.yellow(authNote || `${remote.name} could not be used: ${hit.failed}; building instead`));
|
|
1877
|
+
} else phase("cache", `remote miss (${remote.name}) ${remoteTimer()}`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
await resolveRemoteArtifact();
|
|
1881
|
+
let waitedForBuild = null;
|
|
1882
|
+
let releasedWait = null;
|
|
1883
|
+
async function waitForSharedBuild() {
|
|
1884
|
+
if (!useBuildCache) return true;
|
|
1885
|
+
const waitStarted = now();
|
|
1886
|
+
let failedHolder;
|
|
1887
|
+
while (!apkPath) {
|
|
1888
|
+
let attempt = null;
|
|
1889
|
+
try {
|
|
1890
|
+
attempt = acquireLock({
|
|
1891
|
+
platform: PLATFORM,
|
|
1892
|
+
key: cacheKey,
|
|
1893
|
+
root,
|
|
1894
|
+
logFile: buildLog
|
|
1895
|
+
});
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
const refusal = claimFailure(err, "stim android");
|
|
1898
|
+
if (refusal) {
|
|
1899
|
+
phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
phase("build", chalk.yellow(`could not take the build lock: ${err?.message || err}; building anyway`));
|
|
1903
|
+
}
|
|
1904
|
+
if (attempt?.acquired) {
|
|
1905
|
+
buildLock = attempt;
|
|
1906
|
+
const previous = attempt.tookOver ?? failedHolder;
|
|
1907
|
+
if (previous) phase("build", chalk.yellow(takeoverLine(previous)));
|
|
1908
|
+
break;
|
|
1909
|
+
} else if (attempt?.held) {
|
|
1910
|
+
releasedWait = null;
|
|
1911
|
+
const holder = attempt.held;
|
|
1912
|
+
const who = holder.projectRoot || "another workspace";
|
|
1913
|
+
phase("build", `${who} is already building ${shortHash(hash)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""}`);
|
|
1914
|
+
let waited = null;
|
|
1915
|
+
try {
|
|
1916
|
+
const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
|
|
1917
|
+
if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${holder.pid}) holds ${attempt.path}.`), {
|
|
1918
|
+
code: "STIM_BUILD_WAIT_TIMEOUT",
|
|
1919
|
+
lockPath: attempt.path
|
|
1920
|
+
});
|
|
1921
|
+
waited = await waitForBuild({
|
|
1922
|
+
platform: PLATFORM,
|
|
1923
|
+
key: cacheKey,
|
|
1924
|
+
out,
|
|
1925
|
+
ceilingMs
|
|
1926
|
+
});
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
const refusal = claimFailure(err, "stim android");
|
|
1929
|
+
if (refusal) {
|
|
1930
|
+
phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1933
|
+
const wtErr = err;
|
|
1934
|
+
if (wtErr?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
|
|
1935
|
+
phaseFailure = fail("STIM_BUILD_WAIT_TIMEOUT", wtErr.message, `Check pid ${holder.pid}; if it is not really building, remove ${wtErr.lockPath} and run \`stim android\` again.`, { lastBuildStatus: true });
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
if (waited?.hit) {
|
|
1939
|
+
apkPath = waited.hit ?? null;
|
|
1940
|
+
record.cacheHit = "local";
|
|
1941
|
+
waitedForBuild = {
|
|
1942
|
+
pid: holder.pid,
|
|
1943
|
+
ms: waited.waitedMs
|
|
1944
|
+
};
|
|
1945
|
+
phase("build", `waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache`);
|
|
1946
|
+
} else if (waited?.lockReleased) {
|
|
1947
|
+
failedHolder = void 0;
|
|
1948
|
+
releasedWait = {
|
|
1949
|
+
facts: {
|
|
1950
|
+
pid: holder.pid,
|
|
1951
|
+
ms: waited.waitedMs
|
|
1952
|
+
},
|
|
1953
|
+
who
|
|
1954
|
+
};
|
|
1955
|
+
phase("build", `${who}'s build lock was released; rechecking this workspace before building`);
|
|
1956
|
+
} else {
|
|
1957
|
+
phase("build", chalk.yellow(`${who}'s build ended without an artifact (${waited?.builderFailed}); retrying the build lock`));
|
|
1958
|
+
failedHolder = holder;
|
|
1959
|
+
}
|
|
1960
|
+
} else break;
|
|
1961
|
+
}
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
if (!await waitForSharedBuild()) return phaseFailure;
|
|
1965
|
+
let swapDir = null;
|
|
1966
|
+
let swapFellBack = false;
|
|
1967
|
+
const installableCachedApk = async (key, cachedPath) => {
|
|
1968
|
+
if (!release) return cachedPath;
|
|
1969
|
+
phase("swap", `regenerating this workspace's JS for the cached ${variant} APK`);
|
|
1970
|
+
const swap = await swapApk({
|
|
1971
|
+
root,
|
|
1972
|
+
isExpo,
|
|
1973
|
+
cachedApkPath: cachedPath,
|
|
1974
|
+
keystore: resolveKeystore(root, settings),
|
|
1975
|
+
logWriter: writer,
|
|
1976
|
+
storedAssets: storedAssets(PLATFORM, key)
|
|
1977
|
+
});
|
|
1978
|
+
if (swap?.ok && swap.apkPath) {
|
|
1979
|
+
if (swap.note) phase("swap", chalk.yellow(swap.note));
|
|
1980
|
+
swapDir = swap.tmpDir ?? null;
|
|
1981
|
+
phase("swap", `${swap.hermes ? "hermes bytecode" : "plain JS"} repacked (store), zipaligned and re-signed (${formatDuration(swap.durationMs)})`);
|
|
1982
|
+
return swap.apkPath;
|
|
1983
|
+
}
|
|
1984
|
+
if (swap?.assetMismatch) phase("swap", chalk.yellow(`${swap.reason} -- building fresh instead` + (swap.assetDiff ? " (an APK cannot be made to carry an asset AAPT did not package)" : "")));
|
|
1985
|
+
else phase("swap", chalk.yellow(`failed at ${swap?.step || "unknown step"}: ${swap?.reason || "unknown reason"} -- building fresh instead (a cached ${variant} APK carries its builder's JS; it is never installed after a failed swap)`));
|
|
1986
|
+
for (const line of swap?.lastLines ?? []) phase("", chalk.dim(line));
|
|
1987
|
+
swapFellBack = true;
|
|
1988
|
+
return null;
|
|
1989
|
+
};
|
|
1990
|
+
async function prepareCachedArtifact() {
|
|
1991
|
+
if (apkPath && record.cacheHit) {
|
|
1992
|
+
const prepared = await installableCachedApk(cacheKey, apkPath);
|
|
1993
|
+
apkPath = prepared;
|
|
1994
|
+
if (!prepared) {
|
|
1995
|
+
record.cacheHit = false;
|
|
1996
|
+
waitedForBuild = null;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
async function buildArtifact() {
|
|
2001
|
+
if (!apkPath) try {
|
|
2002
|
+
if (limits.maxBuilds) try {
|
|
2003
|
+
buildSlot = await acquireSlot({
|
|
2004
|
+
max: limits.maxBuilds,
|
|
2005
|
+
root,
|
|
2006
|
+
logFile: buildLog,
|
|
2007
|
+
out
|
|
2008
|
+
});
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
const refusal = claimFailure(err, "stim android");
|
|
2011
|
+
if (refusal) {
|
|
2012
|
+
phaseFailure = fail(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
phase("build", chalk.yellow(`could not take a build slot: ${err?.message || err}; building anyway`));
|
|
2016
|
+
}
|
|
2017
|
+
if (needsPrebuildFor(root, "android", isExpo)) {
|
|
2018
|
+
const pre = await prebuild(root, PLATFORM, writer, { isExpo });
|
|
2019
|
+
if (pre.failed) {
|
|
2020
|
+
phaseFailure = fail(pre.code, pre.reason, pre.remedy, {
|
|
2021
|
+
lastBuildStatus: true,
|
|
2022
|
+
lines: tail(pre.lastLines),
|
|
2023
|
+
logPath: displayPath(root, buildLog)
|
|
2024
|
+
});
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
phase("prebuild", `android/ generated (${formatDuration(pre.durationMs)})`);
|
|
2028
|
+
androidPackage = androidPackage || detectAndroidPackage(root);
|
|
2029
|
+
record.bundleId = androidPackage;
|
|
2030
|
+
const after = await refingerprintAfterMutation({
|
|
2031
|
+
projectRoot: root,
|
|
2032
|
+
platform: PLATFORM,
|
|
2033
|
+
previousHash: hash,
|
|
2034
|
+
fingerprint
|
|
2035
|
+
});
|
|
2036
|
+
if (after?.moved) {
|
|
2037
|
+
storeHash = after.hash;
|
|
2038
|
+
storeSources = after.sources;
|
|
2039
|
+
storeKey = buildCacheKey(PLATFORM, after.hash, buildRunOptions);
|
|
2040
|
+
record.fingerprint = storeHash;
|
|
2041
|
+
record.cacheKey = storeKey;
|
|
2042
|
+
phase("fingerprint", chalk.dim(`${shortHash(hash)} -> ${shortHash(storeHash)} (after prebuild)`));
|
|
2043
|
+
const late = useBuildCache ? resolveCached(PLATFORM, storeKey) : null;
|
|
2044
|
+
if (late) {
|
|
2045
|
+
const prepared = await installableCachedApk(storeKey, late);
|
|
2046
|
+
if (prepared) {
|
|
2047
|
+
apkPath = prepared;
|
|
2048
|
+
record.cacheHit = "local";
|
|
2049
|
+
phase("cache", `hit ${shortHash(storeHash)} (post-prebuild key)`);
|
|
2050
|
+
if (releasedWait) {
|
|
2051
|
+
waitedForBuild = releasedWait.facts;
|
|
2052
|
+
phase("build", `waited ${formatDuration(waitedForBuild.ms)} for ${releasedWait.who}'s build -> installed from cache`);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
if (!apkPath) {
|
|
2059
|
+
phase("build", `compiling ${variant || "debug"} with Gradle`);
|
|
2060
|
+
const built = await build({
|
|
2061
|
+
root,
|
|
2062
|
+
logWriter: writer,
|
|
2063
|
+
variant,
|
|
2064
|
+
abi: buildAbi
|
|
2065
|
+
}, {
|
|
2066
|
+
estimateMs: estimates().coldBuildMs,
|
|
2067
|
+
ccache: optimizations.android.compilerCache === "ccache" ? ccacheFor({
|
|
2068
|
+
root,
|
|
2069
|
+
onNote: out
|
|
2070
|
+
}) : null,
|
|
2071
|
+
cas,
|
|
2072
|
+
buildCache: optimizations.android.gradleBuildCache,
|
|
2073
|
+
pch: optimizations.android.pch,
|
|
2074
|
+
compilerCacheDisabled: optimizations.android.compilerCache === "none"
|
|
2075
|
+
});
|
|
2076
|
+
ccacheActivity = built.ccache ?? CCACHE_UNAVAILABLE;
|
|
2077
|
+
phase("cache", `compilation cache ${ccacheActivityLine(ccacheActivity)}`);
|
|
2078
|
+
if (built.failed) {
|
|
2079
|
+
const diagnostics = built.diagnostics || [];
|
|
2080
|
+
for (const diag of diagnostics) writer.write({
|
|
2081
|
+
src: "build",
|
|
2082
|
+
level: "error",
|
|
2083
|
+
event: "gradle_diagnostic",
|
|
2084
|
+
msg: formatDiagnostic(diag)
|
|
2085
|
+
});
|
|
2086
|
+
phase("build", chalk.red(`FAILED after ${formatDuration(built.durationMs)}`));
|
|
2087
|
+
const extracted = diagnostics.map(formatDiagnostic);
|
|
2088
|
+
if ((built.truncated ?? 0) > 0) extracted.push(`... and ${built.truncated} more diagnostic(s) in the log`);
|
|
2089
|
+
phaseFailure = fail(built.code, built.reason, diagnostics.find((d) => d.remedy)?.remedy || built.remedy || null, {
|
|
2090
|
+
lastBuildStatus: true,
|
|
2091
|
+
diagnostics: extracted,
|
|
2092
|
+
lines: extracted.length ? [] : tail(built.lastLines),
|
|
2093
|
+
logPath: displayPath(root, buildLog)
|
|
2094
|
+
});
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
apkPath = built.apkPath ?? null;
|
|
2098
|
+
stats.setBuildMs(built.durationMs ?? 0);
|
|
2099
|
+
phase("build", `ok (${formatDuration(built.durationMs)})`);
|
|
2100
|
+
if (built.apkNote) phase("build", chalk.yellow(built.apkNote));
|
|
2101
|
+
const beforeBuildHash = storeHash;
|
|
2102
|
+
const afterBuild = await refingerprintAfterMutation({
|
|
2103
|
+
projectRoot: root,
|
|
2104
|
+
platform: PLATFORM,
|
|
2105
|
+
previousHash: beforeBuildHash,
|
|
2106
|
+
fingerprint
|
|
2107
|
+
});
|
|
2108
|
+
if (!afterBuild) {
|
|
2109
|
+
record.fingerprint = null;
|
|
2110
|
+
record.cacheKey = null;
|
|
2111
|
+
phase("fingerprint", chalk.yellow("unavailable after Gradle; the build will be installed but not cached"));
|
|
2112
|
+
} else {
|
|
2113
|
+
if (afterBuild.moved) {
|
|
2114
|
+
storeHash = afterBuild.hash;
|
|
2115
|
+
storeSources = afterBuild.sources;
|
|
2116
|
+
storeKey = buildCacheKey(PLATFORM, afterBuild.hash, buildRunOptions);
|
|
2117
|
+
record.fingerprint = storeHash;
|
|
2118
|
+
record.cacheKey = storeKey;
|
|
2119
|
+
phase("fingerprint", chalk.dim(`${shortHash(beforeBuildHash)} -> ${shortHash(storeHash)} (after Gradle)`));
|
|
2120
|
+
}
|
|
2121
|
+
if (cachePolicy.write) {
|
|
2122
|
+
const assetManifest = release ? captureAssets(root, { variant }) : null;
|
|
2123
|
+
try {
|
|
2124
|
+
const stored = await storeTieredBuild({
|
|
2125
|
+
local: filesystemBuildCapability({
|
|
2126
|
+
resolve: resolveCached,
|
|
2127
|
+
store: storeCached,
|
|
2128
|
+
sources: storeSources,
|
|
2129
|
+
assetManifest
|
|
2130
|
+
}),
|
|
2131
|
+
loadProvider: loadTieredProvider,
|
|
2132
|
+
target: {
|
|
2133
|
+
projectRoot: root,
|
|
2134
|
+
platform: PLATFORM,
|
|
2135
|
+
key: storeKey
|
|
2136
|
+
},
|
|
2137
|
+
sourcePath: apkPath,
|
|
2138
|
+
overwrite: !useBuildCache || swapFellBack,
|
|
2139
|
+
warn: cacheWarn
|
|
2140
|
+
});
|
|
2141
|
+
providerUpload = stored.providerUpload;
|
|
2142
|
+
providerName = stored.providerName ?? providerName;
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
phase("cache", chalk.yellow(`could not store the build: ${err?.message || err}`));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (remote) uploadPending = uploadRemoteBuild({
|
|
2148
|
+
logWriter: writer,
|
|
2149
|
+
provider: remote.provider,
|
|
2150
|
+
platform: PLATFORM,
|
|
2151
|
+
projectRoot: root,
|
|
2152
|
+
fingerprintHash: storeHash,
|
|
2153
|
+
buildPath: apkPath,
|
|
2154
|
+
runOptions: remoteRunOptions
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
} finally {
|
|
2159
|
+
releaseHeldLock();
|
|
2160
|
+
releaseHeldSlot();
|
|
2161
|
+
}
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
await prepareCachedArtifact();
|
|
2165
|
+
if (!await buildArtifact()) return phaseFailure;
|
|
2166
|
+
record.appPath = apkPath;
|
|
2167
|
+
let leaseHandle = null;
|
|
2168
|
+
let stopLeaseSignals = null;
|
|
2169
|
+
const releaseLease = () => {
|
|
2170
|
+
const held = leaseHandle;
|
|
2171
|
+
const stopSignals = stopLeaseSignals;
|
|
2172
|
+
leaseHandle = null;
|
|
2173
|
+
stopLeaseSignals = null;
|
|
2174
|
+
stopSignals?.();
|
|
2175
|
+
try {
|
|
2176
|
+
held?.release();
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
out(phaseLine("lease", chalk.dim(`could not release this run's lease: ${err?.message || err}`)));
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
if (physical) {
|
|
2182
|
+
const acquired = await acquireLease({
|
|
2183
|
+
root,
|
|
2184
|
+
platform: PLATFORM,
|
|
2185
|
+
id: device.serial,
|
|
2186
|
+
deviceName: device.deviceName ?? null,
|
|
2187
|
+
idLabel: "serial",
|
|
2188
|
+
waitSeconds,
|
|
2189
|
+
noWait,
|
|
2190
|
+
installBoundMs: ADB_INSTALL_TIMEOUT_MS,
|
|
2191
|
+
appId: androidPackage,
|
|
2192
|
+
holderAppId: (holder) => getProject(holder)?.androidPackage ?? null,
|
|
2193
|
+
now,
|
|
2194
|
+
warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
|
|
2195
|
+
});
|
|
2196
|
+
if (acquired.status === "refused") return fail(acquired.refusal.code, acquired.refusal.message, acquired.refusal.remedy, { lease: acquired.refusal.lease });
|
|
2197
|
+
leaseHandle = makeRunLease({
|
|
2198
|
+
root,
|
|
2199
|
+
platform: PLATFORM,
|
|
2200
|
+
kind: acquired.status === "leased" ? acquired.kind : null,
|
|
2201
|
+
expiresAt: acquired.status === "leased" ? acquired.expiresAt : null
|
|
2202
|
+
});
|
|
2203
|
+
if (acquired.status === "leased") {
|
|
2204
|
+
stopLeaseSignals = onLeaseSignal(releaseLease);
|
|
2205
|
+
phase("lease", `${acquired.kind} lease on ${device.serial} until ${leaseExpiryText(acquired.expiresAt, now())}`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
try {
|
|
2209
|
+
return await finishAndroidRun({
|
|
2210
|
+
lease: leaseHandle,
|
|
2211
|
+
releaseLease,
|
|
2212
|
+
root,
|
|
2213
|
+
json,
|
|
2214
|
+
metroCheck,
|
|
2215
|
+
useBuildCache,
|
|
2216
|
+
variant,
|
|
2217
|
+
release,
|
|
2218
|
+
isExpo,
|
|
2219
|
+
metroPort,
|
|
2220
|
+
logsDir,
|
|
2221
|
+
emuLog,
|
|
2222
|
+
device,
|
|
2223
|
+
physical,
|
|
2224
|
+
remoteDevice,
|
|
2225
|
+
bootPromise,
|
|
2226
|
+
bootDuration: () => bootDuration,
|
|
2227
|
+
apkPath,
|
|
2228
|
+
androidPackage,
|
|
2229
|
+
swapDir,
|
|
2230
|
+
record,
|
|
2231
|
+
waitedForBuild,
|
|
2232
|
+
ccache: ccacheActivity,
|
|
2233
|
+
uploadPending,
|
|
2234
|
+
providerUpload,
|
|
2235
|
+
providerName,
|
|
2236
|
+
remote,
|
|
2237
|
+
abandonedRemote,
|
|
2238
|
+
started,
|
|
2239
|
+
startedAt,
|
|
2240
|
+
writer,
|
|
2241
|
+
phase,
|
|
2242
|
+
fail,
|
|
2243
|
+
readApkPackage,
|
|
2244
|
+
install,
|
|
2245
|
+
launch,
|
|
2246
|
+
launchRelease,
|
|
2247
|
+
resolveDevClientScheme,
|
|
2248
|
+
verifyLaunched,
|
|
2249
|
+
verifyReleaseLaunched,
|
|
2250
|
+
spawn,
|
|
2251
|
+
kill,
|
|
2252
|
+
pidAlive,
|
|
2253
|
+
verifyCollector,
|
|
2254
|
+
writeLaunch,
|
|
2255
|
+
writeState,
|
|
2256
|
+
now,
|
|
2257
|
+
out,
|
|
2258
|
+
emit,
|
|
2259
|
+
recordRun
|
|
2260
|
+
});
|
|
2261
|
+
} finally {
|
|
2262
|
+
releaseLease();
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
try {
|
|
2266
|
+
return await runFromFingerprint();
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
recordRun({
|
|
2269
|
+
failed: true,
|
|
2270
|
+
durationMs: now() - started
|
|
2271
|
+
});
|
|
2272
|
+
throw error;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function tail(lines, n = FALLBACK_LINES) {
|
|
2276
|
+
return (Array.isArray(lines) ? lines : []).slice(-n);
|
|
2277
|
+
}
|
|
2278
|
+
//#endregion
|
|
2279
|
+
export { NO_DEVICE, NO_FINGERPRINT, NO_METRO, androidDevClientScheme, androidFacts, androidSystemImageSetting, androidVariantSetting, apkDevClientFacts, apkPackage, collectorLogFile, androidCommand as default, displayPath, dumpApkManifest, findAapt, formatDuration, isReleaseVariant, killPreviousCollector, lastBuildRecord, noDeviceDiagnostic, parseXmltree, phaseLine, registerAndroid, resolveSystemImage, resolveVariant, runAndroid, shortHash, startCollector };
|