@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.
- package/dist/builders/android.js +2 -2
- package/dist/builders/ios.js +2 -2
- package/dist/steps/easFunctions.js +6 -0
- package/dist/steps/functions/downloadBuild.d.ts +9 -2
- package/dist/steps/functions/downloadBuild.js +83 -14
- package/dist/steps/functions/installBuild.d.ts +12 -0
- package/dist/steps/functions/installBuild.js +85 -0
- package/dist/steps/functions/installMaestro.d.ts +6 -1
- package/dist/steps/functions/installMaestro.js +107 -33
- package/dist/steps/functions/launchApplication.d.ts +10 -0
- package/dist/steps/functions/launchApplication.js +67 -0
- package/dist/steps/functions/maestroResultParser.d.ts +30 -0
- package/dist/steps/functions/maestroResultParser.js +78 -5
- package/dist/steps/functions/maestroScreenshots.d.ts +6 -0
- package/dist/steps/functions/maestroScreenshots.js +123 -0
- package/dist/steps/functions/maestroTests.js +187 -69
- package/dist/steps/functions/readIpaInfo.d.ts +1 -0
- package/dist/steps/functions/readIpaInfo.js +2 -0
- package/dist/steps/functions/repack.d.ts +5 -2
- package/dist/steps/functions/repack.js +50 -2
- package/dist/steps/functions/restoreBuildCache.d.ts +3 -3
- package/dist/steps/functions/restoreBuildCache.js +21 -7
- package/dist/steps/functions/saveBuildCache.d.ts +3 -3
- package/dist/steps/functions/saveBuildCache.js +21 -6
- package/dist/steps/functions/saveCache.js +3 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +7 -0
- package/dist/steps/functions/startAppiumRemoteSession.d.ts +17 -0
- package/dist/steps/functions/startAppiumRemoteSession.js +244 -0
- package/dist/steps/functions/startArgentRemoteSession.js +7 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +17 -1
- package/dist/steps/functions/uploadToAsc.js +11 -1
- package/dist/steps/utils/appiumEvents.d.ts +10 -0
- package/dist/steps/utils/appiumEvents.js +175 -0
- package/dist/steps/utils/ios/AscApiClient.d.ts +1 -0
- package/dist/steps/utils/ios/AscApiUtils.d.ts +11 -2
- package/dist/steps/utils/ios/AscApiUtils.js +36 -2
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +6 -3
- package/dist/steps/utils/remoteDeviceRunSession.js +81 -52
- package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
- package/dist/utils/AndroidEmulatorUtils.js +46 -4
- package/dist/utils/cacheKey.d.ts +8 -2
- package/dist/utils/cacheKey.js +12 -8
- package/package.json +6 -4
package/dist/builders/android.js
CHANGED
|
@@ -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,
|
package/dist/builders/ios.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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 =
|
|
68
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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}
|
|
208
|
+
PATH: `${binDir}:${global.env.PATH}`,
|
|
160
209
|
});
|
|
161
|
-
env.PATH = `${env.PATH}
|
|
162
|
-
process.env.PATH = `${process.env.PATH}
|
|
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}
|
|
245
|
+
PATH: `${maestroBinDir}:${global.env.PATH}`,
|
|
197
246
|
});
|
|
198
|
-
env.PATH = `${env.PATH}
|
|
199
|
-
process.env.PATH = `${process.env.PATH}
|
|
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)('
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
}
|