stim 1.25.0 → 1.27.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 +1 -1
- package/dist/{acquire-artifact-zA4S7RHE.mjs → acquire-artifact-BKN2Yuhg.mjs} +2 -2
- package/dist/{act-BbHS3oV8.mjs → act-ROviTF_t.mjs} +415 -118
- package/dist/{activity-BpjwrQXU.mjs → activity-Cd-ysFes.mjs} +6 -7
- package/dist/{agent-device-usage-output-Bnkerfod.mjs → agent-device-usage-output-C5nCKWp9.mjs} +5 -5
- package/dist/{android-CNZgRC-4.mjs → android--HBgpvpW.mjs} +5 -5
- package/dist/{android-BGwuK3Mc.mjs → android-Cd6w7_qw.mjs} +134 -57
- package/dist/{android-saBeB4lr.mjs → android-Deb9X3_Y.mjs} +37 -15
- package/dist/{android-Cjcj8MCw.mjs → android-DriNoAVy.mjs} +99 -47
- package/dist/{android-cas-BVqh2Zep.mjs → android-cas-D5e6ecUg.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/{android-project-CTYaETc0.d.mts → android-project-DUGMbq28.d.mts} +55 -1
- package/dist/api.d.mts +20 -7
- package/dist/api.mjs +42 -32
- package/dist/apk-signed-tail-BHQHx3Rm.mjs +142 -0
- package/dist/{artifact-Co8zixVx.mjs → artifact-0AGJYB9O.mjs} +28 -28
- package/dist/{artifact-DN8JNwqS.mjs → artifact-faemIZv5.mjs} +40 -30
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/{attempt-DW-mQqM7.mjs → attempt-ClyJlZPT.mjs} +4 -2
- package/dist/auto-placement-z-H5pJza.mjs +350 -0
- package/dist/{browser-web-CCgZsHmI.mjs → browser-web-slD8AvLp.mjs} +16 -17
- package/dist/{build-cache-CKCzxzxA.mjs → build-cache-gBJ-WKzA.mjs} +141 -37
- package/dist/{build-CAiw8Ezm.mjs → build-hS0QyFlQ.mjs} +38 -31
- package/dist/{build-plan-zCuHGlA8.mjs → build-plan-CBqYbzYM.mjs} +7 -6
- package/dist/{cache-manifest-BPXRONJz.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{budget-HyShrIpL.mjs → ccache-BWTze1Ls.mjs} +634 -1514
- package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
- package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
- package/dist/{cli-C4w607Pz.mjs → cli-ftXDLto1.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-BQsmyp_-.mjs → client-xsdxrhWA.mjs} +77 -116
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BBvS1H6v.mjs → command-output-Dj-WT4BP.mjs} +1 -1
- package/dist/{config-BZhXY15H.mjs → config-Cn94Jc0g.mjs} +343 -18
- package/dist/{launch-DscF1QfM.mjs → dependencies-BGokZ-Ng.mjs} +330 -279
- package/dist/{dependency-state-BwNCYG5Q.mjs → dependency-state-C3OtgG94.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-CHoXXZeg.mjs → dev-client-BMca82xo.mjs} +2 -2
- package/dist/{device-F5-4z9ef.mjs → device-BJm0irrq.mjs} +16 -16
- package/dist/{device-capacity-ZYaRALv5.mjs → device-capacity-6LP7QsaA.mjs} +103 -25
- package/dist/device-host-worker.mjs +19 -21
- package/dist/{device-ios-DkHBrmvH.mjs → device-ios-534c14fz.mjs} +288 -28
- package/dist/{device-lease-run-ogEeJ62K.mjs → device-lease-run-DwsGfjUN.mjs} +5 -5
- package/dist/{device-pool-BIg8MU8l.mjs → device-pool-BnQqvs6o.mjs} +3 -3
- package/dist/{device-B9FqTBYW.mjs → device-tM41-TUq.mjs} +25 -24
- package/dist/{doctor-A_Nr_fDe.mjs → doctor-Dbx1ZpD2.mjs} +1 -1
- package/dist/{doctor-k3P3PizH.mjs → doctor-DyqE9n1J.mjs} +28 -16
- package/dist/{doctor-Bm8TVpGn.mjs → doctor-F-3VUUyN.mjs} +128 -35
- package/dist/{doctor-fingerprint-zlujA1Gs.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +9 -3
- package/dist/doctor-fingerprint.mjs +1 -1
- package/dist/{doctor-watchman-BovI1y3-.mjs → doctor-watchman-CxDbyrrt.mjs} +5 -5
- package/dist/{eas-build-CyDKNTBP.mjs → eas-build-CjNSWmjo.mjs} +4 -4
- package/dist/eas-fallback-9fqb46kW.mjs +87 -0
- package/dist/{eas-session-ledger-BMCA1gdO.mjs → eas-session-ledger-DrP4OuuM.mjs} +6 -6
- package/dist/{error-diagnostics-B1Mj_-7h.mjs → error-diagnostics-snej9k4j.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-eaz403W6.mjs → gc-BRwAY3qT.mjs} +2 -2
- package/dist/{gc-B9jcnmIk.mjs → gc-CGZ1Yg88.mjs} +218 -72
- package/dist/{guide-DR0K0xW9.mjs → guide-_rfEh_zM.mjs} +839 -222
- package/dist/{hosted-android-Bvb2-ZxD.mjs → hosted-android-BMKzUNnY.mjs} +4 -4
- package/dist/{hosted-client-rsRqysJc.mjs → hosted-client-D7Pk_Oo1.mjs} +13 -9
- package/dist/{hosted-logs-CWKGYWNX.mjs → hosted-logs-B1gqQuva.mjs} +18 -12
- package/dist/hosted-logs-sync-Dg3qrZmV.mjs +195 -0
- package/dist/{hosted-macos-d0pv9Tth.mjs → hosted-macos-B9w4H8hc.mjs} +6 -6
- package/dist/{hosted-native-BxTizGU2.mjs → hosted-native-CdgaiuyP.mjs} +17 -17
- package/dist/{idle-shutdown-BIyfl0Zj.mjs → idle-shutdown-Cbvo5KEh.mjs} +21 -9
- package/dist/{ios-BRVM8b0P.mjs → ios-B5HlVMhQ.mjs} +138 -55
- package/dist/{ios-Dvt97HgQ.mjs → ios-Bk4mWbtS.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-BQzvJlzw.mjs → ios-E26qSpkb.mjs} +94 -11
- package/dist/{ios-device-CKSvk0bu.mjs → ios-device-C_QkIf1Z.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-DCUDo06I.mjs → ios-project-D8Jcy_br.mjs} +22 -9
- package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
- package/dist/{ios-state-CT9nV5SP.mjs → ios-state-BZ03NFgF.mjs} +1 -1
- package/dist/{js-swap-Z9HbC4cj.mjs → js-swap-Dg2a945i.mjs} +8 -7
- package/dist/{launch-D0lPvFFu.mjs → launch-CRyBeNsA.mjs} +53 -44
- package/dist/lightened-BCsdAGj_.mjs +171 -0
- package/dist/{logs-BsIRre8x.mjs → logs-CC_-ZzEE.mjs} +10 -174
- package/dist/{logs-Crs9y34Z.mjs → logs-aJQb-RDg.mjs} +112 -38
- package/dist/{macos-CcUlJPMu.mjs → macos-BGmTpnv9.mjs} +1 -1
- package/dist/{macos-D477nfzH.mjs → macos-DlCBhMw5.mjs} +73 -39
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +30 -13
- package/dist/{metro-1pJgCX4U.mjs → metro-CvyD-xwe.mjs} +4 -4
- package/dist/{metro-gateway-Bp9gJQAN.mjs → metro-gateway-BnHdqMO_.mjs} +3 -4
- package/dist/{named-ports-BVbn8SvC.mjs → named-ports-BCyMWA6b.mjs} +3 -3
- package/dist/{native-android-BvMizJn1.mjs → native-android-DlpxCyOP.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-BJscK4vN.mjs → native-gradle-artifact-inputs-C13qT7R_.mjs} +5 -5
- package/dist/{native-runtime-B9w_Qyz3.mjs → native-runtime-DqaIBO5w.mjs} +9 -9
- package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
- package/dist/{native-xcode-inputs-Cv4l-obn.mjs → native-xcode-inputs-B1W8SrxB.mjs} +3 -3
- package/dist/{native-xcode-ios-DGrg90hE.mjs → native-xcode-ios-QPP0Rm68.mjs} +17 -18
- package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
- package/dist/offload-worker.d.mts +3 -1
- package/dist/offload-worker.mjs +34 -25
- package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
- package/dist/{ownership-jxxz-19m.mjs → ownership-Bx-0cku-.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-CYb7RRAx.mjs → phone-reverses-C9yAaKEv.mjs} +18 -4
- package/dist/{placement-log-DjOXJRIH.mjs → placement-log-BXUbiZM6.mjs} +3 -3
- package/dist/{plan-placement-BqKgKHCi.mjs → plan-placement-UxnQjv5Z.mjs} +36 -10
- package/dist/{ports-DtS1Amb0.mjs → ports-uUrghqWN.mjs} +3 -3
- package/dist/prebuild-BDpToDgR.mjs +1246 -0
- package/dist/{project-BiC68mIp.mjs → project-DYj-HVx6.mjs} +17 -18
- package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-DRYqoJc4.mjs → react-native-android-DDBccgJw.mjs} +56 -36
- package/dist/{react-native-build-CwZB-agu.mjs → react-native-build-DaK2IFyP.mjs} +9 -9
- package/dist/{react-native-doctor-BCrytK7h.mjs → react-native-doctor-B-Mw6DcR.mjs} +50 -9
- package/dist/{react-native-ios-Cr1buNFo.mjs → react-native-ios-DjYwUF9U.mjs} +45 -25
- package/dist/{recordings-BaoQ0t-J.mjs → recordings-C20iUZfp.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-Beftw2X3.mjs → reload-DJBeF_rC.mjs} +1 -1
- package/dist/{reload-FbSgMF8x.mjs → reload-fUdkH2_p.mjs} +16 -16
- package/dist/{remote-BuZj6AyU.mjs → remote-y6n99WMW.mjs} +22 -9
- package/dist/{result-CR2vk2cj.mjs → result-CTqBw2Te.mjs} +32 -21
- package/dist/{run-BtzE09Dl.mjs → run-DaCeNKRm.mjs} +3 -3
- package/dist/{selection-D5cxEl3t.mjs → selection-Bs8phwhN.mjs} +183 -62
- package/dist/{server-bare-CH9X0RLn.mjs → server-bare-BOh-iZEj.mjs} +2 -2
- package/dist/{server-command-CaBbDNU2.mjs → server-command-8CuA51Cv.mjs} +6 -6
- package/dist/{server-expo-Ck8EIidM.mjs → server-expo-ABQs2XFA.mjs} +4 -4
- package/dist/server-expo-Ba_Y21vB.mjs +2 -0
- package/dist/{settings-B5tCoq9N.mjs → settings-BDVI8Ivc.mjs} +331 -62
- package/dist/{settings-DzH9mGQZ.mjs → settings-CMHhQNwI.mjs} +24 -3
- package/dist/settings-payload-cm60Zhib.mjs +257 -0
- package/dist/settings-write-BdNRLlBn.mjs +185 -0
- package/dist/settings.schema.json +84 -39
- package/dist/{simslim-BjxszUAb.mjs → simslim-DCQwzoPy.mjs} +4 -3
- package/dist/{slot-launch-QeuJMlnJ.mjs → slot-launch-0l7RqSpr.mjs} +6 -6
- package/dist/{stage-D8xlRU2D.mjs → stage-F4cAfJGi.mjs} +2 -2
- package/dist/{start-C1JLjkNw.mjs → start-DcqnF9p8.mjs} +1 -1
- package/dist/{start-YMpNwLhh.mjs → start-DrPwER_2.mjs} +27 -23
- package/dist/{state-DMRNP7-4.mjs → state-BX1y5syV.mjs} +4 -5
- package/dist/{state-DPaM3u5K.mjs → state-CZxLyetD.mjs} +2 -3
- package/dist/{stats-CP7STzZw.mjs → stats-GxuCbles.mjs} +5 -5
- package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
- package/dist/{status-Fect2w9R.mjs → status-7ULDEJ1e.mjs} +1 -1
- package/dist/{status-CRLiJ-O_.mjs → status-BfnK9pUd.mjs} +25 -26
- package/dist/{status-cNw6i0_t.mjs → status-Bj7ZYvvc.mjs} +1 -1
- package/dist/{status-watch-Bc2J4TZn.mjs → status-watch-BfabQn03.mjs} +5 -5
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-DS0mzR8U.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-COP6PQqw.mjs → stop-CH79S3IZ.mjs} +5 -6
- package/dist/{stop-Db9Kh-UA.mjs → stop-D-EAr25B.mjs} +25 -22
- package/dist/{stop-Br7LKxBE.mjs → stop-x1Yu3He4.mjs} +2 -2
- package/dist/supervisor-run.d.mts +1 -0
- package/dist/supervisor-run.mjs +17 -18
- package/dist/{support-qKcbgmI5.mjs → support-Be0UeVZU.mjs} +10 -10
- package/dist/{support-Cxj9CmII.mjs → support-CNg_YsOk.mjs} +6 -6
- package/dist/{swiftpm-artifact-inputs-iY9fsmOc.mjs → swiftpm-artifact-inputs-uPqYvGBv.mjs} +4 -4
- package/dist/{swiftpm-macos-Bp_sn9P4.mjs → swiftpm-macos-Ch4YjY_A.mjs} +11 -12
- package/dist/toolchain-CbCG6C5Z.mjs +2413 -0
- package/dist/toolchain-JYAOquPg.mjs +2 -0
- package/dist/{trigger-CBMe2YLH.mjs → trigger-DMou-X7j.mjs} +3 -3
- package/dist/trigger-qp2ji5wE.mjs +2 -0
- package/dist/{warm-progress-BV3ZPmcY.mjs → warm-progress-C0RsbngN.mjs} +731 -728
- package/dist/{web-B4Yjyt1G.mjs → web-Cq_AQ4DK.mjs} +9 -10
- package/dist/web-Je6abcpz.mjs +2 -0
- package/dist/web-run.mjs +9 -9
- package/dist/windows-report.mjs +1 -1
- package/dist/{workspace-state-CPSp3mav.mjs → workspace-state-BjqdjnJR.mjs} +46 -8
- package/dist/{workspaces-C9w4aeXF.mjs → workspaces-DFbj07jp.mjs} +1801 -76
- package/dist/{worktree-CclFIe1Z.mjs → worktree-COAJ3-OD.mjs} +934 -511
- package/dist/{worktree-Ct9Gk3KL.mjs → worktree-JGUd_53L.mjs} +1 -1
- package/package.json +8 -8
- package/dist/ccache-h1FKhZeg.mjs +0 -143
- package/dist/created-devices-D2Aca-xu.mjs +0 -52
- package/dist/eas-fallback-gaZi-dJL.mjs +0 -381
- package/dist/hosted-ios-RtVcJH3u.mjs +0 -15
- package/dist/native-run-D5OJsGwA.mjs +0 -276
- package/dist/paths-DQtTgsBf.mjs +0 -42
- package/dist/prebuild-JUaqOLpc.mjs +0 -501
- package/dist/server-expo-C16Zf2k7.mjs +0 -2
- package/dist/settings-write-n0zwfohx.mjs +0 -360
- package/dist/toolchain-DB1aeqxO.mjs +0 -2
- package/dist/toolchain-VzLJzZ7O.mjs +0 -908
- package/dist/trigger-BLsHgAGR.mjs +0 -2
- package/dist/web-DS4UDWvf.mjs +0 -2
- package/dist/xcode-CiIIDnuD.mjs +0 -910
|
@@ -0,0 +1,2413 @@
|
|
|
1
|
+
import { D as ownership_claim_exports, _ as spawnDeclared, b as operationState, g as signalDeclaredSpawns, h as refuseDeclaredSpawns, m as declareSpawnsOn, p as allowDeclaredSpawns, t as getExecutor, u as withWorkspaceProcessLock, v as stopDeclaringSpawnsOn, y as isTermination } from "./exec-aULwtNly.mjs";
|
|
2
|
+
import { E as workspaceDir, T as workspaceDerivedData, l as withWorkspaceStateLockAsync, o as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey } from "./workspace-state-BjqdjnJR.mjs";
|
|
3
|
+
import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
4
|
+
import { a as formatElapsed, l as phaseLine } from "./command-output-Dj-WT4BP.mjs";
|
|
5
|
+
import { a as killOnAbort } from "./operation-exec-Dp4FuIGR.mjs";
|
|
6
|
+
import { o as androidHome } from "./android-DriNoAVy.mjs";
|
|
7
|
+
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
8
|
+
import { p as resolveOptimizations } from "./errors-DHm_U5gv.mjs";
|
|
9
|
+
import { t as cleanLine } from "./server-expo-ABQs2XFA.mjs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir, tmpdir } from "node:os";
|
|
14
|
+
import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { ambientContext, ambientEnv } from "@stim-cli/core";
|
|
16
|
+
import { stimBuildDigest } from "@stim-cli/core/state";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
19
|
+
//#region src/cache/cache-claim.ts
|
|
20
|
+
const LABEL = "cache";
|
|
21
|
+
const POLL_MS = 500;
|
|
22
|
+
const EMPTYING_WAIT_MS = 6e5;
|
|
23
|
+
/**
|
|
24
|
+
* The claim set guarding a cache Stim can only empty whole. It sits beside the cache's resolved directory, so emptying
|
|
25
|
+
* the cache never removes it and every path that reaches the same cache names the same set; before the cache exists,
|
|
26
|
+
* its resolved parent stands in for it.
|
|
27
|
+
*/
|
|
28
|
+
function cacheClaimRoot(dir) {
|
|
29
|
+
return `${existsSync(dir) ? canonicalPath(dir) : join(canonicalPath(dirname(dir)), basename(dir))}.claims`;
|
|
30
|
+
}
|
|
31
|
+
function describe(holder) {
|
|
32
|
+
const { platform, projectRoot } = holder.details;
|
|
33
|
+
const where = typeof projectRoot === "string" ? ` in ${projectRoot}` : "";
|
|
34
|
+
return holder.mode === "shared" ? `the ${typeof platform === "string" ? platform : "iOS"} build (pid ${holder.owner.pid}${where}) may be using it` : `pid ${holder.owner.pid} is emptying it`;
|
|
35
|
+
}
|
|
36
|
+
/** Why the cache at `dir` may be in use, from its claim set, without taking or removing any claim. */
|
|
37
|
+
function cacheUser(dir) {
|
|
38
|
+
const root = cacheClaimRoot(dir);
|
|
39
|
+
const survey = (0, ownership_claim_exports.readClaimSet)(root);
|
|
40
|
+
const problem = survey.unresolved[0];
|
|
41
|
+
if (problem) return `the cache claim ${problem.path} is unresolved, so a build may be using it`;
|
|
42
|
+
const holder = survey.live[0];
|
|
43
|
+
return holder ? describe(holder) : void 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Takes the exclusive claim an emptier holds while it removes the cache's contents; a string is why it cannot.
|
|
47
|
+
* It never waits for a build that holds the cache.
|
|
48
|
+
*/
|
|
49
|
+
function claimForEmptying(dir) {
|
|
50
|
+
try {
|
|
51
|
+
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
52
|
+
root: cacheClaimRoot(dir),
|
|
53
|
+
mode: "exclusive",
|
|
54
|
+
label: LABEL
|
|
55
|
+
});
|
|
56
|
+
if (attempt.acquired) return attempt.acquired;
|
|
57
|
+
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
58
|
+
const holder = attempt.held ?? attempt.waitingFor?.[0];
|
|
59
|
+
return holder ? describe(holder) : `its claim at ${cacheClaimRoot(dir)} could not be taken`;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if ((0, ownership_claim_exports.isClaimRefusal)(error) || (0, ownership_claim_exports.isClaimUnavailable)(error)) return error.message;
|
|
62
|
+
return `its claim at ${cacheClaimRoot(dir)} could not be taken: ${error instanceof Error ? error.message : String(error)}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Takes the shared claim a build holds on the compilation cache it compiles with, waiting while an emptier holds it.
|
|
67
|
+
* A claim set Stim cannot resolve, or no process identity, is a failure: the build never runs unguarded.
|
|
68
|
+
*/
|
|
69
|
+
async function shareCompilationCache({ dir, projectRoot, note, signal }) {
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
let told = false;
|
|
72
|
+
for (;;) {
|
|
73
|
+
signal?.throwIfAborted();
|
|
74
|
+
let attempt;
|
|
75
|
+
let root = `${dir}.claims`;
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
78
|
+
root = cacheClaimRoot(dir);
|
|
79
|
+
attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
80
|
+
root,
|
|
81
|
+
mode: "shared",
|
|
82
|
+
label: LABEL,
|
|
83
|
+
details: {
|
|
84
|
+
platform: "ios",
|
|
85
|
+
projectRoot
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { failure: (0, ownership_claim_exports.claimFailure)(error, null) ?? {
|
|
90
|
+
code: ownership_claim_exports.CLAIM_UNAVAILABLE,
|
|
91
|
+
message: `Stim could not take the compilation cache claim at ${root}: ${error instanceof Error ? error.message : String(error)}.`,
|
|
92
|
+
remedy: `Restore write access to ${dirname(root)}, then retry the same Stim command.`
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
95
|
+
if (attempt.acquired) return { claim: attempt.acquired };
|
|
96
|
+
const emptier = attempt.held ? `pid ${attempt.held.owner.pid}` : "another process";
|
|
97
|
+
const waited = Date.now() - started;
|
|
98
|
+
if (waited >= EMPTYING_WAIT_MS) return { failure: {
|
|
99
|
+
code: "STIM_BUILD_FAILED",
|
|
100
|
+
message: `${emptier} has been emptying the compilation cache at ${dir} for ${formatElapsed(waited)}.`,
|
|
101
|
+
remedy: "Run the same command again once it has finished."
|
|
102
|
+
} };
|
|
103
|
+
if (!told) note(phaseLine("build", `waiting for ${emptier} to finish emptying the compilation cache at ${dir}`));
|
|
104
|
+
told = true;
|
|
105
|
+
await setTimeout$1(POLL_MS);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/engine/native-run.ts
|
|
110
|
+
const NATIVE_RUN_LOCK = "native-run";
|
|
111
|
+
const NATIVE_RUN_WAIT_MS = 18e5;
|
|
112
|
+
const WAIT_HEARTBEAT_MS = 3e4;
|
|
113
|
+
const CANCEL_REQUEST_KEY = "nativeRunCancel";
|
|
114
|
+
const REQUEST_POLL_MS = 250;
|
|
115
|
+
function nativeRunHolder(holder) {
|
|
116
|
+
const { command, platform, slot, interrupt } = holder.details;
|
|
117
|
+
return {
|
|
118
|
+
command: typeof command === "string" ? command : null,
|
|
119
|
+
platform: typeof platform === "string" ? platform : null,
|
|
120
|
+
slot: typeof slot === "string" ? slot : "default",
|
|
121
|
+
takesSignals: interrupt === "signal"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function describeNativeRunHolder(holder, now) {
|
|
125
|
+
const { command, slot } = nativeRunHolder(holder);
|
|
126
|
+
const started = Date.parse(holder.startedAt);
|
|
127
|
+
const facts = [`pid ${holder.owner.pid}`];
|
|
128
|
+
if (Number.isFinite(started)) facts.push(`running for ${formatElapsed(now - started)}`);
|
|
129
|
+
if (command === null) return `another Stim run (${facts.join(", ")})`;
|
|
130
|
+
if (command === "build") return `an artifact build (${facts.join(", ")})`;
|
|
131
|
+
return `\`stim ${command}${slot === "default" ? "" : ` --slot ${slot}`}\` (${facts.join(", ")})`;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Report a wait on the native-run lock: one line as soon as a holder is seen, a line for each new
|
|
135
|
+
* holder, and a heartbeat every WAIT_HEARTBEAT_MS while the same holder keeps it.
|
|
136
|
+
*/
|
|
137
|
+
function nativeRunWaitNotice({ write, now = Date.now, describe = describeNativeRunHolder }) {
|
|
138
|
+
let claimId = null;
|
|
139
|
+
let lastAt = 0;
|
|
140
|
+
return (holder, firstLine) => {
|
|
141
|
+
const at = now();
|
|
142
|
+
if (holder.claimId !== claimId) {
|
|
143
|
+
claimId = holder.claimId;
|
|
144
|
+
lastAt = at;
|
|
145
|
+
write(firstLine ?? `waiting for ${describe(holder, at)} in this workspace to finish`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (at - lastAt < WAIT_HEARTBEAT_MS) return;
|
|
149
|
+
lastAt = at;
|
|
150
|
+
write(`still waiting for ${describe(holder, at)}`);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* What `stop` does about the run holding the native-run lock. A build is interrupted only when the stop
|
|
155
|
+
* leaves it nothing to deploy to: the whole workspace stops, the build targets the stopped slot, or the
|
|
156
|
+
* stopped slot is the workspace's only device. A build for another slot that stays is left running and
|
|
157
|
+
* the stop proceeds without the lock. Only an owner whose identity is proven live is signalled.
|
|
158
|
+
*/
|
|
159
|
+
function decideStopAction({ stopSlot, holder, ownerIdentity, deviceSlots }) {
|
|
160
|
+
if (holder.command === "macos" || holder.command === "build") {
|
|
161
|
+
if (stopSlot !== void 0) return { action: "proceed" };
|
|
162
|
+
return ownerIdentity === "same" ? { action: "interrupt" } : { action: "refuse" };
|
|
163
|
+
}
|
|
164
|
+
if (holder.command !== "ios" && holder.command !== "android") return { action: "wait" };
|
|
165
|
+
if (!(stopSlot === void 0 || holder.slot === stopSlot || deviceSlots.includes(stopSlot) && deviceSlots.every((slot) => slot === stopSlot))) return { action: "proceed" };
|
|
166
|
+
return ownerIdentity === "same" ? { action: "interrupt" } : { action: "refuse" };
|
|
167
|
+
}
|
|
168
|
+
const requestListeners = /* @__PURE__ */ new Map();
|
|
169
|
+
/**
|
|
170
|
+
* Ask the run holding `claimId` to cancel: the request is written to the workspace state, where the run's watch
|
|
171
|
+
* finds it, and a run in this process is told at once.
|
|
172
|
+
*/
|
|
173
|
+
function requestNativeRunCancel(root, claimId, pid = process.pid) {
|
|
174
|
+
updateWorkspaceState(root, (state) => ({
|
|
175
|
+
...state,
|
|
176
|
+
[CANCEL_REQUEST_KEY]: {
|
|
177
|
+
claimId,
|
|
178
|
+
pid,
|
|
179
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
180
|
+
}
|
|
181
|
+
}));
|
|
182
|
+
requestListeners.get(claimId)?.();
|
|
183
|
+
}
|
|
184
|
+
function cancelRequestFor(root, claimId) {
|
|
185
|
+
const request = readWorkspaceState$1(root)?.[CANCEL_REQUEST_KEY];
|
|
186
|
+
if (request?.claimId !== claimId || typeof request.pid !== "number") return null;
|
|
187
|
+
return { pid: request.pid };
|
|
188
|
+
}
|
|
189
|
+
function clearNativeRunCancel(root, claimId) {
|
|
190
|
+
try {
|
|
191
|
+
clearWorkspaceStateKey(root, CANCEL_REQUEST_KEY, (value) => value?.claimId === claimId);
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
/** The active native run of `context`'s operation aborts this signal when it is cancelled. */
|
|
195
|
+
function runCancellationSignal(context) {
|
|
196
|
+
return operationState(context).runSignal;
|
|
197
|
+
}
|
|
198
|
+
/** Why `context`'s native run was cancelled, or null while it was not. */
|
|
199
|
+
function runCancellation(context) {
|
|
200
|
+
return operationState(context).cancelled;
|
|
201
|
+
}
|
|
202
|
+
function cancelledFailure(context, platform, { code, message }) {
|
|
203
|
+
const cancelled = runCancellation(context);
|
|
204
|
+
if (cancelled === null || code === "STIM_CLAIM_UNAVAILABLE") return null;
|
|
205
|
+
return {
|
|
206
|
+
code: "STIM_CANCELLED",
|
|
207
|
+
message: `The ${platform} run was ${cancelled} before it finished.`,
|
|
208
|
+
remedy: `Run \`stim ${platform}\` again when you want the app on a device.`,
|
|
209
|
+
lines: [`it stopped at ${code ?? "an unfinished step"}${message ? `: ${message}` : ""}`]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function cancelOnAbort({ context, root, claim, write, controller }) {
|
|
213
|
+
const state = operationState(context);
|
|
214
|
+
const cancel = () => {
|
|
215
|
+
if (controller.signal.aborted) return;
|
|
216
|
+
const request = cancelRequestFor(root, claim.claimId);
|
|
217
|
+
const cancelled = request ? `cancelled by \`stim stop\` (pid ${request.pid})` : "cancelled by an interrupt";
|
|
218
|
+
state.cancelled = cancelled;
|
|
219
|
+
state.handled = true;
|
|
220
|
+
controller.abort();
|
|
221
|
+
refuseDeclaredSpawns(context, `the run was ${cancelled}`);
|
|
222
|
+
const signalled = signalDeclaredSpawns(context, "SIGINT");
|
|
223
|
+
if (signalled > 0 || request) state.draining = true;
|
|
224
|
+
write(signalled > 0 ? `${cancelled}; stopping the running build tool` : request ? `${cancelled}; stopping before the next build tool or install` : cancelled);
|
|
225
|
+
};
|
|
226
|
+
const onAbort = () => {
|
|
227
|
+
if (!isTermination(context.signal.reason)) cancel();
|
|
228
|
+
};
|
|
229
|
+
const onRequest = () => {
|
|
230
|
+
if (controller.signal.aborted || !cancelRequestFor(root, claim.claimId)) return;
|
|
231
|
+
cancel();
|
|
232
|
+
state.abortOperation?.("stop");
|
|
233
|
+
};
|
|
234
|
+
context.signal.addEventListener("abort", onAbort);
|
|
235
|
+
if (context.signal.aborted) onAbort();
|
|
236
|
+
requestListeners.set(claim.claimId, onRequest);
|
|
237
|
+
const watch = setInterval(onRequest, REQUEST_POLL_MS);
|
|
238
|
+
watch.unref();
|
|
239
|
+
return async () => {
|
|
240
|
+
clearInterval(watch);
|
|
241
|
+
requestListeners.delete(claim.claimId);
|
|
242
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
243
|
+
allowDeclaredSpawns(context);
|
|
244
|
+
await withWorkspaceStateLockAsync(root, () => clearNativeRunCancel(root, claim.claimId)).catch(() => {});
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Run an `ios`, `android` or `macos` invocation under the workspace's native-run lock: the claim records the
|
|
249
|
+
* command, platform and slot, and a wait for another holder is reported on `write`. An abort of `context.signal`
|
|
250
|
+
* forwards an interrupt to the running declared build tool and refuses any later one; a run `stim stop` cancelled
|
|
251
|
+
* stops before its next build tool or install. A `SIGTERM` abort reason is left to the caller.
|
|
252
|
+
*/
|
|
253
|
+
function withNativeBuildRun(context, root, details, fn, { write }) {
|
|
254
|
+
const notice = nativeRunWaitNotice({ write });
|
|
255
|
+
const state = operationState(context);
|
|
256
|
+
return withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN_LOCK, async (claim) => {
|
|
257
|
+
state.cancelled = null;
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
state.runSignal = controller.signal;
|
|
260
|
+
const dispose = cancelOnAbort({
|
|
261
|
+
context,
|
|
262
|
+
root,
|
|
263
|
+
claim,
|
|
264
|
+
write,
|
|
265
|
+
controller
|
|
266
|
+
});
|
|
267
|
+
try {
|
|
268
|
+
return await fn(claim);
|
|
269
|
+
} finally {
|
|
270
|
+
await dispose();
|
|
271
|
+
state.runSignal = void 0;
|
|
272
|
+
}
|
|
273
|
+
}, {
|
|
274
|
+
external: true,
|
|
275
|
+
signal: context.signal,
|
|
276
|
+
waitMs: NATIVE_RUN_WAIT_MS,
|
|
277
|
+
declareSpawnsFor: context,
|
|
278
|
+
details: {
|
|
279
|
+
...details,
|
|
280
|
+
interrupt: state.takesSignals ? "signal" : "request"
|
|
281
|
+
},
|
|
282
|
+
onHeld: (holder) => notice(holder)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/engine/bundler.ts
|
|
287
|
+
const COCOAPODS_SPEC = /^ {4}cocoapods \(([^)\s]+)/m;
|
|
288
|
+
/**
|
|
289
|
+
* The Bundler pin `pod install` in `directory` runs under: the nearest Gemfile from `directory` up to `root`, as
|
|
290
|
+
* Bundler finds it, when its Gemfile.lock resolves cocoapods.
|
|
291
|
+
*/
|
|
292
|
+
function bundlerPin(root, directory = root) {
|
|
293
|
+
for (let dir = directory;; dir = dirname(dir)) {
|
|
294
|
+
const gemfile = join(dir, "Gemfile");
|
|
295
|
+
if (existsSync(gemfile)) return lockedCocoapods(gemfile, join(dir, "Gemfile.lock"));
|
|
296
|
+
const up = relative(root, dir);
|
|
297
|
+
if (!up || up.startsWith("..") || dirname(dir) === dir) return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function lockedCocoapods(gemfile, lockfile) {
|
|
301
|
+
let lock;
|
|
302
|
+
try {
|
|
303
|
+
lock = readFileSync(lockfile, "utf-8");
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const cocoapods = COCOAPODS_SPEC.exec(lock)?.[1];
|
|
308
|
+
return cocoapods ? {
|
|
309
|
+
gemfile,
|
|
310
|
+
lockfile,
|
|
311
|
+
cocoapods
|
|
312
|
+
} : null;
|
|
313
|
+
}
|
|
314
|
+
function podInstallCommand(root, directory, ...args) {
|
|
315
|
+
const pod = [
|
|
316
|
+
"pod",
|
|
317
|
+
"install",
|
|
318
|
+
...args
|
|
319
|
+
].join(" ");
|
|
320
|
+
return bundlerPin(root, directory) ? `bundle exec ${pod}` : pod;
|
|
321
|
+
}
|
|
322
|
+
const BUILD_FAILED = "** BUILD FAILED **";
|
|
323
|
+
const POSITIONED = /^(\S[^\t]*?):(\d+):(?:(\d+):)?\s*(?:fatal\s+)?error:\s+(.+)$/;
|
|
324
|
+
const UNPOSITIONED = /^(\S[^\t]*?)?\berror:\s+(.+)$/;
|
|
325
|
+
const LD_ERROR = /^ld:\s+(?!warning:)(.+)$/;
|
|
326
|
+
const UNDEFINED_HEADER = /^(?:ld:\s+)?Undefined symbols?(?:\s+for architecture\s+\S+)?:\s*$/;
|
|
327
|
+
const UNDEFINED_SYMBOL = /^\s+"?([^",]+)"?,\s+referenced from:\s*$/;
|
|
328
|
+
function undefinedSymbolMessage(symbol) {
|
|
329
|
+
return `Undefined symbol: ${symbol}`;
|
|
330
|
+
}
|
|
331
|
+
const PODS_OUT_OF_SYNC = /The sandbox is not in sync with the Podfile\.lock/;
|
|
332
|
+
const SIGNING = [
|
|
333
|
+
/No profiles for '.*' were found/,
|
|
334
|
+
/requires a development team/i,
|
|
335
|
+
/[Cc]ode ?[Ss]igning [Ee]rror/,
|
|
336
|
+
/No signing certificate/i,
|
|
337
|
+
/code signing is required/i,
|
|
338
|
+
/errSecInternalComponent/,
|
|
339
|
+
/Provisioning profile .* doesn't (?:include|match)/i
|
|
340
|
+
];
|
|
341
|
+
const NO_SUCH_SCHEME = /does not contain a scheme named/;
|
|
342
|
+
const CAS_CORRUPT = /not a IncludeTreeRoot node kind/;
|
|
343
|
+
const COMPILATION_CACHE_CORRUPTION = /OnDiskCAS: corrupt internal reference|CAS operation failed|not a IncludeTreeRoot node kind/;
|
|
344
|
+
const DEVICE_SIGNING_REMEDY = "Set a team for the target's configuration in Xcode > Signing & Capabilities, then run `stim device provision ios`, which lists the Apple Developer account changes and asks before making them (agents run it only with the user's go-ahead), or build once from Xcode to install the profile. Builds never pass -allowProvisioningUpdates, because registering a device or minting a profile changes your Apple Developer account.";
|
|
345
|
+
const SIMULATOR_SIGNING_REMEDY = "Stim builds for the simulator here, which needs no signing. Check CODE_SIGNING_REQUIRED / DEVELOPMENT_TEAM in the target's configuration, or build for a phone with `stim ios --device`.";
|
|
346
|
+
function remedyFor(message, pods, sdk) {
|
|
347
|
+
if (CAS_CORRUPT.test(message)) return "The compilation cache holds a damaged object. Run `stim gc --delete --cache \"compilation cache\"` to empty that cache, then build again. The next build is a cold one.";
|
|
348
|
+
if (PODS_OUT_OF_SYNC.test(message)) return `Run \`${pods ? podInstallCommand(pods.root, pods.directory) : "pod install"}\` in ios/ (stim ios does this when Podfile.lock and Pods/Manifest.lock disagree), then build again.`;
|
|
349
|
+
if (NO_SUCH_SCHEME.test(message)) return "Run `xcodebuild -list` in ios/ to see the schemes this project defines, and share the app scheme so it is visible to the build.";
|
|
350
|
+
for (const pattern of SIGNING) if (pattern.test(message)) return sdk.startsWith("iphoneos") ? DEVICE_SIGNING_REMEDY : SIMULATOR_SIGNING_REMEDY;
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function fileFromPrefix(prefix) {
|
|
354
|
+
const trimmed = String(prefix).trim().replace(/:$/, "");
|
|
355
|
+
if (!trimmed) return null;
|
|
356
|
+
const head = (trimmed.split(": ")[0] ?? "").trim();
|
|
357
|
+
if (!head.includes("/")) return null;
|
|
358
|
+
return head;
|
|
359
|
+
}
|
|
360
|
+
function makeDiagnostic({ file = null, line = null, column = null, message }, pods, sdk) {
|
|
361
|
+
const text = String(message).trim();
|
|
362
|
+
const out = { message: text };
|
|
363
|
+
if (file) out.file = file;
|
|
364
|
+
if (line !== null && line !== void 0) out.line = line;
|
|
365
|
+
if (column !== null && column !== void 0) out.column = column;
|
|
366
|
+
out.message = text;
|
|
367
|
+
const remedy = remedyFor(text, pods, sdk);
|
|
368
|
+
if (remedy) out.remedy = remedy;
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
function dedupeKey(d) {
|
|
372
|
+
return `${d.file || ""}|${d.line || ""}|${d.column || ""}|${d.message}`;
|
|
373
|
+
}
|
|
374
|
+
function createXcodeDiagnosticCollector(root = null, sdk = "iphonesimulator", podDirectory = root) {
|
|
375
|
+
const pods = root ? {
|
|
376
|
+
root,
|
|
377
|
+
directory: podDirectory ?? root
|
|
378
|
+
} : null;
|
|
379
|
+
const out = [];
|
|
380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
381
|
+
let finished = false;
|
|
382
|
+
let inUndefinedSymbols = false;
|
|
383
|
+
const add = (d) => {
|
|
384
|
+
const key = dedupeKey(d);
|
|
385
|
+
if (seen.has(key)) return;
|
|
386
|
+
seen.add(key);
|
|
387
|
+
out.push(d);
|
|
388
|
+
};
|
|
389
|
+
const push = (line) => {
|
|
390
|
+
if (finished) return;
|
|
391
|
+
const raw = line.replace(/\r$/, "");
|
|
392
|
+
if (inUndefinedSymbols) {
|
|
393
|
+
const sym = UNDEFINED_SYMBOL.exec(raw);
|
|
394
|
+
if (sym) {
|
|
395
|
+
const symbol = sym[1];
|
|
396
|
+
if (symbol !== void 0) add(makeDiagnostic({ message: undefinedSymbolMessage(symbol) }, pods, sdk));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (/^\s+\S/.test(line) && !/^\S/.test(line)) return;
|
|
400
|
+
inUndefinedSymbols = false;
|
|
401
|
+
}
|
|
402
|
+
if (raw.includes(BUILD_FAILED)) {
|
|
403
|
+
finished = true;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (UNDEFINED_HEADER.test(raw)) {
|
|
407
|
+
inUndefinedSymbols = true;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const ld = LD_ERROR.exec(raw);
|
|
411
|
+
if (ld) {
|
|
412
|
+
add(makeDiagnostic({ message: `ld: ${ld[1]}` }, pods, sdk));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const positioned = POSITIONED.exec(raw);
|
|
416
|
+
if (positioned) {
|
|
417
|
+
const posMsg = positioned[4];
|
|
418
|
+
if (posMsg === void 0) return;
|
|
419
|
+
add(makeDiagnostic({
|
|
420
|
+
file: positioned[1],
|
|
421
|
+
line: Number(positioned[2]),
|
|
422
|
+
column: positioned[3] === void 0 ? null : Number(positioned[3]),
|
|
423
|
+
message: posMsg
|
|
424
|
+
}, pods, sdk));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const plain = UNPOSITIONED.exec(raw);
|
|
428
|
+
if (plain) {
|
|
429
|
+
const plainMsg = plain[2];
|
|
430
|
+
if (plainMsg === void 0) return;
|
|
431
|
+
add(makeDiagnostic({
|
|
432
|
+
file: fileFromPrefix(plain[1] || ""),
|
|
433
|
+
message: plainMsg
|
|
434
|
+
}, pods, sdk));
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
push,
|
|
439
|
+
diagnostics: out
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function capDiagnostics(diagnostics, max = 10) {
|
|
443
|
+
const list = Array.isArray(diagnostics) ? diagnostics : [];
|
|
444
|
+
if (list.length <= max) return {
|
|
445
|
+
diagnostics: list.slice(),
|
|
446
|
+
truncated: 0
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
diagnostics: list.slice(0, max),
|
|
450
|
+
truncated: list.length - max
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function describeDiagnostic(diagnostic) {
|
|
454
|
+
if (!diagnostic || typeof diagnostic !== "object") return "";
|
|
455
|
+
const { file, line, column, message } = diagnostic;
|
|
456
|
+
if (!file) return String(message ?? "");
|
|
457
|
+
return `${file}${line ? column ? `:${line}:${column}` : `:${line}` : ""}: ${message}`;
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/engine/xcode.ts
|
|
461
|
+
function pickXcodeProject(entries) {
|
|
462
|
+
const names = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string");
|
|
463
|
+
const workspaces = names.filter((e) => e.endsWith(".xcworkspace")).toSorted();
|
|
464
|
+
const projects = names.filter((e) => e.endsWith(".xcodeproj")).toSorted();
|
|
465
|
+
if (workspaces.length > 0) {
|
|
466
|
+
const projectNames = new Set(projects.map((p) => basename(p, ".xcodeproj")));
|
|
467
|
+
const file = workspaces.find((w) => projectNames.has(basename(w, ".xcworkspace"))) || workspaces[0];
|
|
468
|
+
if (file === void 0) return null;
|
|
469
|
+
return {
|
|
470
|
+
kind: "workspace",
|
|
471
|
+
flag: "-workspace",
|
|
472
|
+
file,
|
|
473
|
+
name: basename(file, ".xcworkspace")
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (projects.length > 0) {
|
|
477
|
+
const file = projects[0];
|
|
478
|
+
if (file === void 0) return null;
|
|
479
|
+
return {
|
|
480
|
+
kind: "project",
|
|
481
|
+
flag: "-project",
|
|
482
|
+
file,
|
|
483
|
+
name: basename(file, ".xcodeproj")
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
function parseSchemeList(text) {
|
|
489
|
+
const empty = {
|
|
490
|
+
name: null,
|
|
491
|
+
schemes: []
|
|
492
|
+
};
|
|
493
|
+
if (typeof text !== "string") return empty;
|
|
494
|
+
const start = text.indexOf("{");
|
|
495
|
+
const end = text.lastIndexOf("}");
|
|
496
|
+
if (start < 0 || end <= start) return empty;
|
|
497
|
+
let data;
|
|
498
|
+
try {
|
|
499
|
+
data = JSON.parse(text.slice(start, end + 1));
|
|
500
|
+
} catch {
|
|
501
|
+
return empty;
|
|
502
|
+
}
|
|
503
|
+
if (!data || typeof data !== "object") return empty;
|
|
504
|
+
const record = data;
|
|
505
|
+
const container = record.workspace || record.project;
|
|
506
|
+
if (!container || typeof container !== "object") return empty;
|
|
507
|
+
const info = container;
|
|
508
|
+
const schemes = Array.isArray(info.schemes) ? info.schemes.filter((s) => typeof s === "string" && s.trim() !== "") : [];
|
|
509
|
+
return {
|
|
510
|
+
name: typeof info.name === "string" ? info.name : null,
|
|
511
|
+
schemes
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const TEST_SCHEME = /(?:UI)?Tests$/;
|
|
515
|
+
function pickScheme(schemes, containerName) {
|
|
516
|
+
const list = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "");
|
|
517
|
+
if (list.length === 0) return null;
|
|
518
|
+
const name = typeof containerName === "string" ? containerName.trim() : "";
|
|
519
|
+
if (name) {
|
|
520
|
+
const exact = list.find((s) => s === name);
|
|
521
|
+
if (exact) return exact;
|
|
522
|
+
const insensitive = list.find((s) => s.toLowerCase() === name.toLowerCase());
|
|
523
|
+
if (insensitive) return insensitive;
|
|
524
|
+
}
|
|
525
|
+
const app = list.filter((s) => !TEST_SCHEME.test(s));
|
|
526
|
+
return app.length === 1 ? app[0] : null;
|
|
527
|
+
}
|
|
528
|
+
const LIST_TIMEOUT_MS = 18e4;
|
|
529
|
+
async function probe(executor, file, args, options) {
|
|
530
|
+
const context = ambientContext();
|
|
531
|
+
context?.signal.throwIfAborted();
|
|
532
|
+
try {
|
|
533
|
+
return await executor.runFileAsync(file, args, {
|
|
534
|
+
...options,
|
|
535
|
+
declareFor: context,
|
|
536
|
+
onSpawn: killOnAbort(context?.signal)
|
|
537
|
+
});
|
|
538
|
+
} finally {
|
|
539
|
+
context?.signal.throwIfAborted();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function listSchemes(project, { exec = null } = {}) {
|
|
543
|
+
const executor = exec || getExecutor();
|
|
544
|
+
try {
|
|
545
|
+
return parseSchemeList(await probe(executor, "xcodebuild", [
|
|
546
|
+
project.flag,
|
|
547
|
+
project.path,
|
|
548
|
+
"-list",
|
|
549
|
+
"-json"
|
|
550
|
+
], { timeoutMs: LIST_TIMEOUT_MS }));
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function resolveScheme(project, { exec = null, scheme: requested = null } = {}) {
|
|
556
|
+
const listing = await listSchemes(project, { exec });
|
|
557
|
+
if (listing === null) return { error: {
|
|
558
|
+
code: "STIM_NO_SCHEME",
|
|
559
|
+
message: `Could not list schemes for ${project.path}.`,
|
|
560
|
+
remedy: `Run \`xcodebuild ${project.flag} ${project.path} -list\` to see what it reports.`
|
|
561
|
+
} };
|
|
562
|
+
if (requested !== null) {
|
|
563
|
+
if (listing.schemes.includes(requested)) return {
|
|
564
|
+
scheme: requested,
|
|
565
|
+
schemes: listing.schemes
|
|
566
|
+
};
|
|
567
|
+
return { error: {
|
|
568
|
+
code: "STIM_NO_SCHEME",
|
|
569
|
+
message: `No shared Xcode scheme named ${JSON.stringify(requested)} in ${project.path}. Available schemes: ${listing.schemes.join(", ") || "none"}.`,
|
|
570
|
+
remedy: "Pass an exact available name with --scheme or set it in ios.scheme, or share the intended app scheme in Xcode."
|
|
571
|
+
} };
|
|
572
|
+
}
|
|
573
|
+
let scheme = pickScheme(listing.schemes, listing.name || project.name);
|
|
574
|
+
if (!scheme && project.dir) try {
|
|
575
|
+
const app = JSON.parse(readFileSync(join(project.appRoot ?? join(project.dir, ".."), "app.json"), "utf8"));
|
|
576
|
+
scheme = pickScheme(listing.schemes, app?.name);
|
|
577
|
+
} catch {}
|
|
578
|
+
if (!scheme) {
|
|
579
|
+
const found = listing.schemes.length ? listing.schemes.join(", ") : "none";
|
|
580
|
+
return { error: {
|
|
581
|
+
code: "STIM_NO_SCHEME",
|
|
582
|
+
message: `Could not select an app scheme in ${project.path} (schemes: ${found}).`,
|
|
583
|
+
remedy: listing.schemes.length > 0 ? "Pass --scheme <name>, or set ios.scheme, to select the intended shared app scheme. Stim does not guess between unmatched schemes." : "Share the app scheme in Xcode (Product > Scheme > Manage Schemes, tick Shared) so xcodebuild can see it."
|
|
584
|
+
} };
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
scheme,
|
|
588
|
+
schemes: listing.schemes
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const PREFIX_MAP_TARGET = "/^src";
|
|
592
|
+
function prefixMapping(workspaceRoot) {
|
|
593
|
+
return `${String(workspaceRoot).replace(/\/+$/, "")}=${PREFIX_MAP_TARGET}`;
|
|
594
|
+
}
|
|
595
|
+
function compilationPrefixMappings(workspaceRoot, derivedDataPath) {
|
|
596
|
+
const source = resolve(workspaceRoot);
|
|
597
|
+
const derived = resolve(derivedDataPath);
|
|
598
|
+
const mappings = [prefixMapping(source)];
|
|
599
|
+
const rel = relative(source, derived);
|
|
600
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) mappings.push(`${derived}=/^derived-data`);
|
|
601
|
+
return mappings.map((mapping) => /[\s"\\]/.test(mapping) ? `"${mapping.replace(/["\\]/g, "\\$&")}"` : mapping).join(" ");
|
|
602
|
+
}
|
|
603
|
+
const SWIFT_PREFIX_MAPPING_MIN_SWIFT = {
|
|
604
|
+
major: 6,
|
|
605
|
+
minor: 4
|
|
606
|
+
};
|
|
607
|
+
function swiftPrefixMappingSupported(version) {
|
|
608
|
+
if (!version) return false;
|
|
609
|
+
const min = SWIFT_PREFIX_MAPPING_MIN_SWIFT;
|
|
610
|
+
return version.major > min.major || version.major === min.major && version.minor >= min.minor;
|
|
611
|
+
}
|
|
612
|
+
function compilationCacheSettings({ workspaceRoot, derivedDataPath, casPath, xcodeMajor, swiftVersion = null, swiftCacheCompatible = false, ccache = false, optimizations = resolveOptimizations({}, {}).ios }) {
|
|
613
|
+
if (xcodeMajor === null || xcodeMajor === void 0) return [];
|
|
614
|
+
if (xcodeMajor < 26) return [];
|
|
615
|
+
if (ccache) return [];
|
|
616
|
+
const swiftMappable = swiftPrefixMappingSupported(swiftVersion);
|
|
617
|
+
const auto = swiftMappable && swiftCacheCompatible;
|
|
618
|
+
const swift = optimizations.compilationCache && (optimizations.swiftCompilationCache ?? auto);
|
|
619
|
+
const mappings = optimizations.prefixMapping ? compilationPrefixMappings(workspaceRoot, derivedDataPath) : "";
|
|
620
|
+
return [
|
|
621
|
+
`COMPILATION_CACHE_ENABLE_CACHING=${optimizations.compilationCache ? "YES" : "NO"}`,
|
|
622
|
+
`COMPILATION_CACHE_CAS_PATH=${casPath}`,
|
|
623
|
+
`SWIFT_ENABLE_COMPILE_CACHE=${swift ? "YES" : "NO"}`,
|
|
624
|
+
`CLANG_ENABLE_PREFIX_MAPPING=${optimizations.prefixMapping ? "YES" : "NO"}`,
|
|
625
|
+
`CLANG_OTHER_PREFIX_MAPPINGS=${mappings}`,
|
|
626
|
+
...swift && swiftMappable && optimizations.prefixMapping ? ["SWIFT_ENABLE_PREFIX_MAPPING=YES", `SWIFT_OTHER_PREFIX_MAPPINGS=${mappings}`] : ["SWIFT_ENABLE_PREFIX_MAPPING=NO"]
|
|
627
|
+
];
|
|
628
|
+
}
|
|
629
|
+
function parseSwiftVersion(output) {
|
|
630
|
+
const m = /\bSwift version (\d+)\.(\d+)/.exec(String(output || ""));
|
|
631
|
+
if (!m || m[1] === void 0 || m[2] === void 0) return null;
|
|
632
|
+
const major = parseInt(m[1], 10);
|
|
633
|
+
const minor = parseInt(m[2], 10);
|
|
634
|
+
return Number.isFinite(major) && Number.isFinite(minor) ? {
|
|
635
|
+
major,
|
|
636
|
+
minor
|
|
637
|
+
} : null;
|
|
638
|
+
}
|
|
639
|
+
function parseXcodeMajor(output) {
|
|
640
|
+
const m = /^Xcode\s+(\d+)/m.exec(String(output || ""));
|
|
641
|
+
if (!m) return null;
|
|
642
|
+
const digits = m[1];
|
|
643
|
+
if (digits === void 0) return null;
|
|
644
|
+
const major = parseInt(digits, 10);
|
|
645
|
+
return Number.isFinite(major) ? major : null;
|
|
646
|
+
}
|
|
647
|
+
async function detectXcodeMajorAsync(exec = null) {
|
|
648
|
+
return parseXcodeMajor(await (exec || getExecutor()).runFileQuietAsync("xcodebuild", ["-version"], { timeoutMs: 1e4 }));
|
|
649
|
+
}
|
|
650
|
+
function xcodebuildArgs({ project, scheme, udid = null, destination = null, configuration = "Debug", sdk = "iphonesimulator", derivedDataPath, extraArgs = [], buildSettings = [] }) {
|
|
651
|
+
return [
|
|
652
|
+
project.flag,
|
|
653
|
+
project.path,
|
|
654
|
+
"-scheme",
|
|
655
|
+
scheme,
|
|
656
|
+
"-configuration",
|
|
657
|
+
configuration,
|
|
658
|
+
"-sdk",
|
|
659
|
+
sdk,
|
|
660
|
+
"-destination",
|
|
661
|
+
destination || `id=${udid}`,
|
|
662
|
+
"-derivedDataPath",
|
|
663
|
+
derivedDataPath,
|
|
664
|
+
...extraArgs,
|
|
665
|
+
"build",
|
|
666
|
+
...buildSettings
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
function productsDir(derivedDataPath, { configuration = "Debug", sdk = "iphonesimulator" } = {}) {
|
|
670
|
+
return join(derivedDataPath, "Build", "Products", `${configuration}-${sdk}`);
|
|
671
|
+
}
|
|
672
|
+
function pickAppBundle(entries, preferredName = null) {
|
|
673
|
+
const apps = (Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string" && e.endsWith(".app")).toSorted();
|
|
674
|
+
if (apps.length === 0) return null;
|
|
675
|
+
if (preferredName) {
|
|
676
|
+
const wanted = `${preferredName}.app`;
|
|
677
|
+
const exact = apps.find((a) => a === wanted);
|
|
678
|
+
if (exact) return exact;
|
|
679
|
+
}
|
|
680
|
+
return apps[0];
|
|
681
|
+
}
|
|
682
|
+
function findAppBundle(dir, preferredName = null) {
|
|
683
|
+
let entries;
|
|
684
|
+
try {
|
|
685
|
+
entries = readdirSync(dir);
|
|
686
|
+
} catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const app = pickAppBundle(entries, preferredName);
|
|
690
|
+
return app ? join(dir, app) : null;
|
|
691
|
+
}
|
|
692
|
+
function parseBundleId(plistJson) {
|
|
693
|
+
if (typeof plistJson !== "string") return null;
|
|
694
|
+
let data;
|
|
695
|
+
try {
|
|
696
|
+
data = JSON.parse(plistJson);
|
|
697
|
+
} catch {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (!data || typeof data !== "object") return null;
|
|
701
|
+
const id = data.CFBundleIdentifier;
|
|
702
|
+
return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
|
|
703
|
+
}
|
|
704
|
+
async function readBundleId(appPath, { exec = null } = {}) {
|
|
705
|
+
const executor = exec || getExecutor();
|
|
706
|
+
try {
|
|
707
|
+
const id = parseBundleId(await probe(executor, "plutil", [
|
|
708
|
+
"-convert",
|
|
709
|
+
"json",
|
|
710
|
+
"-o",
|
|
711
|
+
"-",
|
|
712
|
+
join(appPath, "Info.plist")
|
|
713
|
+
], { timeoutMs: 1e4 }));
|
|
714
|
+
if (id) return id;
|
|
715
|
+
} catch {}
|
|
716
|
+
try {
|
|
717
|
+
const value = await probe(executor, "defaults", [
|
|
718
|
+
"read",
|
|
719
|
+
join(appPath, "Info"),
|
|
720
|
+
"CFBundleIdentifier"
|
|
721
|
+
], { timeoutMs: 1e4 });
|
|
722
|
+
const trimmed = String(value).trim();
|
|
723
|
+
return trimmed === "" ? null : trimmed;
|
|
724
|
+
} catch {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function parseBundleExecutable(plistJson) {
|
|
729
|
+
if (typeof plistJson !== "string") return null;
|
|
730
|
+
let data;
|
|
731
|
+
try {
|
|
732
|
+
data = JSON.parse(plistJson);
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
if (!data || typeof data !== "object") return null;
|
|
737
|
+
const name = data.CFBundleExecutable;
|
|
738
|
+
return typeof name === "string" && name.trim() !== "" ? name.trim() : null;
|
|
739
|
+
}
|
|
740
|
+
async function readBundleExecutable(appPath, { exec = null } = {}) {
|
|
741
|
+
const executor = exec || getExecutor();
|
|
742
|
+
try {
|
|
743
|
+
const name = parseBundleExecutable(await probe(executor, "plutil", [
|
|
744
|
+
"-convert",
|
|
745
|
+
"json",
|
|
746
|
+
"-o",
|
|
747
|
+
"-",
|
|
748
|
+
join(appPath, "Info.plist")
|
|
749
|
+
], { timeoutMs: 1e4 }));
|
|
750
|
+
if (name) return name;
|
|
751
|
+
} catch {}
|
|
752
|
+
try {
|
|
753
|
+
const value = await probe(executor, "defaults", [
|
|
754
|
+
"read",
|
|
755
|
+
join(appPath, "Info"),
|
|
756
|
+
"CFBundleExecutable"
|
|
757
|
+
], { timeoutMs: 1e4 });
|
|
758
|
+
const trimmed = String(value).trim();
|
|
759
|
+
return trimmed === "" ? null : trimmed;
|
|
760
|
+
} catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const TAIL_LINES = 5;
|
|
765
|
+
function tailLines(lines, count = TAIL_LINES) {
|
|
766
|
+
return (Array.isArray(lines) ? lines : []).filter((l) => typeof l === "string" && l.trim() !== "").slice(-count);
|
|
767
|
+
}
|
|
768
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
769
|
+
const HEARTBEAT_ACTIVITY = {
|
|
770
|
+
build: "compiling",
|
|
771
|
+
deps: "installing",
|
|
772
|
+
pods: "installing"
|
|
773
|
+
};
|
|
774
|
+
function heartbeatLine(elapsedMs, label = "build", estimateMs = null) {
|
|
775
|
+
const activity = HEARTBEAT_ACTIVITY[label] ?? "running";
|
|
776
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
777
|
+
const inside = estimateMs && estimateMs > 0 ? elapsedMs > estimateMs ? `${elapsed}, usually ~${formatElapsed(estimateMs)}` : `${elapsed} of ~${formatElapsed(estimateMs)}` : elapsed;
|
|
778
|
+
return phaseLine(label, `still ${activity} (${inside})`);
|
|
779
|
+
}
|
|
780
|
+
const defaultSchedule = (run, delayMs) => {
|
|
781
|
+
const timer = setTimeout(run, delayMs);
|
|
782
|
+
timer.unref?.();
|
|
783
|
+
return () => clearTimeout(timer);
|
|
784
|
+
};
|
|
785
|
+
function startBuildHeartbeat({ intervalMs, elapsed, emit, label = "build", estimateMs = null, schedule = defaultSchedule }) {
|
|
786
|
+
if (!(intervalMs > 0)) return () => {};
|
|
787
|
+
let stopped = false;
|
|
788
|
+
let lastBeat = 0;
|
|
789
|
+
let cancel = null;
|
|
790
|
+
function tick() {
|
|
791
|
+
if (stopped) return;
|
|
792
|
+
const ms = elapsed();
|
|
793
|
+
const beat = Math.floor(ms / intervalMs) * intervalMs;
|
|
794
|
+
if (beat > lastBeat) {
|
|
795
|
+
lastBeat = beat;
|
|
796
|
+
emit(heartbeatLine(beat, label, estimateMs));
|
|
797
|
+
}
|
|
798
|
+
cancel = schedule(tick, intervalMs - ms % intervalMs);
|
|
799
|
+
}
|
|
800
|
+
cancel = schedule(tick, intervalMs);
|
|
801
|
+
return () => {
|
|
802
|
+
stopped = true;
|
|
803
|
+
cancel?.();
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function failedResult({ code, diagnostics, durationMs, exitCode = null, transcriptLines = 0, tail = [], compilationCache = COMPILATION_CACHE_UNAVAILABLE, compilationCacheCorruption = null }) {
|
|
807
|
+
const capped = capDiagnostics(diagnostics);
|
|
808
|
+
return {
|
|
809
|
+
ok: false,
|
|
810
|
+
code,
|
|
811
|
+
diagnostics: capped.diagnostics,
|
|
812
|
+
truncated: capped.truncated,
|
|
813
|
+
durationMs,
|
|
814
|
+
exitCode,
|
|
815
|
+
transcriptLines,
|
|
816
|
+
tail,
|
|
817
|
+
compilationCache,
|
|
818
|
+
...compilationCacheCorruption ? { compilationCacheCorruption } : {}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const COMPILATION_CACHE_UNAVAILABLE = {
|
|
822
|
+
status: "unavailable",
|
|
823
|
+
hits: null,
|
|
824
|
+
cacheableTasks: null,
|
|
825
|
+
hitRatePercent: null
|
|
826
|
+
};
|
|
827
|
+
const COMPILATION_CACHE_NOT_RUN = {
|
|
828
|
+
status: "not-run",
|
|
829
|
+
hits: null,
|
|
830
|
+
cacheableTasks: null,
|
|
831
|
+
hitRatePercent: null
|
|
832
|
+
};
|
|
833
|
+
function parseCompilationCacheActivity(line) {
|
|
834
|
+
if (typeof line !== "string") return null;
|
|
835
|
+
const match = line.match(/(?:note:\s*)?(\d+) hits \/ (\d+) cacheable tasks \((\d+(?:\.\d+)?)%\)/);
|
|
836
|
+
if (!match) return null;
|
|
837
|
+
return {
|
|
838
|
+
status: "reported",
|
|
839
|
+
hits: Number(match[1]),
|
|
840
|
+
cacheableTasks: Number(match[2]),
|
|
841
|
+
hitRatePercent: Number(match[3])
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const SWIFT_CACHING_NEEDS_EXPLICIT_MODULES = /warning: swift compiler caching requires explicit module build/;
|
|
845
|
+
function compilationCacheActivityLine(activity) {
|
|
846
|
+
if (activity.status === "reported") {
|
|
847
|
+
const swift = activity.swiftTargetsWithoutExplicitModules ? `; Swift excluded: ${activity.swiftTargetsWithoutExplicitModules} targets build without explicit modules (SWIFT_ENABLE_EXPLICIT_MODULES=NO)` : "";
|
|
848
|
+
return `${activity.hits}/${activity.cacheableTasks} hits (${activity.hitRatePercent}%)${swift}`;
|
|
849
|
+
}
|
|
850
|
+
if (activity.status === "not-run") return "not run; artifact cache supplied the app";
|
|
851
|
+
return "unavailable; Xcode did not report reliable statistics";
|
|
852
|
+
}
|
|
853
|
+
async function buildXcode({ context = ambientContext(), root, udid = null, logWriter, project, scheme = null, configuration = "Debug", applicationTarget, sdk = "iphonesimulator", destination = null, arch = null, derivedDataPath = null, extraArgs = [], compilationCache, startedAt: clockStart, now = () => Date.now(), exec = null, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line) }) {
|
|
854
|
+
if (!root || typeof root !== "string") throw new TypeError("buildXcode requires {root}");
|
|
855
|
+
if (!logWriter || typeof logWriter.write !== "function") throw new TypeError("buildXcode requires {logWriter} with a write() method");
|
|
856
|
+
if (!udid && !destination) throw new TypeError("buildXcode requires {udid} (or an explicit {destination})");
|
|
857
|
+
const executor = exec || getExecutor();
|
|
858
|
+
const dd = derivedDataPath || (scheme ? join(workspaceDerivedData(root), `scheme-${createHash("sha256").update(scheme).digest("hex")}`) : workspaceDerivedData(root));
|
|
859
|
+
const startedAt = clockStart ?? now();
|
|
860
|
+
const elapsed = () => now() - startedAt;
|
|
861
|
+
const reportError = (message, remedy) => {
|
|
862
|
+
logWriter.write({
|
|
863
|
+
src: "build",
|
|
864
|
+
level: "error",
|
|
865
|
+
msg: message,
|
|
866
|
+
event: "build_diagnostic",
|
|
867
|
+
...remedy ? { remedy } : {}
|
|
868
|
+
});
|
|
869
|
+
};
|
|
870
|
+
if (project.error) {
|
|
871
|
+
reportError(project.error.message, project.error.remedy);
|
|
872
|
+
return failedResult({
|
|
873
|
+
code: project.error.code,
|
|
874
|
+
diagnostics: [{
|
|
875
|
+
message: project.error.message,
|
|
876
|
+
remedy: project.error.remedy ?? void 0
|
|
877
|
+
}],
|
|
878
|
+
durationMs: elapsed()
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const resolvedTarget = project;
|
|
882
|
+
const selected = await resolveScheme(resolvedTarget, {
|
|
883
|
+
exec: executor,
|
|
884
|
+
scheme
|
|
885
|
+
});
|
|
886
|
+
if (selected.error) {
|
|
887
|
+
reportError(selected.error.message, selected.error.remedy);
|
|
888
|
+
return failedResult({
|
|
889
|
+
code: selected.error.code,
|
|
890
|
+
diagnostics: [{
|
|
891
|
+
message: selected.error.message,
|
|
892
|
+
remedy: selected.error.remedy
|
|
893
|
+
}],
|
|
894
|
+
durationMs: elapsed()
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const buildScheme = selected.scheme;
|
|
898
|
+
const cacheSettings = typeof compilationCache === "function" ? await compilationCache({
|
|
899
|
+
derivedDataPath: dd,
|
|
900
|
+
exec: executor
|
|
901
|
+
}) : compilationCache || [];
|
|
902
|
+
const debugSettings = configuration === "Debug" && sdk === "iphonesimulator" ? ["DEBUG_INFORMATION_FORMAT=dwarf"] : [];
|
|
903
|
+
const buildSettings = [
|
|
904
|
+
...cacheSettings,
|
|
905
|
+
...debugSettings,
|
|
906
|
+
...arch ? [`ARCHS=${arch}`, "ONLY_ACTIVE_ARCH=YES"] : []
|
|
907
|
+
];
|
|
908
|
+
const casPathSetting = "COMPILATION_CACHE_CAS_PATH=";
|
|
909
|
+
const casDir = cacheSettings.includes("COMPILATION_CACHE_ENABLE_CACHING=YES") ? cacheSettings.find((setting) => setting.startsWith(casPathSetting))?.slice(27) ?? null : null;
|
|
910
|
+
const args = xcodebuildArgs({
|
|
911
|
+
project: resolvedTarget,
|
|
912
|
+
scheme: buildScheme,
|
|
913
|
+
udid,
|
|
914
|
+
destination,
|
|
915
|
+
configuration,
|
|
916
|
+
sdk,
|
|
917
|
+
derivedDataPath: dd,
|
|
918
|
+
extraArgs,
|
|
919
|
+
buildSettings
|
|
920
|
+
});
|
|
921
|
+
logWriter.write({
|
|
922
|
+
src: "build",
|
|
923
|
+
level: "info",
|
|
924
|
+
msg: `xcodebuild ${args.join(" ")}`,
|
|
925
|
+
event: "build_start"
|
|
926
|
+
});
|
|
927
|
+
const diagnosticCollector = createXcodeDiagnosticCollector(root, sdk, resolvedTarget.dir || root);
|
|
928
|
+
const tail = [];
|
|
929
|
+
let transcriptLines = 0;
|
|
930
|
+
let compilationCacheActivity = COMPILATION_CACHE_UNAVAILABLE;
|
|
931
|
+
let swiftTargetsWithoutExplicitModules = 0;
|
|
932
|
+
let compilationCacheCorruption = null;
|
|
933
|
+
const onLine = (line) => {
|
|
934
|
+
const msg = cleanLine(line);
|
|
935
|
+
transcriptLines += 1;
|
|
936
|
+
diagnosticCollector.push(msg);
|
|
937
|
+
if (msg.trim() === "") return;
|
|
938
|
+
if (casDir && !compilationCacheCorruption && COMPILATION_CACHE_CORRUPTION.test(msg)) compilationCacheCorruption = {
|
|
939
|
+
dir: casDir,
|
|
940
|
+
line: msg.trim()
|
|
941
|
+
};
|
|
942
|
+
tail.push(msg);
|
|
943
|
+
if (tail.length > TAIL_LINES) tail.shift();
|
|
944
|
+
logWriter.write({
|
|
945
|
+
src: "build",
|
|
946
|
+
level: "debug",
|
|
947
|
+
msg
|
|
948
|
+
});
|
|
949
|
+
if (SWIFT_CACHING_NEEDS_EXPLICIT_MODULES.test(msg)) swiftTargetsWithoutExplicitModules += 1;
|
|
950
|
+
const parsed = parseCompilationCacheActivity(msg);
|
|
951
|
+
if (parsed) {
|
|
952
|
+
const activity = swiftTargetsWithoutExplicitModules ? {
|
|
953
|
+
...parsed,
|
|
954
|
+
swiftTargetsWithoutExplicitModules
|
|
955
|
+
} : parsed;
|
|
956
|
+
compilationCacheActivity = activity;
|
|
957
|
+
logWriter.write({
|
|
958
|
+
src: "build",
|
|
959
|
+
level: "info",
|
|
960
|
+
msg: compilationCacheActivityLine(activity),
|
|
961
|
+
event: "compilation_cache",
|
|
962
|
+
hits: activity.hits,
|
|
963
|
+
cacheableTasks: activity.cacheableTasks,
|
|
964
|
+
hitRatePercent: activity.hitRatePercent,
|
|
965
|
+
...swiftTargetsWithoutExplicitModules ? { swiftTargetsWithoutExplicitModules } : {}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
let cacheClaim = null;
|
|
970
|
+
if (casDir) {
|
|
971
|
+
const shared = await shareCompilationCache({
|
|
972
|
+
dir: casDir,
|
|
973
|
+
projectRoot: root,
|
|
974
|
+
note: onHeartbeat,
|
|
975
|
+
signal: AbortSignal.any([context?.signal, runCancellationSignal(context)].filter((each) => each !== void 0))
|
|
976
|
+
});
|
|
977
|
+
if ("failure" in shared) {
|
|
978
|
+
reportError(shared.failure.message, shared.failure.remedy);
|
|
979
|
+
return failedResult({
|
|
980
|
+
code: shared.failure.code,
|
|
981
|
+
diagnostics: [{
|
|
982
|
+
message: shared.failure.message,
|
|
983
|
+
remedy: shared.failure.remedy
|
|
984
|
+
}],
|
|
985
|
+
durationMs: elapsed()
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
cacheClaim = shared.claim;
|
|
989
|
+
declareSpawnsOn(context, cacheClaim);
|
|
990
|
+
}
|
|
991
|
+
const releaseCache = () => {
|
|
992
|
+
stopDeclaringSpawnsOn(context, cacheClaim);
|
|
993
|
+
(0, ownership_claim_exports.releaseClaim)(cacheClaim);
|
|
994
|
+
};
|
|
995
|
+
let child;
|
|
996
|
+
try {
|
|
997
|
+
child = spawnDeclared(context, () => executor.spawn("xcodebuild", args, {
|
|
998
|
+
cwd: resolvedTarget.dir || root,
|
|
999
|
+
stdio: [
|
|
1000
|
+
"ignore",
|
|
1001
|
+
"pipe",
|
|
1002
|
+
"pipe"
|
|
1003
|
+
],
|
|
1004
|
+
detached: false,
|
|
1005
|
+
env: {
|
|
1006
|
+
...context?.env ?? process.env,
|
|
1007
|
+
NSUnbufferedIO: "YES",
|
|
1008
|
+
FORCE_COLOR: "0"
|
|
1009
|
+
}
|
|
1010
|
+
}));
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
releaseCache();
|
|
1013
|
+
const message = `Could not run xcodebuild: ${err.message}`;
|
|
1014
|
+
const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
|
|
1015
|
+
reportError(message, remedy);
|
|
1016
|
+
return failedResult({
|
|
1017
|
+
code: "STIM_BUILD_FAILED",
|
|
1018
|
+
diagnostics: [{
|
|
1019
|
+
message,
|
|
1020
|
+
remedy
|
|
1021
|
+
}],
|
|
1022
|
+
durationMs: elapsed()
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const outReader = createLineReader(onLine);
|
|
1026
|
+
const errReader = createLineReader(onLine);
|
|
1027
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1028
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1029
|
+
child.stdout?.on("data", (chunk) => outReader.push(chunk));
|
|
1030
|
+
child.stderr?.on("data", (chunk) => errReader.push(chunk));
|
|
1031
|
+
const stopHeartbeat = startBuildHeartbeat({
|
|
1032
|
+
intervalMs: heartbeatMs,
|
|
1033
|
+
elapsed,
|
|
1034
|
+
emit: onHeartbeat,
|
|
1035
|
+
estimateMs
|
|
1036
|
+
});
|
|
1037
|
+
const outcome = await new Promise((settlePromise) => {
|
|
1038
|
+
let settled = false;
|
|
1039
|
+
const finish = (value) => {
|
|
1040
|
+
if (settled) return;
|
|
1041
|
+
settled = true;
|
|
1042
|
+
outReader.flush();
|
|
1043
|
+
errReader.flush();
|
|
1044
|
+
settlePromise(value);
|
|
1045
|
+
};
|
|
1046
|
+
child.on("close", (code, signal) => finish({
|
|
1047
|
+
code,
|
|
1048
|
+
signal
|
|
1049
|
+
}));
|
|
1050
|
+
child.on("error", (error) => finish({
|
|
1051
|
+
code: null,
|
|
1052
|
+
signal: null,
|
|
1053
|
+
error
|
|
1054
|
+
}));
|
|
1055
|
+
});
|
|
1056
|
+
releaseCache();
|
|
1057
|
+
stopHeartbeat();
|
|
1058
|
+
const durationMs = elapsed();
|
|
1059
|
+
if (outcome.error) {
|
|
1060
|
+
const message = `Could not run xcodebuild: ${outcome.error.message}`;
|
|
1061
|
+
const remedy = "Install Xcode and select it with `sudo xcode-select -s /Applications/Xcode.app`.";
|
|
1062
|
+
reportError(message, remedy);
|
|
1063
|
+
return failedResult({
|
|
1064
|
+
code: "STIM_BUILD_FAILED",
|
|
1065
|
+
diagnostics: [{
|
|
1066
|
+
message,
|
|
1067
|
+
remedy
|
|
1068
|
+
}],
|
|
1069
|
+
durationMs,
|
|
1070
|
+
transcriptLines,
|
|
1071
|
+
tail,
|
|
1072
|
+
compilationCache: compilationCacheActivity
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (outcome.code !== 0) {
|
|
1076
|
+
const diagnostics = diagnosticCollector.diagnostics;
|
|
1077
|
+
const capped = capDiagnostics(diagnostics);
|
|
1078
|
+
for (const d of capped.diagnostics) reportError(describeDiagnostic(d), d.remedy);
|
|
1079
|
+
if (capped.diagnostics.length === 0) reportError(`xcodebuild exited ${outcome.code ?? `on ${outcome.signal}`} with no recognizable diagnostic; see the transcript above.`, null);
|
|
1080
|
+
return failedResult({
|
|
1081
|
+
code: "STIM_BUILD_FAILED",
|
|
1082
|
+
diagnostics,
|
|
1083
|
+
durationMs,
|
|
1084
|
+
exitCode: outcome.code,
|
|
1085
|
+
transcriptLines,
|
|
1086
|
+
tail,
|
|
1087
|
+
compilationCache: compilationCacheActivity,
|
|
1088
|
+
compilationCacheCorruption
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
const products = productsDir(dd, {
|
|
1092
|
+
configuration,
|
|
1093
|
+
sdk
|
|
1094
|
+
});
|
|
1095
|
+
let appPath = null;
|
|
1096
|
+
if (scheme) try {
|
|
1097
|
+
const targets = JSON.parse(await probe(executor, "xcodebuild", [
|
|
1098
|
+
resolvedTarget.flag,
|
|
1099
|
+
resolvedTarget.path,
|
|
1100
|
+
"-scheme",
|
|
1101
|
+
buildScheme,
|
|
1102
|
+
"-configuration",
|
|
1103
|
+
configuration,
|
|
1104
|
+
"-sdk",
|
|
1105
|
+
sdk,
|
|
1106
|
+
"-destination",
|
|
1107
|
+
destination || `id=${udid}`,
|
|
1108
|
+
"-derivedDataPath",
|
|
1109
|
+
dd,
|
|
1110
|
+
...extraArgs,
|
|
1111
|
+
...buildSettings,
|
|
1112
|
+
"-showBuildSettings",
|
|
1113
|
+
"-json"
|
|
1114
|
+
], { timeoutMs: 3e4 }));
|
|
1115
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1116
|
+
for (const item of Array.isArray(targets) ? targets : []) {
|
|
1117
|
+
const settings = item?.buildSettings;
|
|
1118
|
+
if (settings?.PRODUCT_TYPE !== "com.apple.product-type.application" || settings.PLATFORM_NAME !== sdk) continue;
|
|
1119
|
+
if (applicationTarget && (settings.TARGET_NAME !== applicationTarget.name || typeof settings.PROJECT_FILE_PATH !== "string" || realpathSync(settings.PROJECT_FILE_PATH) !== realpathSync(applicationTarget.projectPath))) continue;
|
|
1120
|
+
const dir = settings.TARGET_BUILD_DIR;
|
|
1121
|
+
const name = settings.FULL_PRODUCT_NAME;
|
|
1122
|
+
if (typeof dir !== "string" || !isAbsolute(dir) || typeof name !== "string" || basename(name) !== name || !name.endsWith(".app")) continue;
|
|
1123
|
+
const path = join(dir, name);
|
|
1124
|
+
if (statSync(path).isDirectory()) paths.add(path);
|
|
1125
|
+
}
|
|
1126
|
+
if (paths.size === 1) appPath = [...paths][0];
|
|
1127
|
+
} catch {}
|
|
1128
|
+
else appPath = findAppBundle(products, buildScheme);
|
|
1129
|
+
if (!appPath) {
|
|
1130
|
+
const message = scheme ? `xcodebuild succeeded, but the built app for scheme ${JSON.stringify(buildScheme)} could not be identified unambiguously from its build settings.` : `xcodebuild reported success but no .app is in ${products}.`;
|
|
1131
|
+
const remedy = "Check that the scheme builds one application target for this platform, not a library, test bundle, or several apps.";
|
|
1132
|
+
reportError(message, remedy);
|
|
1133
|
+
return failedResult({
|
|
1134
|
+
code: "STIM_BUILD_FAILED",
|
|
1135
|
+
diagnostics: [{
|
|
1136
|
+
message,
|
|
1137
|
+
remedy
|
|
1138
|
+
}],
|
|
1139
|
+
durationMs,
|
|
1140
|
+
exitCode: 0,
|
|
1141
|
+
transcriptLines,
|
|
1142
|
+
tail,
|
|
1143
|
+
compilationCache: compilationCacheActivity
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const bundleId = await readBundleId(appPath, { exec: executor });
|
|
1147
|
+
if (!bundleId) {
|
|
1148
|
+
const message = `No readable CFBundleIdentifier in ${join(appPath, "Info.plist")}.`;
|
|
1149
|
+
const remedy = "Check PRODUCT_BUNDLE_IDENTIFIER in the target's Debug configuration.";
|
|
1150
|
+
reportError(message, remedy);
|
|
1151
|
+
return failedResult({
|
|
1152
|
+
code: "STIM_BUILD_FAILED",
|
|
1153
|
+
diagnostics: [{
|
|
1154
|
+
message,
|
|
1155
|
+
remedy
|
|
1156
|
+
}],
|
|
1157
|
+
durationMs,
|
|
1158
|
+
exitCode: 0,
|
|
1159
|
+
transcriptLines,
|
|
1160
|
+
tail,
|
|
1161
|
+
compilationCache: compilationCacheActivity
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
logWriter.write({
|
|
1165
|
+
src: "build",
|
|
1166
|
+
level: "info",
|
|
1167
|
+
msg: `BUILD SUCCEEDED ${appPath} (${bundleId}) in ${durationMs}ms`,
|
|
1168
|
+
event: "build_done"
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
ok: true,
|
|
1172
|
+
appPath,
|
|
1173
|
+
bundleId,
|
|
1174
|
+
durationMs,
|
|
1175
|
+
scheme: buildScheme,
|
|
1176
|
+
project: resolvedTarget,
|
|
1177
|
+
derivedDataPath: dd,
|
|
1178
|
+
productsDir: products,
|
|
1179
|
+
transcriptLines,
|
|
1180
|
+
compilationCache: compilationCacheActivity
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/engine/deps.ts
|
|
1185
|
+
const DEPS_ERROR = "STIM_DEPS_FAILED";
|
|
1186
|
+
const BUILD_MARKER = ".last_build_configuration";
|
|
1187
|
+
const TRANSCRIPT_LINES = 2e3;
|
|
1188
|
+
const MAX_POD_DIAGNOSTIC_LINES = 15;
|
|
1189
|
+
const POD_MARKER = /^\[!\]/;
|
|
1190
|
+
const POD_CONTINUATION = /^\s+\S/;
|
|
1191
|
+
const RUBY_HEAD = /^\S.*\.rb:\d+:in\s+(?:`[^']*'|'[^']*'):\s*\S/;
|
|
1192
|
+
const RUBY_FRAME = /^\s*from\s+\S/;
|
|
1193
|
+
const RUBY_CONTEXT_BEFORE = 4;
|
|
1194
|
+
const BUNDLER_FROZEN = /frozen mode|lockfile can't be updated|in deployment mode after changing/i;
|
|
1195
|
+
const BUNDLER_NO_COCOAPODS = /cocoapods is not currently included in the bundle|command not found: pod\b/i;
|
|
1196
|
+
function extractPodDiagnostics(transcript) {
|
|
1197
|
+
if (typeof transcript !== "string" || transcript === "") return null;
|
|
1198
|
+
const lines = transcript.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
1199
|
+
return extractPodBangBlocks(lines) || extractRubyHead(lines);
|
|
1200
|
+
}
|
|
1201
|
+
function extractPodBangBlocks(lines) {
|
|
1202
|
+
const blocks = [];
|
|
1203
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1204
|
+
const line = lines[i];
|
|
1205
|
+
if (line === void 0 || !POD_MARKER.test(line)) continue;
|
|
1206
|
+
const block = [line];
|
|
1207
|
+
let j = i + 1;
|
|
1208
|
+
for (; j < lines.length; j += 1) {
|
|
1209
|
+
const next = lines[j];
|
|
1210
|
+
if (next === void 0 || !POD_CONTINUATION.test(next)) break;
|
|
1211
|
+
block.push(next);
|
|
1212
|
+
}
|
|
1213
|
+
blocks.push(block);
|
|
1214
|
+
i = j - 1;
|
|
1215
|
+
}
|
|
1216
|
+
if (blocks.length === 0) return null;
|
|
1217
|
+
const out = [];
|
|
1218
|
+
let dropped = 0;
|
|
1219
|
+
for (const block of blocks) {
|
|
1220
|
+
const room = MAX_POD_DIAGNOSTIC_LINES - out.length;
|
|
1221
|
+
if (room <= 0) {
|
|
1222
|
+
dropped += block.length;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
out.push(...block.slice(0, room));
|
|
1226
|
+
dropped += Math.max(0, block.length - room);
|
|
1227
|
+
}
|
|
1228
|
+
if (dropped > 0) out.push(`(+${dropped} more [!] lines in the build log)`);
|
|
1229
|
+
return {
|
|
1230
|
+
source: "cocoapods",
|
|
1231
|
+
lines: out
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function extractRubyHead(lines) {
|
|
1235
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1236
|
+
const line = lines[i];
|
|
1237
|
+
if (line === void 0 || !RUBY_HEAD.test(line) || RUBY_FRAME.test(line)) continue;
|
|
1238
|
+
let start = i;
|
|
1239
|
+
while (start > 0 && i - start < RUBY_CONTEXT_BEFORE) {
|
|
1240
|
+
const prev = lines[start - 1];
|
|
1241
|
+
if (prev === void 0 || prev.trim() === "" || RUBY_FRAME.test(prev) || POD_MARKER.test(prev)) break;
|
|
1242
|
+
start -= 1;
|
|
1243
|
+
}
|
|
1244
|
+
let end = i + 1;
|
|
1245
|
+
while (end < lines.length) {
|
|
1246
|
+
const next = lines[end];
|
|
1247
|
+
if (next === void 0 || next.trim() === "" || RUBY_FRAME.test(next)) break;
|
|
1248
|
+
end += 1;
|
|
1249
|
+
}
|
|
1250
|
+
return {
|
|
1251
|
+
source: "ruby",
|
|
1252
|
+
lines: lines.slice(start, end).filter((entry) => entry.trim() !== "").slice(0, MAX_POD_DIAGNOSTIC_LINES)
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
function podsAreStale(lockText, manifestText, dir = "ios/") {
|
|
1258
|
+
const lock = normalize(lockText);
|
|
1259
|
+
const manifest = normalize(manifestText);
|
|
1260
|
+
if (lock === null && manifest === null) return {
|
|
1261
|
+
noPods: true,
|
|
1262
|
+
stale: false
|
|
1263
|
+
};
|
|
1264
|
+
if (lock === null) return {
|
|
1265
|
+
stale: true,
|
|
1266
|
+
reason: `${dir}Podfile.lock is missing but ${dir}Pods exists`
|
|
1267
|
+
};
|
|
1268
|
+
if (manifest === null) return {
|
|
1269
|
+
stale: true,
|
|
1270
|
+
reason: `${dir}Pods/Manifest.lock is missing (pods have never been installed here)`
|
|
1271
|
+
};
|
|
1272
|
+
if (lock !== manifest) return {
|
|
1273
|
+
stale: true,
|
|
1274
|
+
reason: `${dir}Podfile.lock and ${dir}Pods/Manifest.lock differ`
|
|
1275
|
+
};
|
|
1276
|
+
return { stale: false };
|
|
1277
|
+
}
|
|
1278
|
+
function normalize(text) {
|
|
1279
|
+
if (typeof text !== "string") return null;
|
|
1280
|
+
return text.replace(/\r\n/g, "\n").trimEnd();
|
|
1281
|
+
}
|
|
1282
|
+
function readPodState(root, directory = join(root, "ios")) {
|
|
1283
|
+
const rel = relative(root, directory);
|
|
1284
|
+
return {
|
|
1285
|
+
hasPodfile: existsSync(join(directory, "Podfile")),
|
|
1286
|
+
lockText: readOrNull(join(directory, "Podfile.lock")),
|
|
1287
|
+
manifestText: readOrNull(join(directory, "Pods", "Manifest.lock")),
|
|
1288
|
+
dir: rel ? `${rel}/` : ""
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function readOrNull(file) {
|
|
1292
|
+
try {
|
|
1293
|
+
return readFileSync(file, "utf-8");
|
|
1294
|
+
} catch {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const LOGIN_ENV_BEGIN = "@@ruby-env-begin@@";
|
|
1299
|
+
const LOGIN_ENV_END = "@@ruby-env-end@@";
|
|
1300
|
+
const LOGIN_ENV_SCRIPT = `printf '\\n${LOGIN_ENV_BEGIN}\\nPATH=%s\\nGEM_HOME=%s\\nGEM_PATH=%s\\n${LOGIN_ENV_END}\\n' "\$PATH" "\$GEM_HOME" "\$GEM_PATH" > "\$1"`;
|
|
1301
|
+
function parseLoginRubyEnv(output) {
|
|
1302
|
+
const lines = output.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
1303
|
+
const begin = lines.lastIndexOf(LOGIN_ENV_BEGIN);
|
|
1304
|
+
const end = lines.indexOf(LOGIN_ENV_END, begin + 1);
|
|
1305
|
+
if (begin < 0 || end < 0) return null;
|
|
1306
|
+
const out = {};
|
|
1307
|
+
for (const line of lines.slice(begin + 1, end)) {
|
|
1308
|
+
const eq = line.indexOf("=");
|
|
1309
|
+
if (eq < 0) continue;
|
|
1310
|
+
const key = line.slice(0, eq);
|
|
1311
|
+
const value = line.slice(eq + 1);
|
|
1312
|
+
if (value && (key === "PATH" || key === "GEM_HOME" || key === "GEM_PATH")) out[key] = value;
|
|
1313
|
+
}
|
|
1314
|
+
return out;
|
|
1315
|
+
}
|
|
1316
|
+
const loginRubyEnvByEnvironment = /* @__PURE__ */ new WeakMap();
|
|
1317
|
+
const LOGIN_ENV_OPTIONS = {
|
|
1318
|
+
timeoutMs: 1e4,
|
|
1319
|
+
killSignal: "SIGKILL",
|
|
1320
|
+
detachedSilent: true
|
|
1321
|
+
};
|
|
1322
|
+
async function readLoginRubyEnvAsync(shell = ambientEnv().SHELL, platform = process.platform) {
|
|
1323
|
+
const context = ambientContext();
|
|
1324
|
+
context?.signal.throwIfAborted();
|
|
1325
|
+
if (platform !== "darwin" || !shell || !isAbsolute(shell)) return null;
|
|
1326
|
+
const environment = ambientEnv();
|
|
1327
|
+
let cache = loginRubyEnvByEnvironment.get(environment);
|
|
1328
|
+
if (!cache) {
|
|
1329
|
+
cache = /* @__PURE__ */ new Map();
|
|
1330
|
+
loginRubyEnvByEnvironment.set(environment, cache);
|
|
1331
|
+
}
|
|
1332
|
+
if (cache.has(shell)) return cache.get(shell) ?? null;
|
|
1333
|
+
let dir = null;
|
|
1334
|
+
let parsed = null;
|
|
1335
|
+
try {
|
|
1336
|
+
dir = mkdtempSync(join(tmpdir(), "stim-ruby-env-"));
|
|
1337
|
+
const file = join(dir, "env");
|
|
1338
|
+
await getExecutor().runFileAsync(shell, [
|
|
1339
|
+
"-lic",
|
|
1340
|
+
LOGIN_ENV_SCRIPT,
|
|
1341
|
+
"stim",
|
|
1342
|
+
file
|
|
1343
|
+
], {
|
|
1344
|
+
...LOGIN_ENV_OPTIONS,
|
|
1345
|
+
declareFor: context,
|
|
1346
|
+
onSpawn: killOnAbort(context?.signal)
|
|
1347
|
+
});
|
|
1348
|
+
parsed = parseLoginRubyEnv(readFileSync(file, "utf-8"));
|
|
1349
|
+
} catch {
|
|
1350
|
+
parsed = null;
|
|
1351
|
+
} finally {
|
|
1352
|
+
if (dir) rmSync(dir, {
|
|
1353
|
+
recursive: true,
|
|
1354
|
+
force: true
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
context?.signal.throwIfAborted();
|
|
1358
|
+
cache.set(shell, parsed);
|
|
1359
|
+
return parsed;
|
|
1360
|
+
}
|
|
1361
|
+
function rubyPathEntries(login, callerPath, exists) {
|
|
1362
|
+
const have = new Set((callerPath ?? "").split(delimiter));
|
|
1363
|
+
const gemDirs = [login.GEM_HOME, ...(login.GEM_PATH ?? "").split(delimiter)].filter((dir) => !!dir);
|
|
1364
|
+
return (login.PATH ?? "").split(delimiter).filter((entry) => {
|
|
1365
|
+
if (!entry || have.has(entry)) return false;
|
|
1366
|
+
if (gemDirs.some((gem) => entry === gem || entry.startsWith(gem + sep))) return true;
|
|
1367
|
+
return exists(join(entry, "ruby")) || exists(join(entry, "pod"));
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
function prependPath(entries, existing) {
|
|
1371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1372
|
+
return [...entries, ...(existing ?? "").split(delimiter)].filter((entry) => entry && !seen.has(entry) && seen.add(entry)).join(delimiter);
|
|
1373
|
+
}
|
|
1374
|
+
function podEnvForRuby(version, { env = ambientEnv(), home = (process.platform === "win32" ? env.USERPROFILE : env.HOME) || homedir(), exists = existsSync, read = readOrNull, loginEnv = () => null } = {}) {
|
|
1375
|
+
const out = {
|
|
1376
|
+
...env,
|
|
1377
|
+
FORCE_COLOR: "0",
|
|
1378
|
+
CLICOLOR: "0",
|
|
1379
|
+
LANG: env.LANG ?? "en_US.UTF-8",
|
|
1380
|
+
LC_ALL: env.LC_ALL ?? env.LANG ?? "en_US.UTF-8"
|
|
1381
|
+
};
|
|
1382
|
+
const candidates = version ? [
|
|
1383
|
+
{ bin: join(home, ".rbenv", "versions", version, "bin") },
|
|
1384
|
+
{
|
|
1385
|
+
bin: join(home, ".rvm", "rubies", `ruby-${version}`, "bin"),
|
|
1386
|
+
rvm: `ruby-${version}`
|
|
1387
|
+
},
|
|
1388
|
+
{ bin: join(home, ".asdf", "installs", "ruby", version, "bin") },
|
|
1389
|
+
{ bin: join(home, ".local", "share", "mise", "installs", "ruby", version, "bin") }
|
|
1390
|
+
] : [];
|
|
1391
|
+
for (const c of candidates) {
|
|
1392
|
+
if (!exists(c.bin)) continue;
|
|
1393
|
+
if (c.rvm) {
|
|
1394
|
+
const rvm = rvmRuby(join(home, ".rvm"), c.rvm, exists);
|
|
1395
|
+
out.PATH = prependPath(rvm.bins, out.PATH);
|
|
1396
|
+
if (exists(rvm.gems.home)) {
|
|
1397
|
+
out.GEM_HOME = rvm.gems.home;
|
|
1398
|
+
out.GEM_PATH = rvm.gems.path;
|
|
1399
|
+
}
|
|
1400
|
+
return out;
|
|
1401
|
+
}
|
|
1402
|
+
out.PATH = `${c.bin}${delimiter}${out.PATH ?? ""}`;
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
if (env.GEM_HOME) return out;
|
|
1406
|
+
const fallback = defaultRuby(home, exists, read);
|
|
1407
|
+
if (fallback) {
|
|
1408
|
+
out.PATH = prependPath(fallback.bins, out.PATH);
|
|
1409
|
+
if (fallback.gems) {
|
|
1410
|
+
out.GEM_HOME = fallback.gems.home;
|
|
1411
|
+
out.GEM_PATH = fallback.gems.path;
|
|
1412
|
+
}
|
|
1413
|
+
return out;
|
|
1414
|
+
}
|
|
1415
|
+
const login = loginEnv();
|
|
1416
|
+
if (!login) return out;
|
|
1417
|
+
const rubyPath = rubyPathEntries(login, out.PATH, exists);
|
|
1418
|
+
if (rubyPath.length) out.PATH = prependPath(rubyPath, out.PATH);
|
|
1419
|
+
if (login.GEM_HOME) out.GEM_HOME = login.GEM_HOME;
|
|
1420
|
+
if (login.GEM_PATH) out.GEM_PATH = login.GEM_PATH;
|
|
1421
|
+
return out;
|
|
1422
|
+
}
|
|
1423
|
+
/** What `rvm use <name>` sets: the gemset and its `@global` gemset on `GEM_PATH` and their `bin` directories on `PATH`. */
|
|
1424
|
+
function rvmRuby(rvm, name, exists) {
|
|
1425
|
+
const gems = join(rvm, "gems", name);
|
|
1426
|
+
const global = `${gems}@global`;
|
|
1427
|
+
return {
|
|
1428
|
+
bins: [
|
|
1429
|
+
join(gems, "bin"),
|
|
1430
|
+
join(global, "bin"),
|
|
1431
|
+
join(rvm, "rubies", name, "bin")
|
|
1432
|
+
].filter(exists),
|
|
1433
|
+
gems: {
|
|
1434
|
+
home: gems,
|
|
1435
|
+
path: [gems, global].join(delimiter)
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* The Ruby a new login shell makes the default, read from the version manager's files: rvm's `default` alias in
|
|
1441
|
+
* `~/.rvm/config/alias`, or rbenv's global `~/.rbenv/version`.
|
|
1442
|
+
*/
|
|
1443
|
+
function defaultRuby(home, exists, read) {
|
|
1444
|
+
const rvm = join(home, ".rvm");
|
|
1445
|
+
const alias = /^default=(\S+)$/m.exec(read(join(rvm, "config", "alias")) ?? "")?.[1];
|
|
1446
|
+
if (alias && exists(join(rvm, "rubies", alias, "bin"))) return rvmRuby(rvm, alias, exists);
|
|
1447
|
+
const rbenv = read(join(home, ".rbenv", "version"))?.trim();
|
|
1448
|
+
if (rbenv && exists(join(home, ".rbenv", "versions", rbenv, "bin"))) return { bins: [join(home, ".rbenv", "versions", rbenv, "bin")] };
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
async function podEnvForRubyAsync(version) {
|
|
1452
|
+
let loginNeeded = false;
|
|
1453
|
+
const withoutLogin = podEnvForRuby(version, { loginEnv: () => {
|
|
1454
|
+
loginNeeded = true;
|
|
1455
|
+
return null;
|
|
1456
|
+
} });
|
|
1457
|
+
if (!loginNeeded) return withoutLogin;
|
|
1458
|
+
const login = await readLoginRubyEnvAsync();
|
|
1459
|
+
return podEnvForRuby(version, { loginEnv: () => login });
|
|
1460
|
+
}
|
|
1461
|
+
function readRubyVersion(root, { read = readFileSync } = {}) {
|
|
1462
|
+
try {
|
|
1463
|
+
const first = String(read(join(root, ".ruby-version"), "utf-8")).split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("#"));
|
|
1464
|
+
if (!first) return null;
|
|
1465
|
+
return first.replace(/^ruby-/, "");
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async function bundlerEnv(root, gemfile) {
|
|
1471
|
+
return {
|
|
1472
|
+
...await podEnvForRubyAsync(readRubyVersion(root)),
|
|
1473
|
+
BUNDLE_GEMFILE: gemfile,
|
|
1474
|
+
BUNDLE_FROZEN: "true"
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
const BUNDLE_PATH_SETTING = /^BUNDLE_PATH:[ \t]*["']?([^"'\r\n]+?)["']?[ \t]*$/m;
|
|
1478
|
+
function bundlePathInsideProject(root, env) {
|
|
1479
|
+
const fromEnv = env.BUNDLE_PATH;
|
|
1480
|
+
const configured = fromEnv || BUNDLE_PATH_SETTING.exec(readOrNull(join(root, ".bundle", "config")) ?? "")?.[1];
|
|
1481
|
+
if (!configured || configured.startsWith("~")) return null;
|
|
1482
|
+
const resolved = resolve(root, configured);
|
|
1483
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) return null;
|
|
1484
|
+
return {
|
|
1485
|
+
path: relative(root, resolved) || ".",
|
|
1486
|
+
where: fromEnv ? "where the BUNDLE_PATH environment variable points" : "where its own .bundle/config points BUNDLE_PATH"
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
async function runCaptured(ctx) {
|
|
1490
|
+
const startedAt = ctx.now();
|
|
1491
|
+
const transcript = [];
|
|
1492
|
+
const push = (line) => {
|
|
1493
|
+
const msg = stripAnsi(String(line)).trimEnd();
|
|
1494
|
+
if (!msg.trim()) return;
|
|
1495
|
+
transcript.push(msg);
|
|
1496
|
+
if (transcript.length > TRANSCRIPT_LINES) transcript.splice(TRANSCRIPT_LINES / 2, 1);
|
|
1497
|
+
ctx.logWriter?.write?.({
|
|
1498
|
+
src: "build",
|
|
1499
|
+
level: "debug",
|
|
1500
|
+
msg,
|
|
1501
|
+
raw: true,
|
|
1502
|
+
event: ctx.event
|
|
1503
|
+
});
|
|
1504
|
+
};
|
|
1505
|
+
let child;
|
|
1506
|
+
try {
|
|
1507
|
+
child = spawnDeclared(ambientContext(), () => ctx.spawn(ctx.cmd, ctx.args, {
|
|
1508
|
+
cwd: ctx.cwd,
|
|
1509
|
+
stdio: [
|
|
1510
|
+
"ignore",
|
|
1511
|
+
"pipe",
|
|
1512
|
+
"pipe"
|
|
1513
|
+
],
|
|
1514
|
+
env: ctx.env
|
|
1515
|
+
}));
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
return {
|
|
1518
|
+
transcript,
|
|
1519
|
+
code: null,
|
|
1520
|
+
signal: null,
|
|
1521
|
+
error,
|
|
1522
|
+
durationMs: ctx.now() - startedAt
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
const reader = {
|
|
1526
|
+
out: createLineReader(push),
|
|
1527
|
+
err: createLineReader(push)
|
|
1528
|
+
};
|
|
1529
|
+
child.stdout?.setEncoding?.("utf-8");
|
|
1530
|
+
child.stderr?.setEncoding?.("utf-8");
|
|
1531
|
+
child.stdout?.on("data", (chunk) => reader.out.push(chunk));
|
|
1532
|
+
child.stderr?.on("data", (chunk) => reader.err.push(chunk));
|
|
1533
|
+
const stopHeartbeat = ctx.label ? startBuildHeartbeat({
|
|
1534
|
+
intervalMs: ctx.heartbeatMs,
|
|
1535
|
+
elapsed: () => ctx.now() - startedAt,
|
|
1536
|
+
emit: ctx.onHeartbeat,
|
|
1537
|
+
label: ctx.label,
|
|
1538
|
+
estimateMs: ctx.estimateMs ?? null
|
|
1539
|
+
}) : () => {};
|
|
1540
|
+
let result;
|
|
1541
|
+
try {
|
|
1542
|
+
result = await waitForChild(child);
|
|
1543
|
+
} finally {
|
|
1544
|
+
stopHeartbeat();
|
|
1545
|
+
}
|
|
1546
|
+
reader.out.flush();
|
|
1547
|
+
reader.err.flush();
|
|
1548
|
+
return {
|
|
1549
|
+
transcript,
|
|
1550
|
+
code: result.code ?? null,
|
|
1551
|
+
signal: result.signal ?? null,
|
|
1552
|
+
error: result.error ?? null,
|
|
1553
|
+
durationMs: ctx.now() - startedAt
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
async function ensureBundledGems(root, pin, ctx) {
|
|
1557
|
+
const env = await bundlerEnv(root, pin.gemfile);
|
|
1558
|
+
const cwd = dirname(pin.gemfile);
|
|
1559
|
+
const check = await runCaptured({
|
|
1560
|
+
...ctx,
|
|
1561
|
+
cmd: "bundle",
|
|
1562
|
+
args: ["check", "--dry-run"],
|
|
1563
|
+
cwd,
|
|
1564
|
+
env,
|
|
1565
|
+
event: "bundle_check",
|
|
1566
|
+
label: "gems"
|
|
1567
|
+
});
|
|
1568
|
+
if (check.error) return bundlerSpawnFailure("bundle check", check, pin);
|
|
1569
|
+
if (check.code === 0) return { bundler: true };
|
|
1570
|
+
const install = await runCaptured({
|
|
1571
|
+
...ctx,
|
|
1572
|
+
cmd: "bundle",
|
|
1573
|
+
args: ["install"],
|
|
1574
|
+
cwd,
|
|
1575
|
+
env,
|
|
1576
|
+
event: "bundle_install",
|
|
1577
|
+
label: "gems"
|
|
1578
|
+
});
|
|
1579
|
+
if (install.error) return bundlerSpawnFailure("bundle install", install, pin);
|
|
1580
|
+
const inProject = bundlePathInsideProject(cwd, env);
|
|
1581
|
+
const note = inProject ? `\`bundle install\` put this project's gems in ${inProject.path}${sep}, ${inProject.where}; Gemfile.lock itself is never written.` : void 0;
|
|
1582
|
+
if (install.code === 0) return {
|
|
1583
|
+
bundler: true,
|
|
1584
|
+
note
|
|
1585
|
+
};
|
|
1586
|
+
const how = install.signal ? `signal ${install.signal}` : `exit code ${install.code}`;
|
|
1587
|
+
const transcript = install.transcript.join("\n");
|
|
1588
|
+
const extracted = extractPodDiagnostics(transcript);
|
|
1589
|
+
return {
|
|
1590
|
+
bundler: false,
|
|
1591
|
+
failure: {
|
|
1592
|
+
failed: true,
|
|
1593
|
+
code: DEPS_ERROR,
|
|
1594
|
+
reason: `\`bundle install\` failed (${how}).`,
|
|
1595
|
+
remedy: frozenRemedy(pin, transcript) || gemInstallRemedy(root, pin),
|
|
1596
|
+
diagnosticSource: extracted ? extracted.source : "tail",
|
|
1597
|
+
diagnosticLines: extracted ? extracted.lines : [],
|
|
1598
|
+
lastLines: install.transcript.slice(-20),
|
|
1599
|
+
durationMs: install.durationMs
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function bundlerSpawnFailure(command, run, pin) {
|
|
1604
|
+
if (isMissingBinary(run.error)) return {
|
|
1605
|
+
bundler: false,
|
|
1606
|
+
note: missingBundlerNote(pin)
|
|
1607
|
+
};
|
|
1608
|
+
return {
|
|
1609
|
+
bundler: false,
|
|
1610
|
+
failure: {
|
|
1611
|
+
failed: true,
|
|
1612
|
+
code: DEPS_ERROR,
|
|
1613
|
+
reason: `Could not run \`${command}\`: ${run.error?.message || run.error}`,
|
|
1614
|
+
remedy: `Bundler is how this project pins its CocoaPods (${pin.lockfile}). Check that \`bundle\` runs in ${dirname(pin.gemfile)}, then run again.`,
|
|
1615
|
+
lastLines: run.transcript.slice(-20),
|
|
1616
|
+
durationMs: run.durationMs
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function missingBundlerNote(pin) {
|
|
1621
|
+
return `${pin.lockfile} pins this project's CocoaPods, but \`bundle\` is not on PATH, so pods run as plain \`pod install\` with whatever CocoaPods PATH resolves to (\`gem install bundler\` to use the pin).`;
|
|
1622
|
+
}
|
|
1623
|
+
function frozenRemedy(pin, transcript) {
|
|
1624
|
+
if (!BUNDLER_FROZEN.test(transcript)) return void 0;
|
|
1625
|
+
return `Stim runs bundler with BUNDLE_FROZEN so it can never write into your checkout, and ${pin.gemfile} no longer matches ${pin.lockfile}. Run \`cd ${dirname(pin.gemfile)} && bundle install\` yourself, keep the updated Gemfile.lock, then run again.`;
|
|
1626
|
+
}
|
|
1627
|
+
function gemInstallRemedy(root, pin) {
|
|
1628
|
+
const version = readRubyVersion(root);
|
|
1629
|
+
return `${version ? `This repo pins ruby ${version} (.ruby-version), so put that ruby on PATH (rbenv/rvm/asdf/mise) first. ` : ""}Bundler could not install the gems ${pin.lockfile} pins, so \`bundle exec pod install\` cannot run. Run \`cd ${dirname(pin.gemfile)} && bundle install\` and fix what it reports, then run again.`;
|
|
1630
|
+
}
|
|
1631
|
+
function readBuildMarkers(podsDir) {
|
|
1632
|
+
let names;
|
|
1633
|
+
try {
|
|
1634
|
+
names = readdirSync(podsDir);
|
|
1635
|
+
} catch {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
return names.flatMap((name) => {
|
|
1639
|
+
const dir = join(podsDir, name);
|
|
1640
|
+
try {
|
|
1641
|
+
return [{
|
|
1642
|
+
dir,
|
|
1643
|
+
ino: statSync(dir).ino,
|
|
1644
|
+
content: readFileSync(join(dir, BUILD_MARKER))
|
|
1645
|
+
}];
|
|
1646
|
+
} catch {
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function restoreBuildMarkers(markers) {
|
|
1652
|
+
const restored = [];
|
|
1653
|
+
for (const { dir, ino, content } of markers) {
|
|
1654
|
+
const file = join(dir, BUILD_MARKER);
|
|
1655
|
+
try {
|
|
1656
|
+
if (existsSync(file) || statSync(dir).ino !== ino) continue;
|
|
1657
|
+
writeFileSync(file, content);
|
|
1658
|
+
restored.push(basename(dir));
|
|
1659
|
+
} catch {}
|
|
1660
|
+
}
|
|
1661
|
+
return restored;
|
|
1662
|
+
}
|
|
1663
|
+
async function runPodInstall(root, logWriter, { spawnFn = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line), directory = join(root, "ios") } = {}) {
|
|
1664
|
+
const iosDir = directory;
|
|
1665
|
+
if (!existsSync(iosDir)) return {
|
|
1666
|
+
failed: true,
|
|
1667
|
+
code: DEPS_ERROR,
|
|
1668
|
+
reason: `No ${relative(root, iosDir) || "."}/ directory in ${root}, so there is nothing to pod install.`,
|
|
1669
|
+
remedy: "Run `stim ios` on a project with native iOS sources, or let prebuild generate them.",
|
|
1670
|
+
lastLines: []
|
|
1671
|
+
};
|
|
1672
|
+
const ctx = {
|
|
1673
|
+
logWriter,
|
|
1674
|
+
spawn: spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts)),
|
|
1675
|
+
now,
|
|
1676
|
+
heartbeatMs,
|
|
1677
|
+
onHeartbeat
|
|
1678
|
+
};
|
|
1679
|
+
const notes = [];
|
|
1680
|
+
const pin = bundlerPin(root, iosDir);
|
|
1681
|
+
let bundler = false;
|
|
1682
|
+
if (pin) {
|
|
1683
|
+
const gems = await ensureBundledGems(root, pin, ctx);
|
|
1684
|
+
if (gems.failure) return {
|
|
1685
|
+
...gems.failure,
|
|
1686
|
+
notes
|
|
1687
|
+
};
|
|
1688
|
+
if (gems.note) notes.push(gems.note);
|
|
1689
|
+
bundler = gems.bundler;
|
|
1690
|
+
}
|
|
1691
|
+
const args = bundler ? [
|
|
1692
|
+
"bundle",
|
|
1693
|
+
"exec",
|
|
1694
|
+
"pod",
|
|
1695
|
+
"install"
|
|
1696
|
+
] : ["pod", "install"];
|
|
1697
|
+
const command = args.join(" ");
|
|
1698
|
+
const markers = readBuildMarkers(join(iosDir, "Pods"));
|
|
1699
|
+
const run = await runCaptured({
|
|
1700
|
+
...ctx,
|
|
1701
|
+
cmd: args[0],
|
|
1702
|
+
args: args.slice(1),
|
|
1703
|
+
cwd: iosDir,
|
|
1704
|
+
env: bundler && pin ? await bundlerEnv(root, pin.gemfile) : await podEnvForRubyAsync(readRubyVersion(root)),
|
|
1705
|
+
event: "pod_install",
|
|
1706
|
+
label: "pods",
|
|
1707
|
+
estimateMs
|
|
1708
|
+
});
|
|
1709
|
+
const restored = restoreBuildMarkers(markers);
|
|
1710
|
+
if (restored.length) notes.push(`kept the ${BUILD_MARKER} that pod install removed from ${restored.join(", ")}`);
|
|
1711
|
+
if (run.error) return {
|
|
1712
|
+
...(bundler ? missingBundle(run.error, pin) : missingPod(run.error)) || {
|
|
1713
|
+
failed: true,
|
|
1714
|
+
code: "STIM_DEPS_FAILED",
|
|
1715
|
+
reason: `Could not run \`${command}\`: ${run.error?.message || run.error}`,
|
|
1716
|
+
lastLines: run.transcript.slice(-20),
|
|
1717
|
+
durationMs: run.durationMs
|
|
1718
|
+
},
|
|
1719
|
+
command,
|
|
1720
|
+
notes
|
|
1721
|
+
};
|
|
1722
|
+
if (run.code !== 0) {
|
|
1723
|
+
const how = run.signal ? `signal ${run.signal}` : `exit code ${run.code}`;
|
|
1724
|
+
const transcript = run.transcript.join("\n");
|
|
1725
|
+
const extracted = extractPodDiagnostics(transcript);
|
|
1726
|
+
return {
|
|
1727
|
+
failed: true,
|
|
1728
|
+
code: DEPS_ERROR,
|
|
1729
|
+
reason: `\`${command}\` failed (${how}).`,
|
|
1730
|
+
remedy: podFailureRemedy(root, pin, transcript),
|
|
1731
|
+
command,
|
|
1732
|
+
notes,
|
|
1733
|
+
diagnosticSource: extracted ? extracted.source : "tail",
|
|
1734
|
+
diagnosticLines: extracted ? extracted.lines : [],
|
|
1735
|
+
lastLines: run.transcript.slice(-20),
|
|
1736
|
+
durationMs: run.durationMs
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
return {
|
|
1740
|
+
ok: true,
|
|
1741
|
+
command,
|
|
1742
|
+
notes,
|
|
1743
|
+
durationMs: run.durationMs
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function podFailureRemedy(root, pin, transcript) {
|
|
1747
|
+
if (pin) {
|
|
1748
|
+
const frozen = frozenRemedy(pin, transcript);
|
|
1749
|
+
if (frozen) return frozen;
|
|
1750
|
+
if (BUNDLER_NO_COCOAPODS.test(transcript)) return `${pin.lockfile} resolves cocoapods, but bundler found no \`pod\` executable in the bundle it loaded: the installed gems belong to a different Gemfile than ${pin.gemfile}, or that Gemfile's bundle was never installed. Run \`cd ${dirname(pin.gemfile)} && bundle install && bundle exec pod --version\` and make that work, then run again.`;
|
|
1751
|
+
}
|
|
1752
|
+
const pinned = /Could not find proper version of cocoapods/.test(transcript) ? readRubyVersion(root) : null;
|
|
1753
|
+
if (!pinned) return void 0;
|
|
1754
|
+
return `This repo pins ruby ${pinned} (.ruby-version) and its Gemfile's cocoapods was installed under it; the shell's ruby is likely a different version, so bundler looks in the wrong gem home. Put ruby ${pinned} on PATH (rbenv/rvm/asdf/mise) -- \`bundle install\` will NOT fix this.`;
|
|
1755
|
+
}
|
|
1756
|
+
function isMissingBinary(err) {
|
|
1757
|
+
const nodeErr = err;
|
|
1758
|
+
const message = String(nodeErr?.message || err || "");
|
|
1759
|
+
return nodeErr?.code === "ENOENT" || /ENOENT|not found/i.test(message);
|
|
1760
|
+
}
|
|
1761
|
+
function missingBundle(err, pin) {
|
|
1762
|
+
if (!isMissingBinary(err)) return null;
|
|
1763
|
+
return {
|
|
1764
|
+
failed: true,
|
|
1765
|
+
code: DEPS_ERROR,
|
|
1766
|
+
reason: "Bundler is not installed: no `bundle` executable on PATH.",
|
|
1767
|
+
remedy: `${pin?.lockfile ?? "Gemfile.lock"} pins this project's CocoaPods, so pods run through bundler. Install bundler (\`gem install bundler\`) and run again.`,
|
|
1768
|
+
lastLines: []
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const POD_MISSING_REASON = "CocoaPods is not installed: no `pod` executable on PATH.";
|
|
1772
|
+
function missingPod(err) {
|
|
1773
|
+
if (!isMissingBinary(err)) return null;
|
|
1774
|
+
return {
|
|
1775
|
+
failed: true,
|
|
1776
|
+
code: DEPS_ERROR,
|
|
1777
|
+
reason: POD_MISSING_REASON,
|
|
1778
|
+
remedy: "Install CocoaPods (`brew install cocoapods`, or `gem install cocoapods`) and run again.",
|
|
1779
|
+
lastLines: []
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region src/offload/toolchain.ts
|
|
1784
|
+
const distDir = dirname(fileURLToPath(import.meta.url));
|
|
1785
|
+
/**
|
|
1786
|
+
* What a client and a remote machine must agree on to build for each other: what a worker builds and how, the
|
|
1787
|
+
* artifacts it returns and the build and artifact messages. It lives in the `stim` package, whose own code does the
|
|
1788
|
+
* build, because `@stim-cli/core` is a ranged dependency that can resolve newer than the installed `stim`.
|
|
1789
|
+
* `scripts/build-compat.json` records the files a change to which needs a bump or a recorded no-bump.
|
|
1790
|
+
*/
|
|
1791
|
+
const STIM_BUILD_COMPAT = 2;
|
|
1792
|
+
function quiet(file, args) {
|
|
1793
|
+
return getExecutor().runFileQuiet(file, args, { timeoutMs: 2e4 });
|
|
1794
|
+
}
|
|
1795
|
+
async function quietAsync(file, args) {
|
|
1796
|
+
try {
|
|
1797
|
+
return await getExecutor().runFileAsync(file, args, { timeoutMs: 2e4 });
|
|
1798
|
+
} catch {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
/** This Mac's iOS toolchain, with the CocoaPods that `pod install` in `podDirectory` runs; null skips CocoaPods. */
|
|
1803
|
+
async function iosToolchainAsync(root, podDirectory) {
|
|
1804
|
+
const [xcode, simulatorSdk, cocoapods] = await Promise.all([
|
|
1805
|
+
quietAsync("xcodebuild", ["-version"]),
|
|
1806
|
+
quietAsync("xcrun", [
|
|
1807
|
+
"--sdk",
|
|
1808
|
+
"iphonesimulator",
|
|
1809
|
+
"--show-sdk-version"
|
|
1810
|
+
]),
|
|
1811
|
+
podDirectory === null ? null : projectCocoapodsVersion(root, podDirectory)
|
|
1812
|
+
]);
|
|
1813
|
+
return assembleIosToolchain(xcode, simulatorSdk, cocoapods);
|
|
1814
|
+
}
|
|
1815
|
+
function iosToolchainForRuby(rubyVersion, native) {
|
|
1816
|
+
return assembleIosToolchain(quiet("xcodebuild", ["-version"]), quiet("xcrun", [
|
|
1817
|
+
"--sdk",
|
|
1818
|
+
"iphonesimulator",
|
|
1819
|
+
"--show-sdk-version"
|
|
1820
|
+
]), native ? null : cocoapodsVersion(rubyVersion));
|
|
1821
|
+
}
|
|
1822
|
+
function assembleIosToolchain(xcode, simulatorSdk, cocoapods) {
|
|
1823
|
+
return {
|
|
1824
|
+
stimBuild: stimBuildDigest(distDir),
|
|
1825
|
+
compat: STIM_BUILD_COMPAT,
|
|
1826
|
+
arch: process.arch,
|
|
1827
|
+
xcode: xcode ? xcode.trim().replace(/\n/g, " / ") : null,
|
|
1828
|
+
simulatorSdk: simulatorSdk?.trim() ?? null,
|
|
1829
|
+
cocoapods
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function podProbeEnv(env) {
|
|
1833
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
|
|
1834
|
+
}
|
|
1835
|
+
function podVersionLine(output) {
|
|
1836
|
+
return output?.trim().split("\n").pop()?.trim() ?? null;
|
|
1837
|
+
}
|
|
1838
|
+
function cocoapodsVersion(rubyVersion) {
|
|
1839
|
+
return podVersionLine(getExecutor().runFileQuiet("pod", ["--version"], {
|
|
1840
|
+
timeoutMs: 2e4,
|
|
1841
|
+
env: podProbeEnv(podEnvForRuby(rubyVersion))
|
|
1842
|
+
}));
|
|
1843
|
+
}
|
|
1844
|
+
async function projectCocoapodsVersion(root, podDirectory) {
|
|
1845
|
+
const pin = bundlerPin(root, podDirectory);
|
|
1846
|
+
if (pin) return pin.cocoapods;
|
|
1847
|
+
try {
|
|
1848
|
+
return podVersionLine(await getExecutor().runFileAsync("pod", ["--version"], {
|
|
1849
|
+
timeoutMs: 2e4,
|
|
1850
|
+
cwd: podDirectory,
|
|
1851
|
+
env: podProbeEnv(await podEnvForRubyAsync(readRubyVersion(root)))
|
|
1852
|
+
}));
|
|
1853
|
+
} catch {
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function macosToolchain() {
|
|
1858
|
+
return assembleMacosToolchain(quiet("xcodebuild", ["-version"]), quiet("xcrun", [
|
|
1859
|
+
"--sdk",
|
|
1860
|
+
"macosx",
|
|
1861
|
+
"--show-sdk-version"
|
|
1862
|
+
]));
|
|
1863
|
+
}
|
|
1864
|
+
async function macosToolchainAsync() {
|
|
1865
|
+
const [xcode, macosSdk] = await Promise.all([quietAsync("xcodebuild", ["-version"]), quietAsync("xcrun", [
|
|
1866
|
+
"--sdk",
|
|
1867
|
+
"macosx",
|
|
1868
|
+
"--show-sdk-version"
|
|
1869
|
+
])]);
|
|
1870
|
+
return assembleMacosToolchain(xcode, macosSdk);
|
|
1871
|
+
}
|
|
1872
|
+
function assembleMacosToolchain(xcode, macosSdk) {
|
|
1873
|
+
return {
|
|
1874
|
+
stimBuild: stimBuildDigest(distDir),
|
|
1875
|
+
compat: STIM_BUILD_COMPAT,
|
|
1876
|
+
arch: process.arch,
|
|
1877
|
+
xcode: xcode ? xcode.trim().replace(/\n/g, " / ") : null,
|
|
1878
|
+
macosSdk: macosSdk?.trim() ?? null
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
/** The major version in a JDK's `release` file, as `JAVA_VERSION="17.0.19"` (or `"1.8.0_402"` for Java 8) gives it. */
|
|
1882
|
+
function jdkMajor(release) {
|
|
1883
|
+
return /^JAVA_VERSION="(?:1\.)?(\d+)/m.exec(release)?.[1] ?? null;
|
|
1884
|
+
}
|
|
1885
|
+
function releaseMajor(home) {
|
|
1886
|
+
try {
|
|
1887
|
+
return jdkMajor(readFileSync(join(home, "release"), "utf8"));
|
|
1888
|
+
} catch {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* The JDK Gradle's wrapper picks: `JAVA_HOME`, else the `java` on PATH, which is the macOS `/usr/bin/java`
|
|
1894
|
+
* stub (resolved through `java_home`) unless a real JDK comes first.
|
|
1895
|
+
*/
|
|
1896
|
+
function localJdk(env = process.env) {
|
|
1897
|
+
if (env.JAVA_HOME) return releaseMajor(env.JAVA_HOME);
|
|
1898
|
+
const java = getExecutor().findExecutable("java");
|
|
1899
|
+
if (java) try {
|
|
1900
|
+
const fromPath = releaseMajor(dirname(dirname(realpathSync(java))));
|
|
1901
|
+
if (fromPath) return fromPath;
|
|
1902
|
+
} catch {}
|
|
1903
|
+
const home = quiet("/usr/libexec/java_home", [])?.trim();
|
|
1904
|
+
return home ? releaseMajor(home) : null;
|
|
1905
|
+
}
|
|
1906
|
+
/** Where JDKs install, as `JAVA_HOME` values: the one in `env` first, then the usual locations of `platform`. */
|
|
1907
|
+
function jdkCandidates(env = process.env, platform = process.platform, home = homedir()) {
|
|
1908
|
+
const out = env.JAVA_HOME ? [env.JAVA_HOME] : [];
|
|
1909
|
+
const children = (dir) => (listDir(dir) ?? []).map((entry) => join(dir, entry));
|
|
1910
|
+
out.push(...children(join(home, ".sdkman", "candidates", "java")));
|
|
1911
|
+
if (platform === "darwin") {
|
|
1912
|
+
for (const dir of ["/Library/Java/JavaVirtualMachines", join(home, "Library", "Java", "JavaVirtualMachines")]) out.push(...children(dir).map((jvm) => join(jvm, "Contents", "Home")));
|
|
1913
|
+
for (const prefix of ["/opt/homebrew", "/usr/local"]) for (const formula of children(join(prefix, "opt")).filter((dir) => /^openjdk(@\d+)?$/.test(basename(dir)))) out.push(join(formula, "libexec", "openjdk.jdk", "Contents", "Home"));
|
|
1914
|
+
for (const app of children("/Applications").filter((dir) => basename(dir).startsWith("Android Studio"))) out.push(join(app, "Contents", "jbr", "Contents", "Home"));
|
|
1915
|
+
} else if (platform === "win32") {
|
|
1916
|
+
const programs = env.ProgramFiles ?? "C:\\Program Files";
|
|
1917
|
+
for (const vendor of [
|
|
1918
|
+
"Eclipse Adoptium",
|
|
1919
|
+
"Java",
|
|
1920
|
+
"Microsoft",
|
|
1921
|
+
"Zulu",
|
|
1922
|
+
"Android"
|
|
1923
|
+
]) out.push(...children(join(programs, vendor)));
|
|
1924
|
+
out.push(join(programs, "Android", "Android Studio", "jbr"));
|
|
1925
|
+
} else out.push(...children("/usr/lib/jvm"), ...children("/opt").map((dir) => join(dir, "jbr")));
|
|
1926
|
+
return out;
|
|
1927
|
+
}
|
|
1928
|
+
/** The first home of each JDK major among `candidates`, read from each `release` file; a home without `javac` is a JRE. */
|
|
1929
|
+
function jdkHomes(candidates) {
|
|
1930
|
+
const homes = /* @__PURE__ */ new Map();
|
|
1931
|
+
for (const candidate of candidates) {
|
|
1932
|
+
const major = releaseMajor(candidate);
|
|
1933
|
+
const javac = join(candidate, "bin", process.platform === "win32" ? "javac.exe" : "javac");
|
|
1934
|
+
if (major && !homes.has(major) && existsSync(javac)) homes.set(major, candidate);
|
|
1935
|
+
}
|
|
1936
|
+
return homes;
|
|
1937
|
+
}
|
|
1938
|
+
function installedJdkMajors() {
|
|
1939
|
+
const majors = new Set(jdkHomes(jdkCandidates()).keys());
|
|
1940
|
+
const current = localJdk();
|
|
1941
|
+
if (current) majors.add(current);
|
|
1942
|
+
return [...majors].toSorted((a, b) => Number(a) - Number(b));
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Points this process, and the Gradle it starts, at an installed JDK of `major` when its default JDK is another one.
|
|
1946
|
+
* Returns why it cannot, or null.
|
|
1947
|
+
*/
|
|
1948
|
+
function useJdkMajor(major, env = process.env, candidates = jdkCandidates(env)) {
|
|
1949
|
+
if (localJdk(env) === major) return null;
|
|
1950
|
+
const home = jdkHomes(candidates).get(major);
|
|
1951
|
+
if (!home) return `This machine has no JDK ${major} to build on.`;
|
|
1952
|
+
env.JAVA_HOME = home;
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
function androidToolchain() {
|
|
1956
|
+
return {
|
|
1957
|
+
stimBuild: stimBuildDigest(distDir),
|
|
1958
|
+
compat: STIM_BUILD_COMPAT,
|
|
1959
|
+
arch: process.arch,
|
|
1960
|
+
jdk: localJdk()
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
async function androidToolchainAsync(env = process.env) {
|
|
1964
|
+
let jdk = null;
|
|
1965
|
+
if (env.JAVA_HOME) jdk = releaseMajor(env.JAVA_HOME);
|
|
1966
|
+
else {
|
|
1967
|
+
const java = getExecutor().findExecutable("java");
|
|
1968
|
+
if (java) try {
|
|
1969
|
+
jdk = releaseMajor(dirname(dirname(realpathSync(java))));
|
|
1970
|
+
} catch {}
|
|
1971
|
+
if (!jdk) {
|
|
1972
|
+
const home = (await quietAsync("/usr/libexec/java_home", []))?.trim();
|
|
1973
|
+
if (home) jdk = releaseMajor(home);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
stimBuild: stimBuildDigest(distDir),
|
|
1978
|
+
compat: STIM_BUILD_COMPAT,
|
|
1979
|
+
arch: process.arch,
|
|
1980
|
+
jdk
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
/** `ndkVersion`, `buildTools` and `compileSdk` from React Native's `gradle/libs.versions.toml`. */
|
|
1984
|
+
function parseAndroidRequirements(catalog) {
|
|
1985
|
+
const value = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, "m").exec(catalog)?.[1] ?? null;
|
|
1986
|
+
return {
|
|
1987
|
+
ndk: value("ndkVersion"),
|
|
1988
|
+
buildTools: value("buildTools"),
|
|
1989
|
+
compileSdk: value("compileSdk")
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function sdkDownloadDisabled(properties) {
|
|
1993
|
+
return /^\s*android\.builder\.sdkDownload\s*[=:]\s*false\s*$/im.test(properties);
|
|
1994
|
+
}
|
|
1995
|
+
function projectSdkDownload(gradleRoot) {
|
|
1996
|
+
try {
|
|
1997
|
+
return !sdkDownloadDisabled(readFileSync(join(gradleRoot, "gradle.properties"), "utf8"));
|
|
1998
|
+
} catch {
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function androidRequirements(projectRoot, gradleRoot = join(projectRoot, "android")) {
|
|
2003
|
+
const sdkDownload = projectSdkDownload(gradleRoot) ? {} : { sdkDownload: false };
|
|
2004
|
+
try {
|
|
2005
|
+
const pkg = createRequire(join(projectRoot, "package.json")).resolve("react-native/package.json");
|
|
2006
|
+
return {
|
|
2007
|
+
...parseAndroidRequirements(readFileSync(join(dirname(pkg), "gradle", "libs.versions.toml"), "utf8")),
|
|
2008
|
+
...sdkDownload
|
|
2009
|
+
};
|
|
2010
|
+
} catch {
|
|
2011
|
+
return {
|
|
2012
|
+
ndk: null,
|
|
2013
|
+
buildTools: null,
|
|
2014
|
+
compileSdk: null,
|
|
2015
|
+
...sdkDownload
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/** Runtime identifiers that have at least one available iPhone simulator, from `simctl list devices -j`. */
|
|
2020
|
+
function iphoneRuntimes(listed) {
|
|
2021
|
+
const devices = listed?.devices;
|
|
2022
|
+
if (!devices || typeof devices !== "object") return [];
|
|
2023
|
+
return Object.entries(devices).filter(([, list]) => (Array.isArray(list) ? list : []).some(availableIphone)).map(([runtime]) => runtime).toSorted();
|
|
2024
|
+
}
|
|
2025
|
+
function availableIphone(device) {
|
|
2026
|
+
return device.isAvailable === true && typeof device.deviceTypeIdentifier === "string" && device.deviceTypeIdentifier.startsWith("com.apple.CoreSimulator.SimDeviceType.iPhone-");
|
|
2027
|
+
}
|
|
2028
|
+
function listDir(path) {
|
|
2029
|
+
try {
|
|
2030
|
+
return readdirSync(path).filter((entry) => !entry.startsWith(".")).toSorted();
|
|
2031
|
+
} catch {
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
function sdkPackages() {
|
|
2036
|
+
const sdk = androidHome();
|
|
2037
|
+
const platforms = listDir(join(sdk, "platforms"));
|
|
2038
|
+
if (!platforms) return null;
|
|
2039
|
+
return {
|
|
2040
|
+
ndk: listDir(join(sdk, "ndk")) ?? [],
|
|
2041
|
+
buildTools: listDir(join(sdk, "build-tools")) ?? [],
|
|
2042
|
+
platforms
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
/** `CFBundleShortVersionString` and `ProductBuildVersion` of an Xcode app's `Contents/version.plist`. */
|
|
2046
|
+
function parseXcodeVersionPlist(plist) {
|
|
2047
|
+
const value = (key) => new RegExp(`<key>${key}</key>\\s*<string>([^<]+)</string>`).exec(plist)?.[1] ?? null;
|
|
2048
|
+
return {
|
|
2049
|
+
version: value("CFBundleShortVersionString"),
|
|
2050
|
+
build: value("ProductBuildVersion")
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function realPath(path) {
|
|
2054
|
+
try {
|
|
2055
|
+
return realpathSync(path);
|
|
2056
|
+
} catch {
|
|
2057
|
+
return path;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
const realPathOrNull = (path) => path ? realPath(path) : null;
|
|
2061
|
+
function listXcodeApps(checks, root = "/Applications") {
|
|
2062
|
+
const apps = [];
|
|
2063
|
+
for (const entry of listDir(root) ?? []) {
|
|
2064
|
+
if (!entry.endsWith(".app") || !existsSync(join(root, entry, "Contents", "Developer"))) continue;
|
|
2065
|
+
const path = realPath(join(root, entry));
|
|
2066
|
+
if (apps.some((each) => each.path === path)) continue;
|
|
2067
|
+
let plist = "";
|
|
2068
|
+
try {
|
|
2069
|
+
plist = readFileSync(join(path, "Contents", "version.plist"), "utf8");
|
|
2070
|
+
} catch {}
|
|
2071
|
+
const app = {
|
|
2072
|
+
path,
|
|
2073
|
+
...parseXcodeVersionPlist(plist)
|
|
2074
|
+
};
|
|
2075
|
+
if (checks) {
|
|
2076
|
+
const env = {
|
|
2077
|
+
...process.env,
|
|
2078
|
+
DEVELOPER_DIR: join(path, "Contents", "Developer")
|
|
2079
|
+
};
|
|
2080
|
+
const passes = (args) => getExecutor().runFileQuiet("xcodebuild", args, {
|
|
2081
|
+
timeoutMs: 2e4,
|
|
2082
|
+
env
|
|
2083
|
+
}) !== null;
|
|
2084
|
+
app.license = passes(["-license", "check"]);
|
|
2085
|
+
app.firstLaunch = passes(["-checkFirstLaunchStatus"]);
|
|
2086
|
+
}
|
|
2087
|
+
apps.push(app);
|
|
2088
|
+
}
|
|
2089
|
+
return apps;
|
|
2090
|
+
}
|
|
2091
|
+
function xcodeSelectionReport(xcode) {
|
|
2092
|
+
if (process.env.DEVELOPER_DIR) return {};
|
|
2093
|
+
return {
|
|
2094
|
+
developerDir: realPathOrNull(quiet("xcode-select", ["-p"])?.trim() || null),
|
|
2095
|
+
xcodeApps: listXcodeApps(xcode === null)
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
/** Whether the SDK's licenses are accepted: Gradle's SDK download reads `licenses/android-sdk-license`. */
|
|
2099
|
+
function sdkLicensesAccepted(sdk) {
|
|
2100
|
+
try {
|
|
2101
|
+
return readFileSync(join(sdk, "licenses", "android-sdk-license"), "utf8").trim().length > 0;
|
|
2102
|
+
} catch {
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function sdkReport() {
|
|
2107
|
+
const androidSdk = sdkPackages();
|
|
2108
|
+
if (!androidSdk) return { androidSdk };
|
|
2109
|
+
const home = androidHome();
|
|
2110
|
+
return {
|
|
2111
|
+
androidSdk,
|
|
2112
|
+
androidLicenses: sdkLicensesAccepted(home),
|
|
2113
|
+
androidHome: home
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
function workerToolchain(rubyVersion = null, native) {
|
|
2117
|
+
if (native === "gradle") return {
|
|
2118
|
+
...androidToolchain(),
|
|
2119
|
+
os: process.platform,
|
|
2120
|
+
xcode: null,
|
|
2121
|
+
simulatorSdk: null,
|
|
2122
|
+
cocoapods: null,
|
|
2123
|
+
macosSdk: null,
|
|
2124
|
+
bundler: null,
|
|
2125
|
+
runtimes: [],
|
|
2126
|
+
jdks: installedJdkMajors(),
|
|
2127
|
+
...sdkReport()
|
|
2128
|
+
};
|
|
2129
|
+
let listed = null;
|
|
2130
|
+
try {
|
|
2131
|
+
listed = JSON.parse(quiet("xcrun", [
|
|
2132
|
+
"simctl",
|
|
2133
|
+
"list",
|
|
2134
|
+
"devices",
|
|
2135
|
+
"available",
|
|
2136
|
+
"-j"
|
|
2137
|
+
]) ?? "null");
|
|
2138
|
+
} catch {}
|
|
2139
|
+
const ios = iosToolchainForRuby(rubyVersion, native);
|
|
2140
|
+
return {
|
|
2141
|
+
...ios,
|
|
2142
|
+
...process.platform === "darwin" ? xcodeSelectionReport(ios.xcode) : {},
|
|
2143
|
+
os: process.platform,
|
|
2144
|
+
macosSdk: native ? null : quiet("xcrun", [
|
|
2145
|
+
"--sdk",
|
|
2146
|
+
"macosx",
|
|
2147
|
+
"--show-sdk-version"
|
|
2148
|
+
])?.trim() ?? null,
|
|
2149
|
+
bundler: native ? null : quiet("bundle", ["--version"])?.trim() || null,
|
|
2150
|
+
runtimes: iphoneRuntimes(listed),
|
|
2151
|
+
jdk: native ? null : localJdk(),
|
|
2152
|
+
...native ? {} : { jdks: installedJdkMajors() },
|
|
2153
|
+
...native ? { androidSdk: null } : sdkReport()
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
/** The API level of an SDK `platforms/` directory: `android-37.0` and `android-37` are both 37. */
|
|
2157
|
+
const platformLevel = (dir) => dir.replace(/^android-/, "").split(".")[0];
|
|
2158
|
+
function sdkGaps(requires, sdk) {
|
|
2159
|
+
const gaps = [];
|
|
2160
|
+
if (requires.ndk && !sdk.ndk.includes(requires.ndk)) gaps.push({
|
|
2161
|
+
code: "ndk",
|
|
2162
|
+
label: `NDK ${requires.ndk}`,
|
|
2163
|
+
id: `ndk;${requires.ndk}`,
|
|
2164
|
+
size: "about 1-2 GB"
|
|
2165
|
+
});
|
|
2166
|
+
if (requires.buildTools && !sdk.buildTools.includes(requires.buildTools)) gaps.push({
|
|
2167
|
+
code: "build-tools",
|
|
2168
|
+
label: `build-tools ${requires.buildTools}`,
|
|
2169
|
+
id: `build-tools;${requires.buildTools}`
|
|
2170
|
+
});
|
|
2171
|
+
if (requires.compileSdk && !sdk.platforms.some((dir) => platformLevel(dir) === requires.compileSdk)) gaps.push({
|
|
2172
|
+
code: "compile-sdk",
|
|
2173
|
+
label: `platform android-${requires.compileSdk}`,
|
|
2174
|
+
id: `platforms;android-${requires.compileSdk}`
|
|
2175
|
+
});
|
|
2176
|
+
return gaps;
|
|
2177
|
+
}
|
|
2178
|
+
/** Whether Gradle downloads a package the worker's SDK lacks: its licenses are accepted and the project allows it. */
|
|
2179
|
+
function gradleDownloads(requires, worker) {
|
|
2180
|
+
return worker.androidLicenses === true && requires.sdkDownload !== false;
|
|
2181
|
+
}
|
|
2182
|
+
function shellQuote(path, os) {
|
|
2183
|
+
if (/^[\w@%+=:,./\\~-]+$/.test(path)) return path;
|
|
2184
|
+
return os === "win32" ? `"${path}"` : `'${path.replaceAll("'", "'\\''")}'`;
|
|
2185
|
+
}
|
|
2186
|
+
function sdkManagerPath(worker) {
|
|
2187
|
+
if (!worker.androidHome) return "sdkmanager";
|
|
2188
|
+
return shellQuote(worker.os === "win32" ? `${worker.androidHome}\\cmdline-tools\\latest\\bin\\sdkmanager.bat` : `${worker.androidHome}/cmdline-tools/latest/bin/sdkmanager`, worker.os);
|
|
2189
|
+
}
|
|
2190
|
+
function jdkInstall(major, os) {
|
|
2191
|
+
return {
|
|
2192
|
+
commands: [os === "linux" ? `sudo apt install openjdk-${major}-jdk-headless` : os === "win32" ? `winget install EclipseAdoptium.Temurin.${major}.JDK` : `brew install --cask zulu@${major}`],
|
|
2193
|
+
lead: `install JDK ${major}; Stim uses it automatically, no restart needed`
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function androidMismatches(local, requires, worker) {
|
|
2197
|
+
const out = [];
|
|
2198
|
+
if ((!local.jdk || worker.jdk !== local.jdk) && !(local.jdk && worker.jdks?.includes(local.jdk))) out.push({
|
|
2199
|
+
code: "jdk",
|
|
2200
|
+
reason: `JDK ${worker.jdk ?? "none"} there, ${local.jdk ?? "none"} here`,
|
|
2201
|
+
...local.jdk && worker.jdks ? jdkInstall(local.jdk, worker.os) : {}
|
|
2202
|
+
});
|
|
2203
|
+
const sdk = worker.androidSdk;
|
|
2204
|
+
if (!sdk) return [...out, {
|
|
2205
|
+
code: "android-sdk",
|
|
2206
|
+
reason: "no Android SDK there"
|
|
2207
|
+
}];
|
|
2208
|
+
if (gradleDownloads(requires, worker)) return out;
|
|
2209
|
+
const sdkManager = sdkManagerPath(worker);
|
|
2210
|
+
const why = requires.sdkDownload === false ? ", and the project sets android.builder.sdkDownload=false" : worker.androidLicenses === false ? ", and its SDK licenses are not accepted" : "";
|
|
2211
|
+
for (const gap of sdkGaps(requires, sdk)) {
|
|
2212
|
+
const install = `${sdkManager} "${gap.id}"`;
|
|
2213
|
+
const licenses = worker.androidLicenses === false && requires.sdkDownload !== false;
|
|
2214
|
+
out.push({
|
|
2215
|
+
code: gap.code,
|
|
2216
|
+
reason: `no ${gap.label} there${why}`,
|
|
2217
|
+
commands: licenses ? [`${sdkManager} --licenses`, install] : [install],
|
|
2218
|
+
lead: licenses ? "accept the SDK licenses so Gradle downloads the package, or install it" : worker.androidHome ? "install the package into its Android SDK" : "install the package into the Android SDK stim-server uses (its ANDROID_HOME, else the default SDK location)"
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
return out;
|
|
2222
|
+
}
|
|
2223
|
+
function cocoapodsInstall(here) {
|
|
2224
|
+
return {
|
|
2225
|
+
commands: ["brew install cocoapods"],
|
|
2226
|
+
lead: here ? `install CocoaPods (this Mac has ${here}; \`gem install cocoapods -v ${here}\` instead of Homebrew installs that version)` : "install CocoaPods"
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
function cocoapodsDifference(target, worker) {
|
|
2230
|
+
if (target.native || target.cocoapodsPinned) return null;
|
|
2231
|
+
if (worker.os !== void 0 && worker.os !== "darwin") return null;
|
|
2232
|
+
const here = target.local.cocoapods;
|
|
2233
|
+
if (!worker.cocoapods) return {
|
|
2234
|
+
code: "cocoapods",
|
|
2235
|
+
reason: versionMismatch("CocoaPods", null, here),
|
|
2236
|
+
...cocoapodsInstall(here)
|
|
2237
|
+
};
|
|
2238
|
+
return here && worker.cocoapods !== here ? {
|
|
2239
|
+
code: "cocoapods",
|
|
2240
|
+
reason: versionMismatch("CocoaPods", worker.cocoapods, here)
|
|
2241
|
+
} : null;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* What a build on the worker does beyond the match: the SDK packages it lacks and Gradle downloads on the first build,
|
|
2245
|
+
* because its SDK licenses are accepted and the project allows the download. None of them stops the build. For iOS, the
|
|
2246
|
+
* CocoaPods difference a named machine is sent the build despite.
|
|
2247
|
+
*/
|
|
2248
|
+
function toolchainNotes(target, worker) {
|
|
2249
|
+
if (target.platform === "ios") {
|
|
2250
|
+
const pods = cocoapodsDifference(target, worker);
|
|
2251
|
+
return pods ? [{
|
|
2252
|
+
...pods,
|
|
2253
|
+
reason: `${pods.reason}; its pod install runs anyway and a failure is reported`
|
|
2254
|
+
}] : [];
|
|
2255
|
+
}
|
|
2256
|
+
if (target.platform !== "android" || !worker.androidSdk || !gradleDownloads(target.requires, worker)) return [];
|
|
2257
|
+
const sdkManager = sdkManagerPath(worker);
|
|
2258
|
+
return sdkGaps(target.requires, worker.androidSdk).map((gap) => ({
|
|
2259
|
+
code: gap.code,
|
|
2260
|
+
reason: `${gap.label} is not installed there; Gradle downloads it on the first build${gap.size ? ` (${gap.size})` : ""}`,
|
|
2261
|
+
commands: [`${sdkManager} "${gap.id}"`],
|
|
2262
|
+
lead: "install the package now so the first build does not download it"
|
|
2263
|
+
}));
|
|
2264
|
+
}
|
|
2265
|
+
function versionMismatch(tool, there, here) {
|
|
2266
|
+
const side = (version) => version ? version.startsWith(tool) ? version : `${tool} ${version}` : `no ${tool}`;
|
|
2267
|
+
return `${side(there)} there, ${side(here)} here`;
|
|
2268
|
+
}
|
|
2269
|
+
const COMMAND_LINE_TOOLS = "/Library/Developer/CommandLineTools";
|
|
2270
|
+
function xcodeIdentity(xcode) {
|
|
2271
|
+
return {
|
|
2272
|
+
version: xcode ? /Xcode (\S+)/.exec(xcode)?.[1] ?? null : null,
|
|
2273
|
+
build: xcode ? /Build version (\S+)/.exec(xcode)?.[1] ?? null : null
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
const versionParts = (version) => (version ?? "").split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
2277
|
+
function compareVersions(a, b) {
|
|
2278
|
+
const [left, right] = [versionParts(a), versionParts(b)];
|
|
2279
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
2280
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
2281
|
+
if (difference) return difference;
|
|
2282
|
+
}
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
function pickXcodeApp(apps, wanted) {
|
|
2286
|
+
return (wanted.build ? apps.find((app) => app.build === wanted.build) : void 0) ?? (wanted.version ? apps.find((app) => app.version === wanted.version) : void 0) ?? apps.toSorted((a, b) => compareVersions(b.version, a.version))[0];
|
|
2287
|
+
}
|
|
2288
|
+
const developerDirOf = (app) => posix.join(app.path, "Contents", "Developer");
|
|
2289
|
+
function selectXcode(app, selected, worker) {
|
|
2290
|
+
const commands = selected ? [] : [`sudo xcode-select -s ${shellQuote(developerDirOf(app), worker.os)}`];
|
|
2291
|
+
if (app.license === false && app.firstLaunch === false) commands.push("sudo xcodebuild -license accept && sudo xcodebuild -runFirstLaunch");
|
|
2292
|
+
else if (app.license === false) commands.push("sudo xcodebuild -license accept");
|
|
2293
|
+
else if (app.firstLaunch === false) commands.push("sudo xcodebuild -runFirstLaunch");
|
|
2294
|
+
const name = basename(app.path, ".app");
|
|
2295
|
+
return {
|
|
2296
|
+
commands,
|
|
2297
|
+
lead: selected ? `finish setting up ${name}` : `select ${name}${commands.length > 1 ? " and finish setting it up" : ""}`
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
function diagnosesXcode(local, worker) {
|
|
2301
|
+
return worker.xcode === null && worker.developerDir !== void 0 && local.xcode !== null;
|
|
2302
|
+
}
|
|
2303
|
+
function xcodeProblem(local, worker, onWorker = false) {
|
|
2304
|
+
const legacy = {
|
|
2305
|
+
code: "xcode",
|
|
2306
|
+
reason: onWorker ? "no Xcode there" : versionMismatch("Xcode", worker.xcode, local.xcode)
|
|
2307
|
+
};
|
|
2308
|
+
if (worker.developerDir === void 0 || !onWorker && !local.xcode) return legacy;
|
|
2309
|
+
const wanted = onWorker ? {
|
|
2310
|
+
version: null,
|
|
2311
|
+
build: null
|
|
2312
|
+
} : xcodeIdentity(local.xcode);
|
|
2313
|
+
const apps = worker.xcodeApps ?? [];
|
|
2314
|
+
if (worker.xcode) {
|
|
2315
|
+
const match = wanted.build ? apps.find((app) => app.build === wanted.build) : void 0;
|
|
2316
|
+
return match ? {
|
|
2317
|
+
...legacy,
|
|
2318
|
+
...selectXcode(match, false, worker)
|
|
2319
|
+
} : legacy;
|
|
2320
|
+
}
|
|
2321
|
+
const app = pickXcodeApp(apps, wanted);
|
|
2322
|
+
if (!app) return {
|
|
2323
|
+
code: "xcode",
|
|
2324
|
+
reason: "no Xcode there",
|
|
2325
|
+
...wanted.version ? {
|
|
2326
|
+
commands: [`xcodes install ${wanted.version}`],
|
|
2327
|
+
lead: `install Xcode ${wanted.version} (from the App Store, or with xcodes), then run stim doctor again`
|
|
2328
|
+
} : {}
|
|
2329
|
+
};
|
|
2330
|
+
const selected = worker.developerDir === developerDirOf(app);
|
|
2331
|
+
const where = worker.developerDir ? worker.developerDir.startsWith(COMMAND_LINE_TOOLS) ? "Command Line Tools" : worker.developerDir : "nothing";
|
|
2332
|
+
const fix = selectXcode(app, selected, worker);
|
|
2333
|
+
const matches = app.build === wanted.build || app.version === wanted.version;
|
|
2334
|
+
return {
|
|
2335
|
+
code: "xcode",
|
|
2336
|
+
reason: selected ? `no Xcode there: ${basename(app.path)} is selected${fix.commands?.length ? " but not set up" : " but xcodebuild fails"}` : `no Xcode there: xcode-select points at ${where}`,
|
|
2337
|
+
...fix,
|
|
2338
|
+
...wanted.version && !matches ? { lead: `${fix.lead} (this Mac has Xcode ${wanted.version}; xcodes install ${wanted.version} matches it)` } : {}
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function runtimeProblem(runtime, worker, followsXcode) {
|
|
2342
|
+
const reason = `no iPhone simulator on ${runtime} there`;
|
|
2343
|
+
if (followsXcode) return {
|
|
2344
|
+
code: "runtime",
|
|
2345
|
+
reason: `${reason}; this follows from the Xcode problem`,
|
|
2346
|
+
followsFrom: "xcode"
|
|
2347
|
+
};
|
|
2348
|
+
const version = /iOS-(\d+)-(\d+)$/.exec(runtime);
|
|
2349
|
+
return worker.developerDir !== void 0 && version ? {
|
|
2350
|
+
code: "runtime",
|
|
2351
|
+
reason,
|
|
2352
|
+
commands: [`xcodebuild -downloadPlatform iOS -buildVersion ${version[1]}.${version[2]}`],
|
|
2353
|
+
lead: `download the iOS ${version[1]}.${version[2]} simulator runtime if it is not installed (an installed runtime also needs an iPhone simulator)`
|
|
2354
|
+
} : {
|
|
2355
|
+
code: "runtime",
|
|
2356
|
+
reason
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
/** Why a remote machine cannot build like this machine; empty when it can. */
|
|
2360
|
+
function toolchainMismatches(target, worker) {
|
|
2361
|
+
const { local } = target;
|
|
2362
|
+
const out = [];
|
|
2363
|
+
if (!local.stimBuild || (worker.compat !== void 0 ? worker.compat !== local.compat : worker.stimBuild !== local.stimBuild)) out.push({
|
|
2364
|
+
code: "stim-build",
|
|
2365
|
+
reason: `Stim build ${worker.stimBuild ?? "unknown"} there, ${local.stimBuild ?? "unknown"} here`
|
|
2366
|
+
});
|
|
2367
|
+
if (target.platform === "android") return [...out, ...androidMismatches(target.local, target.requires, worker)];
|
|
2368
|
+
if (worker.os !== void 0 && worker.os !== "darwin") {
|
|
2369
|
+
const name = target.platform === "ios" ? "iOS" : "macOS";
|
|
2370
|
+
return [...out, {
|
|
2371
|
+
code: "platform",
|
|
2372
|
+
reason: `${worker.os} there, and ${name} builds need a Mac`
|
|
2373
|
+
}];
|
|
2374
|
+
}
|
|
2375
|
+
if (target.platform === "macos" && target.runsOnWorker) {
|
|
2376
|
+
if (!worker.xcode) out.push(xcodeProblem(target.local, worker, true));
|
|
2377
|
+
if (!worker.macosSdk) out.push({
|
|
2378
|
+
code: "macos-sdk",
|
|
2379
|
+
reason: "no macOS SDK there"
|
|
2380
|
+
});
|
|
2381
|
+
return out;
|
|
2382
|
+
}
|
|
2383
|
+
if (worker.arch !== local.arch) out.push({
|
|
2384
|
+
code: "arch",
|
|
2385
|
+
reason: `CPU ${worker.arch} there, ${local.arch} here`
|
|
2386
|
+
});
|
|
2387
|
+
if (target.platform === "macos") {
|
|
2388
|
+
const macos = target.local;
|
|
2389
|
+
const xcode = !macos.xcode || worker.xcode !== macos.xcode ? xcodeProblem(macos, worker) : null;
|
|
2390
|
+
if (xcode) out.push(xcode);
|
|
2391
|
+
if ((!macos.macosSdk || worker.macosSdk !== macos.macosSdk) && !(xcode && diagnosesXcode(macos, worker))) out.push({
|
|
2392
|
+
code: "macos-sdk",
|
|
2393
|
+
reason: versionMismatch("macOS SDK", worker.macosSdk, macos.macosSdk)
|
|
2394
|
+
});
|
|
2395
|
+
return out;
|
|
2396
|
+
}
|
|
2397
|
+
const ios = target.local;
|
|
2398
|
+
const xcode = !ios.xcode || worker.xcode !== ios.xcode ? xcodeProblem(ios, worker) : null;
|
|
2399
|
+
if (xcode) out.push(xcode);
|
|
2400
|
+
const selection = xcode !== null && diagnosesXcode(ios, worker);
|
|
2401
|
+
if ((!ios.simulatorSdk || worker.simulatorSdk !== ios.simulatorSdk) && !selection) out.push({
|
|
2402
|
+
code: "simulator-sdk",
|
|
2403
|
+
reason: versionMismatch("simulator SDK", worker.simulatorSdk, ios.simulatorSdk)
|
|
2404
|
+
});
|
|
2405
|
+
if (!target.native && target.cocoapodsPinned && !worker.bundler) out.push({
|
|
2406
|
+
code: "bundler",
|
|
2407
|
+
reason: "no Bundler there to run the CocoaPods this project's Gemfile.lock pins"
|
|
2408
|
+
});
|
|
2409
|
+
if (target.runtime && !worker.runtimes.includes(target.runtime)) out.push(runtimeProblem(target.runtime, worker, selection || Boolean(xcode?.commands)));
|
|
2410
|
+
return out;
|
|
2411
|
+
}
|
|
2412
|
+
//#endregion
|
|
2413
|
+
export { clearNativeRunCancel as $, COMPILATION_CACHE_UNAVAILABLE as A, readBundleExecutable as B, POD_MISSING_REASON as C, runCaptured as D, readRubyVersion as E, compilationCacheSettings as F, tailLines as G, resolveScheme as H, detectXcodeMajorAsync as I, bundlerPin as J, xcodebuildArgs as K, parseSwiftVersion as L, SWIFT_PREFIX_MAPPING_MIN_SWIFT as M, buildXcode as N, runPodInstall as O, compilationCacheActivityLine as P, cancelledFailure as Q, parseXcodeMajor as R, DEPS_ERROR as S, readPodState as T, startBuildHeartbeat as U, readBundleId as V, swiftPrefixMappingSupported as W, NATIVE_RUN_LOCK as X, podInstallCommand as Y, NATIVE_RUN_WAIT_MS as Z, sdkDownloadDisabled as _, cocoapodsDifference as a, runCancellation as at, useJdkMajor as b, iphoneRuntimes as c, cacheUser as ct, jdkMajor as d, decideStopAction as et, listXcodeApps as f, parseXcodeVersionPlist as g, parseAndroidRequirements as h, availableIphone as i, requestNativeRunCancel as it, HEARTBEAT_INTERVAL_MS as j, COMPILATION_CACHE_NOT_RUN as k, jdkCandidates as l, claimForEmptying as lt, macosToolchainAsync as m, androidToolchain as n, nativeRunHolder as nt, cocoapodsInstall as o, runCancellationSignal as ot, macosToolchain as p, describeDiagnostic as q, androidToolchainAsync as r, nativeRunWaitNotice as rt, iosToolchainAsync as s, withNativeBuildRun as st, androidRequirements as t, describeNativeRunHolder as tt, jdkHomes as u, toolchainMismatches as v, podsAreStale as w, workerToolchain as x, toolchainNotes as y, pickXcodeProject as z };
|