@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.
- package/dist/builders/android.js +2 -2
- package/dist/builders/ios.js +2 -2
- package/dist/ios/xcode.d.ts +3 -0
- package/dist/ios/xcode.js +22 -0
- package/dist/steps/easFunctions.js +4 -0
- package/dist/steps/functions/downloadBuild.d.ts +9 -2
- package/dist/steps/functions/downloadBuild.js +108 -15
- package/dist/steps/functions/installBuild.d.ts +12 -0
- package/dist/steps/functions/installBuild.js +85 -0
- package/dist/steps/functions/installMaestro.js +92 -11
- package/dist/steps/functions/launchApplication.d.ts +12 -0
- package/dist/steps/functions/launchApplication.js +121 -0
- 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 +19 -6
- package/dist/steps/functions/saveBuildCache.d.ts +3 -3
- package/dist/steps/functions/saveBuildCache.js +17 -4
- package/dist/steps/functions/startServeSimRemoteSession.js +7 -0
- package/dist/steps/functions/uploadToAsc.js +11 -1
- 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/ios/xcactivitylog.js +2 -8
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +4 -2
- package/dist/steps/utils/remoteDeviceRunSession.js +9 -6
- package/dist/utils/IosSimulatorUtils.d.ts +8 -0
- package/dist/utils/IosSimulatorUtils.js +9 -0
- package/dist/utils/cacheKey.d.ts +8 -2
- package/dist/utils/cacheKey.js +12 -8
- package/dist/utils/download.d.ts +4 -0
- package/dist/utils/download.js +10 -0
- 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,
|
|
@@ -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(
|
|
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:
|
|
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 =
|
|
68
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
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
|
-
|
|
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 (
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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>;
|