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