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.
- package/README.md +88 -8
- package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
- package/dist/act-DHAw2A2S.mjs +2667 -0
- package/dist/activity-BzVfHSrY.mjs +1376 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
- package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
- package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
- package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
- package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/api.d.mts +740 -8
- package/dist/api.mjs +585 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
- package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
- package/dist/budget-Bs6SxxzO.mjs +9154 -0
- package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
- package/dist/build-cache-DmyR9Zzc.mjs +602 -0
- package/dist/build-plan-CaFskB7_.mjs +188 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-B7QVOlCr.d.mts +253 -0
- package/dist/client-UXD70ZbM.mjs +1406 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
- package/dist/config-CYcBF478.mjs +949 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
- package/dist/device-DCxH7_By.mjs +1068 -0
- package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
- package/dist/device-host-worker.mjs +65 -61
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
- package/dist/doctor-BLHCYxzi.mjs +2 -0
- package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
- package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
- package/dist/exec-H20xSg2T.d.mts +65 -0
- package/dist/exec-_UbtCQcG.mjs +750 -0
- package/dist/gc-C-WwL0yi.mjs +3 -0
- package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
- package/dist/git-C3iTsJaZ.mjs +73 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
- package/dist/idle-BVeRGroT.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
- package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
- package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
- package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
- package/dist/ios-signing-DTTIChGs.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
- package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
- package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
- package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
- package/dist/logs-BS00Anxf.mjs +352 -0
- package/dist/logs-CMm05HxN.mjs +255 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
- package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +122 -247
- package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
- package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
- package/dist/native-android-JACyjTZs.mjs +844 -0
- package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-DY_RLx3C.mjs +277 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +1 -46
- package/dist/offload-worker.mjs +197 -66
- package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
- package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
- package/dist/prebuild-uD_ipCxF.mjs +449 -0
- package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
- package/dist/pull-request-zjCSAryH.d.mts +120 -0
- package/dist/pull-requests.d.mts +1 -119
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
- package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
- package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
- package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
- package/dist/reload-DzjSl5P_.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
- package/dist/result-DNiS-tTP.mjs +434 -0
- package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
- package/dist/selection-CSl9i5qU.mjs +619 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
- package/dist/server-expo-C-90EhcG.mjs +2 -0
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
- package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
- package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
- package/dist/settings-T1gtc3bf.mjs +114 -0
- package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
- package/dist/settings.schema.json +163 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-BIS0ydsV.mjs +183 -0
- package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
- package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
- package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
- package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
- package/dist/stats-report-BFfMpWWF.mjs +16 -0
- package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
- package/dist/status-CSi6tlRq.mjs +2 -0
- package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-C12we7lz.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
- package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
- package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +63 -27
- package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
- package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
- package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
- package/dist/toolchain-DhKwp9iI.mjs +2 -0
- package/dist/toolchain-Hp9sCpK4.mjs +873 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
- package/dist/trigger-Bf3uDcpv.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
- package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
- package/dist/web-DknQETPs.mjs +2 -0
- package/dist/web-run.mjs +11 -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-process-lock-1_1OoVW5.mjs +57 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
- package/dist/worktree-CK2MpJW5.mjs +2 -0
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
- package/dist/xcode-Zlx3qjEs.mjs +896 -0
- package/package.json +7 -5
- 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/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- 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/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- 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,844 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-_UbtCQcG.mjs";
|
|
2
|
+
import { d as workspaceDir, i as sharedBuildCache } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { J as validateDeviceSlot, U as deviceSlotPlatforms, p as getProject } from "./config-CYcBF478.mjs";
|
|
4
|
+
import { Gt as childEnvironment, tt as settingValueAt } from "./settings-CsJDEXgK.mjs";
|
|
5
|
+
import { r as register } from "./cache-manifest-DkkqguoK.mjs";
|
|
6
|
+
import { Y as statsProjectKey } from "./budget-Bs6SxxzO.mjs";
|
|
7
|
+
import { d as runCancellationSignal } from "./native-run-DY_RLx3C.mjs";
|
|
8
|
+
import { h as resolveBuild, n as buildCacheKey, o as entryDir, t as artifactIn } from "./build-cache-DmyR9Zzc.mjs";
|
|
9
|
+
import { J as pickDefaultSystemImage, P as listInstalledSystemImages, R as ownedAvdDeviceProfile, T as findBuildTool, W as ownedAvdSystemImage, l as androidSystemImageAbi, o as androidHome, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
|
|
10
|
+
import { o as resolveCcache } from "./ccache-BJRZ9pG3.mjs";
|
|
11
|
+
import { r as androidToolchainAsync } from "./toolchain-Hp9sCpK4.mjs";
|
|
12
|
+
import { n as fingerprintNativeInputs } from "./native-inputs-TRTTTRxt.mjs";
|
|
13
|
+
import { a as nativeGradleTransfer, r as nativeGradleLocalSourceDigest, t as gradleOffloadInputs } from "./native-gradle-inputs-Bep9JcSF.mjs";
|
|
14
|
+
import { d as buildGradle, l as androidSdkRefusal } from "./react-native-build-rhk4smwQ.mjs";
|
|
15
|
+
import { a as explainBuildMiss, c as skippedMissReason } from "./prebuild-uD_ipCxF.mjs";
|
|
16
|
+
import { c as resolveAndroidRunPlan } from "./result-DNiS-tTP.mjs";
|
|
17
|
+
import { n as androidProcessRuntime } from "./launch-CD1yVc7U.mjs";
|
|
18
|
+
import { n as planFlagRefusal, r as planPayload } from "./build-plan-CaFskB7_.mjs";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
|
+
import { storeArtifactAsync } from "@stim-cli/core";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { mkdtemp, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
26
|
+
import { pipeline } from "node:stream/promises";
|
|
27
|
+
import { Readable } from "node:stream";
|
|
28
|
+
//#region src/integrations/native-android-cache.ts
|
|
29
|
+
const RECEIPT$1 = "native-android.json";
|
|
30
|
+
async function digest(path, signal) {
|
|
31
|
+
const hash = createHash("sha256");
|
|
32
|
+
for await (const chunk of createReadStream(path, { signal })) hash.update(chunk);
|
|
33
|
+
return hash.digest("hex");
|
|
34
|
+
}
|
|
35
|
+
async function inspectApk(path, sdk, abi, operation, signal) {
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
if (!(await stat(path)).isFile()) throw new Error("The native APK is not a regular file.");
|
|
38
|
+
const signer = findBuildTool(["apksigner"], { home: sdk });
|
|
39
|
+
const aapt = findBuildTool(["aapt"], { home: sdk });
|
|
40
|
+
if (!signer || !aapt) throw new Error("Android SDK apksigner and aapt are required to verify a cached native APK.");
|
|
41
|
+
const before = await digest(path, signal);
|
|
42
|
+
const options = {
|
|
43
|
+
timeoutMs: 3e4,
|
|
44
|
+
signal,
|
|
45
|
+
declareFor: operation
|
|
46
|
+
};
|
|
47
|
+
const executor = getExecutor();
|
|
48
|
+
const signers = [...(await executor.runFileAsync(signer.path, [
|
|
49
|
+
"verify",
|
|
50
|
+
"--print-certs",
|
|
51
|
+
path
|
|
52
|
+
], options)).matchAll(/^(?:Signer #\d+|V\d+(?:\.\d+)? Signer:) certificate SHA-256 digest: ([a-f\d]{64})\s*$/gim)].flatMap((match) => match[1] ? [match[1].toLowerCase()] : []).toSorted();
|
|
53
|
+
if (!signers.length) throw new Error("The native APK has no verified signing certificate.");
|
|
54
|
+
const badging = await executor.runFileAsync(aapt.path, [
|
|
55
|
+
"dump",
|
|
56
|
+
"badging",
|
|
57
|
+
path
|
|
58
|
+
], options);
|
|
59
|
+
const line = badging.split(/\r?\n/).find((value) => value.startsWith("package:"));
|
|
60
|
+
const androidPackage = line?.match(/^package: name='([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+)'/)?.[1];
|
|
61
|
+
if (!line || !androidPackage || /\bsplit=/.test(line)) throw new Error("The native APK is not a standalone Android package.");
|
|
62
|
+
const native = badging.split(/\r?\n/).find((value) => value.startsWith("native-code:"));
|
|
63
|
+
const abis = native ? [...native.matchAll(/'([^']+)'/g)].flatMap((match) => match[1] ? [match[1]] : []).toSorted() : [];
|
|
64
|
+
if (abi && native && !abis.includes(abi)) throw new Error("The native APK does not contain the target ABI.");
|
|
65
|
+
const sha256 = await digest(path, signal);
|
|
66
|
+
if (sha256 !== before) throw new Error("The native APK changed during verification.");
|
|
67
|
+
return {
|
|
68
|
+
androidPackage,
|
|
69
|
+
signers,
|
|
70
|
+
abis,
|
|
71
|
+
sha256
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function verifiedNativeApk(key, path, sdk, abi, operation, signal) {
|
|
75
|
+
try {
|
|
76
|
+
const receipt = JSON.parse(await readFile(join(dirname(path), RECEIPT$1), {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
signal
|
|
79
|
+
}));
|
|
80
|
+
if (receipt.schema !== 1 || receipt.key !== key || receipt.sha256 !== await digest(path, signal)) return null;
|
|
81
|
+
const actual = await inspectApk(path, sdk, abi, operation, signal);
|
|
82
|
+
if (actual.sha256 !== receipt.sha256 || actual.androidPackage !== receipt.androidPackage || JSON.stringify(actual.signers) !== JSON.stringify(receipt.signers) || JSON.stringify(actual.abis) !== JSON.stringify(receipt.abis)) return null;
|
|
83
|
+
return actual;
|
|
84
|
+
} catch {
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function materializeNativeApk(key, path, sdk, abi, operation, signal = runCancellationSignal(operation) ?? operation?.signal ?? getExecutor().reportSignal?.()) {
|
|
90
|
+
const actual = await verifiedNativeApk(key, path, sdk, abi, operation, signal);
|
|
91
|
+
return actual ? {
|
|
92
|
+
apkPath: path,
|
|
93
|
+
directory: null,
|
|
94
|
+
androidPackage: actual.androidPackage
|
|
95
|
+
} : null;
|
|
96
|
+
}
|
|
97
|
+
function nativeAndroidCache(sdk, abi, androidPackage, operation) {
|
|
98
|
+
return {
|
|
99
|
+
resolve: ({ key }) => resolveBuild("android", key),
|
|
100
|
+
async store({ key, sourcePath, overwrite, signal }) {
|
|
101
|
+
const expectedPackage = androidPackage();
|
|
102
|
+
const actual = expectedPackage === null ? await verifiedNativeApk(key, sourcePath, sdk, abi, operation, signal) : await inspectApk(sourcePath, sdk, abi, operation, signal);
|
|
103
|
+
if (!actual) throw new Error("The native APK has no matching verified cache receipt.");
|
|
104
|
+
if (expectedPackage !== null && actual.androidPackage !== expectedPackage) throw new Error("The APK package does not match the Gradle model.");
|
|
105
|
+
register({
|
|
106
|
+
dir: sharedBuildCache(),
|
|
107
|
+
name: "Build cache",
|
|
108
|
+
prune: "entries",
|
|
109
|
+
entriesDepth: 2,
|
|
110
|
+
note: "built .app/.apk keyed on the native fingerprint"
|
|
111
|
+
});
|
|
112
|
+
return storeArtifactAsync(entryDir("android", key), sourcePath, {
|
|
113
|
+
signal,
|
|
114
|
+
overwrite,
|
|
115
|
+
runFile: (file, args) => getExecutor().runFileAsync(file, args, {
|
|
116
|
+
timeoutMs: 12e4,
|
|
117
|
+
signal,
|
|
118
|
+
declareFor: operation
|
|
119
|
+
}),
|
|
120
|
+
async writeMetadata(staging) {
|
|
121
|
+
if (await digest(join(staging, basename(sourcePath)), signal) !== actual.sha256) throw new Error("The native APK changed while it was copied.");
|
|
122
|
+
await writeFile(join(staging, RECEIPT$1), JSON.stringify({
|
|
123
|
+
schema: 1,
|
|
124
|
+
key,
|
|
125
|
+
...actual
|
|
126
|
+
}), { signal });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function plannedNativeApk(key, sdk, abi, operation) {
|
|
133
|
+
const path = artifactIn(entryDir("android", key));
|
|
134
|
+
return path !== null && await materializeNativeApk(key, path, sdk, abi, operation) !== null;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/integrations/native-android-file-cache.ts
|
|
138
|
+
const MAGIC = Buffer.from("STIMAPK1\n");
|
|
139
|
+
const HEADER_BYTES = MAGIC.length + 4;
|
|
140
|
+
const MAX_RECEIPT_BYTES = 1048576;
|
|
141
|
+
const RECEIPT = "native-android.json";
|
|
142
|
+
const fileKey = (key) => `native-android/v1/${key}`;
|
|
143
|
+
async function readExactly(file, size, position, signal) {
|
|
144
|
+
const buffer = Buffer.alloc(size);
|
|
145
|
+
let offset = 0;
|
|
146
|
+
while (offset < size) {
|
|
147
|
+
signal.throwIfAborted();
|
|
148
|
+
const { bytesRead } = await file.read(buffer, offset, size - offset, position + offset);
|
|
149
|
+
if (!bytesRead) throw new Error("The shared native APK entry is truncated.");
|
|
150
|
+
offset += bytesRead;
|
|
151
|
+
}
|
|
152
|
+
return buffer;
|
|
153
|
+
}
|
|
154
|
+
async function unpack(source, directory, signal) {
|
|
155
|
+
const file = await open(source, "r");
|
|
156
|
+
try {
|
|
157
|
+
const header = await readExactly(file, HEADER_BYTES, 0, signal);
|
|
158
|
+
const size = header.readUInt32BE(MAGIC.length);
|
|
159
|
+
if (!header.subarray(0, MAGIC.length).equals(MAGIC) || size === 0 || size > MAX_RECEIPT_BYTES) throw new Error("The shared native APK entry has an unsupported header.");
|
|
160
|
+
const receipt = await readExactly(file, size, HEADER_BYTES, signal);
|
|
161
|
+
const apk = join(directory, "artifact.apk");
|
|
162
|
+
await writeFile(join(directory, RECEIPT), receipt, {
|
|
163
|
+
flag: "wx",
|
|
164
|
+
mode: 384,
|
|
165
|
+
signal
|
|
166
|
+
});
|
|
167
|
+
await pipeline(file.createReadStream({
|
|
168
|
+
start: HEADER_BYTES + size,
|
|
169
|
+
autoClose: false
|
|
170
|
+
}), createWriteStream(apk, {
|
|
171
|
+
flags: "wx",
|
|
172
|
+
mode: 384
|
|
173
|
+
}), { signal });
|
|
174
|
+
return apk;
|
|
175
|
+
} finally {
|
|
176
|
+
await file.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function nativeAndroidFileCache(files, sdk, abi, operation) {
|
|
180
|
+
return {
|
|
181
|
+
async resolve({ key, destinationDir, signal }) {
|
|
182
|
+
const directory = await mkdtemp(join(destinationDir, "native-apk-"));
|
|
183
|
+
let keep = false;
|
|
184
|
+
try {
|
|
185
|
+
const source = await files.resolve({
|
|
186
|
+
key: fileKey(key),
|
|
187
|
+
destinationDir: directory,
|
|
188
|
+
signal
|
|
189
|
+
});
|
|
190
|
+
if (!source) return null;
|
|
191
|
+
const contained = relative(await realpath(directory), await realpath(source));
|
|
192
|
+
if (isAbsolute(contained) || contained === ".." || contained.startsWith(`..${sep}`)) throw new Error("The shared native APK file is outside its download directory.");
|
|
193
|
+
if (!(await stat(source)).isFile()) throw new Error("The shared native APK entry is not a regular file.");
|
|
194
|
+
const apk = await unpack(source, await mkdtemp(join(directory, "unpacked-")), signal);
|
|
195
|
+
if (!await materializeNativeApk(key, apk, sdk, abi, operation, signal)) throw new Error("The shared native APK does not match its verification receipt.");
|
|
196
|
+
keep = true;
|
|
197
|
+
return apk;
|
|
198
|
+
} finally {
|
|
199
|
+
if (!keep) await rm(directory, {
|
|
200
|
+
recursive: true,
|
|
201
|
+
force: true
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
async store({ key, sourcePath, overwrite, signal }) {
|
|
206
|
+
const cached = artifactIn(entryDir("android", key));
|
|
207
|
+
if (!cached) throw new Error("No native APK is stored locally for this key.");
|
|
208
|
+
const metadata = await readFile(join(dirname(cached), RECEIPT), { signal });
|
|
209
|
+
if (metadata.length > MAX_RECEIPT_BYTES) throw new Error("The native APK receipt is too large.");
|
|
210
|
+
const receipt = JSON.parse(metadata.toString("utf8"));
|
|
211
|
+
if (receipt.schema !== 1 || receipt.key !== key) throw new Error("The native APK has no matching cache receipt.");
|
|
212
|
+
const directory = await mkdtemp(join(tmpdir(), "stim-native-apk-file-"));
|
|
213
|
+
try {
|
|
214
|
+
const envelope = join(directory, "artifact");
|
|
215
|
+
const header = Buffer.alloc(HEADER_BYTES);
|
|
216
|
+
MAGIC.copy(header);
|
|
217
|
+
header.writeUInt32BE(metadata.length, MAGIC.length);
|
|
218
|
+
const payload = async function* () {
|
|
219
|
+
yield header;
|
|
220
|
+
yield metadata;
|
|
221
|
+
const hash = createHash("sha256");
|
|
222
|
+
for await (const chunk of createReadStream(sourcePath, { signal })) {
|
|
223
|
+
hash.update(chunk);
|
|
224
|
+
yield chunk;
|
|
225
|
+
}
|
|
226
|
+
if (hash.digest("hex") !== receipt.sha256) throw new Error("The native APK changed while its shared entry was packed.");
|
|
227
|
+
};
|
|
228
|
+
await pipeline(Readable.from(payload()), createWriteStream(envelope, {
|
|
229
|
+
flags: "wx",
|
|
230
|
+
mode: 384
|
|
231
|
+
}), { signal });
|
|
232
|
+
signal.throwIfAborted();
|
|
233
|
+
await files.store({
|
|
234
|
+
key: fileKey(key),
|
|
235
|
+
sourcePath: envelope,
|
|
236
|
+
overwrite,
|
|
237
|
+
signal
|
|
238
|
+
});
|
|
239
|
+
} finally {
|
|
240
|
+
await rm(directory, {
|
|
241
|
+
recursive: true,
|
|
242
|
+
force: true
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/integrations/native-gradle-artifact-inputs.ts
|
|
250
|
+
const NATIVE_GRADLE_CACHE_LIMIT = "Native APK caching requires android.artifactInputs with complete: true and a complete declaration of repeatable pinned inputs. Gradle incremental and task-cache reuse remain available without it.";
|
|
251
|
+
function gradleArtifactInputs(value) {
|
|
252
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(NATIVE_GRADLE_CACHE_LIMIT);
|
|
253
|
+
const localFiles = "localFiles" in value ? value.localFiles : [];
|
|
254
|
+
const environment = "environment" in value ? value.environment : [];
|
|
255
|
+
if (Object.keys(value).some((key) => ![
|
|
256
|
+
"complete",
|
|
257
|
+
"ignored",
|
|
258
|
+
"outputs",
|
|
259
|
+
"localFiles",
|
|
260
|
+
"environment"
|
|
261
|
+
].includes(key)) || !Array.isArray(localFiles) || !localFiles.every((path) => typeof path === "string" && path.length > 0 && !path.includes("\0")) || !Array.isArray(environment) || !environment.every((name) => typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) throw new Error(NATIVE_GRADLE_CACHE_LIMIT);
|
|
262
|
+
let inventory;
|
|
263
|
+
try {
|
|
264
|
+
inventory = gradleOffloadInputs({
|
|
265
|
+
complete: "complete" in value ? value.complete : void 0,
|
|
266
|
+
ignored: "ignored" in value ? value.ignored : void 0,
|
|
267
|
+
outputs: "outputs" in value ? value.outputs : void 0
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw new Error(String(error).replaceAll("android.offloadInputs", "android.artifactInputs"), { cause: error });
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
...inventory,
|
|
274
|
+
localFiles: [...new Set(localFiles)].toSorted(),
|
|
275
|
+
environment: [...new Set(environment)].toSorted()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function optionalText(path, signal, encoding = "utf8") {
|
|
279
|
+
try {
|
|
280
|
+
return await readFile(path, {
|
|
281
|
+
encoding,
|
|
282
|
+
signal
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function javaRelease(env, signal) {
|
|
290
|
+
let home = env.JAVA_HOME;
|
|
291
|
+
if (!home) {
|
|
292
|
+
const java = getExecutor().findExecutable("java");
|
|
293
|
+
if (java) {
|
|
294
|
+
const selected = dirname(dirname(await realpath(java)));
|
|
295
|
+
if ((await optionalText(join(selected, "release"), signal))?.includes("JAVA_VERSION=")) home = selected;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!home && process.platform === "darwin") {
|
|
299
|
+
const child = childEnvironment(env);
|
|
300
|
+
home = (await getExecutor().runFileAsync("/usr/libexec/java_home", [], {
|
|
301
|
+
timeoutMs: 1e4,
|
|
302
|
+
signal,
|
|
303
|
+
...child
|
|
304
|
+
})).trim();
|
|
305
|
+
}
|
|
306
|
+
if (!home || !(await optionalText(join(home, "release"), signal))?.includes("JAVA_VERSION=")) throw new Error("The Gradle wrapper JDK release identity is unavailable.");
|
|
307
|
+
return join(home, "release");
|
|
308
|
+
}
|
|
309
|
+
async function localPropertiesIdentity(properties, sdk, root, signal) {
|
|
310
|
+
const content = await optionalText(properties, signal, "latin1");
|
|
311
|
+
if (content === null) return null;
|
|
312
|
+
const retained = [];
|
|
313
|
+
let selected = null;
|
|
314
|
+
for (const [, line = "", ending = ""] of content.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
|
|
315
|
+
if (/^[ \t\f]*$/.test(line) || /^[ \t\f]*[#!]/.test(line)) {
|
|
316
|
+
retained.push(line + ending);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if ((line.match(/\\+$/)?.[0].length ?? 0) % 2 !== 0) throw new Error("Native APK caching requires single-line local.properties values.");
|
|
320
|
+
const entry = /^[ \t\f]*([A-Za-z0-9_.-]+)(?:[ \t\f]*[:=][ \t\f]*|[ \t\f]+)(.*)$/.exec(line);
|
|
321
|
+
if (!entry) {
|
|
322
|
+
if (/^[ \t\f]*[A-Za-z0-9_.-]+[ \t\f]*$/.test(line) && line.trim() !== "sdk.dir") {
|
|
323
|
+
retained.push(line + ending);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
throw new Error("Native APK caching requires plain, unescaped local.properties keys.");
|
|
327
|
+
}
|
|
328
|
+
if (entry[1] !== "sdk.dir") {
|
|
329
|
+
retained.push(line + ending);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const value = entry[2] ?? "";
|
|
333
|
+
if (!value || [...value].some((character) => character.charCodeAt(0) > 127)) throw new Error("Native APK caching cannot resolve the local.properties SDK.");
|
|
334
|
+
selected = value.replace(/\\u([a-fA-F\d]{4})|\\(.)/g, (full, hex, escaped) => {
|
|
335
|
+
if (hex) return String.fromCharCode(parseInt(hex, 16));
|
|
336
|
+
if (escaped === "u") throw new Error("Native APK caching cannot resolve the local.properties SDK.");
|
|
337
|
+
if (escaped === "t") return " ";
|
|
338
|
+
if (escaped === "r") return "\r";
|
|
339
|
+
if (escaped === "n") return "\n";
|
|
340
|
+
if (escaped === "f") return "\f";
|
|
341
|
+
return escaped ?? full;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (selected !== null && await realpath(resolve(root, selected)) !== sdk) throw new Error("Native APK caching requires the local.properties SDK to match ANDROID_HOME.");
|
|
345
|
+
return retained.join("");
|
|
346
|
+
}
|
|
347
|
+
async function nativeGradleArtifactSnapshot(root, value, build, abi, env, signal) {
|
|
348
|
+
signal?.throwIfAborted();
|
|
349
|
+
const declaration = gradleArtifactInputs(value);
|
|
350
|
+
const localFiles = declaration.localFiles.map((file) => resolve(root, file));
|
|
351
|
+
for (const path of localFiles) {
|
|
352
|
+
const info = await stat(path).catch((error) => {
|
|
353
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
354
|
+
throw error;
|
|
355
|
+
});
|
|
356
|
+
if (info && !info.isFile()) throw new Error("android.artifactInputs.localFiles must name exact regular files.");
|
|
357
|
+
}
|
|
358
|
+
const { complete, ignored, outputs } = declaration;
|
|
359
|
+
const properties = join(root, "local.properties");
|
|
360
|
+
const source = await nativeGradleLocalSourceDigest(root, {
|
|
361
|
+
complete,
|
|
362
|
+
ignored,
|
|
363
|
+
outputs
|
|
364
|
+
}, [properties, ...localFiles], signal);
|
|
365
|
+
const sdkDirectory = await realpath(androidHome(env));
|
|
366
|
+
const home = env.HOME ?? env.USERPROFILE ?? homedir();
|
|
367
|
+
const gradleHome = env.GRADLE_USER_HOME ?? join(home, ".gradle");
|
|
368
|
+
const inputs = [{
|
|
369
|
+
name: "jdk-release",
|
|
370
|
+
path: await javaRelease(env, signal),
|
|
371
|
+
optional: false
|
|
372
|
+
}, ...[
|
|
373
|
+
"gradle.properties",
|
|
374
|
+
"init.gradle",
|
|
375
|
+
"init.gradle.kts",
|
|
376
|
+
"init.d"
|
|
377
|
+
].map((name) => ({
|
|
378
|
+
name: `gradle-user/${name}`,
|
|
379
|
+
path: join(gradleHome, name),
|
|
380
|
+
optional: true
|
|
381
|
+
}))];
|
|
382
|
+
for (const group of [
|
|
383
|
+
"build-tools",
|
|
384
|
+
"platforms",
|
|
385
|
+
"ndk",
|
|
386
|
+
"cmake"
|
|
387
|
+
]) {
|
|
388
|
+
const directory = join(sdkDirectory, group);
|
|
389
|
+
const versions = await readdir(directory).catch((error) => {
|
|
390
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
|
|
391
|
+
throw error;
|
|
392
|
+
});
|
|
393
|
+
for (const version of versions.toSorted()) {
|
|
394
|
+
if (!(await stat(join(directory, version))).isDirectory()) continue;
|
|
395
|
+
const path = join(directory, version, "source.properties");
|
|
396
|
+
if (await optionalText(path, signal) !== null) inputs.push({
|
|
397
|
+
name: `sdk/${group}/${version}`,
|
|
398
|
+
path,
|
|
399
|
+
optional: false
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!inputs.some((input) => input.name.startsWith("sdk/build-tools/"))) throw new Error("Android SDK build-tools version metadata is unavailable.");
|
|
404
|
+
const preferences = [
|
|
405
|
+
join(home, ".android"),
|
|
406
|
+
...env.ANDROID_USER_HOME ? [env.ANDROID_USER_HOME] : [],
|
|
407
|
+
...[
|
|
408
|
+
"ANDROID_PREFS_ROOT",
|
|
409
|
+
"ANDROID_SDK_HOME",
|
|
410
|
+
"TEST_TMPDIR"
|
|
411
|
+
].flatMap((name) => env[name] ? [join(env[name], ".android")] : [])
|
|
412
|
+
];
|
|
413
|
+
for (const [index, directory] of [...new Set(preferences)].entries()) inputs.push({
|
|
414
|
+
name: `debug-keystore/${index}`,
|
|
415
|
+
path: join(directory, "debug.keystore"),
|
|
416
|
+
optional: true
|
|
417
|
+
});
|
|
418
|
+
for (const [index, path] of localFiles.entries()) inputs.push({
|
|
419
|
+
name: `local/${index}`,
|
|
420
|
+
path,
|
|
421
|
+
optional: true
|
|
422
|
+
});
|
|
423
|
+
const environment = Object.fromEntries([.../* @__PURE__ */ new Set([
|
|
424
|
+
...declaration.environment,
|
|
425
|
+
"JAVA_OPTS",
|
|
426
|
+
"JAVA_TOOL_OPTIONS",
|
|
427
|
+
"_JAVA_OPTIONS",
|
|
428
|
+
"GRADLE_OPTS"
|
|
429
|
+
])].toSorted().map((name) => [name, env[name] ?? null]));
|
|
430
|
+
const localProperties = await localPropertiesIdentity(properties, sdkDirectory, root, signal);
|
|
431
|
+
const local = await fingerprintNativeInputs(inputs, {
|
|
432
|
+
parameters: {
|
|
433
|
+
declaration,
|
|
434
|
+
environment,
|
|
435
|
+
localProperties: createHash("sha256").update(JSON.stringify(localProperties)).digest("hex")
|
|
436
|
+
},
|
|
437
|
+
signal
|
|
438
|
+
});
|
|
439
|
+
const hash = createHash("sha256").update("stim-native-gradle-artifact-v1\0").update(JSON.stringify({
|
|
440
|
+
source,
|
|
441
|
+
local: local.hash,
|
|
442
|
+
platform: process.platform,
|
|
443
|
+
arch: process.arch,
|
|
444
|
+
variant: build.variant ?? "debug",
|
|
445
|
+
compilerCache: build.compilerCache,
|
|
446
|
+
gradleBuildCache: build.gradleBuildCache,
|
|
447
|
+
pch: build.pch,
|
|
448
|
+
cas: build.cas,
|
|
449
|
+
abi
|
|
450
|
+
})).digest("hex");
|
|
451
|
+
return {
|
|
452
|
+
hash,
|
|
453
|
+
key: buildCacheKey("android", hash, {
|
|
454
|
+
variant: build.variant ?? "debug",
|
|
455
|
+
...abi ? { abi } : {}
|
|
456
|
+
}),
|
|
457
|
+
sdkDirectory
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/integrations/native-android-plan.ts
|
|
462
|
+
async function planNativeAndroid(root, options, resolved, operation) {
|
|
463
|
+
if (options.easProfile !== void 0) return { refusal: planFlagRefusal("--eas-profile") };
|
|
464
|
+
try {
|
|
465
|
+
const slot = validateDeviceSlot(options.slot);
|
|
466
|
+
const { context, settings } = resolved;
|
|
467
|
+
const { env, signal } = operation;
|
|
468
|
+
const declared = settingValueAt(settings, "android.artifactInputs");
|
|
469
|
+
gradleArtifactInputs(declared);
|
|
470
|
+
const planned = resolveAndroidRunPlan({
|
|
471
|
+
settings,
|
|
472
|
+
settingsContext: context,
|
|
473
|
+
env,
|
|
474
|
+
slot,
|
|
475
|
+
variant: options.variant ?? null,
|
|
476
|
+
systemImage: options.systemImage ?? null,
|
|
477
|
+
deviceProfile: options.deviceProfile ?? null,
|
|
478
|
+
device: null,
|
|
479
|
+
wait: void 0,
|
|
480
|
+
waitConflict: false,
|
|
481
|
+
remote: null,
|
|
482
|
+
buildCache: options.buildCache !== false
|
|
483
|
+
}, {
|
|
484
|
+
runtimeKind: () => "process",
|
|
485
|
+
variantProblem: () => null,
|
|
486
|
+
resolveCompilerCache: ({ optimizations }) => {
|
|
487
|
+
if (optimizations.android.compilerCache === "cas") throw new Error("Native Android planning does not initialize the experimental CAS toolchain.");
|
|
488
|
+
return {
|
|
489
|
+
cas: null,
|
|
490
|
+
optimizations,
|
|
491
|
+
warning: null
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
warn: (label, message) => operation.output("stderr", `${label}: ${message}\n`)
|
|
495
|
+
});
|
|
496
|
+
if (!planned.ok) return { refusal: planned };
|
|
497
|
+
const { build, target } = planned.plan;
|
|
498
|
+
if (target.kind !== "emulator") throw new Error("Native Android planning requires a local emulator target.");
|
|
499
|
+
const recorded = deviceSlotPlatforms(getProject(root), slot)?.android;
|
|
500
|
+
let reuseRecorded = Boolean(recorded?.owned && recorded.avdName && !recorded.setupIncomplete && ownedAvdDirectory(recorded.avdName));
|
|
501
|
+
const recordedImage = reuseRecorded && recorded?.avdName ? ownedAvdSystemImage(recorded.avdName) : null;
|
|
502
|
+
const requestedImage = options.systemImage?.trim();
|
|
503
|
+
if (recordedImage && requestedImage && recordedImage !== requestedImage) {
|
|
504
|
+
if (!recorded?.bootPending) return { refusal: {
|
|
505
|
+
code: "STIM_BAD_ARG",
|
|
506
|
+
message: `The recorded emulator uses ${recordedImage}; --system-image ${requestedImage} only replaces an emulator that never finished booting.`,
|
|
507
|
+
remedy: "Use the existing image or pass --slot <name> to create the requested emulator beside it."
|
|
508
|
+
} };
|
|
509
|
+
reuseRecorded = false;
|
|
510
|
+
}
|
|
511
|
+
if (reuseRecorded && recorded?.avdName && target.deviceProfile) {
|
|
512
|
+
const recordedProfile = ownedAvdDeviceProfile(recorded.avdName);
|
|
513
|
+
if (recordedProfile && recordedProfile !== target.deviceProfile) return { refusal: {
|
|
514
|
+
code: "STIM_BAD_ARG",
|
|
515
|
+
message: `The recorded emulator uses device profile ${recordedProfile}, but ${target.deviceProfile} was requested.`,
|
|
516
|
+
remedy: "Use the existing profile or pass --slot <name> to create the requested emulator beside it."
|
|
517
|
+
} };
|
|
518
|
+
}
|
|
519
|
+
let abi = null;
|
|
520
|
+
if (build.targetAbiOnly && !build.release) {
|
|
521
|
+
if (recorded?.avdName && !recorded.owned) return { refusal: {
|
|
522
|
+
code: "STIM_BAD_ARG",
|
|
523
|
+
message: "The legacy emulator assignment does not establish a build ABI for native planning.",
|
|
524
|
+
remedy: "Run stim android without --plan to reconcile the assignment, or use --slot <name> for an owned emulator."
|
|
525
|
+
} };
|
|
526
|
+
const image = reuseRecorded ? recordedImage : pickDefaultSystemImage(listInstalledSystemImages(), target.systemImage ? { systemImage: target.systemImage } : {})?.pkg;
|
|
527
|
+
abi = androidSystemImageAbi(image);
|
|
528
|
+
if (!abi) throw new Error("No installed emulator image identifies the native Android build ABI.");
|
|
529
|
+
}
|
|
530
|
+
const snapshot = await nativeGradleArtifactSnapshot(root, declared, build, abi, env, signal);
|
|
531
|
+
const hit = build.cache.read && await plannedNativeApk(snapshot.key, snapshot.sdkDirectory, abi, operation);
|
|
532
|
+
return planPayload({
|
|
533
|
+
root,
|
|
534
|
+
platform: "android",
|
|
535
|
+
slot,
|
|
536
|
+
projectKey: statsProjectKey({
|
|
537
|
+
root,
|
|
538
|
+
commonDir: context.gitCommonDir,
|
|
539
|
+
repoRoot: context.repoRoot
|
|
540
|
+
})
|
|
541
|
+
}, {
|
|
542
|
+
fingerprint: snapshot.hash,
|
|
543
|
+
cacheKey: snapshot.key,
|
|
544
|
+
cacheHit: hit ? "local" : false,
|
|
545
|
+
cacheSkipped: !build.cache.read,
|
|
546
|
+
provider: null,
|
|
547
|
+
prebuild: null,
|
|
548
|
+
refusal: null
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
operation.signal.throwIfAborted();
|
|
552
|
+
return { refusal: {
|
|
553
|
+
code: "STIM_BAD_ARG",
|
|
554
|
+
message: error.message,
|
|
555
|
+
remedy: "Check android.artifactInputs and the local Android toolchain, or run stim android without --plan for an uncached build."
|
|
556
|
+
} };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/integrations/native-android.ts
|
|
561
|
+
const CACHE_LIMIT = `${NATIVE_GRADLE_CACHE_LIMIT} Offload requires a separate complete android.offloadInputs declaration and remains uncached.`;
|
|
562
|
+
const MODEL_LIMIT = "Native Android requires AGP with androidComponents, one application module and configuration-on-demand disabled. Gradle resolves the exact variant and APK outputs during the build.";
|
|
563
|
+
function refusal(reason) {
|
|
564
|
+
return {
|
|
565
|
+
code: "STIM_BUILD_FAILED",
|
|
566
|
+
reason,
|
|
567
|
+
remedy: "Select one signed standalone APK for the requested variant and target ABI in the Android application module, then retry. Stim does not sign APKs or install split APK sets."
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function selectNativeApk(value, variant, abi) {
|
|
571
|
+
const model = value;
|
|
572
|
+
if (!model || model.schema !== 1 || typeof model.module !== "string" || !model.module.startsWith(":") || model.variant !== variant || typeof model.applicationId !== "string" || !/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/.test(model.applicationId) || typeof model.sdkDirectory !== "string" || !isAbsolute(model.sdkDirectory) || !Array.isArray(model.elements) || !model.elements.every((element) => element && typeof element.path === "string" && isAbsolute(element.path) && element.path.endsWith(".apk") && Array.isArray(element.filters) && element.filters.every((filter) => filter && typeof filter.type === "string" && typeof filter.value === "string"))) return refusal("Gradle did not export a valid model for the requested native Android variant.");
|
|
573
|
+
const universal = model.elements.filter((element) => element.filters.length === 0);
|
|
574
|
+
const candidates = universal.length ? universal : model.elements.filter((element) => abi !== null && element.filters.length === 1 && element.filters[0].type === "ABI" && element.filters[0].value === abi);
|
|
575
|
+
if (candidates.length !== 1) return refusal(`Native variant "${variant}" has ${candidates.length} installable APK candidates for ${abi ?? "an unspecified ABI"}. Density splits and ambiguous output sets are unsupported.`);
|
|
576
|
+
return {
|
|
577
|
+
apkPath: candidates[0].path,
|
|
578
|
+
androidPackage: model.applicationId
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function locateNativeApk(modelFile, variant, abi) {
|
|
582
|
+
let value;
|
|
583
|
+
try {
|
|
584
|
+
value = JSON.parse(readFileSync(modelFile, "utf8"));
|
|
585
|
+
} catch {
|
|
586
|
+
return refusal("Gradle succeeded without exporting the native Android APK model.");
|
|
587
|
+
}
|
|
588
|
+
const located = selectNativeApk(value, variant, abi);
|
|
589
|
+
if ("code" in located) return located;
|
|
590
|
+
try {
|
|
591
|
+
if (!statSync(located.apkPath).isFile()) return refusal("The APK selected by Gradle is not a file.");
|
|
592
|
+
} catch {
|
|
593
|
+
return refusal("The APK selected by Gradle does not exist.");
|
|
594
|
+
}
|
|
595
|
+
const signer = findBuildTool(["apksigner"], { home: value.sdkDirectory });
|
|
596
|
+
if (!signer) return refusal("No Android SDK apksigner is available to verify the native APK signature.");
|
|
597
|
+
try {
|
|
598
|
+
await getExecutor().runFileAsync(signer.path, ["verify", located.apkPath], { timeoutMs: 3e4 });
|
|
599
|
+
} catch (error) {
|
|
600
|
+
return refusal(`The native APK signature could not be verified: ${error.message}`);
|
|
601
|
+
}
|
|
602
|
+
return located;
|
|
603
|
+
}
|
|
604
|
+
function nativeAndroidProject(root) {
|
|
605
|
+
return {
|
|
606
|
+
isExpo: false,
|
|
607
|
+
packageRemedy: "Check the applicationId reported by AGP for the selected application variant.",
|
|
608
|
+
appIds: () => ({
|
|
609
|
+
bundleId: null,
|
|
610
|
+
androidPackage: null
|
|
611
|
+
}),
|
|
612
|
+
variantProblem: () => null,
|
|
613
|
+
targets: [
|
|
614
|
+
"emulator",
|
|
615
|
+
"physical",
|
|
616
|
+
"hosted"
|
|
617
|
+
],
|
|
618
|
+
eas: false,
|
|
619
|
+
runtimeKind: () => "process",
|
|
620
|
+
plan: (options, resolved, operation) => planNativeAndroid(root, options, resolved, operation),
|
|
621
|
+
runtime: ({ phase }) => androidProcessRuntime(async () => {
|
|
622
|
+
phase("metro", "skipped (native process)");
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
prepared: { metroPort: null }
|
|
626
|
+
};
|
|
627
|
+
}),
|
|
628
|
+
artifact: ({ operation, cacheArtifacts, writer, buildPlan, target, out, estimates, settings }) => {
|
|
629
|
+
const variant = buildPlan.variant ?? "debug";
|
|
630
|
+
const inputs = settingValueAt(settings, "android.offloadInputs");
|
|
631
|
+
const artifactInputs = settingValueAt(settings, "android.artifactInputs");
|
|
632
|
+
let snapshot = null;
|
|
633
|
+
let cacheRefusal = CACHE_LIMIT;
|
|
634
|
+
let compiledPackage = null;
|
|
635
|
+
let compiledSdk = null;
|
|
636
|
+
const signal = runCancellationSignal(operation) ?? operation?.signal;
|
|
637
|
+
const read = () => {
|
|
638
|
+
if (!operation) throw new Error("Native APK caching requires an operation context.");
|
|
639
|
+
return nativeGradleArtifactSnapshot(root, artifactInputs, buildPlan, target.abi, operation.env, signal);
|
|
640
|
+
};
|
|
641
|
+
let transfer = null;
|
|
642
|
+
const modelFile = join(workspaceDir(root), "gradle-build", `native-apk-${encodeURIComponent(variant)}.json`);
|
|
643
|
+
return {
|
|
644
|
+
providerCache: (provider) => {
|
|
645
|
+
if (!provider.files) return void 0;
|
|
646
|
+
if (!snapshot) throw new Error(CACHE_LIMIT);
|
|
647
|
+
return nativeAndroidFileCache(provider.files, snapshot.sdkDirectory, target.abi, operation);
|
|
648
|
+
},
|
|
649
|
+
identity: async () => {
|
|
650
|
+
if (cacheArtifacts === false) return { cacheIneligible: "Native Gradle worker artifacts have no reusable APK identity." };
|
|
651
|
+
try {
|
|
652
|
+
snapshot = await read();
|
|
653
|
+
cacheRefusal = null;
|
|
654
|
+
return snapshot;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
signal?.throwIfAborted();
|
|
657
|
+
cacheRefusal = error instanceof Error ? error.message : String(error);
|
|
658
|
+
return { cacheIneligible: cacheRefusal };
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
cache: () => {
|
|
662
|
+
if (!snapshot) throw new Error(CACHE_LIMIT);
|
|
663
|
+
return nativeAndroidCache(snapshot.sdkDirectory, target.abi, () => compiledPackage, operation);
|
|
664
|
+
},
|
|
665
|
+
prepare: async () => {},
|
|
666
|
+
reconcile: async () => {
|
|
667
|
+
if (snapshot) try {
|
|
668
|
+
snapshot = await read();
|
|
669
|
+
} catch {
|
|
670
|
+
signal?.throwIfAborted();
|
|
671
|
+
snapshot = null;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
identity: snapshot,
|
|
675
|
+
rekeyedBy: [],
|
|
676
|
+
cacheRefusal: null
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
validate: async () => {
|
|
680
|
+
if (!snapshot || compiledSdk !== snapshot.sdkDirectory) return null;
|
|
681
|
+
try {
|
|
682
|
+
return (await read()).hash === snapshot.hash ? snapshot : null;
|
|
683
|
+
} catch {
|
|
684
|
+
signal?.throwIfAborted();
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
materialize: async (key, path) => {
|
|
689
|
+
if (!snapshot) return null;
|
|
690
|
+
try {
|
|
691
|
+
if ((await read()).hash !== snapshot.hash) return null;
|
|
692
|
+
return await materializeNativeApk(key, path, snapshot.sdkDirectory, target.abi, operation);
|
|
693
|
+
} catch {
|
|
694
|
+
signal?.throwIfAborted();
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
explain: (rekeyedBy) => ({
|
|
699
|
+
reason: snapshot ? explainBuildMiss({
|
|
700
|
+
root,
|
|
701
|
+
platform: "android",
|
|
702
|
+
current: {
|
|
703
|
+
hash: snapshot.hash,
|
|
704
|
+
sources: []
|
|
705
|
+
},
|
|
706
|
+
rekeyedBy
|
|
707
|
+
}).reason : skippedMissReason(cacheRefusal ?? CACHE_LIMIT),
|
|
708
|
+
diff: null
|
|
709
|
+
}),
|
|
710
|
+
untrackedLine: () => null,
|
|
711
|
+
legacyCache: null,
|
|
712
|
+
offload: {
|
|
713
|
+
supportsUncachedArtifacts: true,
|
|
714
|
+
uncachedArtifacts: true,
|
|
715
|
+
get unsupported() {
|
|
716
|
+
if (buildPlan.cas) return "Apple Clang CAS builds build here";
|
|
717
|
+
try {
|
|
718
|
+
gradleOffloadInputs(inputs);
|
|
719
|
+
return null;
|
|
720
|
+
} catch (error) {
|
|
721
|
+
return error.message;
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
target: async () => {
|
|
725
|
+
transfer = await nativeGradleTransfer(root, inputs, runCancellationSignal(operation) ?? operation?.signal);
|
|
726
|
+
return {
|
|
727
|
+
platform: "android",
|
|
728
|
+
native: "gradle",
|
|
729
|
+
local: await androidToolchainAsync(operation?.env),
|
|
730
|
+
requires: {
|
|
731
|
+
ndk: null,
|
|
732
|
+
buildTools: null,
|
|
733
|
+
compileSdk: null
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
},
|
|
737
|
+
get request() {
|
|
738
|
+
if (!transfer) throw new Error("Native Gradle offload needs a declared source inventory.");
|
|
739
|
+
return {
|
|
740
|
+
platform: "android",
|
|
741
|
+
isExpo: false,
|
|
742
|
+
native: {
|
|
743
|
+
provider: "gradle",
|
|
744
|
+
transfer
|
|
745
|
+
},
|
|
746
|
+
android: {
|
|
747
|
+
variant,
|
|
748
|
+
abi: target.abi,
|
|
749
|
+
gradleBuildCache: buildPlan.gradleBuildCache,
|
|
750
|
+
pch: buildPlan.pch,
|
|
751
|
+
compilerCache: buildPlan.compilerCache === "none" ? "none" : "ccache"
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
unchanged: async () => transfer !== null && (await nativeGradleTransfer(root, inputs, runCancellationSignal(operation) ?? operation?.signal)).digest === transfer.digest
|
|
756
|
+
},
|
|
757
|
+
compile: async () => {
|
|
758
|
+
const script = ["../shim/native-android.gradle", "../../shim/native-android.gradle"].map((path) => fileURLToPath(new URL(path, import.meta.url))).find(existsSync);
|
|
759
|
+
if (!script) throw new Error("Stim installation is missing shim/native-android.gradle.");
|
|
760
|
+
return await buildGradle({
|
|
761
|
+
root,
|
|
762
|
+
logWriter: writer,
|
|
763
|
+
variant,
|
|
764
|
+
project: {
|
|
765
|
+
directory: root,
|
|
766
|
+
gradlew: join(root, process.platform === "win32" ? "gradlew.bat" : "gradlew"),
|
|
767
|
+
module: ":",
|
|
768
|
+
outputsDir: root
|
|
769
|
+
},
|
|
770
|
+
task: ":stimExportAndroidApk",
|
|
771
|
+
projectArgs: [
|
|
772
|
+
"--init-script",
|
|
773
|
+
script,
|
|
774
|
+
`-Pstim.native.variant=${variant}`,
|
|
775
|
+
`-Pstim.native.model=${modelFile}`
|
|
776
|
+
],
|
|
777
|
+
locate: async () => {
|
|
778
|
+
const located = await locateNativeApk(modelFile, variant, target.abi);
|
|
779
|
+
if (!("code" in located)) {
|
|
780
|
+
compiledSdk = await realpath(JSON.parse(readFileSync(modelFile, "utf8")).sdkDirectory);
|
|
781
|
+
compiledPackage = located.androidPackage ?? null;
|
|
782
|
+
}
|
|
783
|
+
return located;
|
|
784
|
+
}
|
|
785
|
+
}, {
|
|
786
|
+
estimateMs: estimates().coldBuildMs,
|
|
787
|
+
onHeartbeat: out,
|
|
788
|
+
onNote: out,
|
|
789
|
+
ccache: buildPlan.compilerCache === "ccache" ? resolveCcache({
|
|
790
|
+
root,
|
|
791
|
+
onNote: out,
|
|
792
|
+
env: operation?.env
|
|
793
|
+
}) : null,
|
|
794
|
+
cas: buildPlan.cas,
|
|
795
|
+
buildCache: buildPlan.gradleBuildCache,
|
|
796
|
+
pch: buildPlan.pch,
|
|
797
|
+
compilerCacheDisabled: buildPlan.compilerCache === "none"
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function nativeAndroidDoctor(root) {
|
|
805
|
+
return { inspect: ({ settings }) => {
|
|
806
|
+
let cache = CACHE_LIMIT;
|
|
807
|
+
try {
|
|
808
|
+
gradleArtifactInputs(settingValueAt(settings, "android.artifactInputs"));
|
|
809
|
+
cache = "Local native APK caching uses the project-declared complete inputs. Build workers remain uncached and need their separate android.offloadInputs declaration.";
|
|
810
|
+
} catch {}
|
|
811
|
+
const sdkPath = androidHome();
|
|
812
|
+
const sdk = androidSdkRefusal({
|
|
813
|
+
sdkPath,
|
|
814
|
+
sdkExists: existsSync(sdkPath),
|
|
815
|
+
hasLocalProperties: existsSync(join(root, "local.properties")),
|
|
816
|
+
localPropertiesPath: "local.properties"
|
|
817
|
+
});
|
|
818
|
+
const wrapper = join(root, process.platform === "win32" ? "gradlew.bat" : "gradlew");
|
|
819
|
+
return [
|
|
820
|
+
{
|
|
821
|
+
level: "note",
|
|
822
|
+
title: "Native Android Gradle project",
|
|
823
|
+
detail: `${MODEL_LIMIT} ${cache} Metro and EAS are unsupported; local, physical and hosted targets use native process readiness.`,
|
|
824
|
+
fix: null
|
|
825
|
+
},
|
|
826
|
+
...sdk ? [{
|
|
827
|
+
level: "cost",
|
|
828
|
+
code: "android-sdk-missing",
|
|
829
|
+
title: "No Android SDK was found",
|
|
830
|
+
detail: sdk.reason,
|
|
831
|
+
fix: sdk.remedy
|
|
832
|
+
}] : [],
|
|
833
|
+
...!existsSync(wrapper) ? [{
|
|
834
|
+
level: "cost",
|
|
835
|
+
code: "android-wrapper-missing",
|
|
836
|
+
title: "The Gradle wrapper for this host is missing",
|
|
837
|
+
detail: wrapper,
|
|
838
|
+
fix: "Restore the project Gradle wrapper."
|
|
839
|
+
}] : []
|
|
840
|
+
];
|
|
841
|
+
} };
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
export { nativeAndroidDoctor, nativeAndroidProject };
|