@expo/build-tools 22.2.0 → 22.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/builders/android.js +2 -2
  2. package/dist/builders/ios.js +2 -2
  3. package/dist/ios/xcode.d.ts +3 -0
  4. package/dist/ios/xcode.js +22 -0
  5. package/dist/steps/easFunctions.js +4 -0
  6. package/dist/steps/functions/downloadBuild.d.ts +9 -2
  7. package/dist/steps/functions/downloadBuild.js +108 -15
  8. package/dist/steps/functions/installBuild.d.ts +12 -0
  9. package/dist/steps/functions/installBuild.js +85 -0
  10. package/dist/steps/functions/installMaestro.js +92 -11
  11. package/dist/steps/functions/launchApplication.d.ts +12 -0
  12. package/dist/steps/functions/launchApplication.js +121 -0
  13. package/dist/steps/functions/readIpaInfo.d.ts +1 -0
  14. package/dist/steps/functions/readIpaInfo.js +2 -0
  15. package/dist/steps/functions/repack.d.ts +5 -2
  16. package/dist/steps/functions/repack.js +50 -2
  17. package/dist/steps/functions/restoreBuildCache.d.ts +3 -3
  18. package/dist/steps/functions/restoreBuildCache.js +19 -6
  19. package/dist/steps/functions/saveBuildCache.d.ts +3 -3
  20. package/dist/steps/functions/saveBuildCache.js +17 -4
  21. package/dist/steps/functions/startServeSimRemoteSession.js +7 -0
  22. package/dist/steps/functions/uploadToAsc.js +11 -1
  23. package/dist/steps/utils/ios/AscApiClient.d.ts +1 -0
  24. package/dist/steps/utils/ios/AscApiUtils.d.ts +11 -2
  25. package/dist/steps/utils/ios/AscApiUtils.js +36 -2
  26. package/dist/steps/utils/ios/xcactivitylog.js +2 -8
  27. package/dist/steps/utils/remoteDeviceRunSession.d.ts +4 -2
  28. package/dist/steps/utils/remoteDeviceRunSession.js +9 -6
  29. package/dist/utils/IosSimulatorUtils.d.ts +8 -0
  30. package/dist/utils/IosSimulatorUtils.js +9 -0
  31. package/dist/utils/cacheKey.d.ts +8 -2
  32. package/dist/utils/cacheKey.js +12 -8
  33. package/dist/utils/download.d.ts +4 -0
  34. package/dist/utils/download.js +10 -0
  35. package/package.json +6 -4
@@ -81,7 +81,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
81
81
  await (0, restoreBuildCache_1.restoreCcacheAsync)({
82
82
  logger: ctx.logger,
83
83
  workingDirectory,
84
- platform: ctx.job.platform,
84
+ target: { platform: ctx.job.platform },
85
85
  env: ctx.env,
86
86
  secrets: ctx.job.secrets,
87
87
  });
@@ -203,7 +203,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
203
203
  await (0, saveBuildCache_1.saveCcacheAsync)({
204
204
  logger: ctx.logger,
205
205
  workingDirectory,
206
- platform: ctx.job.platform,
206
+ target: { platform: ctx.job.platform },
207
207
  evictUsedBefore,
208
208
  env: ctx.env,
209
209
  secrets: ctx.job.secrets,
@@ -97,7 +97,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
97
97
  await (0, restoreBuildCache_1.restoreCcacheAsync)({
98
98
  logger: ctx.logger,
99
99
  workingDirectory,
100
- platform: ctx.job.platform,
100
+ target: { platform: ctx.job.platform, simulator: ctx.job.simulator === true },
101
101
  env: ctx.env,
102
102
  secrets: ctx.job.secrets,
103
103
  });
@@ -224,7 +224,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
224
224
  await (0, saveBuildCache_1.saveCcacheAsync)({
225
225
  logger: ctx.logger,
226
226
  workingDirectory,
227
- platform: ctx.job.platform,
227
+ target: { platform: ctx.job.platform, simulator: ctx.job.simulator === true },
228
228
  evictUsedBefore,
229
229
  env: ctx.env,
230
230
  secrets: ctx.job.secrets,
@@ -0,0 +1,3 @@
1
+ export declare function getXcodeVersionAsync({ env }: {
2
+ env: NodeJS.ProcessEnv;
3
+ }): Promise<string>;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getXcodeVersionAsync = getXcodeVersionAsync;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
8
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
+ async function getXcodeVersionAsync({ env }) {
10
+ let stdout;
11
+ try {
12
+ ({ stdout } = await (0, turtle_spawn_1.default)('xcodebuild', ['-version'], { stdio: 'pipe', env }));
13
+ }
14
+ catch (error) {
15
+ throw new eas_build_job_1.SystemError('Failed to get Xcode version', { cause: error });
16
+ }
17
+ const version = /^Xcode\s+(\d+(?:\.\d+)*)$/m.exec(stdout)?.[1];
18
+ if (!version) {
19
+ throw new eas_build_job_1.SystemError(`Failed to determine Xcode version from: ${stdout.trim()}`);
20
+ }
21
+ return version;
22
+ }
@@ -20,8 +20,10 @@ const generateGymfileFromTemplate_1 = require("./functions/generateGymfileFromTe
20
20
  const getCredentialsForBuildTriggeredByGitHubIntegration_1 = require("./functions/getCredentialsForBuildTriggeredByGitHubIntegration");
21
21
  const injectAndroidCredentials_1 = require("./functions/injectAndroidCredentials");
22
22
  const installMaestro_1 = require("./functions/installMaestro");
23
+ const installBuild_1 = require("./functions/installBuild");
23
24
  const installNodeModules_1 = require("./functions/installNodeModules");
24
25
  const installPods_1 = require("./functions/installPods");
26
+ const launchApplication_1 = require("./functions/launchApplication");
25
27
  const prebuild_1 = require("./functions/prebuild");
26
28
  const readAppConfig_1 = require("./functions/readAppConfig");
27
29
  const readIpaInfo_1 = require("./functions/readIpaInfo");
@@ -70,6 +72,8 @@ function getEasFunctions(ctx) {
70
72
  (0, prebuild_1.createPrebuildBuildFunction)(),
71
73
  (0, readIpaInfo_1.createReadIpaInfoBuildFunction)(),
72
74
  (0, downloadBuild_1.createDownloadBuildFunction)(ctx),
75
+ (0, installBuild_1.createInstallBuildFunction)(),
76
+ (0, launchApplication_1.createLaunchApplicationFunction)(),
73
77
  (0, export_1.createEasExportBuildFunction)(),
74
78
  (0, deploy_1.createEasDeployBuildFunction)(),
75
79
  (0, repack_1.createRepackBuildFunction)(),
@@ -2,13 +2,20 @@ import { bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
3
  import { Client } from '@urql/core';
4
4
  import { CustomBuildContext } from '../../customBuildContext';
5
+ type DownloadBuildSource = {
6
+ buildId: string;
7
+ applicationArchiveUrl?: never;
8
+ } | {
9
+ buildId?: never;
10
+ applicationArchiveUrl: string;
11
+ };
5
12
  export declare function createDownloadBuildFunction(ctx: CustomBuildContext): BuildFunction;
6
- export declare function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }: {
13
+ export declare function downloadBuildAsync(params: DownloadBuildSource & {
7
14
  logger: bunyan;
8
- buildId: string;
9
15
  graphqlClient: Client;
10
16
  robotAccessToken: string | null;
11
17
  extensions: string[];
12
18
  }): Promise<{
13
19
  artifactPath: string;
14
20
  }>;
21
+ export {};
@@ -8,15 +8,19 @@ exports.downloadBuildAsync = downloadBuildAsync;
8
8
  const eas_build_job_1 = require("@expo/eas-build-job");
9
9
  const results_1 = require("@expo/results");
10
10
  const steps_1 = require("@expo/steps");
11
+ const content_disposition_1 = __importDefault(require("content-disposition"));
11
12
  const fast_glob_1 = require("fast-glob");
12
13
  const gql_tada_1 = require("gql.tada");
13
14
  const node_fetch_1 = __importDefault(require("node-fetch"));
15
+ const node_assert_1 = __importDefault(require("node:assert"));
14
16
  const node_fs_1 = __importDefault(require("node:fs"));
15
17
  const node_os_1 = __importDefault(require("node:os"));
16
18
  const node_path_1 = __importDefault(require("node:path"));
17
19
  const stream_1 = __importDefault(require("stream"));
18
20
  const util_1 = require("util");
19
21
  const zod_1 = require("zod");
22
+ const bplist_parser_1 = __importDefault(require("bplist-parser"));
23
+ const plist_1 = __importDefault(require("plist"));
20
24
  const artifacts_1 = require("../../utils/artifacts");
21
25
  const files_1 = require("../../utils/files");
22
26
  const retryOnDNSFailure_1 = require("../../utils/retryOnDNSFailure");
@@ -44,7 +48,12 @@ function createDownloadBuildFunction(ctx) {
44
48
  inputProviders: [
45
49
  steps_1.BuildStepInput.createProvider({
46
50
  id: 'build_id',
47
- required: true,
51
+ required: false,
52
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
53
+ }),
54
+ steps_1.BuildStepInput.createProvider({
55
+ id: 'application_archive_url',
56
+ required: false,
48
57
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
49
58
  }),
50
59
  steps_1.BuildStepInput.createProvider({
@@ -64,11 +73,29 @@ function createDownloadBuildFunction(ctx) {
64
73
  const { logger } = stepsCtx;
65
74
  const extensions = zod_1.z.array(zod_1.z.string()).parse(inputs.extensions.value);
66
75
  logger.info(`Expected extensions: [${extensions.join(', ')}]`);
67
- const buildId = zod_1.z.string().uuid().parse(inputs.build_id.value);
68
- logger.info(`Downloading build ${buildId}...`);
76
+ const buildId = inputs.build_id.value
77
+ ? zod_1.z.string().uuid().parse(inputs.build_id.value)
78
+ : undefined;
79
+ const applicationArchiveUrl = inputs.application_archive_url.value
80
+ ? parseHttpApplicationArchiveUrl(inputs.application_archive_url.value)
81
+ : undefined;
82
+ let source;
83
+ if (buildId) {
84
+ if (applicationArchiveUrl) {
85
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of build_id or application_archive_url.');
86
+ }
87
+ source = { buildId };
88
+ }
89
+ else {
90
+ if (!applicationArchiveUrl) {
91
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass build_id or application_archive_url.');
92
+ }
93
+ source = { applicationArchiveUrl };
94
+ }
95
+ logger.info(buildId ? `Downloading build ${buildId}...` : `Downloading application archive...`);
69
96
  const { artifactPath } = await downloadBuildAsync({
70
97
  logger,
71
- buildId,
98
+ ...source,
72
99
  graphqlClient: ctx.graphqlClient,
73
100
  robotAccessToken: stepsCtx.global.staticContext.job.secrets?.robotAccessToken ?? null,
74
101
  extensions,
@@ -92,21 +119,32 @@ async function fetchApplicationArchiveUrlAsync({ buildId, graphqlClient, }) {
92
119
  }
93
120
  return applicationArchiveUrl;
94
121
  }
95
- async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }) {
122
+ async function downloadBuildAsync(params) {
123
+ const { logger, graphqlClient, robotAccessToken, extensions } = params;
124
+ let downloadUrl;
125
+ let headers;
126
+ if (params.applicationArchiveUrl) {
127
+ if (params.buildId) {
128
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of buildId or applicationArchiveUrl.');
129
+ }
130
+ downloadUrl = parseHttpApplicationArchiveUrl(params.applicationArchiveUrl);
131
+ headers = undefined;
132
+ }
133
+ else if (params.buildId) {
134
+ const buildId = zod_1.z.string().uuid().parse(params.buildId);
135
+ downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
136
+ headers = robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined;
137
+ }
138
+ else {
139
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass buildId or applicationArchiveUrl.');
140
+ }
96
141
  const downloadDestinationDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'download_build-downloaded-'));
97
- const downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
98
- const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, {
99
- headers: robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined,
100
- });
142
+ const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, { headers });
101
143
  if (!response.ok) {
102
144
  const textResult = await (0, results_1.asyncResult)(response.text());
103
145
  throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
104
146
  }
105
- // URL may contain percent-encoded characters, e.g. my%20file.apk
106
- // this replaces all non-alphanumeric characters (excluding dot) with underscore
107
- const archiveFilename = node_path_1.default
108
- .basename(new URL(response.url).pathname)
109
- .replace(/([^a-z0-9.-]+)/gi, '_');
147
+ const archiveFilename = resolveArchiveFilename({ response, extensions });
110
148
  const archivePath = node_path_1.default.join(downloadDestinationDirectory, archiveFilename);
111
149
  await streamPipeline(response.body, node_fs_1.default.createWriteStream(archivePath));
112
150
  const { size } = await node_fs_1.default.promises.stat(archivePath);
@@ -127,9 +165,64 @@ async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessT
127
165
  onlyFiles: false,
128
166
  onlyDirectories: false,
129
167
  });
168
+ let matchingFilesRoot = extractionDirectory;
169
+ if (matchingFiles.length === 0 &&
170
+ extensions.includes('app') &&
171
+ (await isIosAppBundleAsync(extractionDirectory))) {
172
+ const appBundlePath = `${extractionDirectory}.app`;
173
+ await node_fs_1.default.promises.rename(extractionDirectory, appBundlePath);
174
+ matchingFiles.push(appBundlePath);
175
+ matchingFilesRoot = node_path_1.default.dirname(appBundlePath);
176
+ }
130
177
  if (matchingFiles.length === 0) {
131
178
  throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_NO_MATCHING_FILES', `No ${extensions.map(ext => `.${ext}`).join(', ')} entries found in the archive.`);
132
179
  }
133
- logger.info(`Found ${matchingFiles.length} matching ${(0, strings_1.pluralize)(matchingFiles.length, 'entry')}:\n${matchingFiles.map(f => `- ${node_path_1.default.relative(extractionDirectory, f)}`).join('\n')}`);
180
+ logger.info(`Found ${matchingFiles.length} matching ${(0, strings_1.pluralize)(matchingFiles.length, 'entry')}:\n${matchingFiles.map(f => `- ${node_path_1.default.relative(matchingFilesRoot, f)}`).join('\n')}`);
134
181
  return { artifactPath: matchingFiles[0] };
135
182
  }
183
+ async function isIosAppBundleAsync(directory) {
184
+ try {
185
+ const infoPlist = await node_fs_1.default.promises.readFile(node_path_1.default.join(directory, 'Info.plist'));
186
+ const isBinaryPlist = infoPlist.subarray(0, 8).toString('ascii') === 'bplist00';
187
+ const parsedInfoPlist = (isBinaryPlist
188
+ ? bplist_parser_1.default.parseBuffer(infoPlist)[0]
189
+ : plist_1.default.parse(infoPlist.toString('utf8')));
190
+ return parsedInfoPlist?.CFBundlePackageType === 'APPL';
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ function parseHttpApplicationArchiveUrl(value) {
197
+ try {
198
+ const applicationArchiveUrl = zod_1.z.string().parse(value);
199
+ const parsedUrl = new URL(applicationArchiveUrl);
200
+ (0, node_assert_1.default)(parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:');
201
+ return applicationArchiveUrl;
202
+ }
203
+ catch {
204
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_APPLICATION_ARCHIVE_URL', 'application_archive_url must be a valid HTTP or HTTPS URL.');
205
+ }
206
+ }
207
+ function resolveArchiveFilename({ response, extensions, }) {
208
+ const contentDispositionHeader = response.headers.get('content-disposition');
209
+ let headerFilename;
210
+ if (contentDispositionHeader) {
211
+ try {
212
+ headerFilename = content_disposition_1.default.parse(contentDispositionHeader).parameters.filename;
213
+ }
214
+ catch {
215
+ // Ignore malformed Content-Disposition headers and fall back to the response URL.
216
+ }
217
+ }
218
+ const urlFilename = node_path_1.default.basename(new URL(response.url).pathname);
219
+ let archiveFilename = node_path_1.default.basename(headerFilename ?? urlFilename ?? '');
220
+ if (!archiveFilename || archiveFilename === '.' || archiveFilename === '..') {
221
+ archiveFilename = 'application';
222
+ }
223
+ if (!node_path_1.default.extname(archiveFilename) && extensions.length === 1) {
224
+ archiveFilename = `${archiveFilename}.${extensions[0]}`;
225
+ }
226
+ // URL and header filenames may contain percent-encoded or unsafe filesystem characters.
227
+ return archiveFilename.replace(/([^a-z0-9.-]+)/gi, '_');
228
+ }
@@ -0,0 +1,12 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
3
+ export declare function createInstallBuildFunction(): BuildFunction;
4
+ export declare function installBuildAsync({ artifactPath, runtimePlatform, env, logger, }: {
5
+ artifactPath: string;
6
+ runtimePlatform: BuildRuntimePlatform;
7
+ env: BuildStepEnv;
8
+ logger: bunyan;
9
+ }): Promise<{
10
+ applicationIdentifier: string;
11
+ activityName?: string;
12
+ }>;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createInstallBuildFunction = createInstallBuildFunction;
7
+ exports.installBuildAsync = installBuildAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
+ const steps_1 = require("@expo/steps");
10
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const zod_1 = require("zod");
14
+ function createInstallBuildFunction() {
15
+ return new steps_1.BuildFunction({
16
+ namespace: 'eas',
17
+ id: 'install_build',
18
+ name: 'Install build',
19
+ __metricsId: 'eas/install_build',
20
+ inputProviders: [
21
+ steps_1.BuildStepInput.createProvider({
22
+ id: 'artifact_path',
23
+ required: true,
24
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
25
+ }),
26
+ ],
27
+ outputProviders: [
28
+ steps_1.BuildStepOutput.createProvider({
29
+ id: 'application_identifier',
30
+ required: true,
31
+ }),
32
+ steps_1.BuildStepOutput.createProvider({
33
+ id: 'activity_name',
34
+ required: false,
35
+ }),
36
+ ],
37
+ fn: async ({ global, logger }, { inputs, outputs, env }) => {
38
+ const artifactPath = zod_1.z.string().min(1).parse(inputs.artifact_path.value);
39
+ const { applicationIdentifier, activityName } = await installBuildAsync({
40
+ artifactPath,
41
+ runtimePlatform: global.runtimePlatform,
42
+ env,
43
+ logger,
44
+ });
45
+ outputs.application_identifier.set(applicationIdentifier);
46
+ if (activityName) {
47
+ outputs.activity_name.set(activityName);
48
+ }
49
+ },
50
+ });
51
+ }
52
+ async function installBuildAsync({ artifactPath, runtimePlatform, env, logger, }) {
53
+ const artifactStat = await node_fs_1.default.promises.stat(artifactPath).catch(err => {
54
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', `Build artifact does not exist at ${artifactPath}.`, { cause: err });
55
+ });
56
+ if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
57
+ if (node_path_1.default.extname(artifactPath) !== '.app' || !artifactStat.isDirectory()) {
58
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', 'iOS Simulator sessions require a .app build artifact.');
59
+ }
60
+ const infoPlistPath = node_path_1.default.join(artifactPath, 'Info.plist');
61
+ const { stdout } = await (0, turtle_spawn_1.default)('plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', infoPlistPath], { stdio: 'pipe', env });
62
+ const applicationIdentifier = stdout.trim();
63
+ if (!applicationIdentifier) {
64
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_MISSING_IDENTIFIER', `Could not read CFBundleIdentifier from ${infoPlistPath}.`);
65
+ }
66
+ logger.info(`Installing ${artifactPath} on the iOS Simulator.`);
67
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'install', 'booted', artifactPath], { env, logger });
68
+ return { applicationIdentifier };
69
+ }
70
+ if (node_path_1.default.extname(artifactPath) !== '.apk' || !artifactStat.isFile()) {
71
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', 'Android Emulator sessions require an .apk build artifact.');
72
+ }
73
+ const { stdout } = await (0, turtle_spawn_1.default)('aapt2', ['dump', 'badging', artifactPath], {
74
+ stdio: 'pipe',
75
+ env,
76
+ });
77
+ const applicationIdentifier = stdout.match(/package: name='([^']+)'/)?.[1];
78
+ const activityName = stdout.match(/launchable-activity: name='([^']+)'/)?.[1];
79
+ if (!applicationIdentifier) {
80
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_MISSING_IDENTIFIER', `Could not read an Android application identifier from ${artifactPath}.`);
81
+ }
82
+ logger.info(`Installing ${artifactPath} on the Android Emulator.`);
83
+ await (0, turtle_spawn_1.default)('adb', ['install', '-r', artifactPath], { env, logger });
84
+ return { applicationIdentifier, activityName };
85
+ }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createInstallMaestroBuildFunction = createInstallMaestroBuildFunction;
7
7
  exports.installIdbFromBrew = installIdbFromBrew;
8
+ const downloader_1 = __importDefault(require("@expo/downloader"));
8
9
  const eas_build_job_1 = require("@expo/eas-build-job");
9
10
  const results_1 = require("@expo/results");
10
11
  const steps_1 = require("@expo/steps");
@@ -16,6 +17,10 @@ const path_1 = __importDefault(require("path"));
16
17
  const semver_1 = __importDefault(require("semver"));
17
18
  const maestroBackend_1 = require("./maestroBackend");
18
19
  const datadog_1 = require("../../datadog");
20
+ const xcode_1 = require("../../ios/xcode");
21
+ const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
22
+ const download_1 = require("../../utils/download");
23
+ const MAESTRO_RUNNER_WDA_CACHE_DOWNLOAD_TIMEOUT_MS = 20_000;
19
24
  function createInstallMaestroBuildFunction() {
20
25
  return new steps_1.BuildFunction({
21
26
  namespace: 'eas',
@@ -118,10 +123,12 @@ function createInstallMaestroBuildFunction() {
118
123
  requestedVersion === 'latest' ||
119
124
  (requestedVersion === undefined &&
120
125
  (!currentMaestroRunnerVersion || semver_1.default.gt(currentMaestroRunnerVersion, '1.1.15'))))) {
121
- const xcodeVersion = await getXcodeVersion({ env });
126
+ // `getXcodeVersionAsync` returns the raw version (e.g. "16.4"), which
127
+ // `semver.coerce` normalizes (e.g. "16.4.0") for comparison and display.
128
+ const xcodeVersion = semver_1.default.coerce(await (0, xcode_1.getXcodeVersionAsync)({ env }));
122
129
  // maestro-runner 1.1.16 added `arch` to its xcodebuild destination. Xcode versions
123
130
  // below 26 reject that option, so use the last compatible maestro-runner version.
124
- if (semver_1.default.lt(xcodeVersion, '26.0.0')) {
131
+ if (xcodeVersion && semver_1.default.lt(xcodeVersion, '26.0.0')) {
125
132
  if (requestedMaestroRunnerVersion) {
126
133
  throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', `maestro-runner ${requestedVersion} is not compatible with Xcode ${xcodeVersion}. Use maestro-runner 1.1.15 or an Xcode 26+ image.`);
127
134
  }
@@ -147,6 +154,12 @@ function createInstallMaestroBuildFunction() {
147
154
  logger.error(maestroVersionResult.reason, 'Failed to get Maestro version.');
148
155
  throw new Error(`Failed to ensure ${backend} is installed.`);
149
156
  }
157
+ if (backend === 'maestro-runner' && global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
158
+ await installMaestroRunnerWdaCache({
159
+ logger,
160
+ env,
161
+ });
162
+ }
150
163
  logger.info(`${backend} ${maestroVersionResult.value} is ready.`);
151
164
  outputs.maestro_version.set(maestroVersionResult.value);
152
165
  datadog_1.Datadog.distribution('eas.maestro.install', 1, {
@@ -156,19 +169,87 @@ function createInstallMaestroBuildFunction() {
156
169
  },
157
170
  });
158
171
  }
159
- async function getXcodeVersion({ env }) {
160
- let stdout;
172
+ async function installMaestroRunnerWdaCache({ logger, env, }) {
173
+ if (!env.HOME) {
174
+ logger.warn('Skipping the prebuilt WebDriverAgent cache because the $HOME environment variable is empty.');
175
+ return;
176
+ }
177
+ const maestroRunnerHome = path_1.default.join(env.HOME, '.maestro-runner');
161
178
  try {
162
- ({ stdout } = await (0, turtle_spawn_1.default)('xcodebuild', ['-version'], { stdio: 'pipe', env }));
179
+ const wdaVersion = await getMaestroRunnerWdaVersion({ maestroRunnerHome });
180
+ if (!wdaVersion) {
181
+ logger.info('Skipping the prebuilt WebDriverAgent cache because the installed WDA version is unknown.');
182
+ return;
183
+ }
184
+ const xcodeVersion = await (0, xcode_1.getXcodeVersionAsync)({ env });
185
+ const iosRuntimes = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableRuntimesAsync({ env });
186
+ const iosRuntimeIdentifierPrefix = 'com.apple.CoreSimulator.SimRuntime.iOS-';
187
+ // maestro-runner derives its WDA cache key from the runtime identifier, not from the runtime
188
+ // version. These values can differ for patched runtimes, for example the iOS-26-3 identifier
189
+ // can report version 26.3.1.
190
+ const iosRuntimeVersions = [
191
+ ...new Set(iosRuntimes.map(runtime => runtime.identifier.slice(iosRuntimeIdentifierPrefix.length).replaceAll('-', '.'))),
192
+ ];
193
+ if (iosRuntimeVersions.length === 0) {
194
+ logger.info('Skipping the prebuilt WebDriverAgent cache because no iOS runtime is available.');
195
+ return;
196
+ }
197
+ const tempDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'install_maestro_runner_wda_cache'));
198
+ try {
199
+ const archivePath = path_1.default.join(tempDirectory, 'wda-cache.tar.gz');
200
+ const directArchiveUrl = `https://storage.googleapis.com/turtle-v2/maestro-runner-wda-cache/` +
201
+ `xcode-${encodeURIComponent(xcodeVersion)}-wda-${encodeURIComponent(wdaVersion)}.tar.gz`;
202
+ const proxiedArchiveUrl = (0, download_1.getProxiedDownloadUrl)({
203
+ directUrl: directArchiveUrl,
204
+ proxyBaseUrl: env.EAS_BUILD_COCOAPODS_CACHE_URL,
205
+ });
206
+ logger.info(`Downloading the prebuilt WebDriverAgent cache for Xcode ${xcodeVersion}`);
207
+ if (proxiedArchiveUrl) {
208
+ try {
209
+ await (0, downloader_1.default)(proxiedArchiveUrl, archivePath, {
210
+ retry: 3,
211
+ timeout: MAESTRO_RUNNER_WDA_CACHE_DOWNLOAD_TIMEOUT_MS,
212
+ });
213
+ }
214
+ catch (err) {
215
+ logger.debug({ err }, 'Failed to download the prebuilt WebDriverAgent cache via the proxy; falling back to the direct URL.');
216
+ await (0, downloader_1.default)(directArchiveUrl, archivePath, {
217
+ retry: 3,
218
+ timeout: MAESTRO_RUNNER_WDA_CACHE_DOWNLOAD_TIMEOUT_MS,
219
+ });
220
+ }
221
+ }
222
+ else {
223
+ await (0, downloader_1.default)(directArchiveUrl, archivePath, {
224
+ retry: 3,
225
+ timeout: MAESTRO_RUNNER_WDA_CACHE_DOWNLOAD_TIMEOUT_MS,
226
+ });
227
+ }
228
+ await fs_1.default.promises.mkdir(maestroRunnerHome, { recursive: true });
229
+ await (0, turtle_spawn_1.default)('tar', ['-xzf', archivePath, '-C', maestroRunnerHome], { logger, env });
230
+ const genericProductsDirectory = path_1.default.join(maestroRunnerHome, 'cache', 'wda-builds', 'generic', 'DerivedData', 'Build', 'Products');
231
+ for (const runtimeVersion of iosRuntimeVersions) {
232
+ const productsDirectory = path_1.default.join(maestroRunnerHome, 'cache', 'wda-builds', `sim-ios${runtimeVersion}-iphone`, 'DerivedData', 'Build', 'Products');
233
+ await fs_1.default.promises.cp(genericProductsDirectory, productsDirectory, { recursive: true });
234
+ }
235
+ logger.info(`Installed the prebuilt WebDriverAgent cache for iOS ${iosRuntimeVersions.join(', ')}.`);
236
+ }
237
+ finally {
238
+ await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
239
+ }
163
240
  }
164
- catch (error) {
165
- throw new eas_build_job_1.SystemError('Failed to get Xcode version', { cause: error });
241
+ catch (err) {
242
+ logger.warn({ err }, 'Failed to install the prebuilt WebDriverAgent cache. maestro-runner will build WebDriverAgent when it is needed.');
166
243
  }
167
- const xcodeVersion = semver_1.default.coerce(/^Xcode\s+(\S+)/m.exec(stdout)?.[1])?.version;
168
- if (!xcodeVersion) {
169
- throw new eas_build_job_1.SystemError(`Failed to parse Xcode version from xcodebuild output: ${stdout.trim()}`);
244
+ }
245
+ async function getMaestroRunnerWdaVersion({ maestroRunnerHome, }) {
246
+ try {
247
+ const packageJson = JSON.parse(await fs_1.default.promises.readFile(path_1.default.join(maestroRunnerHome, 'drivers', 'ios', 'WebDriverAgent', 'package.json'), 'utf8'));
248
+ return typeof packageJson.version === 'string' ? packageJson.version : null;
249
+ }
250
+ catch {
251
+ return null;
170
252
  }
171
- return xcodeVersion;
172
253
  }
173
254
  async function getMaestroVersion({ env, backend, }) {
174
255
  switch (backend) {
@@ -0,0 +1,12 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
3
+ export declare function createLaunchApplicationFunction(): BuildFunction;
4
+ export declare function launchApplicationAsync({ applicationIdentifier, activityName, launchArgs, openUrl, runtimePlatform, env, logger, }: {
5
+ applicationIdentifier: string;
6
+ activityName?: string;
7
+ launchArgs?: string[];
8
+ openUrl?: string;
9
+ runtimePlatform: BuildRuntimePlatform;
10
+ env: BuildStepEnv;
11
+ logger: bunyan;
12
+ }): Promise<void>;