@expo/build-tools 22.0.0 → 22.3.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 (43) hide show
  1. package/dist/builders/android.js +2 -2
  2. package/dist/builders/ios.js +2 -2
  3. package/dist/steps/easFunctions.js +6 -0
  4. package/dist/steps/functions/downloadBuild.d.ts +9 -2
  5. package/dist/steps/functions/downloadBuild.js +83 -14
  6. package/dist/steps/functions/installBuild.d.ts +12 -0
  7. package/dist/steps/functions/installBuild.js +85 -0
  8. package/dist/steps/functions/installMaestro.d.ts +6 -1
  9. package/dist/steps/functions/installMaestro.js +107 -33
  10. package/dist/steps/functions/launchApplication.d.ts +10 -0
  11. package/dist/steps/functions/launchApplication.js +67 -0
  12. package/dist/steps/functions/maestroResultParser.d.ts +30 -0
  13. package/dist/steps/functions/maestroResultParser.js +78 -5
  14. package/dist/steps/functions/maestroScreenshots.d.ts +6 -0
  15. package/dist/steps/functions/maestroScreenshots.js +123 -0
  16. package/dist/steps/functions/maestroTests.js +187 -69
  17. package/dist/steps/functions/readIpaInfo.d.ts +1 -0
  18. package/dist/steps/functions/readIpaInfo.js +2 -0
  19. package/dist/steps/functions/repack.d.ts +5 -2
  20. package/dist/steps/functions/repack.js +50 -2
  21. package/dist/steps/functions/restoreBuildCache.d.ts +3 -3
  22. package/dist/steps/functions/restoreBuildCache.js +21 -7
  23. package/dist/steps/functions/saveBuildCache.d.ts +3 -3
  24. package/dist/steps/functions/saveBuildCache.js +21 -6
  25. package/dist/steps/functions/saveCache.js +3 -0
  26. package/dist/steps/functions/startAgentDeviceRemoteSession.js +7 -0
  27. package/dist/steps/functions/startAppiumRemoteSession.d.ts +17 -0
  28. package/dist/steps/functions/startAppiumRemoteSession.js +244 -0
  29. package/dist/steps/functions/startArgentRemoteSession.js +7 -0
  30. package/dist/steps/functions/startServeSimRemoteSession.js +17 -1
  31. package/dist/steps/functions/uploadToAsc.js +11 -1
  32. package/dist/steps/utils/appiumEvents.d.ts +10 -0
  33. package/dist/steps/utils/appiumEvents.js +175 -0
  34. package/dist/steps/utils/ios/AscApiClient.d.ts +1 -0
  35. package/dist/steps/utils/ios/AscApiUtils.d.ts +11 -2
  36. package/dist/steps/utils/ios/AscApiUtils.js +36 -2
  37. package/dist/steps/utils/remoteDeviceRunSession.d.ts +6 -3
  38. package/dist/steps/utils/remoteDeviceRunSession.js +81 -52
  39. package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
  40. package/dist/utils/AndroidEmulatorUtils.js +46 -4
  41. package/dist/utils/cacheKey.d.ts +8 -2
  42. package/dist/utils/cacheKey.js +12 -8
  43. 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,
@@ -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");
@@ -43,6 +45,7 @@ const sendSlackMessage_1 = require("./functions/sendSlackMessage");
43
45
  const startAgentDeviceRemoteSession_1 = require("./functions/startAgentDeviceRemoteSession");
44
46
  const startAndroidEmulator_1 = require("./functions/startAndroidEmulator");
45
47
  const startArgentRemoteSession_1 = require("./functions/startArgentRemoteSession");
48
+ const startAppiumRemoteSession_1 = require("./functions/startAppiumRemoteSession");
46
49
  const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
47
50
  const startIosSimulator_1 = require("./functions/startIosSimulator");
48
51
  const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorRecordings");
@@ -69,6 +72,8 @@ function getEasFunctions(ctx) {
69
72
  (0, prebuild_1.createPrebuildBuildFunction)(),
70
73
  (0, readIpaInfo_1.createReadIpaInfoBuildFunction)(),
71
74
  (0, downloadBuild_1.createDownloadBuildFunction)(ctx),
75
+ (0, installBuild_1.createInstallBuildFunction)(),
76
+ (0, launchApplication_1.createLaunchApplicationFunction)(),
72
77
  (0, export_1.createEasExportBuildFunction)(),
73
78
  (0, deploy_1.createEasDeployBuildFunction)(),
74
79
  (0, repack_1.createRepackBuildFunction)(),
@@ -90,6 +95,7 @@ function getEasFunctions(ctx) {
90
95
  (0, parseXcactivitylog_1.parseXcactivitylogFunction)(),
91
96
  (0, startAgentDeviceRemoteSession_1.createStartAgentDeviceRemoteSessionBuildFunction)(ctx),
92
97
  (0, startArgentRemoteSession_1.createStartArgentRemoteSessionBuildFunction)(ctx),
98
+ (0, startAppiumRemoteSession_1.createStartAppiumRemoteSessionBuildFunction)(ctx),
93
99
  (0, startAndroidEmulator_1.createStartAndroidEmulatorBuildFunction)(),
94
100
  (0, startCuttlefishDevice_1.createStartCuttlefishDeviceBuildFunction)(),
95
101
  (0, startIosSimulator_1.createStartIosSimulatorBuildFunction)(),
@@ -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,9 +8,11 @@ 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"));
@@ -44,7 +46,12 @@ function createDownloadBuildFunction(ctx) {
44
46
  inputProviders: [
45
47
  steps_1.BuildStepInput.createProvider({
46
48
  id: 'build_id',
47
- required: true,
49
+ required: false,
50
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
51
+ }),
52
+ steps_1.BuildStepInput.createProvider({
53
+ id: 'application_archive_url',
54
+ required: false,
48
55
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
49
56
  }),
50
57
  steps_1.BuildStepInput.createProvider({
@@ -64,11 +71,29 @@ function createDownloadBuildFunction(ctx) {
64
71
  const { logger } = stepsCtx;
65
72
  const extensions = zod_1.z.array(zod_1.z.string()).parse(inputs.extensions.value);
66
73
  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}...`);
74
+ const buildId = inputs.build_id.value
75
+ ? zod_1.z.string().uuid().parse(inputs.build_id.value)
76
+ : undefined;
77
+ const applicationArchiveUrl = inputs.application_archive_url.value
78
+ ? parseHttpApplicationArchiveUrl(inputs.application_archive_url.value)
79
+ : undefined;
80
+ let source;
81
+ if (buildId) {
82
+ if (applicationArchiveUrl) {
83
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of build_id or application_archive_url.');
84
+ }
85
+ source = { buildId };
86
+ }
87
+ else {
88
+ if (!applicationArchiveUrl) {
89
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass build_id or application_archive_url.');
90
+ }
91
+ source = { applicationArchiveUrl };
92
+ }
93
+ logger.info(buildId ? `Downloading build ${buildId}...` : `Downloading application archive...`);
69
94
  const { artifactPath } = await downloadBuildAsync({
70
95
  logger,
71
- buildId,
96
+ ...source,
72
97
  graphqlClient: ctx.graphqlClient,
73
98
  robotAccessToken: stepsCtx.global.staticContext.job.secrets?.robotAccessToken ?? null,
74
99
  extensions,
@@ -92,21 +117,32 @@ async function fetchApplicationArchiveUrlAsync({ buildId, graphqlClient, }) {
92
117
  }
93
118
  return applicationArchiveUrl;
94
119
  }
95
- async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }) {
120
+ async function downloadBuildAsync(params) {
121
+ const { logger, graphqlClient, robotAccessToken, extensions } = params;
122
+ let downloadUrl;
123
+ let headers;
124
+ if (params.applicationArchiveUrl) {
125
+ if (params.buildId) {
126
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of buildId or applicationArchiveUrl.');
127
+ }
128
+ downloadUrl = parseHttpApplicationArchiveUrl(params.applicationArchiveUrl);
129
+ headers = undefined;
130
+ }
131
+ else if (params.buildId) {
132
+ const buildId = zod_1.z.string().uuid().parse(params.buildId);
133
+ downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
134
+ headers = robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined;
135
+ }
136
+ else {
137
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass buildId or applicationArchiveUrl.');
138
+ }
96
139
  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
- });
140
+ const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, { headers });
101
141
  if (!response.ok) {
102
142
  const textResult = await (0, results_1.asyncResult)(response.text());
103
143
  throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
104
144
  }
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, '_');
145
+ const archiveFilename = resolveArchiveFilename({ response, extensions });
110
146
  const archivePath = node_path_1.default.join(downloadDestinationDirectory, archiveFilename);
111
147
  await streamPipeline(response.body, node_fs_1.default.createWriteStream(archivePath));
112
148
  const { size } = await node_fs_1.default.promises.stat(archivePath);
@@ -133,3 +169,36 @@ async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessT
133
169
  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')}`);
134
170
  return { artifactPath: matchingFiles[0] };
135
171
  }
172
+ function parseHttpApplicationArchiveUrl(value) {
173
+ try {
174
+ const applicationArchiveUrl = zod_1.z.string().parse(value);
175
+ const parsedUrl = new URL(applicationArchiveUrl);
176
+ (0, node_assert_1.default)(parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:');
177
+ return applicationArchiveUrl;
178
+ }
179
+ catch {
180
+ 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.');
181
+ }
182
+ }
183
+ function resolveArchiveFilename({ response, extensions, }) {
184
+ const contentDispositionHeader = response.headers.get('content-disposition');
185
+ let headerFilename;
186
+ if (contentDispositionHeader) {
187
+ try {
188
+ headerFilename = content_disposition_1.default.parse(contentDispositionHeader).parameters.filename;
189
+ }
190
+ catch {
191
+ // Ignore malformed Content-Disposition headers and fall back to the response URL.
192
+ }
193
+ }
194
+ const urlFilename = node_path_1.default.basename(new URL(response.url).pathname);
195
+ let archiveFilename = node_path_1.default.basename(headerFilename ?? urlFilename ?? '');
196
+ if (!archiveFilename || archiveFilename === '.' || archiveFilename === '..') {
197
+ archiveFilename = 'application';
198
+ }
199
+ if (!node_path_1.default.extname(archiveFilename) && extensions.length === 1) {
200
+ archiveFilename = `${archiveFilename}.${extensions[0]}`;
201
+ }
202
+ // URL and header filenames may contain percent-encoded or unsafe filesystem characters.
203
+ return archiveFilename.replace(/([^a-z0-9.-]+)/gi, '_');
204
+ }
@@ -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
+ }
@@ -1,2 +1,7 @@
1
- import { BuildFunction } from '@expo/steps';
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildStepEnv } from '@expo/steps';
2
3
  export declare function createInstallMaestroBuildFunction(): BuildFunction;
4
+ export declare function installIdbFromBrew({ logger, env, }: {
5
+ logger: bunyan;
6
+ env: BuildStepEnv;
7
+ }): Promise<void>;
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createInstallMaestroBuildFunction = createInstallMaestroBuildFunction;
7
+ exports.installIdbFromBrew = installIdbFromBrew;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
9
  const results_1 = require("@expo/results");
8
10
  const steps_1 = require("@expo/steps");
9
11
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
@@ -11,6 +13,7 @@ const assert_1 = __importDefault(require("assert"));
11
13
  const fs_1 = __importDefault(require("fs"));
12
14
  const os_1 = __importDefault(require("os"));
13
15
  const path_1 = __importDefault(require("path"));
16
+ const semver_1 = __importDefault(require("semver"));
14
17
  const maestroBackend_1 = require("./maestroBackend");
15
18
  const datadog_1 = require("../../datadog");
16
19
  function createInstallMaestroBuildFunction() {
@@ -94,17 +97,49 @@ function createInstallMaestroBuildFunction() {
94
97
  logger.info('Installing IDB');
95
98
  await installIdbFromBrew({ logger, env });
96
99
  }
97
- // Skip installing if the input sets a specific Maestro version to install
98
- // and it is already installed which happens when developing on a local computer.
99
- if (!currentMaestroVersion ||
100
- (requestedVersion && requestedVersion !== currentMaestroVersion)) {
101
- switch (backend) {
102
- case 'maestro':
100
+ switch (backend) {
101
+ case 'maestro':
102
+ // Skip installing if the input sets a specific Maestro version to install
103
+ // and it is already installed, either on a build image or a local computer.
104
+ if (!currentMaestroVersion ||
105
+ (requestedVersion && requestedVersion !== currentMaestroVersion)) {
103
106
  await installMaestro({ version: requestedVersion, global, logger, env });
104
- break;
105
- case 'maestro-runner':
106
- await installMaestroRunner({ version: requestedVersion, global, logger, env });
107
- break;
107
+ }
108
+ break;
109
+ case 'maestro-runner': {
110
+ let maestroRunnerVersionToInstall = requestedVersion;
111
+ const currentMaestroRunnerVersion = semver_1.default.coerce(currentMaestroVersion)?.version;
112
+ const requestedMaestroRunnerVersion = requestedVersion && requestedVersion !== 'latest'
113
+ ? semver_1.default.valid(requestedVersion)
114
+ : null;
115
+ if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
116
+ ((requestedMaestroRunnerVersion &&
117
+ semver_1.default.gte(requestedMaestroRunnerVersion, '1.1.16')) ||
118
+ requestedVersion === 'latest' ||
119
+ (requestedVersion === undefined &&
120
+ (!currentMaestroRunnerVersion || semver_1.default.gt(currentMaestroRunnerVersion, '1.1.15'))))) {
121
+ const xcodeVersion = await getXcodeVersion({ env });
122
+ // maestro-runner 1.1.16 added `arch` to its xcodebuild destination. Xcode versions
123
+ // below 26 reject that option, so use the last compatible maestro-runner version.
124
+ if (semver_1.default.lt(xcodeVersion, '26.0.0')) {
125
+ if (requestedMaestroRunnerVersion) {
126
+ 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
+ }
128
+ maestroRunnerVersionToInstall = '1.1.15';
129
+ logger.info(`Xcode ${xcodeVersion} requires maestro-runner ${maestroRunnerVersionToInstall}.`);
130
+ }
131
+ }
132
+ if (!currentMaestroVersion ||
133
+ (maestroRunnerVersionToInstall &&
134
+ maestroRunnerVersionToInstall !== currentMaestroVersion)) {
135
+ await installMaestroRunner({
136
+ version: maestroRunnerVersionToInstall,
137
+ global,
138
+ logger,
139
+ env,
140
+ });
141
+ }
142
+ break;
108
143
  }
109
144
  }
110
145
  const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
@@ -121,6 +156,20 @@ function createInstallMaestroBuildFunction() {
121
156
  },
122
157
  });
123
158
  }
159
+ async function getXcodeVersion({ env }) {
160
+ let stdout;
161
+ try {
162
+ ({ stdout } = await (0, turtle_spawn_1.default)('xcodebuild', ['-version'], { stdio: 'pipe', env }));
163
+ }
164
+ catch (error) {
165
+ throw new eas_build_job_1.SystemError('Failed to get Xcode version', { cause: error });
166
+ }
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()}`);
170
+ }
171
+ return xcodeVersion;
172
+ }
124
173
  async function getMaestroVersion({ env, backend, }) {
125
174
  switch (backend) {
126
175
  case 'maestro': {
@@ -156,10 +205,10 @@ async function installMaestroRunner({ global, version, logger, env, }) {
156
205
  const binDir = path_1.default.join(env.HOME, '.maestro-runner', 'bin');
157
206
  global.updateEnv({
158
207
  ...global.env,
159
- PATH: `${global.env.PATH}:${binDir}`,
208
+ PATH: `${binDir}:${global.env.PATH}`,
160
209
  });
161
- env.PATH = `${env.PATH}:${binDir}`;
162
- process.env.PATH = `${process.env.PATH}:${binDir}`;
210
+ env.PATH = `${binDir}:${env.PATH}`;
211
+ process.env.PATH = `${binDir}:${process.env.PATH}`;
163
212
  }
164
213
  finally {
165
214
  await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
@@ -193,10 +242,10 @@ async function installMaestro({ global, version, logger, env, }) {
193
242
  const maestroBinDir = path_1.default.join(maestroDir, 'bin');
194
243
  global.updateEnv({
195
244
  ...global.env,
196
- PATH: `${global.env.PATH}:${maestroBinDir}`,
245
+ PATH: `${maestroBinDir}:${global.env.PATH}`,
197
246
  });
198
- env.PATH = `${env.PATH}:${maestroBinDir}`;
199
- process.env.PATH = `${process.env.PATH}:${maestroBinDir}`;
247
+ env.PATH = `${maestroBinDir}:${env.PATH}`;
248
+ process.env.PATH = `${maestroBinDir}:${process.env.PATH}`;
200
249
  }
201
250
  finally {
202
251
  await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
@@ -204,7 +253,7 @@ async function installMaestro({ global, version, logger, env, }) {
204
253
  }
205
254
  async function isIdbInstalled({ env }) {
206
255
  try {
207
- await (0, turtle_spawn_1.default)('idb', ['-h'], { ignoreStdio: true, env });
256
+ await (0, turtle_spawn_1.default)('idb_companion', ['--version'], { ignoreStdio: true, env });
208
257
  return true;
209
258
  }
210
259
  catch {
@@ -212,22 +261,47 @@ async function isIdbInstalled({ env }) {
212
261
  }
213
262
  }
214
263
  async function installIdbFromBrew({ logger, env, }) {
215
- // Unfortunately our Mac images sometimes have two Homebrew
216
- // installations. We should use the ARM64 one, located in /opt/homebrew.
217
- const brewPath = '/opt/homebrew/bin/brew';
218
- const localEnv = {
219
- ...env,
220
- HOMEBREW_NO_AUTO_UPDATE: '1',
221
- HOMEBREW_NO_INSTALL_CLEANUP: '1',
222
- };
223
- await (0, turtle_spawn_1.default)(brewPath, ['tap', 'facebook/fb'], {
224
- env: localEnv,
225
- logger,
226
- });
227
- await (0, turtle_spawn_1.default)(brewPath, ['install', 'idb-companion'], {
228
- env: localEnv,
229
- logger,
230
- });
264
+ try {
265
+ // Unfortunately our Mac images sometimes have two Homebrew
266
+ // installations. We should use the ARM64 one, located in /opt/homebrew.
267
+ const brewPath = '/opt/homebrew/bin/brew';
268
+ const localEnv = {
269
+ ...env,
270
+ HOMEBREW_NO_AUTO_UPDATE: '1',
271
+ HOMEBREW_NO_INSTALL_CLEANUP: '1',
272
+ };
273
+ logger.info('Tapping facebook/fb...');
274
+ await (0, turtle_spawn_1.default)(brewPath, ['tap', 'facebook/fb'], {
275
+ env: localEnv,
276
+ logger,
277
+ });
278
+ const brewRepo = await (0, turtle_spawn_1.default)(brewPath, ['--repo', 'facebook/fb'], {
279
+ env: localEnv,
280
+ });
281
+ const tapPath = brewRepo.stdout.trim();
282
+ // c0386793f59da10c619787f2aa18d938ef1d69c9 is hash for 1.1.8 release,
283
+ // last known compatible version + post_install fix.
284
+ const gitSha = 'c0386793f59da10c619787f2aa18d938ef1d69c9';
285
+ logger.info('Checking out facebook/fb at idb_companion@1.1.8...');
286
+ await (0, turtle_spawn_1.default)('git', ['fetch', 'origin', gitSha], {
287
+ cwd: tapPath,
288
+ logger,
289
+ });
290
+ await (0, turtle_spawn_1.default)('git', ['checkout', gitSha], {
291
+ cwd: tapPath,
292
+ logger,
293
+ });
294
+ logger.info('Installing idb_companion v1.1.8...');
295
+ await (0, turtle_spawn_1.default)(brewPath, ['install', 'facebook/fb/idb-companion'], {
296
+ env: localEnv,
297
+ logger,
298
+ });
299
+ }
300
+ catch (err) {
301
+ throw new eas_build_job_1.SystemError('Failed to install idb-companion required for Maestro to run.', {
302
+ cause: err,
303
+ });
304
+ }
231
305
  }
232
306
  async function isJavaInstalled({ env }) {
233
307
  try {
@@ -0,0 +1,10 @@
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, runtimePlatform, env, logger, }: {
5
+ applicationIdentifier: string;
6
+ activityName?: string;
7
+ runtimePlatform: BuildRuntimePlatform;
8
+ env: BuildStepEnv;
9
+ logger: bunyan;
10
+ }): Promise<void>;
@@ -0,0 +1,67 @@
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.createLaunchApplicationFunction = createLaunchApplicationFunction;
7
+ exports.launchApplicationAsync = launchApplicationAsync;
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
+ function createLaunchApplicationFunction() {
12
+ return new steps_1.BuildFunction({
13
+ namespace: 'eas',
14
+ id: 'launch_application',
15
+ name: 'Launch application',
16
+ __metricsId: 'eas/launch_application',
17
+ inputProviders: [
18
+ steps_1.BuildStepInput.createProvider({
19
+ id: 'application_identifier',
20
+ required: true,
21
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
22
+ }),
23
+ steps_1.BuildStepInput.createProvider({
24
+ id: 'activity_name',
25
+ required: false,
26
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
27
+ }),
28
+ ],
29
+ fn: async ({ global, logger }, { inputs, env }) => {
30
+ const applicationIdentifier = parseNonEmptyStringInput(inputs.application_identifier.value, 'application_identifier');
31
+ const activityName = inputs.activity_name.value === undefined
32
+ ? undefined
33
+ : parseNonEmptyStringInput(inputs.activity_name.value, 'activity_name');
34
+ await launchApplicationAsync({
35
+ applicationIdentifier,
36
+ activityName,
37
+ runtimePlatform: global.runtimePlatform,
38
+ env,
39
+ logger,
40
+ });
41
+ },
42
+ });
43
+ }
44
+ async function launchApplicationAsync({ applicationIdentifier, activityName, runtimePlatform, env, logger, }) {
45
+ if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
46
+ logger.info(`Launching ${applicationIdentifier}.`);
47
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'launch', 'booted', applicationIdentifier], {
48
+ env,
49
+ logger,
50
+ });
51
+ return;
52
+ }
53
+ if (!activityName) {
54
+ throw new eas_build_job_1.UserError('EAS_LAUNCH_APPLICATION_MISSING_ACTIVITY', 'Launching an Android application requires activity_name.');
55
+ }
56
+ logger.info(`Launching ${applicationIdentifier}.`);
57
+ await (0, turtle_spawn_1.default)('adb', ['shell', 'am', 'start', '-n', `${applicationIdentifier}/${activityName}`], {
58
+ env,
59
+ logger,
60
+ });
61
+ }
62
+ function parseNonEmptyStringInput(value, inputName) {
63
+ if (typeof value !== 'string' || value.length === 0) {
64
+ throw new eas_build_job_1.UserError('EAS_LAUNCH_APPLICATION_INVALID_INPUT', `Input "${inputName}" must be a non-empty string. Pass the "${inputName}" output from eas/install_build.`);
65
+ }
66
+ return value;
67
+ }