stim 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/android-BW8i_YxU.mjs +204 -0
- package/dist/android-Bcmev2s2.mjs +2334 -0
- package/dist/android-K63_0UfF.mjs +727 -0
- package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/app-install-NFxUahnH.mjs +1149 -0
- package/dist/build-lock-BsVoLDaq.mjs +645 -0
- package/dist/build-slots-D0b6sJyo.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.d.mts +1 -0
- package/dist/collector-run.mjs +7 -5
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
- package/dist/deps-BLBsTNDS.mjs +435 -0
- package/dist/dev-client-RpvXxCXV.mjs +620 -0
- package/dist/device-CoCPmazS.mjs +250 -0
- package/dist/device-lease-Dpik41RY.mjs +335 -0
- package/dist/device-pool-B5pEVa3t.mjs +408 -0
- package/dist/device-remote-E81KAdXf.mjs +1209 -0
- package/dist/doctor-CqPeMsSm.mjs +1130 -0
- package/dist/doctor-D_CX7ib5.mjs +530 -0
- package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
- package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
- package/dist/gc-B7KkDinF.mjs +1665 -0
- package/dist/guide-BS2Xc24o.mjs +4503 -0
- package/dist/ios-B7KLgIp5.mjs +2733 -0
- package/dist/ios-D1h1Di39.mjs +451 -0
- package/dist/ios-device-D3pzTKQE.mjs +81 -0
- package/dist/ios-device-xNt0Lg6v.mjs +413 -0
- package/dist/logs-DcX9AvRF.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-COslRK_F.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-CGPart6T.mjs +60 -0
- package/dist/ownership-CYOyqxUo.mjs +475 -0
- package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
- package/dist/reclaim-C5Jmuahx.mjs +348 -0
- package/dist/reload-Z8nKifAk.mjs +332 -0
- package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
- package/dist/server-expo-Drd7ksHu.mjs +2 -0
- package/dist/settings-DwU1_U_q.mjs +708 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-BrY4EuIg.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
- package/dist/stats-BCWEVLrM.mjs +307 -0
- package/dist/stats-BsF15Bti.mjs +62 -0
- package/dist/status-DFI1Hfpg.mjs +393 -0
- package/dist/stop-B7DR4awK.mjs +3 -0
- package/dist/stop-Btg770vV.mjs +591 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
- package/dist/worktree-BPTbI-LN.mjs +1322 -0
- package/dist/worktree-C79xx_kM.mjs +543 -0
- package/dist/xcode-DeKCribl.mjs +1832 -0
- package/package.json +5 -5
- package/shim/bundle-response.cjs +5 -0
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-CyylIdq9.mjs";
|
|
2
|
+
import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-CMtIaTk9.mjs";
|
|
3
|
+
import { b as sharedBuildCache, w as workspaceDerivedData } from "./state-CZ-ER-8d.mjs";
|
|
4
|
+
import { S as makeTemporaryDirectory, a as dirtyFingerprintFiles, f as listWorktrees, g as repoRoot, s as gitCommonDir, w as temporaryRoot, x as filesystemDevice } from "./worktree-C79xx_kM.mjs";
|
|
5
|
+
import { i as detectIsExpo, t as appProjectProblem } from "./project-BBTVABUz.mjs";
|
|
6
|
+
import { d as resolveEasCliBin, i as checkEasAuth$1, l as ownerFromConfig, u as providerFromConfig, v as POOL_SETTING_REMEDY, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
|
|
7
|
+
import { A as resolveSettings, E as remoteAndroidSetting, I as unknownSettingKeys, M as settingOrigin, N as settingShapeErrors, O as remoteIosSetting, P as settingsLayers, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, w as projectOptimizations, y as mergeSettingsLayers } from "./settings-DwU1_U_q.mjs";
|
|
8
|
+
import { d as quotedPath, u as plural } from "./command-output-8wHMEde0.mjs";
|
|
9
|
+
import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, u as listAllIosSims } from "./ios-D1h1Di39.mjs";
|
|
10
|
+
import { m as fingerprintProject, r as listBuildLocks, u as diffFingerprintSources } from "./build-lock-BsVoLDaq.mjs";
|
|
11
|
+
import { n as listBuildSlots } from "./build-slots-D0b6sJyo.mjs";
|
|
12
|
+
import { y as listAdbDevices } from "./android-K63_0UfF.mjs";
|
|
13
|
+
import { E as simslimIsOnPath, S as liveOwnedDeviceCount, a as ccacheEnabled, g as podInstallCommand } from "./xcode-DeKCribl.mjs";
|
|
14
|
+
import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-D6HPZ6UQ.mjs";
|
|
15
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
16
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
+
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
|
|
18
|
+
import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
|
|
19
|
+
import * as expoFingerprint from "@expo/fingerprint";
|
|
20
|
+
//#region src/doctor-storage.ts
|
|
21
|
+
function checkStorageLayout(projectRoot, { platform, device = filesystemDevice, stagingRoot = temporaryRoot } = {}) {
|
|
22
|
+
const findings = [];
|
|
23
|
+
const temporaryFix = "Unset STIM_TMPDIR / machine tempDir to select same-volume staging automatically, or point the override at a writable directory on the relevant volume outside Git working trees.";
|
|
24
|
+
const check = (operation, paths, fix) => {
|
|
25
|
+
if (new Set(paths.map(device)).size < 2) return;
|
|
26
|
+
findings.push({
|
|
27
|
+
level: "cost",
|
|
28
|
+
title: `${operation} crosses filesystems`,
|
|
29
|
+
detail: `${paths.join(" -> ")}. These copies cannot share file blocks across volumes and can consume the full artifact size in space and I/O. cp -c can silently perform a full copy without an error.`,
|
|
30
|
+
fix
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const target = repoRoot(projectRoot) ?? projectRoot;
|
|
35
|
+
check("Worktree copy", [listWorktrees(target)[0]?.path ?? target, target], "Keep the source checkout and linked worktree on the same volume to share file blocks when warming.");
|
|
36
|
+
const cache = sharedBuildCache();
|
|
37
|
+
check("Cached app/APK staging", [cache, stagingRoot(join(cache, "artifact.app"))], temporaryFix);
|
|
38
|
+
const cacheFix = "Place STIM_BUILD_CACHE / machine caches.buildCache on the build-output volume, or accept the full-copy cost of keeping the cache on a separate volume.";
|
|
39
|
+
if (platform === "ios" || platform !== "android" && existsSync(join(projectRoot, "ios"))) {
|
|
40
|
+
const output = join(workspaceDerivedData(projectRoot), "Build", "Products");
|
|
41
|
+
check("iOS build-cache storage", [output, cache], cacheFix);
|
|
42
|
+
check("iOS device app staging", [output, stagingRoot(join(output, "artifact.app"))], temporaryFix);
|
|
43
|
+
}
|
|
44
|
+
if (platform === "android" || platform !== "ios" && existsSync(join(projectRoot, "android"))) check("Android build-cache storage", [apkOutputsDir(projectRoot), cache], cacheFix);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
findings.push({
|
|
47
|
+
level: "cost",
|
|
48
|
+
title: "Could not resolve temporary storage",
|
|
49
|
+
detail: String(error.message),
|
|
50
|
+
fix: temporaryFix
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/doctor-ios-architectures.ts
|
|
57
|
+
const COMPILED_PRODUCTS = /* @__PURE__ */ new Set([
|
|
58
|
+
"com.apple.product-type.application",
|
|
59
|
+
"com.apple.product-type.application.on-demand-install-capable",
|
|
60
|
+
"com.apple.product-type.app-extension",
|
|
61
|
+
"com.apple.product-type.framework",
|
|
62
|
+
"com.apple.product-type.library.static",
|
|
63
|
+
"com.apple.product-type.library.dynamic"
|
|
64
|
+
]);
|
|
65
|
+
function architectureList(value) {
|
|
66
|
+
if (typeof value !== "string" || /[^\w\s-]/.test(value)) return null;
|
|
67
|
+
return [...new Set(value.split(/\s+/).filter(Boolean))];
|
|
68
|
+
}
|
|
69
|
+
function parseIosDebugArchitectures(output) {
|
|
70
|
+
const report = {
|
|
71
|
+
affected: [],
|
|
72
|
+
unknown: false
|
|
73
|
+
};
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = JSON.parse(output);
|
|
77
|
+
} catch {
|
|
78
|
+
return {
|
|
79
|
+
...report,
|
|
80
|
+
unknown: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(data) || data.length === 0) return {
|
|
84
|
+
...report,
|
|
85
|
+
unknown: true
|
|
86
|
+
};
|
|
87
|
+
let inspected = false;
|
|
88
|
+
for (const entry of data) {
|
|
89
|
+
if (!entry || typeof entry !== "object" || entry.error || !entry.buildSettings) {
|
|
90
|
+
report.unknown = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const settings = entry.buildSettings;
|
|
94
|
+
if (settings.CONFIGURATION !== "Debug" || settings.PLATFORM_NAME !== "iphonesimulator") {
|
|
95
|
+
report.unknown = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!settings.PRODUCT_TYPE) continue;
|
|
99
|
+
if (settings.PRODUCT_TYPE === "com.apple.product-type.bundle") {
|
|
100
|
+
inspected = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!COMPILED_PRODUCTS.has(settings.PRODUCT_TYPE)) {
|
|
104
|
+
report.unknown = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
inspected = true;
|
|
108
|
+
if (settings.ONLY_ACTIVE_ARCH === "YES") continue;
|
|
109
|
+
const archs = architectureList(settings.ARCHS);
|
|
110
|
+
const valid = settings.VALID_ARCHS === void 0 ? archs : architectureList(settings.VALID_ARCHS);
|
|
111
|
+
const excluded = architectureList(settings.EXCLUDED_ARCHS ?? "");
|
|
112
|
+
if (settings.ONLY_ACTIVE_ARCH !== "NO" || !archs?.length || !valid || !excluded || typeof entry.target !== "string") {
|
|
113
|
+
report.unknown = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const architectures = archs.filter((arch) => valid.includes(arch) && !excluded.includes(arch));
|
|
117
|
+
if (architectures.length > 1) report.affected.push({
|
|
118
|
+
target: entry.target,
|
|
119
|
+
architectures
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...report,
|
|
124
|
+
unknown: report.unknown || !inspected
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function inspectIosDebugArchitectures(projectRoot) {
|
|
128
|
+
const iosRoot = join(projectRoot, "ios");
|
|
129
|
+
if (!existsSync(iosRoot)) return [];
|
|
130
|
+
const findings = [];
|
|
131
|
+
const unknown = [];
|
|
132
|
+
let projects;
|
|
133
|
+
try {
|
|
134
|
+
projects = readdirSync(iosRoot).filter((name) => name.endsWith(".xcodeproj")).toSorted().map((name) => join(iosRoot, name));
|
|
135
|
+
} catch {
|
|
136
|
+
projects = [];
|
|
137
|
+
}
|
|
138
|
+
if (!projects.length) unknown.push("app project");
|
|
139
|
+
const pods = join(iosRoot, "Pods", "Pods.xcodeproj");
|
|
140
|
+
if (existsSync(pods)) projects.push(pods);
|
|
141
|
+
else if (existsSync(join(iosRoot, "Podfile"))) unknown.push("Pods project (not generated)");
|
|
142
|
+
const deadline = Date.now() + 6e4;
|
|
143
|
+
for (const project of projects) {
|
|
144
|
+
const label = relative(projectRoot, project);
|
|
145
|
+
const timeoutMs = Math.min(3e4, deadline - Date.now());
|
|
146
|
+
if (timeoutMs <= 0) {
|
|
147
|
+
unknown.push(label);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const report = parseIosDebugArchitectures(getExecutor().runFile("xcodebuild", [
|
|
152
|
+
"-project",
|
|
153
|
+
project,
|
|
154
|
+
"-alltargets",
|
|
155
|
+
"-configuration",
|
|
156
|
+
"Debug",
|
|
157
|
+
"-sdk",
|
|
158
|
+
"iphonesimulator",
|
|
159
|
+
"-showBuildSettings",
|
|
160
|
+
"-json",
|
|
161
|
+
"-disableAutomaticPackageResolution",
|
|
162
|
+
"-skipPackageUpdates"
|
|
163
|
+
], {
|
|
164
|
+
cwd: iosRoot,
|
|
165
|
+
timeoutMs
|
|
166
|
+
}));
|
|
167
|
+
if (report.unknown) unknown.push(label);
|
|
168
|
+
if (report.affected.length) {
|
|
169
|
+
const targets = report.affected.slice(0, 5).map(({ target, architectures }) => `${target} (${architectures.join(", ")})`).join("; ");
|
|
170
|
+
findings.push({
|
|
171
|
+
code: "ios-debug-architectures",
|
|
172
|
+
level: "cost",
|
|
173
|
+
title: "iOS Debug targets build multiple simulator architectures",
|
|
174
|
+
detail: `${label}: ${report.affected.length} Debug target(s) resolve ONLY_ACTIVE_ARCH=NO with multiple architectures after exclusions: ${targets}${report.affected.length > 5 ? `; plus ${report.affected.length - 5} more` : ""}. This can compile extra native code when targeting one simulator.`,
|
|
175
|
+
fix: "Review the Debug ONLY_ACTIVE_ARCH override in the project, xcconfig, or Podfile post_install helpers. Prefer YES for local Debug simulator builds where appropriate; preserve intentional Release and distribution settings. Regenerate Pods through the project workflow after changing a helper, then rerun stim doctor --platform ios. Doctor --fix does not change architecture settings."
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
unknown.push(label);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (unknown.length) findings.push({
|
|
183
|
+
code: "ios-debug-architectures-unknown",
|
|
184
|
+
level: "note",
|
|
185
|
+
title: "iOS Debug architecture settings could not be fully inspected",
|
|
186
|
+
detail: `Effective Debug simulator settings are unavailable for ${unknown.join(", ")}. Architecture policy is not verified for those targets. Metadata inspection has a 60-second total budget.`,
|
|
187
|
+
fix: "Ensure Xcode can read the generated app and Pods projects, then rerun stim doctor --platform ios. Doctor does not evaluate Podfiles or install Pods to inspect architecture settings."
|
|
188
|
+
});
|
|
189
|
+
return findings;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/doctor-cxx.ts
|
|
193
|
+
function parseCmakeCacheLauncher(source) {
|
|
194
|
+
if (typeof source !== "string") return null;
|
|
195
|
+
return /^CMAKE_CXX_COMPILER_LAUNCHER(?::[A-Z]+)?=(.*)$/m.exec(source)?.[1]?.trim() || null;
|
|
196
|
+
}
|
|
197
|
+
function children(path) {
|
|
198
|
+
try {
|
|
199
|
+
return readdirSync(path).toSorted();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return [];
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function moduleRoots(root) {
|
|
206
|
+
const modules = [join(root, "android", "app")];
|
|
207
|
+
const dependencies = join(root, "node_modules");
|
|
208
|
+
for (const name of children(dependencies)) {
|
|
209
|
+
if (name.startsWith(".")) continue;
|
|
210
|
+
const entry = join(dependencies, name);
|
|
211
|
+
const packages = name.startsWith("@") ? children(entry).map((child) => join(entry, child)) : [entry];
|
|
212
|
+
modules.push(...packages.map((pkg) => join(pkg, "android")));
|
|
213
|
+
}
|
|
214
|
+
return modules;
|
|
215
|
+
}
|
|
216
|
+
function cacheFiles(base, depth = 0) {
|
|
217
|
+
const files = [];
|
|
218
|
+
for (const name of children(base)) {
|
|
219
|
+
if (depth === 0 && /^stim-[a-f0-9]{16}$/.test(name)) continue;
|
|
220
|
+
const path = join(base, name);
|
|
221
|
+
const entry = lstatSync(path);
|
|
222
|
+
if (entry.isSymbolicLink()) continue;
|
|
223
|
+
if (name === "CMakeCache.txt" && entry.isFile()) files.push(path);
|
|
224
|
+
else if (depth < 3 && entry.isDirectory() && name !== "CMakeFiles") files.push(...cacheFiles(path, depth + 1));
|
|
225
|
+
}
|
|
226
|
+
return files;
|
|
227
|
+
}
|
|
228
|
+
function readCxxLauncherStates(root) {
|
|
229
|
+
return moduleRoots(root).flatMap((module) => cacheFiles(join(module, ".cxx")).map((path) => ({
|
|
230
|
+
path: relative(root, path),
|
|
231
|
+
launcher: parseCmakeCacheLauncher(readFileSync(path, "utf8"))
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
234
|
+
function declaredLauncher(module) {
|
|
235
|
+
return [
|
|
236
|
+
"build.gradle",
|
|
237
|
+
"build.gradle.kts",
|
|
238
|
+
"CMakeLists.txt"
|
|
239
|
+
].some((name) => {
|
|
240
|
+
const path = join(module, name);
|
|
241
|
+
return existsSync(path) && Boolean(projectCmakeLauncher(readFileSync(path, "utf8")));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function contained(root, path) {
|
|
245
|
+
const rel = relative(root, path);
|
|
246
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
247
|
+
}
|
|
248
|
+
function selectedCompilerCache(root) {
|
|
249
|
+
try {
|
|
250
|
+
return resolveAndroidCompilerCache({
|
|
251
|
+
optimizations: projectOptimizations(root),
|
|
252
|
+
use: readAndroidCasToolchain
|
|
253
|
+
}).optimizations.android.compilerCache;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Keep every native build stopped until repair finishes. The supplemental
|
|
260
|
+
* cache-lock check cannot detect uncached builds, release-swap fallback, or direct Gradle.
|
|
261
|
+
*/
|
|
262
|
+
function repairCxxLauncherState(root) {
|
|
263
|
+
const canonicalRoot = realpathSync(root);
|
|
264
|
+
const result = {
|
|
265
|
+
removed: [],
|
|
266
|
+
refused: []
|
|
267
|
+
};
|
|
268
|
+
if (selectedCompilerCache(root) !== "ccache") return result;
|
|
269
|
+
if (!getExecutor().runQuiet("command -v ccache", { timeoutMs: 5e3 })) return result;
|
|
270
|
+
const appOverrides = declaredLauncher(join(root, "android", "app"));
|
|
271
|
+
const targets = readCxxLauncherStates(root).filter(({ launcher }) => {
|
|
272
|
+
if (!launcher) return true;
|
|
273
|
+
const command = launcher.split(";")[0]?.trim();
|
|
274
|
+
return Boolean(command && isAbsolute(command) && basename(command) === "ccache" && !existsSync(command));
|
|
275
|
+
});
|
|
276
|
+
for (const state of targets) {
|
|
277
|
+
const target = dirname(join(root, state.path));
|
|
278
|
+
const label = relative(root, target);
|
|
279
|
+
try {
|
|
280
|
+
let cxx = target;
|
|
281
|
+
while (contained(root, cxx) && basename(cxx) !== ".cxx") cxx = dirname(cxx);
|
|
282
|
+
if (basename(cxx) !== ".cxx" || target === cxx) throw new Error("unrecognized generated CMake layout");
|
|
283
|
+
if (appOverrides || declaredLauncher(dirname(cxx))) throw new Error("project configures its own compiler launcher");
|
|
284
|
+
const canonicalTarget = realpathSync(target);
|
|
285
|
+
if (!contained(canonicalRoot, canonicalTarget)) throw new Error("generated directory resolves outside this checkout");
|
|
286
|
+
if (realpathSync(cxx) !== join(realpathSync(dirname(cxx)), ".cxx")) throw new Error(".cxx is a symbolic link");
|
|
287
|
+
const gitPath = relative(canonicalRoot, canonicalTarget);
|
|
288
|
+
if (getExecutor().runFile("git", [
|
|
289
|
+
"ls-files",
|
|
290
|
+
"-z",
|
|
291
|
+
"--",
|
|
292
|
+
`:(literal)${gitPath}`
|
|
293
|
+
], {
|
|
294
|
+
cwd: canonicalRoot,
|
|
295
|
+
timeoutMs: 5e3
|
|
296
|
+
})) throw new Error("directory contains tracked files");
|
|
297
|
+
getExecutor().runFile("git", [
|
|
298
|
+
"check-ignore",
|
|
299
|
+
"-q",
|
|
300
|
+
"--",
|
|
301
|
+
gitPath
|
|
302
|
+
], {
|
|
303
|
+
cwd: canonicalRoot,
|
|
304
|
+
timeoutMs: 5e3
|
|
305
|
+
});
|
|
306
|
+
const androidLocks = listBuildLocks().filter((lock) => lock.platform === "android");
|
|
307
|
+
if (androidLocks.some((lock) => lock.unresolved)) throw new Error("an Android build lock holder cannot be identified, so a build may be active");
|
|
308
|
+
if (androidLocks.some((lock) => {
|
|
309
|
+
if (!lock.alive || !lock.projectRoot) return false;
|
|
310
|
+
return realpathSync(lock.projectRoot) === canonicalRoot;
|
|
311
|
+
})) throw new Error("an Android build is active in this checkout");
|
|
312
|
+
if (realpathSync(target) !== canonicalTarget) throw new Error("directory changed during inspection");
|
|
313
|
+
rmSync(target, { recursive: true });
|
|
314
|
+
result.removed.push(label);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
result.refused.push({
|
|
317
|
+
path: label,
|
|
318
|
+
reason: error.message
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/doctor-config.ts
|
|
326
|
+
function readMachineSettings(context) {
|
|
327
|
+
try {
|
|
328
|
+
const layers = settingsLayers(context);
|
|
329
|
+
return {
|
|
330
|
+
settings: mergeSettingsLayers(layers.map((layer) => layer.settings)),
|
|
331
|
+
layers,
|
|
332
|
+
corrupt: null
|
|
333
|
+
};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error.code !== "STIM_CONFIG_CORRUPT") throw error;
|
|
336
|
+
return {
|
|
337
|
+
settings: {},
|
|
338
|
+
layers: [],
|
|
339
|
+
corrupt: {
|
|
340
|
+
level: "note",
|
|
341
|
+
title: "The Stim config is not valid JSON",
|
|
342
|
+
detail: `${String(error.message).split("\n")[0]}. Every setting and device record lives in that file, so no other machine-level check can run until it parses.`,
|
|
343
|
+
fix: configCorruptRepair()
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const PATH_SETTING_ENVIRONMENT = { "optimizations.android.casToolchain": "STIM_ANDROID_CAS_TOOLCHAIN" };
|
|
349
|
+
const PATH_SETTINGS_TRIMMED_BY_THEIR_CONSUMER = /* @__PURE__ */ new Set(["android.keystore"]);
|
|
350
|
+
function checkMachineSettings({ settings, layers, projectRoot, optimizations = null, reportedElsewhere = [], exists = existsSync, env = process.env }) {
|
|
351
|
+
const findings = [];
|
|
352
|
+
const fallback = optimizations?.android.compilerCacheFallback ?? null;
|
|
353
|
+
const fallbackEntry = fallback ? settingOrigin(layers, fallback.key) : null;
|
|
354
|
+
const attributed = fallback && (fallback.fromEnvironment || fallbackEntry) ? fallback : null;
|
|
355
|
+
const reported = attributed?.manifest && !exists(attributed.manifest) ? null : attributed;
|
|
356
|
+
const skip = new Set(reportedElsewhere);
|
|
357
|
+
if (reported) skip.add(reported.key);
|
|
358
|
+
if (!optimizations) for (const key of SETTINGS_WITH_RESOLVE_TIME_FALLBACK) skip.add(key);
|
|
359
|
+
for (const key of PATH_SETTINGS) {
|
|
360
|
+
const variable = PATH_SETTING_ENVIRONMENT[key];
|
|
361
|
+
if (skip.has(key) || variable && skip.has(variable)) continue;
|
|
362
|
+
const override = variable ? env[variable] : void 0;
|
|
363
|
+
if (variable && override) {
|
|
364
|
+
const named = missingPath(override, projectRoot, exists);
|
|
365
|
+
if (named) findings.push({
|
|
366
|
+
level: "note",
|
|
367
|
+
title: "An environment variable points at a path that is not there",
|
|
368
|
+
detail: `${variable} in the environment names ${named}, which does not exist.`,
|
|
369
|
+
fix: `Point ${variable} at the path it should name, or unset it.`
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const entry = settingOrigin(layers, key);
|
|
374
|
+
if (!entry || typeof entry.value !== "string") continue;
|
|
375
|
+
const named = missingPath(entry.value, projectRoot, exists, PATH_SETTINGS_TRIMMED_BY_THEIR_CONSUMER.has(key));
|
|
376
|
+
if (!named) continue;
|
|
377
|
+
findings.push({
|
|
378
|
+
level: "note",
|
|
379
|
+
title: "A setting points at a path that is not there",
|
|
380
|
+
detail: `${key} in ${entry.file} names ${named}, which does not exist.`,
|
|
381
|
+
fix: `Point ${key} at the path it should name, or remove it from ${entry.file}.`
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (reported && optimizations) findings.push(compilerCacheFinding({
|
|
385
|
+
fallback: reported,
|
|
386
|
+
optimizations,
|
|
387
|
+
file: fallbackEntry?.file ?? null
|
|
388
|
+
}));
|
|
389
|
+
for (const key of unknownSettingKeys(settings)) {
|
|
390
|
+
const file = settingOrigin(layers, key)?.file ?? null;
|
|
391
|
+
findings.push({
|
|
392
|
+
level: "note",
|
|
393
|
+
title: "A key in the config is inert",
|
|
394
|
+
detail: `${key}${file ? ` in ${file}` : ""} is not read by Stim, so its value changes nothing.`,
|
|
395
|
+
fix: `Remove ${key}${file ? ` from ${file}` : ""}, or run \`stim guide settings\` for the name that replaced it.`
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return findings;
|
|
399
|
+
}
|
|
400
|
+
function compilerCacheFinding({ fallback, optimizations, file }) {
|
|
401
|
+
const detail = compilerCacheFallbackMessage({
|
|
402
|
+
fallback,
|
|
403
|
+
compilerCache: optimizations.android.compilerCache === "none" ? "none" : "ccache",
|
|
404
|
+
file
|
|
405
|
+
});
|
|
406
|
+
if (fallback.key === "optimizations.android.compilerCache") return {
|
|
407
|
+
level: "note",
|
|
408
|
+
title: "A setting needs a companion this config does not supply",
|
|
409
|
+
detail,
|
|
410
|
+
fix: "Set optimizations.android.casToolchain to the toolchain JSON manifest, or set optimizations.android.compilerCache to ccache."
|
|
411
|
+
};
|
|
412
|
+
const title = fallback.fromEnvironment ? "An environment variable holds a value Stim cannot use" : "A setting holds a value Stim cannot use";
|
|
413
|
+
if (fallback.manifest) return {
|
|
414
|
+
level: "note",
|
|
415
|
+
title,
|
|
416
|
+
detail,
|
|
417
|
+
fix: `Repair ${fallback.manifest}, or ${fallback.fromEnvironment ? `unset ${fallback.key}` : `remove ${fallback.key}${file ? ` from ${file}` : ""}`}.`
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
level: "note",
|
|
421
|
+
title,
|
|
422
|
+
detail,
|
|
423
|
+
fix: fallback.fromEnvironment ? "Point STIM_ANDROID_CAS_TOOLCHAIN at an absolute path to the toolchain JSON manifest, or unset it." : "Set optimizations.android.casToolchain to an absolute path to the toolchain JSON manifest, or remove it."
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function missingPath(value, projectRoot, exists, trim = false) {
|
|
427
|
+
const opened = trim ? value.trim() : value;
|
|
428
|
+
if (opened.trim() === "") return null;
|
|
429
|
+
const path = isAbsolute(opened) ? opened : resolve(projectRoot, opened);
|
|
430
|
+
if (exists(path)) return null;
|
|
431
|
+
return opened === opened.trim() ? path : JSON.stringify(path);
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/doctor.ts
|
|
435
|
+
function finding(level, title, detail, fix) {
|
|
436
|
+
return {
|
|
437
|
+
level,
|
|
438
|
+
title,
|
|
439
|
+
detail,
|
|
440
|
+
fix
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function readJson(path) {
|
|
444
|
+
try {
|
|
445
|
+
return JSON.parse(readFileSync$1(path, "utf-8"));
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function mainCheckoutProjectRoot(projectRoot) {
|
|
451
|
+
const currentRepoRoot = repoRoot(projectRoot);
|
|
452
|
+
if (!currentRepoRoot) return projectRoot;
|
|
453
|
+
try {
|
|
454
|
+
const main = listWorktrees(currentRepoRoot)[0];
|
|
455
|
+
if (!main) return projectRoot;
|
|
456
|
+
const projectRel = relative$1(currentRepoRoot, realpathSync$1(projectRoot));
|
|
457
|
+
if (projectRel.startsWith("..")) return projectRoot;
|
|
458
|
+
return resolve$1(main.path, projectRel);
|
|
459
|
+
} catch {
|
|
460
|
+
return projectRoot;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function installedNpmTreeIsValid(projectRoot) {
|
|
464
|
+
try {
|
|
465
|
+
getExecutor().runFile("npm", [
|
|
466
|
+
"ls",
|
|
467
|
+
"--all",
|
|
468
|
+
"--json",
|
|
469
|
+
"--silent"
|
|
470
|
+
], {
|
|
471
|
+
cwd: projectRoot,
|
|
472
|
+
timeoutMs: 3e4
|
|
473
|
+
});
|
|
474
|
+
return true;
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const DEPENDENCY_STATES = [
|
|
480
|
+
{
|
|
481
|
+
lock: "pnpm-lock.yaml",
|
|
482
|
+
installed: ["node_modules"],
|
|
483
|
+
command: "pnpm install"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
lock: "yarn.lock",
|
|
487
|
+
installed: [
|
|
488
|
+
"node_modules",
|
|
489
|
+
".pnp.cjs",
|
|
490
|
+
".pnp.js"
|
|
491
|
+
],
|
|
492
|
+
command: "yarn install"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
lock: "bun.lock",
|
|
496
|
+
installed: ["node_modules"],
|
|
497
|
+
command: "bun install"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
lock: "bun.lockb",
|
|
501
|
+
installed: ["node_modules"],
|
|
502
|
+
command: "bun install"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
lock: "package-lock.json",
|
|
506
|
+
installed: ["node_modules"],
|
|
507
|
+
command: "npm ci"
|
|
508
|
+
}
|
|
509
|
+
];
|
|
510
|
+
function dependencyState(projectRoot) {
|
|
511
|
+
const root = repoRoot(projectRoot) ?? projectRoot;
|
|
512
|
+
let dir = projectRoot;
|
|
513
|
+
while (true) {
|
|
514
|
+
const state = DEPENDENCY_STATES.find((candidate) => existsSync$1(join$1(dir, candidate.lock)));
|
|
515
|
+
if (state) return {
|
|
516
|
+
...state,
|
|
517
|
+
root: dir
|
|
518
|
+
};
|
|
519
|
+
if (dir === root) return null;
|
|
520
|
+
const parent = dirname$1(dir);
|
|
521
|
+
if (parent === dir || relative$1(root, parent).startsWith("..")) return null;
|
|
522
|
+
dir = parent;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function hasInstalledDependencies(projectRoot, markers = [
|
|
526
|
+
"node_modules",
|
|
527
|
+
".pnp.cjs",
|
|
528
|
+
".pnp.js"
|
|
529
|
+
]) {
|
|
530
|
+
return markers.some((entry) => existsSync$1(join$1(projectRoot, entry)));
|
|
531
|
+
}
|
|
532
|
+
function locallyKnownUpstream(projectRoot) {
|
|
533
|
+
try {
|
|
534
|
+
const name = getExecutor().runFile("git", [
|
|
535
|
+
"-C",
|
|
536
|
+
projectRoot,
|
|
537
|
+
"rev-parse",
|
|
538
|
+
"--abbrev-ref",
|
|
539
|
+
"--symbolic-full-name",
|
|
540
|
+
"@{upstream}"
|
|
541
|
+
], { timeoutMs: 5e3 });
|
|
542
|
+
const counts = getExecutor().runFile("git", [
|
|
543
|
+
"-C",
|
|
544
|
+
projectRoot,
|
|
545
|
+
"rev-list",
|
|
546
|
+
"--left-right",
|
|
547
|
+
"--count",
|
|
548
|
+
"HEAD...@{upstream}"
|
|
549
|
+
], { timeoutMs: 5e3 }).trim().split(/\s+/).map(Number);
|
|
550
|
+
const ahead = counts[0] ?? NaN;
|
|
551
|
+
const behind = counts[1] ?? NaN;
|
|
552
|
+
if (!name || !Number.isInteger(ahead) || !Number.isInteger(behind)) return null;
|
|
553
|
+
return {
|
|
554
|
+
name,
|
|
555
|
+
ahead,
|
|
556
|
+
behind
|
|
557
|
+
};
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function brokenPodLinks(podsRoot) {
|
|
563
|
+
try {
|
|
564
|
+
return getExecutor().runFile("find", [
|
|
565
|
+
"-L",
|
|
566
|
+
podsRoot,
|
|
567
|
+
"-type",
|
|
568
|
+
"l",
|
|
569
|
+
"-print"
|
|
570
|
+
], { timeoutMs: 1e4 }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
571
|
+
} catch {
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function hasIosWarmOutput(root) {
|
|
576
|
+
if (existsSync$1(join$1(root, "ios", "build"))) return true;
|
|
577
|
+
const derivedData = workspaceDerivedData(root);
|
|
578
|
+
if (existsSync$1(join$1(derivedData, "Build", "Products"))) return true;
|
|
579
|
+
try {
|
|
580
|
+
return readdirSync$1(derivedData).some((entry) => /^scheme-[a-f0-9]{64}$/.test(entry) && existsSync$1(join$1(derivedData, entry, "Build", "Products")));
|
|
581
|
+
} catch {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function hasLinkedWorktree(projectRoot) {
|
|
586
|
+
const root = repoRoot(projectRoot);
|
|
587
|
+
if (!root) return false;
|
|
588
|
+
return listWorktrees(root).filter((entry) => !entry.prunable).length > 1;
|
|
589
|
+
}
|
|
590
|
+
function headBranch(root) {
|
|
591
|
+
const ref = getExecutor().runFileQuiet("git", [
|
|
592
|
+
"-C",
|
|
593
|
+
root,
|
|
594
|
+
"symbolic-ref",
|
|
595
|
+
"--quiet",
|
|
596
|
+
"HEAD"
|
|
597
|
+
])?.trim();
|
|
598
|
+
return ref?.startsWith("refs/heads/") ? ref.slice(11) : null;
|
|
599
|
+
}
|
|
600
|
+
function mainOperation(root) {
|
|
601
|
+
const gitDir = getExecutor().runFileQuiet("git", [
|
|
602
|
+
"-C",
|
|
603
|
+
root,
|
|
604
|
+
"rev-parse",
|
|
605
|
+
"--path-format=absolute",
|
|
606
|
+
"--git-dir"
|
|
607
|
+
])?.trim();
|
|
608
|
+
if (!gitDir) return null;
|
|
609
|
+
if (existsSync$1(join$1(gitDir, "rebase-merge")) || existsSync$1(join$1(gitDir, "rebase-apply"))) return "rebase";
|
|
610
|
+
return existsSync$1(join$1(gitDir, "MERGE_HEAD")) ? "merge" : null;
|
|
611
|
+
}
|
|
612
|
+
function dirtyTrackedPaths(root) {
|
|
613
|
+
return (getExecutor().runFileQuiet("git", [
|
|
614
|
+
"-C",
|
|
615
|
+
root,
|
|
616
|
+
"diff",
|
|
617
|
+
"--name-only",
|
|
618
|
+
"HEAD"
|
|
619
|
+
]) ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
620
|
+
}
|
|
621
|
+
function resolveDefaultBranch(root) {
|
|
622
|
+
const worktree = resolveSettings({
|
|
623
|
+
gitCommonDir: gitCommonDir(root),
|
|
624
|
+
repoRoot: repoRoot(root) ?? root
|
|
625
|
+
}).worktree;
|
|
626
|
+
const configured = worktree && typeof worktree === "object" && !Array.isArray(worktree) ? worktree.defaultBranch : void 0;
|
|
627
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
628
|
+
const head = getExecutor().runFileQuiet("git", [
|
|
629
|
+
"-C",
|
|
630
|
+
root,
|
|
631
|
+
"symbolic-ref",
|
|
632
|
+
"--short",
|
|
633
|
+
"refs/remotes/origin/HEAD"
|
|
634
|
+
])?.trim();
|
|
635
|
+
if (!head) return null;
|
|
636
|
+
const cut = head.indexOf("/");
|
|
637
|
+
return cut > 0 ? head.slice(cut + 1) : head;
|
|
638
|
+
}
|
|
639
|
+
function seedFindings(mainRoot, upstream) {
|
|
640
|
+
const findings = [];
|
|
641
|
+
const root = repoRoot(mainRoot);
|
|
642
|
+
const quotedRoot = quotedPath(root ?? mainRoot);
|
|
643
|
+
if (upstream && upstream.behind > 0 && upstream.ahead === 0) findings.push(finding("note", `The source checkout is ${plural(upstream.behind, "commit")} behind ${upstream.name}`, "The count uses the locally known upstream ref. A fetch can reveal additional commits. A later rebase or merge can change native inputs and invalidate work done from the older base.", `Run \`stim worktree warm --refresh\` from a linked worktree to fetch and fast-forward the source checkout, or \`git -C ${quotedRoot} fetch --prune\` and inspect the branch yourself.`));
|
|
644
|
+
if (root === null) return findings;
|
|
645
|
+
const operation = mainOperation(root);
|
|
646
|
+
if (operation) {
|
|
647
|
+
findings.push(finding("note", `The source checkout has a ${operation} in progress`, "`stim worktree warm --refresh` refuses a source checkout it cannot move, and the branch stays where the interrupted operation left it.", `Finish it, or run \`git -C ${quotedRoot} ${operation} --abort\`.`));
|
|
648
|
+
return findings;
|
|
649
|
+
}
|
|
650
|
+
const dirty = dirtyTrackedPaths(root);
|
|
651
|
+
if (dirty.length) findings.push(finding("note", `The source checkout has ${plural(dirty.length, "uncommitted tracked change")}`, `\`stim worktree warm --refresh\` refuses a source checkout it cannot move, so the seed stays where it is until this is cleared. First: ${dirty[0]}.`, `Commit them, or run \`git -C ${quotedRoot} stash push -u -m warm-refresh\`.`));
|
|
652
|
+
const branch = headBranch(root);
|
|
653
|
+
if (branch === null) {
|
|
654
|
+
findings.push(finding("note", "The source checkout has a detached HEAD", "`stim worktree warm --refresh` refuses: there is no branch to fast-forward.", `Run \`git -C ${quotedRoot} checkout <branch>\`.`));
|
|
655
|
+
return findings;
|
|
656
|
+
}
|
|
657
|
+
if (upstream && upstream.ahead > 0 && upstream.behind > 0) findings.push(finding("note", `The source checkout has diverged from ${upstream.name}`, `${branch} is ${upstream.ahead} ahead of and ${upstream.behind} behind ${upstream.name}, so it cannot fast-forward. \`stim worktree warm --refresh\` refuses rather than merging or resetting.`, `Rebase or merge ${branch} onto ${upstream.name} yourself.`));
|
|
658
|
+
const defaultBranch = resolveDefaultBranch(root);
|
|
659
|
+
if (defaultBranch !== null && defaultBranch !== branch) findings.push(finding("note", `The source checkout is on ${branch}, not the default branch ${defaultBranch}`, `Every worktree warmed from here carries ${branch}'s dependencies. \`stim worktree warm --refresh\` warns and continues; it never switches a branch under another checkout.`, `Run \`git -C ${quotedRoot} checkout ${defaultBranch}\`.`));
|
|
660
|
+
return findings;
|
|
661
|
+
}
|
|
662
|
+
function checkMainCheckout(projectRoot, { npmTreeValid, brokenPods = void 0, upstream = void 0, linkedWorktrees = void 0, platform } = {}) {
|
|
663
|
+
const mainRoot = mainCheckoutProjectRoot(projectRoot);
|
|
664
|
+
const findings = [];
|
|
665
|
+
const dependencies = dependencyState(mainRoot);
|
|
666
|
+
if (dependencies) {
|
|
667
|
+
const installed = hasInstalledDependencies(dependencies.root, dependencies.installed);
|
|
668
|
+
const installCommand = `cd ${quotedPath(dependencies.root)} && ${dependencies.command}`;
|
|
669
|
+
if (!installed) findings.push(finding("cost", "The source checkout has no installed dependencies", `A worktree cannot carry dependencies from ${dependencies.root}, so its first build must install them from scratch.`, `Run \`${installCommand}\` before creating native worktrees, or let \`stim worktree warm --refresh\` run it from a linked worktree.`));
|
|
670
|
+
else if (dependencies.lock === "package-lock.json") {
|
|
671
|
+
if ((npmTreeValid === void 0 ? installedNpmTreeIsValid(dependencies.root) : npmTreeValid) === false) findings.push(finding("cost", "The source checkout dependency tree is stale", `npm reports that ${dependencies.root}/node_modules does not match the project dependency graph. Copying it makes each worktree start from the same invalid state.`, `Run \`${installCommand}\` before creating native worktrees, or let \`stim worktree warm --refresh\` run it from a linked worktree.`));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const podfileLock = join$1(mainRoot, "ios", "Podfile.lock");
|
|
675
|
+
const podManifest = join$1(mainRoot, "ios", "Pods", "Manifest.lock");
|
|
676
|
+
const podsRoot = join$1(mainRoot, "ios", "Pods");
|
|
677
|
+
if (platform !== "android" && existsSync$1(podfileLock)) {
|
|
678
|
+
let podsState = null;
|
|
679
|
+
if (!existsSync$1(podManifest)) podsState = "missing";
|
|
680
|
+
else try {
|
|
681
|
+
if (readFileSync$1(podfileLock, "utf-8") !== readFileSync$1(podManifest, "utf-8")) podsState = "stale";
|
|
682
|
+
} catch {
|
|
683
|
+
podsState = "stale";
|
|
684
|
+
}
|
|
685
|
+
if (podsState) {
|
|
686
|
+
const iosRoot = join$1(mainRoot, "ios");
|
|
687
|
+
const podCommand = `cd ${quotedPath(iosRoot)} && ${podInstallCommand(mainRoot)}`;
|
|
688
|
+
findings.push(finding("cost", `The source checkout CocoaPods state is ${podsState}`, `ios/Pods cannot be reused safely because its Manifest.lock ${podsState === "missing" ? "is absent" : "does not match ios/Podfile.lock"}.`, `Run \`${podCommand}\` before creating native worktrees, or let \`stim worktree warm --refresh\` run it from a linked worktree.`));
|
|
689
|
+
}
|
|
690
|
+
const broken = brokenPods === void 0 && existsSync$1(podsRoot) ? brokenPodLinks(podsRoot) : brokenPods || [];
|
|
691
|
+
if (broken.length) {
|
|
692
|
+
const iosRoot = join$1(mainRoot, "ios");
|
|
693
|
+
const podCommand = `cd ${quotedPath(iosRoot)} && ${podInstallCommand(mainRoot, "--clean-install")}`;
|
|
694
|
+
findings.push(finding("cost", "The source checkout CocoaPods state has broken links", `${broken.length} symlink${broken.length === 1 ? "" : "s"} under ios/Pods point to missing files. Worktrees copy these broken links and can fail during compilation. First: ${broken[0]}.`, `Run \`${podCommand}\` before creating native worktrees.`));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const coldPlatforms = [platform !== "android" && existsSync$1(join$1(mainRoot, "ios")) && !hasIosWarmOutput(mainRoot) ? "iOS" : null, platform !== "ios" && existsSync$1(join$1(mainRoot, "android")) && !existsSync$1(join$1(mainRoot, "android", "build")) && !existsSync$1(join$1(mainRoot, "android", "app", "build")) ? "Android" : null].filter((coldPlatform) => coldPlatform !== null);
|
|
698
|
+
if (coldPlatforms.length) findings.push(finding("note", `The source checkout has no ${coldPlatforms.join(" or ")} warm build output`, "The shared Stim artifact or compilation cache can still be warm. Building the source checkout once gives later native worktrees the strongest warm starting point.", `When more native worktrees are expected, run \`stim start\`, \`stim ${coldPlatforms[0] === "iOS" ? "ios" : "android"}\`, and \`stim stop\` from ${mainRoot}.`));
|
|
699
|
+
if (linkedWorktrees === void 0 ? hasLinkedWorktree(projectRoot) : linkedWorktrees) findings.push(...seedFindings(mainRoot, upstream === void 0 ? locallyKnownUpstream(mainRoot) : upstream));
|
|
700
|
+
return findings;
|
|
701
|
+
}
|
|
702
|
+
function checkMetroCache(metroConfigSource) {
|
|
703
|
+
if (metroConfigSource == null) return null;
|
|
704
|
+
const lines = String(metroConfigSource).split("\n");
|
|
705
|
+
const mentions = lines.filter((line) => /cacheStores/.test(line));
|
|
706
|
+
if (!mentions.length) return null;
|
|
707
|
+
if (!lines.every((line, i) => !/cacheStores/.test(line) || isConditional(lines, i))) return null;
|
|
708
|
+
return finding("note", "metro.config.js mentions cacheStores, but not unconditionally", `Every line naming it is inside a conditional, and doctor reads this file rather than executing it, so it cannot tell whether the store is installed. Under \`stim start\` this costs nothing -- Stim appends its own store whether the project's is on or off -- but outside Stim a cacheStores that is off by default costs exactly what having none costs: ${mentions.map((l) => l.trim()).join(" / ")}`, "Only for Metro runs Stim does not host: confirm it applies without env vars -- a store behind an opt-in flag is not shared until every workspace sets the flag.");
|
|
709
|
+
}
|
|
710
|
+
function isConditional(lines, index) {
|
|
711
|
+
const line = lines[index];
|
|
712
|
+
if (line === void 0) return false;
|
|
713
|
+
if (isConditionalLine(line)) return true;
|
|
714
|
+
if (!/^\s+\S/.test(line)) return false;
|
|
715
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
716
|
+
const prev = lines[i];
|
|
717
|
+
if (prev === void 0) continue;
|
|
718
|
+
if (prev.trim() === "") continue;
|
|
719
|
+
return isConditionalLine(prev);
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
function isConditionalLine(line) {
|
|
724
|
+
return /process\.env/.test(line) || line.includes("?") || /(^|[^\w])if([^\w]|$)/.test(line);
|
|
725
|
+
}
|
|
726
|
+
function checkCompilationCache(podfileSource, xcodeMajor) {
|
|
727
|
+
if (podfileSource == null) return null;
|
|
728
|
+
if (xcodeMajor != null && xcodeMajor < 26) return null;
|
|
729
|
+
if (!/COMPILATION_CACHE_ENABLE_CACHING/.test(podfileSource)) return null;
|
|
730
|
+
if (/COMPILATION_CACHE_CAS_PATH/.test(podfileSource)) return null;
|
|
731
|
+
return finding("note", "The Podfile enables compilation caching but leaves the CAS at its default path", "The default CAS lives at the DerivedData root, and DerivedData is per-workspace -- so nothing is actually shared between worktrees, which is the only reason to turn it on. Builds Stim drives are unaffected: they override COMPILATION_CACHE_CAS_PATH to a shared path on the xcodebuild command line, which wins over the project setting. This costs only the builds you run outside Stim.", "Nothing to do for Stim. For builds outside it: set COMPILATION_CACHE_CAS_PATH to a fixed path outside DerivedData -- ~/.stim/compilation-cache is where Stim puts its own, so the two share entries instead of filling two caches.");
|
|
732
|
+
}
|
|
733
|
+
function checkCcacheConflict(podfileSource, podfileProperties) {
|
|
734
|
+
if (podfileSource == null) return null;
|
|
735
|
+
if (!ccacheEnabled(podfileProperties)) return null;
|
|
736
|
+
return finding("cost", "ccache is enabled, so Stim leaves Xcode compilation caching off", "The ccache launcher script is what disables explicitly built modules, which compilation caching requires -- so enabling both tends to mean neither works. Stim will not add its compilation-cache settings to a build whose project has apple.ccacheEnabled=true, and in its default configuration ccache hashes the working directory and every absolute include path, so it misses across worktrees. (Stim relocates ccache itself on Android, where it drives the compile and can set CCACHE_BASEDIR and CCACHE_NOHASHDIR; the Podfile launcher script here is the project's, not Stim's.)", "Pick one, and on Xcode 26 the compilation cache is the one that survives a different workspace path -- Stim supplies it on its own builds as soon as ccache is off. Turn it off where the value comes FROM: on Expo that is the expo-build-properties plugin in the app config (ios.ccacheEnabled), because prebuild rewrites ios/Podfile.properties.json from it; on a bare project edit ios/Podfile.properties.json directly. Then re-run pod install (or let `stim ios` do it).");
|
|
737
|
+
}
|
|
738
|
+
function checkCcacheInstalled(onPath) {
|
|
739
|
+
if (onPath) return null;
|
|
740
|
+
return finding("cost", "ccache is not on PATH, so Android C++ recompiles in every worktree", "Stim resolves the launcher with `command -v ccache` -- the same PATH lookup the build itself uses -- and found nothing, so it passes no CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER to Gradle and every AGP CMake task compiles without one. Those tasks are uncacheable by Gradle, so not one C++ object crosses a worktree. Measured on trailhead (arm64, fresh worktree): 49.6s without the launcher against 34.2s with it.", "brew install ccache, then delete android/app/.cxx and node_modules/**/android/.cxx once so CMake reconfigures with the launcher. The shell that runs Stim must have the install location on PATH -- agent shells often lack /opt/homebrew/bin.");
|
|
741
|
+
}
|
|
742
|
+
function launcherPath(launcher) {
|
|
743
|
+
const command = launcher?.split(";")[0]?.trim();
|
|
744
|
+
return command && isAbsolute$1(command) ? command : null;
|
|
745
|
+
}
|
|
746
|
+
function checkCxxCompilerLauncher({ states, ccacheOnPath, launcherExists = existsSync$1 }) {
|
|
747
|
+
const missing = states.map((state) => ({
|
|
748
|
+
path: state.path,
|
|
749
|
+
command: launcherPath(state.launcher)
|
|
750
|
+
})).filter((state) => state.command !== null && !launcherExists(state.command));
|
|
751
|
+
if (missing.length > 0) {
|
|
752
|
+
const named = [...new Set(missing.map((state) => state.command))].join(", ");
|
|
753
|
+
return finding("cost", "A configured CMake cache names a compiler launcher that is not on this machine", `${plural(missing.length, "CMakeCache.txt file")} under this project name ${named}, and CMake runs that path for every C++ compile. Until the cache is reconfigured the build fails there rather than falling back: ${missing[0]?.path}.`, "With ccache installed, run `stim doctor --fix --platform android` in this checkout to clear affected generated .cxx configurations. Custom launchers require project-specific repair. Stop native builds before repairing.");
|
|
754
|
+
}
|
|
755
|
+
if (!ccacheOnPath) return null;
|
|
756
|
+
const stale = states.filter((state) => state.launcher === null);
|
|
757
|
+
if (stale.length === 0) return null;
|
|
758
|
+
return finding("cost", "The configured CMake cache predates the ccache launcher, so C++ compiles still bypass it", `CMake seeds CMAKE_CXX_COMPILER_LAUNCHER from the environment on a fresh configure only. ${plural(stale.length, "CMakeCache.txt file")} here names no launcher: ${stale[0]?.path}. These configurations can bypass ccache; cache misses reported by ccache itself still represent compilations that used the launcher.`, "Stop native builds, then run `stim doctor --fix --platform android` in this checkout. It clears affected ignored, untracked .cxx configurations; the next `stim android` configures them with ccache. Existing custom launcher settings require project-specific repair.");
|
|
759
|
+
}
|
|
760
|
+
function ccacheIsOnPath() {
|
|
761
|
+
try {
|
|
762
|
+
return Boolean(getExecutor().runQuiet("command -v ccache", { timeoutMs: 5e3 }));
|
|
763
|
+
} catch {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function checkBuildCacheProvider(appConfig, sdkMajor, isExpo = true, dynamicConfig = null) {
|
|
768
|
+
if (!isExpo) return null;
|
|
769
|
+
if (!appConfig && dynamicConfig) return finding("note", `Cannot check the build cache provider in ${dynamicConfig}`, "This config is code, so it is not readable without executing it. A provider is optional -- stim ios/android have their own cache -- but if this project DOES set one, confirm by hand that it is on the key this SDK reads.", `${sdkMajor && sdkMajor <= 53 ? `SDK ${sdkMajor} reads expo.experiments.buildCacheProvider and ignores the top-level key in silence.` : "Use the top-level expo.buildCacheProvider; the experiments key still works as a fallback."} Run \`npx expo config --json\` and look for buildCacheProvider. If one is already set -- including "eas" -- that satisfies this; Stim never replaces it.`);
|
|
770
|
+
if (!appConfig) return null;
|
|
771
|
+
const expo = appConfig.expo ?? appConfig;
|
|
772
|
+
const topLevel = expo.buildCacheProvider;
|
|
773
|
+
const experimental = expo.experiments?.buildCacheProvider;
|
|
774
|
+
if (!topLevel && !experimental) return null;
|
|
775
|
+
if (sdkMajor && sdkMajor <= 53 && topLevel && !experimental) return finding("cost", "buildCacheProvider is at the top level, but this SDK only reads it from experiments", `SDK ${sdkMajor}'s CLI resolves exp.experiments.buildCacheProvider and nothing else. The top-level key is ignored in silence, so the provider is never called and every build is a full build.`, "Move it to expo.experiments.buildCacheProvider.");
|
|
776
|
+
if (sdkMajor && sdkMajor >= 54 && experimental && !topLevel) return finding("note", "buildCacheProvider is still under experiments", `It works -- SDK ${sdkMajor} falls back to the experiments key -- but the setting was promoted out of experiments, and the top-level key is the one that will keep working.`, "Move it to expo.buildCacheProvider.");
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function checkEasAuth({ provider, owner = null, auth = null } = {}) {
|
|
780
|
+
if (provider !== "eas") return null;
|
|
781
|
+
const status = typeof auth === "function" ? auth({ owner }) : auth;
|
|
782
|
+
if (!status || status.ok) return null;
|
|
783
|
+
if (status.unknown) return finding("note", "Could not check the EAS session", `\`eas whoami\` did not give a definite answer (${status.unknown}), so whether this project's EAS build cache can be reached is unknown. Offline is the ordinary reason, and it is not a problem: the cache simply does not answer until the machine is back on the network.`, null);
|
|
784
|
+
if (status.code === "no-cli") return finding("cost", "The build cache provider is \"eas\", but no eas-cli is installed", "The provider shells out to `npx eas-cli` on every lookup and every upload. With no eas-cli resolvable, npx downloads one on the fly (slow, and a version nobody chose) or the call fails -- and the provider swallows that failure and returns null, so every build looks like a cache miss and nothing says why.", status.remedy ?? null);
|
|
785
|
+
if (status.code === "logged-out") return finding("cost", "Not logged in to EAS, so the shared build cache never answers", "eas-build-cache-provider catches its own errors and returns null, so an unauthenticated lookup reads as a plain cache miss: every build compiles, nothing is uploaded for anybody else, and no line in any log mentions authentication.", status.remedy ?? null);
|
|
786
|
+
if (status.code === "wrong-account") return finding("note", `EAS is authenticated as ${status.account}, but this project's owner is ${status.owner}`, `A session on an account that does not cover ${status.owner} cannot read or write that account's builds, so the shared cache silently does nothing here. This is a NOTE and not a hard failure on purpose: \`eas whoami\` only enumerates accounts for some actors (a robot prints a display name that is not an account name at all), the list may be incomplete, and access is the server's decision rather than this list's. Confirm before acting on it.`, status.remedy ?? null);
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
function checkConcurrency({ maxBuilds = 0, maxDevices = 0, liveDevices = 0, activeBuilds = 0 } = {}) {
|
|
790
|
+
if (!maxBuilds && !maxDevices) return null;
|
|
791
|
+
return finding("note", "Concurrency limits are set", `${`maxBuilds ${maxBuilds || "unlimited"}, maxDevices ${maxDevices || "unlimited"}`}. Right now ${liveDevices} Stim device(s) are booted and ${activeBuilds} build slot(s) are in use on this machine. At the device cap a new \`stim ios\`/\`android\` is refused with STIM_AT_CAPACITY (stop an environment or raise it); at the build cap a compile waits for a free slot.`, null);
|
|
792
|
+
}
|
|
793
|
+
function checkRemoteDevice({ configured = null, daemonInEnv = false, agentDeviceOnPath = false, easCliResolvable = false } = {}) {
|
|
794
|
+
if (!configured) return null;
|
|
795
|
+
if (!agentDeviceOnPath) return finding("cost", "A remote device is configured, but agent-device is missing", `agent-device drives the selected remote backend. Without it, \`stim ios --remote ${configured}\` and \`stim android --remote ${configured}\` refuse before device work.`, "npm i -g agent-device");
|
|
796
|
+
if (configured === "proxy") {
|
|
797
|
+
if (daemonInEnv) return finding("note", "This project uses a remote proxy", "The proxy backend connects through AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN. Stim does not create or stop the remote device.", null);
|
|
798
|
+
return finding("cost", "The remote proxy credentials are missing", "The proxy backend requires AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN.", "Export AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN.");
|
|
799
|
+
}
|
|
800
|
+
if (!easCliResolvable) return finding("cost", "A remote device is configured, but there is no eas-cli to create a session with", "The eas backend creates an EAS Simulator session. It needs eas-cli and an account with EAS Simulator access. Neither a project copy nor one on PATH was found.", "Install eas-cli.");
|
|
801
|
+
return finding("note", "This project uses a remote device", "`ios --remote eas` / `android --remote eas` create an EAS Simulator session named stim-<label> and end it on `stop` and `worktree remove`. The build still runs on this machine; only the device is elsewhere. Native device logs are not captured on a remote device -- the Metro half of the timeline is unaffected.", null);
|
|
802
|
+
}
|
|
803
|
+
function checkSimSlim({ configured = false, profileError = null, onPath = false } = {}) {
|
|
804
|
+
if (profileError) return finding("cost", "The SimSlim profile is invalid", profileError, "Set ios.simslimProfile to a readable JSON profile inside the repository.");
|
|
805
|
+
if (!configured) return finding("note", "SimSlim is recommended for parallel iOS simulator work", "A reviewed profile can reduce background simulator services and memory use. It is optional: disabling services can affect tests that depend on them, and installing the binary alone does not apply a profile.", "Run `stim guide lifecycle simslim` to install SimSlim and configure ios.simslimProfile. Stim does not enable a profile through doctor --fix.");
|
|
806
|
+
if (onPath) return null;
|
|
807
|
+
return finding("cost", "A SimSlim profile is configured, but SimSlim is not installed", "`stim ios` needs the `simslim` command to apply the profile to its owned simulator.", "brew install mobai-app/tap/simslim");
|
|
808
|
+
}
|
|
809
|
+
function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null, easAuth = checkEasAuth$1, concurrency = getConcurrencyLimits, liveDevices = null, activeBuilds = null, remoteEnv = process.env, lookupAgentDevice = null, lookupEasCli = null, lookupSimSlim = null, memoryPressure = readHostMemoryPressure, lookupCcache = null, platform } = {}) {
|
|
810
|
+
const read = (rel) => {
|
|
811
|
+
const p = join$1(projectRoot, rel);
|
|
812
|
+
if (!existsSync$1(p)) return null;
|
|
813
|
+
try {
|
|
814
|
+
return readFile(p, "utf-8");
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
const settingsRepoRoot = repoRoot(projectRoot) ?? projectRoot;
|
|
820
|
+
const machineSettings = readMachineSettings({
|
|
821
|
+
projectPath: projectRoot,
|
|
822
|
+
gitCommonDir: gitCommonDir(projectRoot),
|
|
823
|
+
repoRoot: settingsRepoRoot
|
|
824
|
+
});
|
|
825
|
+
if (machineSettings.corrupt) return [machineSettings.corrupt];
|
|
826
|
+
const projectSettings = machineSettings.settings;
|
|
827
|
+
const pkg = readJson(join$1(projectRoot, "package.json"));
|
|
828
|
+
const appConfig = readJson(join$1(projectRoot, "app.json"));
|
|
829
|
+
const dynamicConfig = appConfig ? null : [
|
|
830
|
+
"app.config.ts",
|
|
831
|
+
"app.config.js",
|
|
832
|
+
"app.config.mjs"
|
|
833
|
+
].find((f) => existsSync$1(join$1(projectRoot, f))) || null;
|
|
834
|
+
const podfileProperties = readJson(join$1(projectRoot, "ios", "Podfile.properties.json"));
|
|
835
|
+
const podfile = read(join$1("ios", "Podfile"));
|
|
836
|
+
const metroConfig = read("metro.config.js") ?? read("metro.config.cjs");
|
|
837
|
+
const isExpo = detectIsExpo(projectRoot);
|
|
838
|
+
const expoRange = (pkg?.dependencies)?.expo || "";
|
|
839
|
+
const sdkMajor = parseInt(String(expoRange).replace(/[^\d.]/g, "").split(".")[0] ?? "", 10) || null;
|
|
840
|
+
const provider = appConfig ? providerFromConfig(appConfig) : null;
|
|
841
|
+
const owner = appConfig ? ownerFromConfig(appConfig) : null;
|
|
842
|
+
const limits = typeof concurrency === "function" ? concurrency() : concurrency;
|
|
843
|
+
let concurrencyFinding = null;
|
|
844
|
+
if (limits && (limits.maxBuilds || limits.maxDevices)) concurrencyFinding = checkConcurrency({
|
|
845
|
+
maxBuilds: limits.maxBuilds,
|
|
846
|
+
maxDevices: limits.maxDevices,
|
|
847
|
+
liveDevices: liveDevices ? liveDevices() : countLiveDevices(),
|
|
848
|
+
activeBuilds: activeBuilds ? activeBuilds() : countActiveBuilds()
|
|
849
|
+
});
|
|
850
|
+
const settingShapeFindings = settingShapeErrors(projectSettings).map((error) => finding("cost", "A setting has the wrong type", error, SETTING_SHAPE_REMEDY));
|
|
851
|
+
let optimizations = null;
|
|
852
|
+
try {
|
|
853
|
+
optimizations = resolveAndroidCompilerCache({
|
|
854
|
+
optimizations: resolveOptimizations(projectSettings),
|
|
855
|
+
use: readAndroidCasToolchain
|
|
856
|
+
}).optimizations;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
settingShapeFindings.push(finding("cost", "Invalid optimization setting", error.message, SETTING_SHAPE_REMEDY));
|
|
859
|
+
}
|
|
860
|
+
const remoteBuildCache = optimizations?.buildCache && optimizations.remoteBuildCache;
|
|
861
|
+
const easFinding = remoteBuildCache && provider === "eas" ? checkEasAuth({
|
|
862
|
+
provider,
|
|
863
|
+
owner,
|
|
864
|
+
auth: easAuth({
|
|
865
|
+
projectRoot,
|
|
866
|
+
owner
|
|
867
|
+
})
|
|
868
|
+
}) : null;
|
|
869
|
+
for (const poolPlatform of ["ios", "android"]) {
|
|
870
|
+
if (platform && platform !== poolPlatform) continue;
|
|
871
|
+
const poolSettingError = parkedMaxSetting(poolPlatform).error;
|
|
872
|
+
if (poolSettingError) settingShapeFindings.push(finding("cost", `The ${poolPlatform === "ios" ? "simulator" : "emulator"} pool bound is not a number`, poolSettingError, POOL_SETTING_REMEDY));
|
|
873
|
+
}
|
|
874
|
+
let simslimProfile = null;
|
|
875
|
+
let simslimProfileError = null;
|
|
876
|
+
if (platform !== "android") try {
|
|
877
|
+
simslimProfile = iosSimSlimProfileSetting(projectSettings, projectRoot);
|
|
878
|
+
} catch (error) {
|
|
879
|
+
simslimProfileError = String(error?.message || error);
|
|
880
|
+
}
|
|
881
|
+
const simslimFinding = platform === "android" || !simslimProfile && !simslimProfileError && remoteIosSetting(projectSettings) ? null : checkSimSlim({
|
|
882
|
+
configured: Boolean(simslimProfile),
|
|
883
|
+
profileError: simslimProfileError,
|
|
884
|
+
onPath: simslimProfile ? lookupSimSlim ? lookupSimSlim() : simslimIsOnPath() : false
|
|
885
|
+
});
|
|
886
|
+
const remoteBackends = [.../* @__PURE__ */ new Set([...platform !== "android" ? [remoteIosSetting(projectSettings)] : [], ...platform !== "ios" ? [remoteAndroidSetting(projectSettings)] : []])].filter((backend) => backend !== null);
|
|
887
|
+
const daemonInEnv = Boolean(remoteEnv.AGENT_DEVICE_DAEMON_BASE_URL?.trim() && remoteEnv.AGENT_DEVICE_DAEMON_AUTH_TOKEN?.trim());
|
|
888
|
+
const agentDeviceOnPath = remoteBackends.length ? lookupAgentDevice ? lookupAgentDevice() : agentDeviceIsOnPath() : false;
|
|
889
|
+
const easCliResolvable = remoteBackends.includes("eas") ? lookupEasCli ? lookupEasCli() : Boolean(resolveEasCliBin(projectRoot)) : false;
|
|
890
|
+
const remoteFindings = remoteBackends.map((backend) => checkRemoteDevice({
|
|
891
|
+
configured: backend,
|
|
892
|
+
daemonInEnv,
|
|
893
|
+
agentDeviceOnPath,
|
|
894
|
+
easCliResolvable
|
|
895
|
+
})).filter((remoteFinding) => remoteFinding !== null);
|
|
896
|
+
const memoryAdvice = platform !== "android" && !remoteIosSetting(projectSettings) ? hostMemoryPressureAdvice(memoryPressure()) : null;
|
|
897
|
+
return [
|
|
898
|
+
checkAppProject(projectRoot),
|
|
899
|
+
...checkMainCheckout(projectRoot, { platform }),
|
|
900
|
+
...platform === "android" ? [] : inspectIosDebugArchitectures(mainCheckoutProjectRoot(projectRoot)),
|
|
901
|
+
...checkStorageLayout(projectRoot, { platform }),
|
|
902
|
+
optimizations?.metroSharedCache ? checkMetroCache(metroConfig) : null,
|
|
903
|
+
platform === "android" || !optimizations?.ios.compilationCache ? null : checkCompilationCache(podfile, xcodeMajor),
|
|
904
|
+
platform === "android" || !optimizations?.ios.compilationCache ? null : checkCcacheConflict(podfile, podfileProperties),
|
|
905
|
+
...platform === "ios" || optimizations?.android.compilerCache !== "ccache" ? [] : androidCcacheFindings(projectRoot, platform, lookupCcache),
|
|
906
|
+
remoteBuildCache ? checkBuildCacheProvider(appConfig, sdkMajor, isExpo, dynamicConfig) : null,
|
|
907
|
+
easFinding,
|
|
908
|
+
concurrencyFinding,
|
|
909
|
+
memoryAdvice ? finding("cost", "Host memory pressure can stall the iOS simulator", memoryAdvice, "Free host memory, then retry. Run `stim guide lifecycle simslim` for the optional memory reduction setup.") : null,
|
|
910
|
+
simslimFinding,
|
|
911
|
+
...remoteFindings,
|
|
912
|
+
...settingShapeFindings,
|
|
913
|
+
...checkMachineSettings({
|
|
914
|
+
settings: projectSettings,
|
|
915
|
+
layers: machineSettings.layers,
|
|
916
|
+
projectRoot,
|
|
917
|
+
optimizations,
|
|
918
|
+
reportedElsewhere: simslimProfileError ? ["ios.simslimProfile"] : []
|
|
919
|
+
})
|
|
920
|
+
].filter((f) => Boolean(f));
|
|
921
|
+
}
|
|
922
|
+
function androidCcacheFindings(projectRoot, platform, lookupCcache) {
|
|
923
|
+
if (platform !== "android" && !existsSync$1(join$1(projectRoot, "android"))) return [];
|
|
924
|
+
const onPath = lookupCcache ? lookupCcache() : ccacheIsOnPath();
|
|
925
|
+
let cxx;
|
|
926
|
+
try {
|
|
927
|
+
cxx = checkCxxCompilerLauncher({
|
|
928
|
+
states: readCxxLauncherStates(projectRoot),
|
|
929
|
+
ccacheOnPath: onPath
|
|
930
|
+
});
|
|
931
|
+
} catch (error) {
|
|
932
|
+
cxx = finding("cost", "CMake launcher state could not be inspected", String(error), "Restore read access and rerun doctor.");
|
|
933
|
+
}
|
|
934
|
+
if (cxx) cxx.code = "android-cmake-launcher";
|
|
935
|
+
return [checkCcacheInstalled(onPath), cxx];
|
|
936
|
+
}
|
|
937
|
+
function checkAppProject(projectRoot) {
|
|
938
|
+
const problem = appProjectProblem(projectRoot);
|
|
939
|
+
if (!problem) return null;
|
|
940
|
+
return finding("cost", problem.kind === "unreadable" ? "This package.json does not parse" : "This directory is not a React Native or Expo app", `${problem.message} \`stim start\`, \`stim ios\` and \`stim android\` refuse here with STIM_NO_PROJECT, so nothing below was measured against an app.`, problem.remedy);
|
|
941
|
+
}
|
|
942
|
+
function agentDeviceIsOnPath() {
|
|
943
|
+
try {
|
|
944
|
+
return Boolean(getExecutor().runQuiet("command -v agent-device", { timeoutMs: 5e3 }));
|
|
945
|
+
} catch {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function countLiveDevices() {
|
|
950
|
+
let sims = [];
|
|
951
|
+
let adb = {
|
|
952
|
+
emulators: [],
|
|
953
|
+
physical: [],
|
|
954
|
+
unhealthy: []
|
|
955
|
+
};
|
|
956
|
+
let config = null;
|
|
957
|
+
try {
|
|
958
|
+
sims = listAllIosSims() || [];
|
|
959
|
+
} catch {}
|
|
960
|
+
try {
|
|
961
|
+
adb = listAdbDevices() || adb;
|
|
962
|
+
} catch {}
|
|
963
|
+
try {
|
|
964
|
+
config = loadConfig();
|
|
965
|
+
} catch {}
|
|
966
|
+
return liveOwnedDeviceCount({
|
|
967
|
+
sims,
|
|
968
|
+
adbEmulators: adb.emulators || [],
|
|
969
|
+
config
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
function countActiveBuilds() {
|
|
973
|
+
try {
|
|
974
|
+
return listBuildSlots().filter((s) => s.alive).length;
|
|
975
|
+
} catch {
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function checkFingerprintParity({ projectHash, worktreeHash, changed = [], dirtyFiles = [] } = {}) {
|
|
980
|
+
if (!projectHash || !worktreeHash || projectHash === worktreeHash) return null;
|
|
981
|
+
const names = changed.slice(0, 3).join(", ");
|
|
982
|
+
return finding("note", "This checkout does not fingerprint like a fresh worktree of HEAD", `A clean detached worktree of HEAD computes a different @expo/fingerprint hash than this checkout, so worktrees will MISS the cache entries this checkout fills (and vice versa) until the two agree.${changed.length ? ` The differing source${changed.length === 1 ? "" : "s"}: ${names}${changed.length > 3 ? ` (and ${changed.length - 3} more)` : ""}.` : ""} ${dirtyFiles.length ? `The likely cause is uncommitted changes to tracked fingerprint inputs -- git reports ${dirtyFiles.slice(0, 3).join(", ")}${dirtyFiles.length > 3 ? ` (and ${dirtyFiles.length - 3} more)` : ""} dirty in this checkout.` : "The likely cause is uncommitted changes to tracked fingerprint inputs (this check compared against a clean worktree of HEAD)."} (To measure this, doctor ran a real fingerprint twice and briefly created a temporary git worktree -- .git/worktrees metadata was touched and cleaned up.)`, "Commit the dirty fingerprint inputs, or list the build-irrelevant ones in .fingerprintignore (same syntax as .gitignore, at the project root; Stim already ignores android/local.properties and android/.idea). Only the ones that genuinely cannot change the native build belong there -- generated reports, local env files, a lockfile whose checksums embed absolute machine paths. Never ignore a real native input (a Podfile, a gradle file, the app config) to force a hit: that trades a slow build for a wrong one.");
|
|
983
|
+
}
|
|
984
|
+
function gitMetadataAt(path) {
|
|
985
|
+
try {
|
|
986
|
+
const git = lstatSync$1(join$1(path, ".git"));
|
|
987
|
+
return git.isFile() || git.isDirectory();
|
|
988
|
+
} catch {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function listDirectory(dir) {
|
|
993
|
+
try {
|
|
994
|
+
return readdirSync$1(dir);
|
|
995
|
+
} catch {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function hasLinkedPackageWithGit(projectRoot) {
|
|
1000
|
+
const root = repoRoot(projectRoot) ?? projectRoot;
|
|
1001
|
+
for (let dir = projectRoot;; dir = dirname$1(dir)) {
|
|
1002
|
+
if (linkedPackageWithGitIn(join$1(dir, "node_modules"))) return true;
|
|
1003
|
+
if (dir === root || dirname$1(dir) === dir || relative$1(root, dirname$1(dir)).startsWith("..")) return false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function linkedPackageWithGitIn(nodeModules) {
|
|
1007
|
+
return listDirectory(nodeModules).filter((entry) => !entry.startsWith(".")).flatMap((entry) => entry.startsWith("@") ? listDirectory(join$1(nodeModules, entry)).map((child) => join$1(entry, child)) : [entry]).some((name) => {
|
|
1008
|
+
const root = join$1(nodeModules, name);
|
|
1009
|
+
try {
|
|
1010
|
+
return lstatSync$1(root).isSymbolicLink() && gitMetadataAt(root);
|
|
1011
|
+
} catch {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* A linked native library (a `link:` or `file:` dependency, or a workspace
|
|
1018
|
+
* symlink) brings its checkout's Git metadata into a directory the fingerprint
|
|
1019
|
+
* hashes whole. That metadata changes with every Git operation in the linked
|
|
1020
|
+
* checkout, so workspaces rarely hash alike even when the library's native
|
|
1021
|
+
* sources match. The fingerprint's own debug output says which directories
|
|
1022
|
+
* still hash a .git entry, whatever ignore mechanism the project uses. Linked
|
|
1023
|
+
* packages can be hoisted, so the gate looks at every node_modules from the
|
|
1024
|
+
* app up to the repository root.
|
|
1025
|
+
*/
|
|
1026
|
+
async function detectLinkedLibraryGitMetadata(projectRoot, { createFingerprint = expoFingerprint.createFingerprintAsync, platform } = {}) {
|
|
1027
|
+
const mainRoot = mainCheckoutProjectRoot(projectRoot);
|
|
1028
|
+
if (!hasLinkedPackageWithGit(mainRoot)) return null;
|
|
1029
|
+
let sources;
|
|
1030
|
+
try {
|
|
1031
|
+
sources = (await fingerprintProject(mainRoot, {
|
|
1032
|
+
platform,
|
|
1033
|
+
createFingerprint,
|
|
1034
|
+
debug: true
|
|
1035
|
+
}))?.sources ?? [];
|
|
1036
|
+
} catch {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const directories = sources.flatMap((source) => {
|
|
1040
|
+
if (source.type !== "dir" || typeof source.filePath !== "string") return [];
|
|
1041
|
+
return (source.debugInfo?.children ?? []).some((child) => child?.path === `${source.filePath}/.git`) ? [source.filePath] : [];
|
|
1042
|
+
});
|
|
1043
|
+
if (!directories.length) return null;
|
|
1044
|
+
const several = directories.length > 1;
|
|
1045
|
+
const entries = directories.flatMap((dir) => [`${dir}/.git`, `${dir}/.git/**/*`]);
|
|
1046
|
+
return {
|
|
1047
|
+
...finding("note", `${several ? `${directories.length} linked native libraries carry` : "A linked native library carries"} Git metadata into the fingerprint`, `${directories.map((dir) => `${dir}/.git`).join(", ")} ${several ? "are" : "is"} inside a directory the fingerprint hashes whole. Git rewrites that metadata on every commit, checkout, or worktree of the linked checkout (a worktree turns a .git directory into a pointer file), so this workspace and its worktrees rarely compute the same fingerprint even when the library's native sources match, and each one compiles instead of reusing the artifact.`, `When the native build does not read that Git state, add ${entries.map((entry) => `\`${entry}\``).join(", ")} to .fingerprintignore at the project root: the first form matches the pointer file a worktree has, the second excludes a .git directory's contents. Ignore only the .git entry, not the package: its native sources still have to move the key.`),
|
|
1048
|
+
code: "linked-library-git-metadata"
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
async function detectFingerprintParity(projectRoot, { createFingerprint = expoFingerprint.createFingerprintAsync, differ = expoFingerprint.diffFingerprints, dirtyFiles = dirtyFingerprintFiles, platform: selectedPlatform } = {}) {
|
|
1052
|
+
const exec = getExecutor();
|
|
1053
|
+
if (exec.runFileQuiet("git", [
|
|
1054
|
+
"-C",
|
|
1055
|
+
projectRoot,
|
|
1056
|
+
"rev-parse",
|
|
1057
|
+
"--git-dir"
|
|
1058
|
+
], { timeoutMs: 1e4 }) == null) return null;
|
|
1059
|
+
if (hasInstalledDependencies(projectRoot)) return null;
|
|
1060
|
+
const platform = selectedPlatform ?? (existsSync$1(join$1(projectRoot, "ios")) ? "ios" : existsSync$1(join$1(projectRoot, "android")) ? "android" : void 0);
|
|
1061
|
+
let base;
|
|
1062
|
+
try {
|
|
1063
|
+
base = makeTemporaryDirectory(projectRoot, "stim-parity-");
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const worktree = join$1(base, "head");
|
|
1068
|
+
if (exec.runFileQuiet("git", [
|
|
1069
|
+
"-C",
|
|
1070
|
+
projectRoot,
|
|
1071
|
+
"worktree",
|
|
1072
|
+
"add",
|
|
1073
|
+
"--detach",
|
|
1074
|
+
worktree,
|
|
1075
|
+
"HEAD"
|
|
1076
|
+
], { timeoutMs: 6e4 }) == null) {
|
|
1077
|
+
rmSync$1(base, {
|
|
1078
|
+
recursive: true,
|
|
1079
|
+
force: true
|
|
1080
|
+
});
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
const project = await fingerprintProject(projectRoot, {
|
|
1085
|
+
platform,
|
|
1086
|
+
createFingerprint
|
|
1087
|
+
});
|
|
1088
|
+
const clean = await fingerprintProject(worktree, {
|
|
1089
|
+
platform,
|
|
1090
|
+
createFingerprint
|
|
1091
|
+
});
|
|
1092
|
+
if (!project || !clean) return null;
|
|
1093
|
+
if (project.hash === clean.hash) return null;
|
|
1094
|
+
const changed = diffFingerprintSources({
|
|
1095
|
+
previous: clean.sources,
|
|
1096
|
+
previousHash: clean.hash,
|
|
1097
|
+
current: project,
|
|
1098
|
+
differ
|
|
1099
|
+
});
|
|
1100
|
+
return checkFingerprintParity({
|
|
1101
|
+
projectHash: project.hash,
|
|
1102
|
+
worktreeHash: clean.hash,
|
|
1103
|
+
changed,
|
|
1104
|
+
dirtyFiles: dirtyFiles(projectRoot)
|
|
1105
|
+
});
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
} finally {
|
|
1109
|
+
exec.runFileQuiet("git", [
|
|
1110
|
+
"-C",
|
|
1111
|
+
projectRoot,
|
|
1112
|
+
"worktree",
|
|
1113
|
+
"remove",
|
|
1114
|
+
"--force",
|
|
1115
|
+
worktree
|
|
1116
|
+
], { timeoutMs: 3e4 });
|
|
1117
|
+
rmSync$1(base, {
|
|
1118
|
+
recursive: true,
|
|
1119
|
+
force: true
|
|
1120
|
+
});
|
|
1121
|
+
exec.runFileQuiet("git", [
|
|
1122
|
+
"-C",
|
|
1123
|
+
projectRoot,
|
|
1124
|
+
"worktree",
|
|
1125
|
+
"prune"
|
|
1126
|
+
], { timeoutMs: 1e4 });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
//#endregion
|
|
1130
|
+
export { installedNpmTreeIsValid as a, repairCxxLauncherState as c, hasInstalledDependencies as i, detectFingerprintParity as n, locallyKnownUpstream as o, detectLinkedLibraryGitMetadata as r, runDoctor as s, dependencyState as t };
|