stim 1.20.2 → 1.21.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.
Files changed (217) hide show
  1. package/README.md +88 -8
  2. package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
  3. package/dist/act-DHAw2A2S.mjs +2667 -0
  4. package/dist/activity-BzVfHSrY.mjs +1376 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
  6. package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
  7. package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
  8. package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
  9. package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
  10. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
  11. package/dist/android-cas-compiler.mjs +2 -2
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/api.d.mts +740 -8
  14. package/dist/api.mjs +585 -1
  15. package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
  16. package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
  17. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
  18. package/dist/budget-Bs6SxxzO.mjs +9154 -0
  19. package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
  20. package/dist/build-cache-DmyR9Zzc.mjs +602 -0
  21. package/dist/build-plan-CaFskB7_.mjs +188 -0
  22. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
  25. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
  26. package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
  27. package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/client-B7QVOlCr.d.mts +253 -0
  30. package/dist/client-UXD70ZbM.mjs +1406 -0
  31. package/dist/collector-run.d.mts +4 -40
  32. package/dist/collector-run.mjs +9 -8
  33. package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
  34. package/dist/config-CYcBF478.mjs +949 -0
  35. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
  36. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
  37. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
  38. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
  39. package/dist/device-DCxH7_By.mjs +1068 -0
  40. package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
  41. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
  42. package/dist/device-host-worker.mjs +65 -61
  43. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
  44. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
  45. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
  46. package/dist/doctor-BLHCYxzi.mjs +2 -0
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
  49. package/dist/doctor-fingerprint.mjs +19 -0
  50. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
  51. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
  52. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
  53. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
  54. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
  55. package/dist/exec-H20xSg2T.d.mts +65 -0
  56. package/dist/exec-_UbtCQcG.mjs +750 -0
  57. package/dist/gc-C-WwL0yi.mjs +3 -0
  58. package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
  59. package/dist/git-C3iTsJaZ.mjs +73 -0
  60. package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
  61. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
  62. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
  63. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
  64. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
  65. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
  66. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
  67. package/dist/idle-BVeRGroT.d.mts +24 -0
  68. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
  69. package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
  70. package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
  71. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
  73. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
  74. package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
  75. package/dist/ios-signing-DTTIChGs.mjs +489 -0
  76. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
  77. package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
  78. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
  79. package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
  80. package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
  81. package/dist/logs-BS00Anxf.mjs +352 -0
  82. package/dist/logs-CMm05HxN.mjs +255 -0
  83. package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
  84. package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/maintenance-run.mjs +122 -247
  87. package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
  88. package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
  89. package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
  90. package/dist/native-android-JACyjTZs.mjs +844 -0
  91. package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
  92. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  93. package/dist/native-run-DY_RLx3C.mjs +277 -0
  94. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
  95. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
  96. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
  97. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  98. package/dist/offload-worker.d.mts +1 -46
  99. package/dist/offload-worker.mjs +197 -66
  100. package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
  101. package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
  102. package/dist/paths-BsJMoPP2.mjs +12 -0
  103. package/dist/paths-DQtTgsBf.mjs +42 -0
  104. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
  105. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
  106. package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
  107. package/dist/prebuild-uD_ipCxF.mjs +449 -0
  108. package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
  109. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
  110. package/dist/pull-request-zjCSAryH.d.mts +120 -0
  111. package/dist/pull-requests.d.mts +1 -119
  112. package/dist/pull-requests.mjs +1 -1
  113. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
  114. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
  115. package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
  116. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
  117. package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
  118. package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
  119. package/dist/reload-DzjSl5P_.mjs +2 -0
  120. package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
  121. package/dist/result-DNiS-tTP.mjs +434 -0
  122. package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
  123. package/dist/selection-CSl9i5qU.mjs +619 -0
  124. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
  125. package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
  126. package/dist/server-expo-C-90EhcG.mjs +2 -0
  127. package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
  128. package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
  129. package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
  130. package/dist/settings-T1gtc3bf.mjs +114 -0
  131. package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
  132. package/dist/settings.schema.json +163 -16
  133. package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
  134. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
  135. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  136. package/dist/stage-BIS0ydsV.mjs +183 -0
  137. package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
  138. package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
  139. package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
  140. package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
  141. package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
  142. package/dist/stats-report-BFfMpWWF.mjs +16 -0
  143. package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
  144. package/dist/status-CSi6tlRq.mjs +2 -0
  145. package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
  146. package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
  147. package/dist/status-worker.d.mts +1 -0
  148. package/dist/status-worker.mjs +19 -0
  149. package/dist/stim-desktop-C12we7lz.mjs +112 -0
  150. package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
  151. package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
  152. package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
  153. package/dist/supervisor-run.d.mts +8 -17
  154. package/dist/supervisor-run.mjs +63 -27
  155. package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
  156. package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
  157. package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
  158. package/dist/toolchain-DhKwp9iI.mjs +2 -0
  159. package/dist/toolchain-Hp9sCpK4.mjs +873 -0
  160. package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
  161. package/dist/trigger-Bf3uDcpv.mjs +2 -0
  162. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
  163. package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
  164. package/dist/web-DknQETPs.mjs +2 -0
  165. package/dist/web-run.mjs +11 -12
  166. package/dist/windows-report.d.mts +1 -0
  167. package/dist/windows-report.mjs +18 -0
  168. package/dist/windows-report.ps1 +43 -0
  169. package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
  170. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
  171. package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
  172. package/dist/worktree-CK2MpJW5.mjs +2 -0
  173. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
  174. package/dist/xcode-Zlx3qjEs.mjs +896 -0
  175. package/package.json +7 -5
  176. package/shim/android-optimizations.gradle +7 -5
  177. package/dist/activity-BSV745Kz.mjs +0 -629
  178. package/dist/api-qvOzWyfO.mjs +0 -154
  179. package/dist/api-run.mjs +0 -294
  180. package/dist/app-install-BIKtMfPq.mjs +0 -896
  181. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  182. package/dist/budget-f7516Zk0.mjs +0 -4106
  183. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  184. package/dist/cancellation-C_22VQTH.mjs +0 -24
  185. package/dist/client-qQAaXSUs.mjs +0 -1771
  186. package/dist/config-BYDxlMY3.mjs +0 -562
  187. package/dist/device-DuiPJQSn.mjs +0 -290
  188. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  189. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  190. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  191. package/dist/exec-DqKo9GKJ.mjs +0 -268
  192. package/dist/git-DDfFzkMm.mjs +0 -45
  193. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  194. package/dist/logs-BGrEUaEs.mjs +0 -383
  195. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  196. package/dist/native-android-D5qc3-_v.mjs +0 -226
  197. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  198. package/dist/plan-C4TjdPsR.mjs +0 -260
  199. package/dist/prebuild-DYbucPXy.mjs +0 -212
  200. package/dist/preview-78a9kR_-.mjs +0 -1011
  201. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  202. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  203. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  204. package/dist/result-CaJg5Iat.mjs +0 -186
  205. package/dist/server-expo-Bigriskl.mjs +0 -2
  206. package/dist/settings-BORNaaCO.mjs +0 -402
  207. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  208. package/dist/stage-KNTQopqv.mjs +0 -157
  209. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  210. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  211. package/dist/toolchain-BFkAcstd.mjs +0 -2
  212. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  213. package/dist/trigger-9_USgmxz.mjs +0 -2
  214. package/dist/web-Dq-i3wzJ.mjs +0 -2
  215. package/dist/worktree-DpLdCHga.mjs +0 -2
  216. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  217. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -0,0 +1,1406 @@
1
+ import { _ as debugLog, m as reportSignal, t as getExecutor, u as operationState } from "./exec-_UbtCQcG.mjs";
2
+ import { $ as openAuthenticated, _ as loadConfig$1, at as sayHello, it as resolveEndpoint } from "./config-CYcBF478.mjs";
3
+ import { en as throwIfCancelled } from "./settings-CsJDEXgK.mjs";
4
+ import { n as gitAsync, t as git } from "./git-C3iTsJaZ.mjs";
5
+ import { r as createNdjsonWriter } from "./ndjson-CgE_o3Dl.mjs";
6
+ import { X as emptyCaches, br as discoverCaches, nt as planCacheEmptying } from "./budget-Bs6SxxzO.mjs";
7
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
8
+ import { d as runCancellationSignal } from "./native-run-DY_RLx3C.mjs";
9
+ import { n as CCACHE_UNAVAILABLE, t as CCACHE_NOT_RUN } from "./ccache-BJRZ9pG3.mjs";
10
+ import { n as COMPILATION_CACHE_UNAVAILABLE, t as COMPILATION_CACHE_NOT_RUN } from "./xcode-Zlx3qjEs.mjs";
11
+ import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
12
+ import { h as readRubyVersion, u as toolchainMismatches } from "./toolchain-Hp9sCpK4.mjs";
13
+ import { l as pairedMachines, r as namedBuildMachine, t as OffloadRefusal } from "./selection-CSl9i5qU.mjs";
14
+ import { r as manifestDigest } from "./native-inputs-TRTTTRxt.mjs";
15
+ import { a as nativeGradleTransfer, c as checkNativeTransferMembership, d as sourceManifest, l as gitVisiblePaths, u as nativeTransferManifest } from "./native-gradle-inputs-Bep9JcSF.mjs";
16
+ import { createHash, webcrypto } from "node:crypto";
17
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
18
+ import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
19
+ import { LOG_ROTATE_BYTES, ambientContext, rotatedLogPath, withDirLock } from "@stim-cli/core";
20
+ import { MAINTENANCE_LOG_NAME, OFFLOAD_MODES, automaticMachineEnabled, isJsonObject, maintenanceDir, maintenanceNdjsonFile, requireAutomaticMachine, runId, saturation, workspaceLogsDir } from "@stim-cli/core/state";
21
+ import { WebSocket } from "ws";
22
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
23
+ import { readFile, readlink } from "node:fs/promises";
24
+ //#region src/maintenance/log.ts
25
+ function maintenanceLogger(settings, pass, trigger) {
26
+ const file = maintenanceNdjsonFile();
27
+ const rotated = rotatedLogPath(file);
28
+ withDirLock(`${file}.lock`, () => {
29
+ try {
30
+ if (Date.now() - statSync(rotated).mtimeMs > settings.logRetentionDays * 864e5) unlinkSync(rotated);
31
+ } catch {}
32
+ });
33
+ const writer = createNdjsonWriter(file, { maxBytes: settings.logMaxMb * 1024 ** 2 });
34
+ return {
35
+ write(event, level, msg, fields = {}) {
36
+ if (level === "debug" && !settings.logChecks) return null;
37
+ const record = {
38
+ ...fields,
39
+ ts: Date.now(),
40
+ src: "maintenance",
41
+ level,
42
+ msg,
43
+ event,
44
+ pass,
45
+ trigger,
46
+ mode: settings.mode === "on" ? "on" : "report"
47
+ };
48
+ if (!writer.write(record)) throw writer.lastError ?? /* @__PURE__ */ new Error(`Could not append ${file}`);
49
+ if (typeof fields.workspace === "string" && [
50
+ "maintenance_action",
51
+ "maintenance_failure",
52
+ "maintenance_skip"
53
+ ].includes(event)) {
54
+ const workspace = createNdjsonWriter(join(workspaceLogsDir(fields.workspace), MAINTENANCE_LOG_NAME), { maxBytes: LOG_ROTATE_BYTES });
55
+ try {
56
+ if (!workspace.write(record)) throw workspace.lastError ?? /* @__PURE__ */ new Error("Could not append workspace maintenance log");
57
+ } finally {
58
+ workspace.close();
59
+ }
60
+ }
61
+ return record;
62
+ },
63
+ close: () => writer.close()
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/cache/compilation-cache-corruption.ts
68
+ const COMPILATION_CACHE_CORRUPT = "STIM_COMPILATION_CACHE_CORRUPT";
69
+ function emptyCompilationCache(dir) {
70
+ const results = [];
71
+ const cache = discoverCaches().find((candidate) => canonicalPath(candidate.dir) === canonicalPath(dir));
72
+ if (cache?.source !== "registered" || cache.prune !== "atomic") return `${dir} is not a registered cache Stim empties whole`;
73
+ const [planned] = planCacheEmptying([cache], true);
74
+ emptyCaches(planned ? [planned] : [], {
75
+ log: () => {},
76
+ record: (kind, status, label, { id = null, bytes = null, detail = null } = {}) => {
77
+ results.push({
78
+ kind,
79
+ status,
80
+ label,
81
+ id,
82
+ bytes,
83
+ detail
84
+ });
85
+ },
86
+ fail: () => {}
87
+ });
88
+ const refused = results.find((result) => result.status !== "done");
89
+ return refused ? refused.detail ?? `could not empty ${dir}` : null;
90
+ }
91
+ function recordInMaintenanceLog(workspace, msg, fields, cleared) {
92
+ try {
93
+ mkdirSync(maintenanceDir(), { recursive: true });
94
+ const logger = maintenanceLogger(resolveMaintenanceSettings(), `build-${process.pid}`, "ios build");
95
+ try {
96
+ logger.write(cleared ? "maintenance_action" : "maintenance_failure", cleared ? "info" : "warn", msg, {
97
+ ...fields,
98
+ workspace
99
+ });
100
+ } finally {
101
+ logger.close();
102
+ }
103
+ } catch {}
104
+ }
105
+ /**
106
+ * Empties the shared compilation cache a failed build reported corrupt, through the path `gc --delete --cache` uses,
107
+ * unless another build may be using it. It never retries the build; the result is the refusal to print.
108
+ */
109
+ function clearCorruptCompilationCache(corruption, workspace) {
110
+ const { dir, line } = corruption;
111
+ let blocked;
112
+ try {
113
+ blocked = emptyCompilationCache(dir);
114
+ } catch (error) {
115
+ blocked = error instanceof Error ? error.message : String(error);
116
+ }
117
+ const fields = {
118
+ target: dir,
119
+ category: "compilation-cache",
120
+ reason: line,
121
+ ...blocked ? { blocked } : {}
122
+ };
123
+ if (blocked) {
124
+ const message = `The shared compilation cache at ${dir} is corrupt (the compiler reported: ${line}). Stim did not empty it: ${blocked}.`;
125
+ recordInMaintenanceLog(workspace, message, fields, false);
126
+ return {
127
+ code: COMPILATION_CACHE_CORRUPT,
128
+ message,
129
+ remedy: "Once the reason above is resolved, run `stim gc --delete --cache \"compilation cache\"`, then run the same command again."
130
+ };
131
+ }
132
+ const message = `The shared compilation cache at ${dir} was corrupt (the compiler reported: ${line}). Stim emptied it.`;
133
+ recordInMaintenanceLog(workspace, message, fields, true);
134
+ return {
135
+ code: COMPILATION_CACHE_CORRUPT,
136
+ message,
137
+ remedy: "Run the same command again. Its build starts with an empty compilation cache."
138
+ };
139
+ }
140
+ //#endregion
141
+ //#region src/remote-log.ts
142
+ const sinks = /* @__PURE__ */ new WeakMap();
143
+ /** Where this operation's failed remote requests are written for `stim logs`. */
144
+ function setRemoteLogSink(context, next) {
145
+ if (next) sinks.set(context, next);
146
+ else sinks.delete(context);
147
+ }
148
+ function reportRemoteFailure(context, event, fields) {
149
+ try {
150
+ if (context) sinks.get(context)?.({
151
+ src: "build",
152
+ level: "warn",
153
+ event,
154
+ ...fields
155
+ });
156
+ } catch {}
157
+ }
158
+ //#endregion
159
+ //#region src/offload/client.ts
160
+ const CONNECT_TIMEOUT_MS = 1e4;
161
+ const OFFER_TIMEOUT_MS = 2e4;
162
+ const REQUEST_TIMEOUT_MS = 12e4;
163
+ const JOB_TIMEOUT_MS = 39e5;
164
+ const PING_MS = 15e3;
165
+ const SILENT_MS = 6e4;
166
+ const CLOSE_TIMEOUT_MS = 2e3;
167
+ const CLOSE_ABNORMAL = 1006;
168
+ const TURNED_AWAY = /* @__PURE__ */ new Set([
169
+ "unauthorized",
170
+ "forbidden",
171
+ "protocol-unsupported",
172
+ "server-key-mismatch"
173
+ ]);
174
+ const RESUME_WINDOW_MS = 18e4;
175
+ const RESUME_DELAY_MS = 2e3;
176
+ const RESUME_MAX_DELAY_MS = 15e3;
177
+ const PAGE_BYTES = 49152;
178
+ const CHUNK_BYTES = 61440;
179
+ const MAX_BUFFERED = 8388608;
180
+ const DIGEST_BYTES = 32;
181
+ function offloadMode(env = ambientContext()?.env ?? process.env) {
182
+ const raw = env.STIM_REMOTE_BUILD_MODE || loadConfig$1()?.remote?.buildMode;
183
+ return OFFLOAD_MODES.includes(raw) ? raw : "auto";
184
+ }
185
+ function buildPlacementCandidates(selected) {
186
+ const config = loadConfig$1();
187
+ return {
188
+ localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local", config),
189
+ mode: selected === "local" ? "off" : offloadMode(),
190
+ machines: selected === "local" ? [] : namedBuildMachine(selected) ? pairedMachines([selected]).slice(0, 1) : pairedMachines().filter((entry) => automaticMachineEnabled("build", entry.machine, config))
191
+ };
192
+ }
193
+ function remotePhaseText(name, msg, machine) {
194
+ const text = msg.trim();
195
+ return `${text.startsWith(`${name} `) ? text.slice(name.length).trimStart() : text} (on ${machine})`;
196
+ }
197
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
198
+ function capacityText(capacity) {
199
+ const builds = capacity.maxBuilds > 0 ? `${capacity.builds} of ${capacity.maxBuilds} build slots busy` : plural(capacity.builds, "build");
200
+ return `load ${capacity.loadPerCore}/core, ${builds}`;
201
+ }
202
+ function placementLoad(choice) {
203
+ const { loadPerCore, builds, maxBuilds } = choice.offer.capacity;
204
+ if (typeof loadPerCore !== "number" || typeof builds !== "number") return "";
205
+ return `; ${capacityText({
206
+ loadPerCore,
207
+ builds,
208
+ maxBuilds: maxBuilds ?? 0
209
+ })} there`;
210
+ }
211
+ /**
212
+ * Whether a build may go to a remote machine at all, before any machine is asked. `auto` keeps the build here while
213
+ * this machine has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
214
+ * A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
215
+ */
216
+ function offloadPlacement({ mode, machines, here, unsupported, localEnabled = true, selected = "auto" }) {
217
+ if (namedBuildMachine(selected)) {
218
+ if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote machine cannot take it`);
219
+ if (machines === 0) throw new OffloadRefusal(selected, "no remote machine is paired");
220
+ return {
221
+ offload: true,
222
+ code: "named",
223
+ reason: `selected with --remote-build ${selected}`
224
+ };
225
+ }
226
+ if (selected === "local") return {
227
+ offload: false,
228
+ code: "local-selected",
229
+ reason: "--remote-build local"
230
+ };
231
+ if (!localEnabled) {
232
+ const reason = mode === "off" ? "remote.buildMode is off" : unsupported ?? (machines === 0 ? "no enabled remote machine is paired" : null);
233
+ if (reason) throw new OffloadRefusal("auto", `${reason}; local is disabled in remote.buildPoolDisabled`);
234
+ return {
235
+ offload: true,
236
+ code: "local-disabled",
237
+ reason: "local is disabled in remote.buildPoolDisabled"
238
+ };
239
+ }
240
+ if (mode === "off") return {
241
+ offload: false,
242
+ code: "mode-off",
243
+ reason: "remote.buildMode is off"
244
+ };
245
+ if (machines === 0) return {
246
+ offload: false,
247
+ code: "no-remote-mac",
248
+ reason: "no remote machine is paired"
249
+ };
250
+ if (unsupported) return {
251
+ offload: false,
252
+ code: "unsupported",
253
+ reason: unsupported
254
+ };
255
+ if (mode === "force") return {
256
+ offload: true,
257
+ code: "forced",
258
+ reason: "remote.buildMode is force"
259
+ };
260
+ const busy = saturation(here);
261
+ return busy ? {
262
+ offload: true,
263
+ code: "this-mac-busy",
264
+ reason: `this machine is busy: ${busy} (${capacityText(here)})`
265
+ } : {
266
+ offload: false,
267
+ code: "this-mac-free",
268
+ reason: `${capacityText(here)} here`
269
+ };
270
+ }
271
+ function matchingArtifact(offer, identity) {
272
+ const cached = offer.artifact;
273
+ return identity && cached && cached.cacheKey === identity.cacheKey && cached.fingerprint === identity.fingerprint && typeof cached.name === "string" && basename(cached.name) === cached.name && cached.name.endsWith(".app") && Number.isSafeInteger(cached.size) && cached.size > 0 && typeof cached.sha256 === "string" && /^[a-f0-9]{64}$/.test(cached.sha256) ? cached : null;
274
+ }
275
+ const gb = (bytes) => (bytes / 1024 ** 3).toFixed(1);
276
+ /** Every reason a machine's offer cannot take this build, in either mode; empty when it can. */
277
+ function offerProblems(offer, target) {
278
+ const problems = toolchainMismatches(target, offer.toolchain);
279
+ const capacity = offer.capacity;
280
+ const { running, max, diskFreeBytes, minDiskFreeBytes, declined } = capacity;
281
+ if (diskFreeBytes !== null && diskFreeBytes < minDiskFreeBytes) problems.push({
282
+ code: "disk",
283
+ reason: `${gb(diskFreeBytes)} GB free, needs ${gb(minDiskFreeBytes)} GB`
284
+ });
285
+ else if (declined || declined === void 0 && running >= max) {
286
+ const why = declined ?? `already running ${running} offloaded build(s), its limit`;
287
+ const load = typeof capacity.loadPerCore === "number" && typeof capacity.builds === "number" ? `; ${capacityText({
288
+ loadPerCore: capacity.loadPerCore,
289
+ builds: capacity.builds,
290
+ maxBuilds: capacity.maxBuilds ?? 0
291
+ })}` : "";
292
+ problems.push({
293
+ code: "busy",
294
+ reason: `busy (${why}${load})`
295
+ });
296
+ }
297
+ return problems;
298
+ }
299
+ /**
300
+ * The asked machines that can take the build, best first, and one reason per machine that cannot. A machine with any
301
+ * `offerProblems` never takes it. In `auto`, a machine must also be expected to build faster than this machine: while
302
+ * every local build slot is busy any machine that accepts will do, otherwise its load per core must be lower than
303
+ * this machine's. A named selection considers only that machine and skips this local-capacity gating.
304
+ * A machine that reports no load (a stim-server older than capacity) takes an `auto` build only while
305
+ * every local slot is busy. The rest rank warmest first, then least loaded.
306
+ */
307
+ function pickOffer({ mode, here, offers, target, localEnabled = true, selected = "auto", artifact, cacheOnly = false }) {
308
+ const slotsFull = here.maxBuilds > 0 && here.builds >= here.maxBuilds;
309
+ const reasons = [];
310
+ const candidates = [];
311
+ const skip = (machine, code, msg, detail) => {
312
+ reasons.push(`${machine}: ${msg}`);
313
+ candidates.push({
314
+ machine,
315
+ code,
316
+ msg,
317
+ ...detail ? { detail } : {}
318
+ });
319
+ };
320
+ const ranked = [];
321
+ offers.forEach(({ machine, offer, failure }, index) => {
322
+ if (namedBuildMachine(selected) && machine !== selected) return;
323
+ if (!offer) return skip(machine, "unreachable", failure ?? "no offer");
324
+ if (matchingArtifact(offer, artifact)) {
325
+ ranked.push({
326
+ index,
327
+ score: Number.POSITIVE_INFINITY,
328
+ load: 0
329
+ });
330
+ candidates.push({
331
+ machine,
332
+ code: "artifact-hit",
333
+ msg: "has the exact completed artifact"
334
+ });
335
+ return;
336
+ }
337
+ if (cacheOnly) return skip(machine, "artifact-miss", "no matching completed artifact");
338
+ const problems = offerProblems(offer, target);
339
+ if (problems.length) return skip(machine, problemCode(problems[0]), problems.map((problem) => problem.reason).join("; "), problems.map((problem) => problem.code));
340
+ const load = offer.capacity.loadPerCore;
341
+ if (selected === "auto" && localEnabled && mode === "auto" && !slotsFull) {
342
+ if (typeof load !== "number") return skip(machine, "load", "capacity unknown (older stim-server) while this machine has a free slot");
343
+ if (load >= here.loadPerCore) return skip(machine, "load", `no less loaded (load ${load}/core there, ${here.loadPerCore}/core here)`);
344
+ }
345
+ const score = Number(offer.warm.checkout) + Number(offer.warm.dependencies) + Number(offer.warm.build);
346
+ ranked.push({
347
+ index,
348
+ score,
349
+ load: typeof load === "number" ? load : Number.POSITIVE_INFINITY
350
+ });
351
+ candidates.push({
352
+ machine,
353
+ code: "accepted",
354
+ msg: typeof load === "number" ? `can take the build (load ${load}/core there)` : "can take the build"
355
+ });
356
+ });
357
+ ranked.sort((a, b) => b.score - a.score || a.load - b.load);
358
+ return {
359
+ order: ranked.map((each) => each.index),
360
+ reasons,
361
+ candidates
362
+ };
363
+ }
364
+ function problemCode({ code }) {
365
+ return code === "disk" || code === "busy" || code === "unreachable" ? code : code === "runtime" ? "no-matching-device" : "version-mismatch";
366
+ }
367
+ function closeOnExit(socket, context) {
368
+ if (!context) return;
369
+ const state = operationState(context);
370
+ if (!state.takesSignals) return;
371
+ const close = () => socket.close(1e3);
372
+ const unregister = () => state.exitCleanups.delete(close);
373
+ state.exitCleanups.add(close);
374
+ socket.once("close", unregister);
375
+ return unregister;
376
+ }
377
+ /**
378
+ * One authenticated connection to another machine's stim-server, for build offload or device hosting. Closing it cancels
379
+ * its build jobs there; a connection that drops, or that stays silent for `SILENT_MS` after `watch()`, leaves them
380
+ * running for a while so a new connection can `build.attach` to them.
381
+ */
382
+ var BuildConnection = class BuildConnection {
383
+ nextId = 2;
384
+ pending = /* @__PURE__ */ new Map();
385
+ progress = null;
386
+ binary = null;
387
+ closed = null;
388
+ heard = Date.now();
389
+ watched = false;
390
+ ended = false;
391
+ keepalive;
392
+ socket;
393
+ features;
394
+ context;
395
+ unregisterExitCleanup;
396
+ constructor(socket, features, context) {
397
+ this.socket = socket;
398
+ this.features = features;
399
+ this.context = context;
400
+ this.unregisterExitCleanup = closeOnExit(socket, context);
401
+ this.keepalive = setInterval(() => {
402
+ if (this.watched && Date.now() - this.heard > SILENT_MS) return void socket.terminate();
403
+ socket.ping();
404
+ }, PING_MS);
405
+ this.keepalive.unref();
406
+ socket.on("pong", () => this.heard = Date.now());
407
+ socket.on("message", (data, isBinary) => {
408
+ this.heard = Date.now();
409
+ const frame = Buffer.isBuffer(data) ? data : Buffer.concat(Array.isArray(data) ? data : [Buffer.from(data)]);
410
+ if (isBinary) return this.binary?.(frame);
411
+ let message;
412
+ try {
413
+ message = JSON.parse(frame.toString("utf8"));
414
+ } catch {
415
+ return;
416
+ }
417
+ if (!isJsonObject(message)) return;
418
+ if (message.event === "build.progress") return this.progress?.(message);
419
+ const resolve = typeof message.id === "number" ? this.pending.get(message.id) : void 0;
420
+ if (resolve) {
421
+ this.pending.delete(message.id);
422
+ resolve(message);
423
+ }
424
+ });
425
+ socket.on("close", (code, reason) => {
426
+ clearInterval(this.keepalive);
427
+ this.closed = `the connection closed (${code}${reason.length ? ` ${reason.toString()}` : ""})`;
428
+ for (const resolve of this.pending.values()) resolve({ error: {
429
+ code: "closed",
430
+ message: this.closed
431
+ } });
432
+ this.pending.clear();
433
+ const dropped = code === CLOSE_ABNORMAL;
434
+ this.progress?.({
435
+ job: "",
436
+ outcome: {
437
+ ok: false,
438
+ code: dropped ? "dropped" : "closed",
439
+ message: this.closed
440
+ }
441
+ });
442
+ });
443
+ socket.on("error", () => {});
444
+ }
445
+ /** Resolves why it could not connect; `refused` when the machine answered and turned this machine away. */
446
+ static async open(target, serverKey, timeoutMs, capability = "build", signal) {
447
+ const context = ambientContext();
448
+ const started = performance.now();
449
+ const result = await BuildConnection.connect(target, serverKey, timeoutMs, capability, signal, context);
450
+ const ms = Math.round(performance.now() - started);
451
+ debugLog.log("remote_connect", {
452
+ host: target.host,
453
+ capability,
454
+ ms,
455
+ timeoutMs,
456
+ ...result instanceof BuildConnection ? { ok: true } : {
457
+ ok: false,
458
+ failure: result.failure,
459
+ code: result.code
460
+ }
461
+ });
462
+ if (!(result instanceof BuildConnection)) reportRemoteFailure(context, "remote_connect_failed", {
463
+ host: target.host,
464
+ capability,
465
+ ms,
466
+ timeoutMs,
467
+ msg: result.failure,
468
+ code: result.code
469
+ });
470
+ return result;
471
+ }
472
+ static async connect(target, serverKey, timeoutMs, capability, signal, context) {
473
+ const started = Date.now();
474
+ const socket = await openAuthenticated(target, {
475
+ access: capability,
476
+ serverKey
477
+ }, timeoutMs, signal, context);
478
+ if (!("send" in socket)) return socket;
479
+ const reply = await sayHello(socket, {
480
+ client: {
481
+ name: "stim",
482
+ version: "1",
483
+ runId: runId()
484
+ },
485
+ access: capability
486
+ }, Math.max(1, timeoutMs - (Date.now() - started)));
487
+ if ("error" in reply) {
488
+ socket.terminate();
489
+ return {
490
+ failure: reply.error.message,
491
+ refused: TURNED_AWAY.has(reply.error.code),
492
+ code: reply.error.code
493
+ };
494
+ }
495
+ const capabilities = reply.result.capabilities;
496
+ if (!Array.isArray(capabilities) || !capabilities.includes(capability)) {
497
+ socket.terminate();
498
+ return {
499
+ failure: `it has not granted this machine ${capability === "build" ? "build" : "hosting"} access`,
500
+ refused: true,
501
+ code: "forbidden"
502
+ };
503
+ }
504
+ const features = reply.result.features;
505
+ return new BuildConnection(socket, Array.isArray(features) ? features.filter((each) => typeof each === "string") : [], context);
506
+ }
507
+ supports(feature) {
508
+ return this.features.includes(feature);
509
+ }
510
+ request(method, params, timeoutMs = REQUEST_TIMEOUT_MS) {
511
+ if (this.closed) return Promise.resolve({ error: {
512
+ code: "closed",
513
+ message: this.closed
514
+ } });
515
+ const id = this.nextId++;
516
+ const started = performance.now();
517
+ const done = (reply) => {
518
+ const ms = Math.round(performance.now() - started);
519
+ debugLog.log("remote_request", {
520
+ method,
521
+ ms,
522
+ ..."error" in reply ? {
523
+ ok: false,
524
+ code: reply.error.code
525
+ } : { ok: true }
526
+ });
527
+ if ("error" in reply) reportRemoteFailure(this.context, "remote_request_failed", {
528
+ method,
529
+ ms,
530
+ code: reply.error.code,
531
+ msg: reply.error.message
532
+ });
533
+ return reply;
534
+ };
535
+ return new Promise((resolve) => {
536
+ const timer = setTimeout(() => {
537
+ this.pending.delete(id);
538
+ resolve(done({ error: {
539
+ code: "timeout",
540
+ message: `${method} got no reply in ${timeoutMs / 1e3} s`
541
+ } }));
542
+ }, timeoutMs);
543
+ this.pending.set(id, (reply) => {
544
+ clearTimeout(timer);
545
+ resolve(done(reply));
546
+ });
547
+ this.socket.send(JSON.stringify({
548
+ id,
549
+ method,
550
+ params
551
+ }));
552
+ });
553
+ }
554
+ async sendBinary(frame) {
555
+ if (this.closed) return;
556
+ this.socket.send(frame, { binary: true });
557
+ while (this.socket.bufferedAmount > MAX_BUFFERED && !this.closed) await new Promise((resolve) => setTimeout(resolve, 5));
558
+ }
559
+ onProgress(listener) {
560
+ this.progress = listener;
561
+ }
562
+ onBinary(listener) {
563
+ this.binary = listener;
564
+ }
565
+ get failure() {
566
+ return this.closed;
567
+ }
568
+ /**
569
+ * Treats `SILENT_MS` without a frame as a dropped connection from now on. Only while waiting on a job: a long
570
+ * upload or this process hashing the checkout can delay pongs without anything being wrong.
571
+ */
572
+ watch() {
573
+ this.heard = Date.now();
574
+ this.watched = true;
575
+ }
576
+ /** Closes with a close frame, which tells the machine to cancel this connection's jobs. */
577
+ close() {
578
+ if (this.ended) return;
579
+ this.forget();
580
+ if (this.socket.readyState === WebSocket.CLOSED) return;
581
+ const timer = setTimeout(() => this.socket.terminate(), CLOSE_TIMEOUT_MS);
582
+ this.socket.once("close", () => clearTimeout(timer));
583
+ this.socket.close(1e3);
584
+ }
585
+ /** Ends the connection without a close frame, which leaves its jobs on the machine for a new connection. */
586
+ drop() {
587
+ if (this.ended) return;
588
+ this.forget();
589
+ this.socket.terminate();
590
+ }
591
+ forget() {
592
+ this.ended = true;
593
+ this.closed ??= "the connection was closed by this client";
594
+ for (const resolve of this.pending.values()) resolve({ error: {
595
+ code: "closed",
596
+ message: this.closed
597
+ } });
598
+ this.pending.clear();
599
+ this.progress = null;
600
+ this.binary = null;
601
+ clearInterval(this.keepalive);
602
+ this.socket.removeAllListeners("close");
603
+ this.unregisterExitCleanup?.();
604
+ }
605
+ };
606
+ function replyError(reply) {
607
+ return "error" in reply ? `${reply.error.code}: ${reply.error.message}` : null;
608
+ }
609
+ /** The repository on the remote machine: one area per git common dir, shared by all its worktrees. */
610
+ function repoIdentity(projectRoot) {
611
+ const repoRoot = realpathSync(git(projectRoot, ["rev-parse", "--show-toplevel"]));
612
+ return repositoryIdentity(projectRoot, repoRoot, realpathSync(git(repoRoot, [
613
+ "rev-parse",
614
+ "--path-format=absolute",
615
+ "--git-common-dir"
616
+ ])));
617
+ }
618
+ function repositoryIdentity(projectRoot, repoRoot, common) {
619
+ const digest = createHash("sha256").update(common).digest("hex").slice(0, 12);
620
+ const name = (basename(dirname(common)) || "repo").replace(/[^A-Za-z0-9._-]/g, "_").replace(/^[^A-Za-z0-9]+/, "");
621
+ const lock = [
622
+ "pnpm-lock.yaml",
623
+ "yarn.lock",
624
+ "package-lock.json"
625
+ ].find((file) => existsSync(join(repoRoot, file)));
626
+ return {
627
+ repoRoot,
628
+ project: relative(repoRoot, realpathSync(projectRoot)),
629
+ repo: `${name.slice(0, 60) || "repo"}-${digest}`,
630
+ lockfile: lock ? createHash("sha256").update(readFileSync(join(repoRoot, lock))).digest("hex") : null
631
+ };
632
+ }
633
+ /** Why the native inputs cannot be sent to a remote machine, such as an ignored file among them, or null when they can. */
634
+ async function nativeTransferRefusal(repoRoot, snapshot, signal) {
635
+ try {
636
+ checkNativeTransferMembership(snapshot, await gitVisiblePaths(repoRoot, signal));
637
+ return null;
638
+ } catch (error) {
639
+ signal?.throwIfAborted();
640
+ return error.message.split("\n")[0];
641
+ }
642
+ }
643
+ async function blobContent(repoRoot, file, signal) {
644
+ const absolute = join(repoRoot, file.path);
645
+ return file.kind === "directory" ? Buffer.alloc(0) : file.kind === "link" ? Buffer.from(await readlink(absolute)) : await readFile(absolute, { signal });
646
+ }
647
+ /** Closes every connection the choice still holds. */
648
+ function closeOffload(choice) {
649
+ choice.connection.close();
650
+ for (const each of choice.runnersUp.splice(0)) each.connection.close();
651
+ }
652
+ const NATIVE_BUILD_FEATURE = {
653
+ xcode: "native-xcode-build",
654
+ gradle: "native-gradle-build"
655
+ };
656
+ const unsupportedNative = (provider) => `This worker does not support native ${provider === "xcode" ? "Xcode" : "Gradle"} builds.`;
657
+ const ANDROID_PACKAGE = /^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/;
658
+ function nativeTransferInput(native) {
659
+ return native?.provider === "xcode" ? native.snapshot : native?.transfer;
660
+ }
661
+ /** Connects to one paired machine and asks it for an offer; the caller closes the connection it returns. */
662
+ async function probeMachine(credential, identity, { connectMs = CONNECT_TIMEOUT_MS, offerMs = OFFER_TIMEOUT_MS } = {}) {
663
+ const target = await resolveEndpoint(credential, void 0, reportSignal(ambientContext()));
664
+ if (typeof target === "string") return {
665
+ credential,
666
+ failure: target
667
+ };
668
+ const connection = await BuildConnection.open(target, credential.serverKey, connectMs, "build", reportSignal(ambientContext()));
669
+ if (!(connection instanceof BuildConnection)) return {
670
+ credential,
671
+ failure: connection.failure
672
+ };
673
+ if (identity.native && !connection.supports(NATIVE_BUILD_FEATURE[identity.native])) {
674
+ connection.close();
675
+ return {
676
+ credential,
677
+ failure: unsupportedNative(identity.native)
678
+ };
679
+ }
680
+ const native = identity.native === "gradle" || identity.native && connection.supports("native-xcode-toolchain") ? identity.native : void 0;
681
+ const reply = await connection.request("build.offer", {
682
+ repo: identity.repo,
683
+ ...identity.lockfile ? { lockfile: identity.lockfile } : {},
684
+ ...native ? { native } : identity.rubyVersion ? { rubyVersion: identity.rubyVersion } : {},
685
+ ...native === "xcode" && identity.artifact && connection.supports("build-artifact-cache") ? { artifact: identity.artifact } : {}
686
+ }, offerMs);
687
+ const failure = replyError(reply);
688
+ if (failure || !("result" in reply)) {
689
+ connection.close();
690
+ return {
691
+ credential,
692
+ failure: failure ?? "no offer"
693
+ };
694
+ }
695
+ return {
696
+ credential,
697
+ connection,
698
+ offer: reply.result
699
+ };
700
+ }
701
+ /**
702
+ * Asks every paired machine for an offer in parallel and keeps the connection to the one `pickOffer` chooses, or
703
+ * returns why none takes the build. A named selection probes only its credential and skips auto load gating.
704
+ */
705
+ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machines = pairedMachines(), selected = "auto", onCandidates, artifact, cacheOnly = false }) {
706
+ const started = Date.now();
707
+ let identity;
708
+ try {
709
+ identity = repoIdentity(projectRoot);
710
+ } catch (error) {
711
+ return `this app is not in a git checkout (${error.message.split("\n")[0]})`;
712
+ }
713
+ if (namedBuildMachine(selected)) machines = machines.filter((each) => each.machine === selected).slice(0, 1);
714
+ else machines = machines.filter((each) => automaticMachineEnabled("build", each.machine));
715
+ const rubyVersion = readRubyVersion(projectRoot) ?? void 0;
716
+ const asked = await Promise.all(machines.map((credential) => probeMachine(credential, {
717
+ ...identity,
718
+ rubyVersion,
719
+ ...target.platform !== "macos" ? { native: target.native } : {},
720
+ ...artifact ? { artifact } : {}
721
+ })));
722
+ const { order, reasons, candidates } = pickOffer({
723
+ localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local"),
724
+ selected,
725
+ mode,
726
+ here,
727
+ target,
728
+ artifact,
729
+ cacheOnly,
730
+ offers: asked.map((each) => ({
731
+ machine: each.credential.machine,
732
+ offer: "offer" in each ? each.offer : null,
733
+ ..."failure" in each ? { failure: each.failure } : {}
734
+ }))
735
+ });
736
+ asked.forEach((each, at) => {
737
+ if (!order.includes(at) && "connection" in each) each.connection.close();
738
+ });
739
+ onCandidates?.(candidates);
740
+ if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote machine is paired";
741
+ for (const reason of reasons) note(reason);
742
+ const [first, ...rest] = order.map((at) => {
743
+ const pick = asked[at];
744
+ return {
745
+ machine: pick.credential.machine,
746
+ credential: pick.credential,
747
+ connection: pick.connection,
748
+ offer: pick.offer
749
+ };
750
+ });
751
+ return {
752
+ ...first,
753
+ mode,
754
+ here,
755
+ target,
756
+ offerMs: Date.now() - started,
757
+ identity,
758
+ runnersUp: rest,
759
+ automatic: selected === "auto",
760
+ cacheOnly
761
+ };
762
+ }
763
+ function validArtifactIdentity(request, fingerprint) {
764
+ if (request.platform === "macos") return true;
765
+ return request.native?.provider === "gradle" ? fingerprint === null : typeof fingerprint === "string" && fingerprint.length > 0;
766
+ }
767
+ const ARTIFACT_NAME = {
768
+ ios: /^[^/]+\.app$/,
769
+ android: /^[^/]+\.apk$/,
770
+ macos: /^[^/]+\.app$/
771
+ };
772
+ /**
773
+ * Reconnects to the machine that runs `job` and takes the job over with `build.attach`, retrying until
774
+ * `RESUME_WINDOW_MS`, shorter than the machine's grace for a dropped connection, has passed. Returns why it
775
+ * could not.
776
+ */
777
+ async function resumeJob(credential, job, abandoned, signal, native) {
778
+ const deadline = Date.now() + RESUME_WINDOW_MS;
779
+ let delay = RESUME_DELAY_MS;
780
+ let last = "no attempt";
781
+ while (!abandoned()) {
782
+ const target = await resolveEndpoint(credential);
783
+ const connection = typeof target === "string" ? {
784
+ failure: target,
785
+ refused: false
786
+ } : await BuildConnection.open(target, credential.serverKey, CONNECT_TIMEOUT_MS);
787
+ if (!(connection instanceof BuildConnection)) {
788
+ if (connection.refused) return `it turned this machine away (${connection.failure})`;
789
+ last = connection.failure;
790
+ } else {
791
+ if (abandoned()) {
792
+ connection.close();
793
+ break;
794
+ }
795
+ if (native && !connection.supports(NATIVE_BUILD_FEATURE[native])) {
796
+ connection.close();
797
+ return unsupportedNative(native);
798
+ }
799
+ const early = [];
800
+ connection.onProgress((event) => early.push(event));
801
+ const cancel = () => connection.close();
802
+ signal?.addEventListener("abort", cancel, { once: true });
803
+ const reply = await connection.request("build.attach", { job }, OFFER_TIMEOUT_MS);
804
+ signal?.removeEventListener("abort", cancel);
805
+ if (abandoned()) {
806
+ connection.close();
807
+ break;
808
+ }
809
+ if ("result" in reply) {
810
+ const outcome = isJsonObject(reply.result) ? reply.result.outcome : null;
811
+ return {
812
+ connection,
813
+ outcome: isJsonObject(outcome) ? outcome : null,
814
+ early
815
+ };
816
+ }
817
+ last = replyError(reply);
818
+ if (reply.error.code !== "closed" && reply.error.code !== "timeout") {
819
+ connection.close();
820
+ return `the machine did not hand the build back (${last})`;
821
+ }
822
+ connection.drop();
823
+ }
824
+ if (abandoned() || Date.now() + delay > deadline) break;
825
+ await setTimeout$1(delay, void 0, { signal }).catch(() => {});
826
+ delay = Math.min(delay * 2, RESUME_MAX_DELAY_MS);
827
+ }
828
+ return `no connection to it within ${RESUME_WINDOW_MS / 6e4} min (${last})`;
829
+ }
830
+ /** Mirrors the checkout on the machine: the manifest in pages, then every blob it lacks. */
831
+ async function syncSource(connection, identity, onEnter, native, signal) {
832
+ onEnter("sync");
833
+ const syncStarted = Date.now();
834
+ const manifest = native && "declaration" in native ? (await nativeGradleTransfer(join(identity.repoRoot, identity.project), native.declaration, signal)).files : native ? await nativeTransferManifest(identity.repoRoot, native, await sourceManifest(identity.repoRoot, false, signal), signal) : await sourceManifest(identity.repoRoot, false, signal);
835
+ if (native && "declaration" in native && manifestDigest(manifest) !== native.digest) return { failure: "The native Gradle source changed before upload." };
836
+ const bySha = new Map(manifest.map((file) => [file.sha256, file]));
837
+ const missing = [];
838
+ for (let index = 0; index < manifest.length || index === 0;) {
839
+ const page = [];
840
+ let bytes = 0;
841
+ while (index < manifest.length && (page.length === 0 || bytes < PAGE_BYTES)) {
842
+ const file = manifest[index++];
843
+ bytes += Buffer.byteLength(JSON.stringify(file)) + 1;
844
+ page.push(file);
845
+ }
846
+ const done = index >= manifest.length;
847
+ const reply = await connection.request("build.sync", {
848
+ repo: identity.repo,
849
+ files: page,
850
+ done
851
+ });
852
+ const failure = replyError(reply);
853
+ if (failure || !("result" in reply)) return { failure: `sync: ${failure ?? "no reply"}` };
854
+ missing.push(...reply.result.missing ?? []);
855
+ if (done) break;
856
+ }
857
+ let uploadedBytes = 0;
858
+ for (const digest of missing) {
859
+ const file = bySha.get(digest);
860
+ if (!file) return { failure: `sync: the machine asked for an unknown blob ${digest}` };
861
+ const content = await blobContent(identity.repoRoot, file, signal);
862
+ if (Buffer.from(await webcrypto.subtle.digest("SHA-256", content)).toString("hex") !== digest) return { failure: `sync: ${file.path} changed while syncing` };
863
+ const header = Buffer.from(digest, "hex");
864
+ for (let offset = 0; offset < content.length || offset === 0; offset += CHUNK_BYTES) {
865
+ await connection.sendBinary(Buffer.concat([header, content.subarray(offset, offset + CHUNK_BYTES)]));
866
+ if (content.length === 0) break;
867
+ }
868
+ uploadedBytes += content.length;
869
+ if (connection.failure) return { failure: `sync: ${connection.failure}` };
870
+ }
871
+ return {
872
+ files: manifest.length,
873
+ uploaded: missing.length,
874
+ uploadedBytes,
875
+ syncMs: Date.now() - syncStarted,
876
+ digest: manifestDigest(manifest)
877
+ };
878
+ }
879
+ function stagedArtifactEscapes(stagingDir, artifactPath, hasContents) {
880
+ if ((hasContents ? [artifactPath, join(artifactPath, "Contents")] : [artifactPath]).some((path) => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink())) return true;
881
+ if (!existsSync(artifactPath)) return false;
882
+ const inside = relative(realpathSync(stagingDir), realpathSync(artifactPath));
883
+ return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
884
+ }
885
+ async function fetchArtifact({ connection, method, params, artifact, request, stagingDir, machine, cacheHit, checkCancelled }) {
886
+ const name = artifact.name;
887
+ const archive = join(stagingDir, "app.tgz");
888
+ const fd = openSync(archive, "w");
889
+ const hash = createHash("sha256");
890
+ let received = 0;
891
+ let badFrame = false;
892
+ connection.onBinary((frame) => {
893
+ if (frame.length < DIGEST_BYTES || frame.subarray(0, DIGEST_BYTES).toString("hex") !== artifact.sha256) {
894
+ badFrame = true;
895
+ return;
896
+ }
897
+ const data = frame.subarray(DIGEST_BYTES);
898
+ if (cacheHit && received + data.length > Number(artifact.size)) {
899
+ badFrame = true;
900
+ return;
901
+ }
902
+ try {
903
+ writeSync(fd, data);
904
+ } catch {
905
+ badFrame = true;
906
+ return;
907
+ }
908
+ hash.update(data);
909
+ received += data.length;
910
+ });
911
+ const fetched = await connection.request(method, params, 9e5);
912
+ connection.onBinary(null);
913
+ closeSync(fd);
914
+ checkCancelled();
915
+ const fetchFailure = replyError(fetched);
916
+ if (fetchFailure || !("result" in fetched) || fetched.result === null) return { failure: `fetch: ${fetchFailure ?? "the cached artifact was removed"}` };
917
+ const digest = hash.digest("hex");
918
+ const declared = fetched.result;
919
+ if (badFrame) return { failure: "fetch: an artifact frame had another digest or could not be written here" };
920
+ if (digest !== artifact.sha256 || declared.sha256 !== digest || declared.size !== received) return { failure: `fetch: the artifact's sha256 ${digest.slice(0, 12)} does not match ${String(artifact.sha256).slice(0, 12)}` };
921
+ if (cacheHit) {
922
+ if ((await getExecutor().runFileAsync("tar", ["-tf", archive], { timeoutMs: 6e5 })).split("\n").filter(Boolean).some((member) => {
923
+ const clean = member.replace(/^\.\//, "");
924
+ return !(clean === name || clean.startsWith(`${name}/`)) || clean.split("/").includes("..");
925
+ })) return { failure: "fetch: the archive contains paths outside its app" };
926
+ }
927
+ await getExecutor().runFileAsync("tar", [
928
+ "-xf",
929
+ archive,
930
+ "-C",
931
+ stagingDir
932
+ ], { timeoutMs: 6e5 });
933
+ checkCancelled();
934
+ rmSync(archive, { force: true });
935
+ const artifactPath = join(stagingDir, name);
936
+ if (stagedArtifactEscapes(stagingDir, artifactPath, request.platform === "macos")) return { failure: `fetch: ${machine} returned ${name} as or through a symbolic link or outside the staging directory` };
937
+ if (cacheHit) {
938
+ const pending = [artifactPath];
939
+ const root = realpathSync(artifactPath);
940
+ for (const path of pending) {
941
+ const stat = lstatSync(path);
942
+ if (stat.isDirectory()) pending.push(...readdirSync(path).map((entry) => join(path, entry)));
943
+ else if (stat.isSymbolicLink()) {
944
+ const inside = relative(root, realpathSync(path));
945
+ if (inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside)) return { failure: "fetch: an app link resolves outside its bundle" };
946
+ }
947
+ }
948
+ }
949
+ if (request.platform === "ios" && !existsSync(join(artifactPath, "Info.plist"))) return { failure: `fetch: ${name} has no Info.plist` };
950
+ if (request.platform === "android" && !(lstatSync(artifactPath, { throwIfNoEntry: false })?.isFile() ?? false)) return { failure: `fetch: ${name} is not a file` };
951
+ if (request.platform === "macos") {
952
+ for (const segments of [["Contents", "Info.plist"], [
953
+ "Contents",
954
+ "MacOS",
955
+ request.product
956
+ ]]) if (!lstatSync(join(artifactPath, ...segments), { throwIfNoEntry: false })?.isFile()) return { failure: `fetch: ${name} has no regular ${segments.join("/")}` };
957
+ }
958
+ return {
959
+ artifactPath,
960
+ digest,
961
+ received,
962
+ declared
963
+ };
964
+ }
965
+ function keepCorruptCacheMessage(failed, result) {
966
+ return result.code === "STIM_COMPILATION_CACHE_CORRUPT" ? {
967
+ ...failed,
968
+ compilationCacheCorrupt: String(result.message ?? "")
969
+ } : failed;
970
+ }
971
+ /**
972
+ * Builds on the chosen machine and brings the `.app` or `.apk` back into `stagingDir`, verified against the
973
+ * sha256 the machine reports. The caller re-fingerprints and stores it.
974
+ */
975
+ async function offloadBuild(input) {
976
+ const { choice, expectedFingerprint, request, stagingDir, onPhase, onEnter, onRecord, note } = input;
977
+ let cacheHit = false;
978
+ let keepStaging = false;
979
+ const { identity } = choice;
980
+ const native = request.platform !== "macos" ? request.native : void 0;
981
+ let sourceDigest = null;
982
+ const started = Date.now();
983
+ const fail = (reason) => {
984
+ closeOffload(choice);
985
+ return {
986
+ ok: false,
987
+ machine: choice.machine,
988
+ reason: reason.split("\n")[0].slice(0, 300)
989
+ };
990
+ };
991
+ const signal = runCancellationSignal(ambientContext());
992
+ const checkCancelled = () => throwIfCancelled(signal, "offloaded build");
993
+ let settle;
994
+ let settled = false;
995
+ const outcome = new Promise((resolve) => {
996
+ settle = (value) => {
997
+ settled = true;
998
+ resolve(value);
999
+ };
1000
+ });
1001
+ const cancel = () => {
1002
+ settle({
1003
+ ok: false,
1004
+ code: "cancelled"
1005
+ });
1006
+ closeOffload(choice);
1007
+ };
1008
+ signal?.addEventListener("abort", cancel, { once: true });
1009
+ const retryCachedMiss = async (reason) => {
1010
+ checkCancelled();
1011
+ note(`cache: ${choice.machine}: ${reason}`);
1012
+ const next = choice.runnersUp.shift();
1013
+ if (next) {
1014
+ choice.connection.close();
1015
+ Object.assign(choice, next);
1016
+ } else {
1017
+ if (choice.cacheOnly) return fail(reason);
1018
+ choice.connection.close();
1019
+ const probe = await probeMachine(choice.credential, {
1020
+ ...identity,
1021
+ ...native ? { native: native.provider } : {}
1022
+ });
1023
+ if ("failure" in probe) return fail(probe.failure);
1024
+ const placement = pickOffer({
1025
+ mode: choice.mode,
1026
+ here: choice.here,
1027
+ target: choice.target,
1028
+ selected: choice.automatic ? "auto" : choice.machine,
1029
+ localEnabled: !choice.automatic || automaticMachineEnabled("build", "local"),
1030
+ offers: [{
1031
+ machine: choice.machine,
1032
+ offer: probe.offer
1033
+ }]
1034
+ });
1035
+ if (placement.order.length === 0) {
1036
+ probe.connection.close();
1037
+ return fail(placement.reasons.join("; "));
1038
+ }
1039
+ Object.assign(choice, probe, { offer: {
1040
+ ...probe.offer,
1041
+ artifact: null
1042
+ } });
1043
+ }
1044
+ const retried = await offloadBuild(input);
1045
+ keepStaging = retried.ok;
1046
+ return retried;
1047
+ };
1048
+ try {
1049
+ checkCancelled();
1050
+ if (!validArtifactIdentity(request, expectedFingerprint)) return fail("The build request has an invalid reusable artifact identity.");
1051
+ if (native?.provider === "xcode" && native.snapshot.hash !== expectedFingerprint) return fail("The native source snapshot does not match the requested artifact identity.");
1052
+ const cachedIdentity = native?.provider === "xcode" && typeof expectedFingerprint === "string" ? {
1053
+ fingerprint: expectedFingerprint,
1054
+ cacheKey: native.cacheKey
1055
+ } : void 0;
1056
+ let job = null;
1057
+ let early = [];
1058
+ let resuming = false;
1059
+ const reattach = async (why) => {
1060
+ if (resuming || settled) return;
1061
+ resuming = true;
1062
+ note(`offload: the connection to ${choice.machine} dropped (${why}); reattaching to the build there`);
1063
+ const resumed = await resumeJob(choice.credential, job, () => settled, signal, native?.provider);
1064
+ resuming = false;
1065
+ if (settled) {
1066
+ if (typeof resumed !== "string") resumed.connection.close();
1067
+ return;
1068
+ }
1069
+ if (typeof resumed === "string") return settle({
1070
+ ok: false,
1071
+ code: "closed",
1072
+ message: `${why}; ${resumed}`
1073
+ });
1074
+ choice.connection = resumed.connection;
1075
+ resumed.connection.watch();
1076
+ note(`offload: reattached to the build on ${choice.machine}`);
1077
+ resumed.connection.onProgress(handle);
1078
+ if (resumed.outcome) settle(resumed.outcome);
1079
+ for (const event of resumed.early.splice(0)) handle(event);
1080
+ };
1081
+ const handle = (event) => {
1082
+ if (event.job && event.job !== job) return;
1083
+ if (event.outcome?.code === "dropped" && !event.job && job !== null) {
1084
+ reattach(String(event.outcome.message));
1085
+ return;
1086
+ }
1087
+ if (event.outcome) return settle(event.outcome);
1088
+ if (event.record) onRecord(event.record);
1089
+ if (event.phase && typeof event.msg === "string") {
1090
+ onEnter(event.phase);
1091
+ onPhase(event.phase, event.msg);
1092
+ }
1093
+ };
1094
+ let syncMs = 0;
1095
+ let uploadedBytes = 0;
1096
+ let workerStarted = 0;
1097
+ const moveOn = (why) => {
1098
+ const next = choice.runnersUp.shift();
1099
+ if (!next) return false;
1100
+ note(`placement: ${next.machine} (${choice.machine} could not take the build: ${why})`);
1101
+ choice.connection.close();
1102
+ Object.assign(choice, next);
1103
+ return true;
1104
+ };
1105
+ for (;;) {
1106
+ if (choice.automatic && !automaticMachineEnabled("build", choice.machine)) {
1107
+ const reason = `${choice.machine} is disabled in remote.buildPoolDisabled`;
1108
+ if (moveOn(reason)) continue;
1109
+ return fail(reason);
1110
+ }
1111
+ if (native && !choice.connection.supports(NATIVE_BUILD_FEATURE[native.provider])) {
1112
+ const reason = unsupportedNative(native.provider);
1113
+ if (moveOn(reason)) continue;
1114
+ return fail(reason);
1115
+ }
1116
+ const cached = native?.provider === "xcode" && choice.connection.supports("build-artifact-cache") ? matchingArtifact(choice.offer, cachedIdentity) : null;
1117
+ if (cached) {
1118
+ cacheHit = true;
1119
+ settle({
1120
+ ok: true,
1121
+ artifact: cached,
1122
+ fingerprint: expectedFingerprint
1123
+ });
1124
+ break;
1125
+ }
1126
+ if (choice.cacheOnly) return fail("The exact artifact is no longer available.");
1127
+ const synced = await syncSource(choice.connection, identity, onEnter, nativeTransferInput(native), signal);
1128
+ checkCancelled();
1129
+ if ("failure" in synced) {
1130
+ if (moveOn(synced.failure)) continue;
1131
+ return fail(synced.failure);
1132
+ }
1133
+ ({syncMs, uploadedBytes} = synced);
1134
+ sourceDigest = synced.digest;
1135
+ onPhase("sync", `${synced.files} files, uploaded ${synced.uploaded} (${mb(uploadedBytes)}) in ${seconds(syncMs)}`);
1136
+ workerStarted = Date.now();
1137
+ early = [];
1138
+ choice.connection.onProgress((event) => job === null ? early.push(event) : handle(event));
1139
+ if (choice.automatic) requireAutomaticMachine("build", choice.machine);
1140
+ checkCancelled();
1141
+ const reply = await choice.connection.request("build.start", {
1142
+ repo: identity.repo,
1143
+ project: identity.project,
1144
+ platform: request.platform,
1145
+ fingerprint: request.platform === "macos" ? synced.digest : expectedFingerprint,
1146
+ ...native ? { native: {
1147
+ provider: native.provider,
1148
+ sourceDigest: synced.digest,
1149
+ ...native.provider === "xcode" ? {
1150
+ cacheKey: native.cacheKey,
1151
+ arch: native.arch,
1152
+ parameters: native.snapshot.parameters
1153
+ } : { inputs: native.transfer.declaration }
1154
+ } } : {},
1155
+ ...request.platform === "macos" ? { macos: {
1156
+ product: request.product,
1157
+ infoPlist: request.infoPlist,
1158
+ bundleId: request.bundleId,
1159
+ ...request.resources !== void 0 ? { resources: request.resources } : {},
1160
+ ...request.assetCatalog !== void 0 ? { assetCatalog: request.assetCatalog } : {}
1161
+ } } : {
1162
+ configuration: request.platform === "ios" ? request.configuration : null,
1163
+ scheme: request.platform === "ios" ? request.scheme : null,
1164
+ iosProjectPath: request.platform === "ios" ? request.iosProjectPath ?? null : null,
1165
+ runtime: request.platform === "ios" ? request.runtime : null,
1166
+ packageName: packageName(join(identity.repoRoot, identity.project)),
1167
+ isExpo: request.isExpo,
1168
+ optimizations: request.platform === "ios" && isJsonObject(request.optimizations) ? request.optimizations : null,
1169
+ ...request.platform === "android" ? { android: request.android } : {}
1170
+ },
1171
+ stimBuild: choice.target.local.stimBuild
1172
+ });
1173
+ checkCancelled();
1174
+ if ("result" in reply) {
1175
+ job = reply.result.job;
1176
+ break;
1177
+ }
1178
+ const refused = `start: ${replyError(reply)}`;
1179
+ if (reply.error.code === "bad-request" || !moveOn(refused)) return fail(refused);
1180
+ }
1181
+ if (!cacheHit) for (const each of choice.runnersUp.splice(0)) each.connection.close();
1182
+ choice.connection.watch();
1183
+ for (const event of early.splice(0)) handle(event);
1184
+ const timer = setTimeout(() => settle({
1185
+ ok: false,
1186
+ code: "timeout",
1187
+ message: "the build did not finish in time"
1188
+ }), JOB_TIMEOUT_MS);
1189
+ const result = await outcome;
1190
+ clearTimeout(timer);
1191
+ checkCancelled();
1192
+ const { connection, machine } = choice;
1193
+ connection.onProgress(null);
1194
+ const workerMs = cacheHit ? 0 : Date.now() - workerStarted;
1195
+ if (result.ok !== true) return keepCorruptCacheMessage(fail(`${String(result.code ?? "failed")}: ${String(result.message ?? "")}`), result);
1196
+ if (!cacheHit && native && (result.sourceDigest !== sourceDigest || result.fingerprint !== expectedFingerprint)) return fail("The worker returned a different native source or artifact identity.");
1197
+ if (native?.provider === "gradle" && !ANDROID_PACKAGE.test(String(result.androidPackage ?? ""))) return fail("The worker returned no verified native APK package.");
1198
+ const artifact = result.artifact;
1199
+ const name = typeof artifact?.name === "string" ? artifact.name : "";
1200
+ if (!ARTIFACT_NAME[request.platform].test(name) || typeof artifact.sha256 !== "string") return fail(`the machine reported no .${request.platform === "android" ? "apk" : "app"} artifact`);
1201
+ onEnter("fetch");
1202
+ checkCancelled();
1203
+ const fetchStarted = Date.now();
1204
+ rmSync(stagingDir, {
1205
+ recursive: true,
1206
+ force: true
1207
+ });
1208
+ mkdirSync(stagingDir, { recursive: true });
1209
+ const downloaded = await fetchArtifact({
1210
+ connection,
1211
+ artifact: {
1212
+ name,
1213
+ size: artifact.size,
1214
+ sha256: artifact.sha256
1215
+ },
1216
+ request,
1217
+ stagingDir,
1218
+ machine,
1219
+ cacheHit,
1220
+ checkCancelled,
1221
+ method: cacheHit ? "build.cached-artifact" : "build.artifact",
1222
+ params: cacheHit && native?.provider === "xcode" ? {
1223
+ repo: identity.repo,
1224
+ artifact: {
1225
+ fingerprint: expectedFingerprint,
1226
+ cacheKey: native.cacheKey
1227
+ }
1228
+ } : { job }
1229
+ });
1230
+ if ("failure" in downloaded) return cacheHit ? await retryCachedMiss(downloaded.failure) : fail(downloaded.failure);
1231
+ const { artifactPath, digest, received, declared } = downloaded;
1232
+ const fetchMs = Date.now() - fetchStarted;
1233
+ closeOffload(choice);
1234
+ keepStaging = true;
1235
+ return {
1236
+ ok: true,
1237
+ ...cacheHit ? { cacheHit: true } : {},
1238
+ machine,
1239
+ artifactPath,
1240
+ ...typeof declared.handoff === "string" && /^[0-9a-f]{64}$/.test(declared.handoff) ? { handoff: {
1241
+ serverKey: choice.credential.serverKey,
1242
+ token: declared.handoff,
1243
+ sha256: digest
1244
+ } } : {},
1245
+ ...offloadCacheActivity(request.platform, result.compilationCache, cacheHit),
1246
+ ...native?.provider === "gradle" ? { androidPackage: result.androidPackage } : {},
1247
+ timings: {
1248
+ offerMs: choice.offerMs,
1249
+ syncMs,
1250
+ workerMs,
1251
+ fetchMs,
1252
+ totalMs: Date.now() - started + choice.offerMs,
1253
+ worker: isJsonObject(result.timings) ? result.timings : {},
1254
+ uploadedBytes,
1255
+ artifactBytes: received
1256
+ }
1257
+ };
1258
+ } catch (error) {
1259
+ checkCancelled();
1260
+ const reason = error.message.split("\n")[0] ?? String(error);
1261
+ return cacheHit ? await retryCachedMiss(reason) : fail(reason);
1262
+ } finally {
1263
+ signal?.removeEventListener("abort", cancel);
1264
+ if (!keepStaging) rmSync(stagingDir, {
1265
+ recursive: true,
1266
+ force: true
1267
+ });
1268
+ }
1269
+ }
1270
+ function offloadCacheActivity(platform, activity, cacheHit) {
1271
+ if (cacheHit) return {
1272
+ compilationCache: COMPILATION_CACHE_NOT_RUN,
1273
+ ccache: CCACHE_NOT_RUN
1274
+ };
1275
+ return {
1276
+ compilationCache: platform === "ios" ? compilationActivity(activity) : COMPILATION_CACHE_UNAVAILABLE,
1277
+ ccache: platform === "android" ? ccacheActivity(activity) : CCACHE_UNAVAILABLE
1278
+ };
1279
+ }
1280
+ function packageName(projectRoot) {
1281
+ try {
1282
+ const name = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")).name;
1283
+ return typeof name === "string" ? name : null;
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ const numberOrNull = (field) => typeof field === "number" ? field : null;
1289
+ function compilationActivity(value) {
1290
+ if (!isJsonObject(value) || value.status !== "reported") return COMPILATION_CACHE_UNAVAILABLE;
1291
+ return {
1292
+ status: "reported",
1293
+ hits: numberOrNull(value.hits),
1294
+ cacheableTasks: numberOrNull(value.cacheableTasks),
1295
+ hitRatePercent: numberOrNull(value.hitRatePercent)
1296
+ };
1297
+ }
1298
+ function ccacheActivity(value) {
1299
+ if (!isJsonObject(value) || value.status !== "reported") return CCACHE_UNAVAILABLE;
1300
+ return {
1301
+ status: "reported",
1302
+ hits: numberOrNull(value.hits),
1303
+ misses: numberOrNull(value.misses),
1304
+ hitRatePercent: numberOrNull(value.hitRatePercent)
1305
+ };
1306
+ }
1307
+ const mb = (bytes) => `${(bytes / 1024 ** 2).toFixed(1)} MB`;
1308
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
1309
+ function simulatorRuntime(udid) {
1310
+ try {
1311
+ const listed = JSON.parse(getExecutor().runFileQuiet("xcrun", [
1312
+ "simctl",
1313
+ "list",
1314
+ "devices",
1315
+ "-j"
1316
+ ], { timeoutMs: 2e4 }) ?? "{}");
1317
+ for (const [runtime, devices] of Object.entries(listed.devices ?? {})) if (devices.some((device) => device.udid === udid)) return runtime;
1318
+ } catch {}
1319
+ return null;
1320
+ }
1321
+ /**
1322
+ * Every reason a machine's offer would not take this app's builds, judged for each of `targets`. A target the machine
1323
+ * cannot build at all, such as iOS on Linux, is left out while it can build another of them. With several targets, a
1324
+ * reason only one platform has names it.
1325
+ */
1326
+ function machineProblems(offer, targets) {
1327
+ const judged = targets.map((target) => ({
1328
+ target,
1329
+ found: offerProblems(offer, target)
1330
+ }));
1331
+ const buildable = judged.filter(({ found }) => !found.some((problem) => problem.code === "platform"));
1332
+ const problems = [];
1333
+ for (const { target, found } of buildable.length ? buildable : judged) for (const problem of found) {
1334
+ const labeled = targets.length > 1 && !SHARED_PROBLEMS.has(problem.code) ? {
1335
+ ...problem,
1336
+ reason: `${PLATFORM_LABEL[target.platform]}: ${problem.reason}`
1337
+ } : problem;
1338
+ if (!problems.some((each) => each.code === labeled.code && each.reason === labeled.reason)) problems.push(labeled);
1339
+ }
1340
+ return problems;
1341
+ }
1342
+ const DOCTOR_CONNECT_MS = 5e3;
1343
+ const DOCTOR_OFFER_MS = 8e3;
1344
+ const PLATFORM_LABEL = {
1345
+ ios: "iOS",
1346
+ android: "Android",
1347
+ macos: "macOS"
1348
+ };
1349
+ const SHARED_PROBLEMS = /* @__PURE__ */ new Set([
1350
+ "stim-build",
1351
+ "disk",
1352
+ "busy"
1353
+ ]);
1354
+ /**
1355
+ * Doctor's offload check for `projectRoot`: one bounded offer per machine, judged by `machineProblems` for the builds
1356
+ * `targets` returns. No build runs.
1357
+ */
1358
+ function offloadCheck(projectRoot, targets) {
1359
+ let identity;
1360
+ const readIdentity = async () => {
1361
+ try {
1362
+ const repoRoot = realpathSync(await gitAsync(projectRoot, ["rev-parse", "--show-toplevel"]));
1363
+ return repositoryIdentity(projectRoot, repoRoot, realpathSync(await gitAsync(repoRoot, [
1364
+ "rev-parse",
1365
+ "--path-format=absolute",
1366
+ "--git-common-dir"
1367
+ ])));
1368
+ } catch (error) {
1369
+ reportSignal(ambientContext())?.throwIfAborted();
1370
+ return error instanceof Error ? error.message.split("\n")[0] ?? "git failed" : "git failed";
1371
+ }
1372
+ };
1373
+ let resolved = null;
1374
+ return async (credential) => {
1375
+ const repository = await (identity ??= readIdentity());
1376
+ if (typeof repository === "string") return {
1377
+ capacity: null,
1378
+ problems: [{
1379
+ code: "checkout",
1380
+ reason: `this app is not in a git checkout (${repository})`
1381
+ }]
1382
+ };
1383
+ const probe = await probeMachine(credential, {
1384
+ ...repository,
1385
+ rubyVersion: readRubyVersion(projectRoot) ?? void 0
1386
+ }, {
1387
+ connectMs: DOCTOR_CONNECT_MS,
1388
+ offerMs: DOCTOR_OFFER_MS
1389
+ });
1390
+ if ("failure" in probe) return {
1391
+ capacity: null,
1392
+ problems: [{
1393
+ code: "unreachable",
1394
+ reason: probe.failure
1395
+ }]
1396
+ };
1397
+ probe.connection.close();
1398
+ resolved ??= targets();
1399
+ return {
1400
+ capacity: probe.offer.capacity,
1401
+ problems: machineProblems(probe.offer, await resolved)
1402
+ };
1403
+ };
1404
+ }
1405
+ //#endregion
1406
+ export { nativeTransferRefusal as a, offloadPlacement as c, simulatorRuntime as d, setRemoteLogSink as f, maintenanceLogger as h, closeOffload as i, placementLoad as l, clearCorruptCompilationCache as m, buildPlacementCandidates as n, offloadBuild as o, COMPILATION_CACHE_CORRUPT as p, chooseBuildMachine as r, offloadCheck as s, BuildConnection as t, remotePhaseText as u };