@expo/build-tools 18.1.0 → 18.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/android/gradle.js +1 -1
  2. package/dist/buildErrors/buildErrorHandlers.d.ts +2 -1
  3. package/dist/buildErrors/buildErrorHandlers.js +6 -2
  4. package/dist/buildErrors/detectError.d.ts +1 -1
  5. package/dist/buildErrors/detectError.js +10 -20
  6. package/dist/buildErrors/userErrorHandlers.d.ts +2 -2
  7. package/dist/buildErrors/userErrorHandlers.js +20 -20
  8. package/dist/builders/android.js +12 -2
  9. package/dist/builders/custom.js +1 -0
  10. package/dist/common/installDependencies.js +8 -2
  11. package/dist/common/setup.js +4 -6
  12. package/dist/context.d.ts +2 -0
  13. package/dist/context.js +3 -1
  14. package/dist/customBuildContext.d.ts +5 -1
  15. package/dist/customBuildContext.js +22 -0
  16. package/dist/generic.d.ts +1 -3
  17. package/dist/generic.js +13 -15
  18. package/dist/ios/credentials/provisioningProfile.d.ts +2 -1
  19. package/dist/ios/credentials/provisioningProfile.js +10 -11
  20. package/dist/steps/functions/downloadArtifact.js +3 -3
  21. package/dist/steps/functions/downloadBuild.js +2 -2
  22. package/dist/steps/functions/internalMaestroTest.js +70 -29
  23. package/dist/steps/functions/maestroResultParser.js +134 -80
  24. package/dist/steps/functions/readIpaInfo.js +9 -9
  25. package/dist/steps/functions/reportMaestroTestResults.js +14 -7
  26. package/dist/steps/functions/restoreCache.js +0 -4
  27. package/dist/steps/functions/startAndroidEmulator.js +121 -32
  28. package/dist/steps/functions/uploadToAsc.d.ts +9 -0
  29. package/dist/steps/functions/uploadToAsc.js +55 -6
  30. package/dist/steps/utils/android/gradle.js +1 -1
  31. package/dist/steps/utils/android/gradleConfig.d.ts +3 -0
  32. package/dist/steps/utils/android/gradleConfig.js +15 -1
  33. package/dist/steps/utils/ios/AscApiUtils.js +5 -5
  34. package/dist/steps/utils/ios/credentials/provisioningProfile.d.ts +2 -1
  35. package/dist/steps/utils/ios/credentials/provisioningProfile.js +10 -11
  36. package/dist/utils/AndroidEmulatorUtils.d.ts +9 -2
  37. package/dist/utils/AndroidEmulatorUtils.js +40 -11
  38. package/dist/utils/stepMetrics.d.ts +2 -2
  39. package/dist/utils/stepMetrics.js +4 -10
  40. package/package.json +4 -4
  41. package/dist/android/gradleConfig.d.ts +0 -3
  42. package/dist/android/gradleConfig.js +0 -47
  43. package/dist/templates/EasBuildGradle.d.ts +0 -1
  44. package/dist/templates/EasBuildGradle.js +0 -57
@@ -9,6 +9,8 @@ const steps_1 = require("@expo/steps");
9
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
10
10
  const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
11
11
  const retry_1 = require("../../utils/retry");
12
+ const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
13
+ const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
12
14
  function createStartAndroidEmulatorBuildFunction() {
13
15
  return new steps_1.BuildFunction({
14
16
  namespace: 'eas',
@@ -68,24 +70,71 @@ function createStartAndroidEmulatorBuildFunction() {
68
70
  retryIntervalMs: 1_000,
69
71
  },
70
72
  });
71
- logger.info('Creating emulator device');
72
- await AndroidEmulatorUtils_1.AndroidEmulatorUtils.createAsync({
73
- deviceName,
74
- systemImagePackage,
75
- deviceIdentifier: deviceIdentifier ?? null,
76
- env,
73
+ let emulatorPromise = null;
74
+ let serialId = null;
75
+ await (0, retry_1.retryAsync)(async (attemptCount) => {
76
+ const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
77
+ const attempt = attemptCount + 1;
78
+ const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
79
+ const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
80
+ let attemptSerialId = null;
81
+ try {
82
+ logger.info(`Creating emulator device${attemptSuffix}.`);
83
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.createAsync({
84
+ deviceName,
85
+ systemImagePackage,
86
+ deviceIdentifier: deviceIdentifier ?? null,
87
+ env,
88
+ logger,
89
+ });
90
+ logger.info(`Starting emulator device${attemptSuffix}.`);
91
+ const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
92
+ deviceName,
93
+ env,
94
+ });
95
+ attemptSerialId = startResult.serialId;
96
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
97
+ env,
98
+ serialId: attemptSerialId,
99
+ timeoutMs,
100
+ logger,
101
+ });
102
+ logger.info(`${deviceName} is ready.`);
103
+ serialId = attemptSerialId;
104
+ emulatorPromise = startResult.emulatorPromise;
105
+ }
106
+ catch (err) {
107
+ logger.warn({ err }, `${deviceName} failed to start on attempt ${attempt}/${maxAttempts}.`);
108
+ try {
109
+ if (attemptSerialId) {
110
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
111
+ serialId: attemptSerialId,
112
+ deviceName,
113
+ env,
114
+ });
115
+ }
116
+ else {
117
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
118
+ deviceName,
119
+ env,
120
+ });
121
+ }
122
+ }
123
+ catch (cleanupErr) {
124
+ logger.warn({ err: cleanupErr }, `Failed to clean up ${deviceName}.`);
125
+ }
126
+ throw err;
127
+ }
128
+ }, {
77
129
  logger,
130
+ retryOptions: {
131
+ retries: ANDROID_STARTUP_RETRIES_COUNT,
132
+ retryIntervalMs: 1_000,
133
+ },
78
134
  });
79
- logger.info('Starting emulator device');
80
- const { emulatorPromise, serialId } = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
81
- deviceName,
82
- env,
83
- });
84
- await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
85
- env,
86
- serialId,
87
- });
88
- logger.info(`${deviceName} is ready.`);
135
+ if (!serialId || !emulatorPromise) {
136
+ throw new Error(`Failed to start emulator ${deviceName}.`);
137
+ }
89
138
  const count = Number(inputs.count.value ?? 1);
90
139
  if (count > 1) {
91
140
  logger.info(`Requested ${count} emulators, shutting down ${deviceName} for cloning.`);
@@ -98,24 +147,64 @@ function createStartAndroidEmulatorBuildFunction() {
98
147
  await (0, results_1.asyncResult)(emulatorPromise);
99
148
  for (let i = 0; i < count; i++) {
100
149
  const cloneIdentifier = `eas-simulator-${i + 1}`;
101
- logger.info(`Cloning ${deviceName} to ${cloneIdentifier}...`);
102
- await AndroidEmulatorUtils_1.AndroidEmulatorUtils.cloneAsync({
103
- sourceDeviceName: deviceName,
104
- destinationDeviceName: cloneIdentifier,
105
- env,
150
+ await (0, retry_1.retryAsync)(async (attemptCount) => {
151
+ const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
152
+ const attempt = attemptCount + 1;
153
+ const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
154
+ const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
155
+ let cloneSerialId = null;
156
+ try {
157
+ logger.info(`Cloning ${deviceName} to ${cloneIdentifier}${attemptSuffix}.`);
158
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.cloneAsync({
159
+ sourceDeviceName: deviceName,
160
+ destinationDeviceName: cloneIdentifier,
161
+ env,
162
+ logger,
163
+ });
164
+ logger.info(`Starting emulator device${attemptSuffix}.`);
165
+ const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
166
+ deviceName: cloneIdentifier,
167
+ env,
168
+ });
169
+ cloneSerialId = startResult.serialId;
170
+ logger.info('Waiting for emulator to become ready');
171
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
172
+ serialId: cloneSerialId,
173
+ env,
174
+ timeoutMs,
175
+ logger,
176
+ });
177
+ logger.info(`${cloneIdentifier} is ready.`);
178
+ }
179
+ catch (err) {
180
+ logger.warn({ err }, `${cloneIdentifier} failed to start on attempt ${attempt}/${maxAttempts}.`);
181
+ try {
182
+ if (cloneSerialId) {
183
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
184
+ serialId: cloneSerialId,
185
+ deviceName: cloneIdentifier,
186
+ env,
187
+ });
188
+ }
189
+ else {
190
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
191
+ deviceName: cloneIdentifier,
192
+ env,
193
+ });
194
+ }
195
+ }
196
+ catch (cleanupErr) {
197
+ logger.warn({ err: cleanupErr }, `Failed to clean up ${cloneIdentifier}.`);
198
+ }
199
+ throw err;
200
+ }
201
+ }, {
106
202
  logger,
203
+ retryOptions: {
204
+ retries: ANDROID_STARTUP_RETRIES_COUNT,
205
+ retryIntervalMs: 1_000,
206
+ },
107
207
  });
108
- logger.info('Starting emulator device');
109
- const { serialId } = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
110
- deviceName: cloneIdentifier,
111
- env,
112
- });
113
- logger.info('Waiting for emulator to become ready');
114
- await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
115
- serialId,
116
- env,
117
- });
118
- logger.info(`${cloneIdentifier} is ready.`);
119
208
  }
120
209
  }
121
210
  },
@@ -3,3 +3,12 @@ export declare function createUploadToAscBuildFunction(): BuildFunction;
3
3
  export declare function isClosedVersionTrainError(messages: {
4
4
  code: string;
5
5
  }[]): boolean;
6
+ export declare function isInvalidBundleIdentifierError(messages: {
7
+ code: string;
8
+ }[]): boolean;
9
+ export declare function isMissingPurposeStringError(messages: {
10
+ code: string;
11
+ }[]): boolean;
12
+ export declare function parseMissingUsageDescriptionKeys(messages: {
13
+ description: string;
14
+ }[]): string[];
@@ -38,7 +38,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.createUploadToAscBuildFunction = createUploadToAscBuildFunction;
40
40
  exports.isClosedVersionTrainError = isClosedVersionTrainError;
41
- const errors_1 = require("@expo/eas-build-job/dist/errors");
41
+ exports.isInvalidBundleIdentifierError = isInvalidBundleIdentifierError;
42
+ exports.isMissingPurposeStringError = isMissingPurposeStringError;
43
+ exports.parseMissingUsageDescriptionKeys = parseMissingUsageDescriptionKeys;
44
+ const eas_build_job_1 = require("@expo/eas-build-job");
45
+ const results_1 = require("@expo/results");
42
46
  const steps_1 = require("@expo/steps");
43
47
  const fs_extra_1 = __importDefault(require("fs-extra"));
44
48
  const jose = __importStar(require("jose"));
@@ -48,6 +52,7 @@ const promises_1 = require("node:timers/promises");
48
52
  const zod_1 = require("zod");
49
53
  const AscApiClient_1 = require("../utils/ios/AscApiClient");
50
54
  const AscApiUtils_1 = require("../utils/ios/AscApiUtils");
55
+ const readIpaInfo_1 = require("./readIpaInfo");
51
56
  function createUploadToAscBuildFunction() {
52
57
  return new steps_1.BuildFunction({
53
58
  namespace: 'eas',
@@ -130,9 +135,10 @@ function createUploadToAscBuildFunction() {
130
135
  .setExpirationTime('20m')
131
136
  .sign(privateKey);
132
137
  const client = new AscApiClient_1.AscApiClient({ token, logger: stepsCtx.logger });
133
- stepsCtx.logger.info('Reading App information...');
138
+ stepsCtx.logger.info(`Reading App information for Apple app identifier: ${appleAppIdentifier}...`);
134
139
  const appResponse = await AscApiUtils_1.AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
135
- stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${appResponse.data.attributes.bundleId})...`);
140
+ const ascAppBundleIdentifier = appResponse.data.attributes.bundleId;
141
+ stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`);
136
142
  stepsCtx.logger.info('Creating Build Upload...');
137
143
  const buildUploadResponse = await AscApiUtils_1.AscApiUtils.createBuildUploadAsync({
138
144
  client,
@@ -241,9 +247,34 @@ function createUploadToAscBuildFunction() {
241
247
  stepsCtx.logger.error(`Errors:\n${itemizeMessages(errors)}\n`);
242
248
  }
243
249
  if (state.state === 'FAILED') {
250
+ if (isInvalidBundleIdentifierError(errors)) {
251
+ const ipaInfoResult = await (0, results_1.asyncResult)((0, readIpaInfo_1.readIpaInfoAsync)(ipaPath));
252
+ const ipaBundleIdentifier = ipaInfoResult.ok
253
+ ? ipaInfoResult.value.bundleIdentifier
254
+ : null;
255
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` +
256
+ `IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` +
257
+ `App Store Connect app bundle identifier: ${ascAppBundleIdentifier}\n\n` +
258
+ 'Bundle identifier cannot be changed for an existing App Store Connect app. ' +
259
+ 'If you selected the wrong app, change the Apple app identifier in the submit profile. ' +
260
+ 'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).');
261
+ }
262
+ if (isMissingPurposeStringError(errors)) {
263
+ const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
264
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING', `Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` +
265
+ `${missingUsageDescriptionKeys.length > 0
266
+ ? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join('\n- ')}\n\n`
267
+ : ''}` +
268
+ 'Add the missing keys with clear user-facing explanations, then rebuild and submit again.\n' +
269
+ 'If you use Continuous Native Generation (CNG), update `ios.infoPlist` in app.json/app.config.js.\n' +
270
+ 'If you do not use CNG, update your app target Info.plist directly.', {
271
+ docsUrl: 'https://docs.expo.dev/guides/permissions/#ios',
272
+ });
273
+ }
244
274
  if (isClosedVersionTrainError(errors)) {
245
- throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', `Build upload was rejected by App Store Connect because the ${bundleShortVersion} version train is closed. ` +
246
- 'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version and submit again.');
275
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', `Build upload was rejected by App Store Connect because the ${bundleShortVersion} app version is not accepted for new build submissions. ` +
276
+ 'This usually means the version train is closed or lower than a previously approved version. ' +
277
+ 'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.');
247
278
  }
248
279
  throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
249
280
  }
@@ -259,7 +290,25 @@ function itemizeMessages(messages) {
259
290
  return `- ${messages.map(m => `${m.description} (${m.code})`).join('\n- ')}`;
260
291
  }
261
292
  function isClosedVersionTrainError(messages) {
262
- return (messages.length > 0 && messages.every(message => ['90062', '90186'].includes(message.code)));
293
+ return (messages.length > 0 &&
294
+ messages.every(message => ['90062', '90186', '90478'].includes(message.code)));
295
+ }
296
+ function isInvalidBundleIdentifierError(messages) {
297
+ return (messages.length > 0 && messages.every(message => ['90054', '90055'].includes(message.code)));
298
+ }
299
+ function isMissingPurposeStringError(messages) {
300
+ return messages.length > 0 && messages.every(message => message.code === '90683');
301
+ }
302
+ function parseMissingUsageDescriptionKeys(messages) {
303
+ const usageDescriptionKeyRegex = /\b(\w+UsageDescription)\b/g;
304
+ const keys = new Set();
305
+ for (const message of messages) {
306
+ const matches = message.description.matchAll(usageDescriptionKeyRegex);
307
+ for (const match of matches) {
308
+ keys.add(match[1]);
309
+ }
310
+ }
311
+ return [...keys];
263
312
  }
264
313
  async function uploadChunksAsync({ uploadOperations, ipaPath, logger, }) {
265
314
  const fd = await fs_extra_1.default.open(ipaPath, 'r');
@@ -25,7 +25,7 @@ async function runGradleCommand({ logger, gradleCommand, androidDir, env, extraE
25
25
  return line;
26
26
  }
27
27
  },
28
- env: { ...env, ...extraEnv },
28
+ env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
29
29
  });
30
30
  if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
31
31
  adjustOOMScore(spawnPromise, logger);
@@ -1,6 +1,9 @@
1
+ import { Android } from '@expo/eas-build-job';
1
2
  import { bunyan } from '@expo/logger';
3
+ import { BuildContext } from '../../../context';
2
4
  export declare function injectCredentialsGradleConfig(logger: bunyan, workingDir: string): Promise<void>;
3
5
  export declare function injectConfigureVersionGradleConfig(logger: bunyan, workingDir: string, { versionCode, versionName }: {
4
6
  versionCode?: string;
5
7
  versionName?: string;
6
8
  }): Promise<void>;
9
+ export declare function warnIfLegacyEasBuildGradleExists(ctx: BuildContext<Android.Job>): Promise<void>;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.injectCredentialsGradleConfig = injectCredentialsGradleConfig;
7
7
  exports.injectConfigureVersionGradleConfig = injectConfigureVersionGradleConfig;
8
+ exports.warnIfLegacyEasBuildGradleExists = warnIfLegacyEasBuildGradleExists;
8
9
  const config_plugins_1 = require("@expo/config-plugins");
9
10
  const template_file_1 = require("@expo/template-file");
10
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -41,6 +42,19 @@ async function deleteEasBuildConfigureVersionGradle(workingDir) {
41
42
  const targetPath = getEasBuildConfigureVersionGradlePath(workingDir);
42
43
  await fs_extra_1.default.remove(targetPath);
43
44
  }
45
+ let legacyEasBuildGradleWarningEmitted = false;
46
+ async function warnIfLegacyEasBuildGradleExists(ctx) {
47
+ const legacyGradlePath = getLegacyEasBuildGradlePath(ctx.getReactNativeProjectDirectory());
48
+ if ((await fs_extra_1.default.pathExists(legacyGradlePath)) &&
49
+ (process.env.NODE_ENV === 'test' || !legacyEasBuildGradleWarningEmitted)) {
50
+ legacyEasBuildGradleWarningEmitted = true;
51
+ ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
52
+ ctx.markBuildPhaseHasWarnings();
53
+ }
54
+ }
55
+ function getLegacyEasBuildGradlePath(projectRoot) {
56
+ return path_1.default.join(projectRoot, 'android/app/eas-build.gradle');
57
+ }
44
58
  function getEasBuildInjectCredentialsGradlePath(workingDir) {
45
59
  return path_1.default.join(workingDir, 'android/app/eas-build-inject-android-credentials.gradle');
46
60
  }
@@ -83,6 +97,6 @@ function hasLine(haystack, needle) {
83
97
  return (haystack
84
98
  .replace(/\r\n/g, '\n')
85
99
  .split('\n')
86
- // Check for both single and double quotes
100
+ // Check for both single and double quotes.
87
101
  .some(line => line === needle || line === needle.replace(/"/g, "'")));
88
102
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AscApiUtils = void 0;
4
- const errors_1 = require("@expo/eas-build-job/dist/errors");
4
+ const eas_build_job_1 = require("@expo/eas-build-job");
5
5
  const AscApiClient_1 = require("./AscApiClient");
6
6
  var AscApiUtils;
7
7
  (function (AscApiUtils) {
@@ -25,13 +25,13 @@ var AscApiUtils;
25
25
  // Don't hide the original NOT_FOUND error with a secondary lookup failure.
26
26
  throw error;
27
27
  }
28
- throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_APP_NOT_FOUND', `App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
28
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_APP_NOT_FOUND', `App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
29
29
  'Verify the configured application identifier and that the App Store Connect API key has access to the application in the correct App Store Connect account.' +
30
30
  (visibleAppsSummary
31
31
  ? `\n\nExample applications visible to this API key:\n${visibleAppsSummary}`
32
32
  : ''), {
33
- cause: error,
34
33
  docsUrl: 'https://expo.fyi/asc-app-id',
34
+ cause: error,
35
35
  });
36
36
  }
37
37
  }
@@ -64,11 +64,11 @@ var AscApiUtils;
64
64
  const isDuplicateVersionError = errors.length > 0 &&
65
65
  errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
66
66
  if (isDuplicateVersionError) {
67
- throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE', `Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
67
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE', `Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
68
68
  'App Store Connect requires unique build numbers within each app version (version train). ' +
69
69
  'Increment it by setting ios.buildNumber in app.json, or set "autoIncrement": true in eas.json (recommended). Then rebuild and resubmit.', {
70
- cause: error,
71
70
  docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
71
+ cause: error,
72
72
  });
73
73
  }
74
74
  throw error;
@@ -23,11 +23,12 @@ export default class ProvisioningProfile {
23
23
  get data(): ProvisioningProfileData;
24
24
  private readonly profilePath;
25
25
  private profileData?;
26
+ private developerCertificates;
26
27
  constructor(profile: Buffer, keychainPath: string, target: string, certificateCommonName: string);
27
28
  init(logger: bunyan): Promise<void>;
28
29
  destroy(logger: bunyan): Promise<void>;
29
30
  verifyCertificate(fingerprint: string): void;
30
31
  private load;
31
32
  private resolveDistributionType;
32
- private genDerCertFingerprint;
33
+ private getAllDerCertFingerprints;
33
34
  }
@@ -34,6 +34,7 @@ class ProvisioningProfile {
34
34
  }
35
35
  profilePath;
36
36
  profileData;
37
+ developerCertificates = [];
37
38
  constructor(profile, keychainPath, target, certificateCommonName) {
38
39
  this.profile = profile;
39
40
  this.keychainPath = keychainPath;
@@ -58,10 +59,11 @@ class ProvisioningProfile {
58
59
  await fs_extra_1.default.remove(this.profilePath);
59
60
  }
60
61
  verifyCertificate(fingerprint) {
61
- const devCertFingerprint = this.genDerCertFingerprint();
62
- if (devCertFingerprint !== fingerprint) {
63
- throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match.
64
- Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`);
62
+ const devCertFingerprints = this.getAllDerCertFingerprints();
63
+ if (!devCertFingerprints.includes(fingerprint)) {
64
+ throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match.\n` +
65
+ `Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
66
+ `distribution certificate fingerprint = ${fingerprint}`);
65
67
  }
66
68
  }
67
69
  async load() {
@@ -85,6 +87,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
85
87
  }
86
88
  const applicationIdentifier = plistData.Entitlements['application-identifier'];
87
89
  const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
90
+ this.developerCertificates = plistData.DeveloperCertificates.map((cert) => Buffer.from(cert, 'base64'));
88
91
  this.profileData = {
89
92
  path: this.profilePath,
90
93
  target: this.target,
@@ -92,7 +95,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
92
95
  teamId: plistData.TeamIdentifier[0],
93
96
  uuid: plistData.UUID,
94
97
  name: plistData.Name,
95
- developerCertificate: Buffer.from(plistData.DeveloperCertificates[0], 'base64'),
98
+ developerCertificate: this.developerCertificates[0],
96
99
  certificateCommonName: this.certificateCommonName,
97
100
  distributionType: this.resolveDistributionType(plistData),
98
101
  };
@@ -108,12 +111,8 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
108
111
  return DistributionType.APP_STORE;
109
112
  }
110
113
  }
111
- genDerCertFingerprint() {
112
- return crypto_1.default
113
- .createHash('sha1')
114
- .update(new Uint8Array(this.data.developerCertificate))
115
- .digest('hex')
116
- .toUpperCase();
114
+ getAllDerCertFingerprints() {
115
+ return this.developerCertificates.map(cert => crypto_1.default.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase());
117
116
  }
118
117
  }
119
118
  exports.default = ProvisioningProfile;
@@ -41,9 +41,11 @@ export declare namespace AndroidEmulatorUtils {
41
41
  emulatorPromise: SpawnPromise<SpawnResult>;
42
42
  serialId: AndroidDeviceSerialId;
43
43
  }>;
44
- function waitForReadyAsync({ serialId, env, }: {
44
+ function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
45
45
  serialId: AndroidDeviceSerialId;
46
46
  env: NodeJS.ProcessEnv;
47
+ timeoutMs?: number;
48
+ logger?: bunyan;
47
49
  }): Promise<void>;
48
50
  function collectLogsAsync({ serialId, env, }: {
49
51
  serialId: AndroidDeviceSerialId;
@@ -51,10 +53,15 @@ export declare namespace AndroidEmulatorUtils {
51
53
  }): Promise<{
52
54
  outputPath: string;
53
55
  }>;
54
- function deleteAsync({ serialId, env, }: {
56
+ function stopAsync({ serialId, env, }: {
55
57
  serialId: AndroidDeviceSerialId;
56
58
  env: NodeJS.ProcessEnv;
57
59
  }): Promise<void>;
60
+ function deleteAsync({ serialId, deviceName, env, }: {
61
+ serialId?: AndroidDeviceSerialId;
62
+ deviceName?: AndroidVirtualDeviceName;
63
+ env: NodeJS.ProcessEnv;
64
+ }): Promise<void>;
58
65
  function startScreenRecordingAsync({ serialId, env, }: {
59
66
  serialId: AndroidDeviceSerialId;
60
67
  env: NodeJS.ProcessEnv;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AndroidEmulatorUtils = void 0;
7
+ const results_1 = require("@expo/results");
7
8
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
8
9
  const assert_1 = __importDefault(require("assert"));
9
10
  const fast_glob_1 = __importDefault(require("fast-glob"));
@@ -14,6 +15,7 @@ const promises_1 = require("node:timers/promises");
14
15
  const retry_1 = require("./retry");
15
16
  var AndroidEmulatorUtils;
16
17
  (function (AndroidEmulatorUtils) {
18
+ const RETRY_INTERVAL_MS = 1_000;
17
19
  AndroidEmulatorUtils.defaultSystemImagePackage = `system-images;android-30;default;${process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64'}`;
18
20
  async function getAvailableDevicesAsync({ env, }) {
19
21
  const result = await (0, turtle_spawn_1.default)('avdmanager', ['list', 'device', '--compact', '--null'], { env });
@@ -253,21 +255,37 @@ var AndroidEmulatorUtils;
253
255
  return { emulatorPromise, serialId };
254
256
  }
255
257
  AndroidEmulatorUtils.startAsync = startAsync;
256
- async function waitForReadyAsync({ serialId, env, }) {
258
+ async function waitForReadyAsync({ serialId, env, timeoutMs = 3 * 60 * 1_000, logger, }) {
259
+ const retries = Math.max(0, Math.ceil(timeoutMs / RETRY_INTERVAL_MS) - 1);
257
260
  await (0, retry_1.retryAsync)(async () => {
258
261
  const { stdout } = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'getprop', 'sys.boot_completed'], { env });
259
262
  if (!stdout.startsWith('1')) {
260
263
  throw new Error(`Emulator (${serialId}) boot has not completed.`);
261
264
  }
265
+ const hasNetworkConnection = await hasNetworkConnectionAsync({ serialId, env });
266
+ if (!hasNetworkConnection) {
267
+ throw new Error(`Emulator (${serialId}) network is not ready.`);
268
+ }
262
269
  }, {
263
- // Retry every second for 3 minutes.
264
270
  retryOptions: {
265
- retries: 3 * 60,
266
- retryIntervalMs: 1_000,
271
+ retries,
272
+ retryIntervalMs: RETRY_INTERVAL_MS,
267
273
  },
274
+ logger,
268
275
  });
269
276
  }
270
277
  AndroidEmulatorUtils.waitForReadyAsync = waitForReadyAsync;
278
+ async function hasNetworkConnectionAsync({ serialId, env, }) {
279
+ const networkReadyCheckCommand = env.ANDROID_EMULATOR_NETWORK_READY_COMMAND?.trim();
280
+ if (networkReadyCheckCommand) {
281
+ const customNetworkCheckResult = await (0, results_1.asyncResult)((0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', networkReadyCheckCommand], { env }));
282
+ return customNetworkCheckResult.ok;
283
+ }
284
+ const netcatResult = await (0, results_1.asyncResult)((0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'nc', '-w', '1', '1.1.1.1', '443'],
285
+ // Close stdin to make netcat exit cleanly on Android images that don't support `-z`.
286
+ { env, stdio: ['ignore', 'pipe', 'pipe'] }));
287
+ return netcatResult.ok;
288
+ }
271
289
  async function collectLogsAsync({ serialId, env, }) {
272
290
  const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'android-emulator-logs-'));
273
291
  const outputPath = node_path_1.default.join(outputDir, `${serialId}.log`);
@@ -298,11 +316,7 @@ var AndroidEmulatorUtils;
298
316
  return { outputPath };
299
317
  }
300
318
  AndroidEmulatorUtils.collectLogsAsync = collectLogsAsync;
301
- async function deleteAsync({ serialId, env, }) {
302
- const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
303
- env,
304
- });
305
- const deviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
319
+ async function stopAsync({ serialId, env, }) {
306
320
  await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'kill'], { env });
307
321
  await (0, retry_1.retryAsync)(async () => {
308
322
  const devices = await getAttachedDevicesAsync({ env });
@@ -312,10 +326,25 @@ var AndroidEmulatorUtils;
312
326
  }, {
313
327
  retryOptions: {
314
328
  retries: 3 * 60,
315
- retryIntervalMs: 1_000,
329
+ retryIntervalMs: RETRY_INTERVAL_MS,
316
330
  },
317
331
  });
318
- await (0, turtle_spawn_1.default)('avdmanager', ['delete', 'avd', '-n', deviceName], { env });
332
+ }
333
+ AndroidEmulatorUtils.stopAsync = stopAsync;
334
+ async function deleteAsync({ serialId, deviceName, env, }) {
335
+ let resolvedDeviceName = deviceName;
336
+ if (!resolvedDeviceName && serialId) {
337
+ const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
338
+ env,
339
+ });
340
+ resolvedDeviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
341
+ }
342
+ (0, assert_1.default)(resolvedDeviceName, 'Either "serialId" or "deviceName" must resolve to a device name.');
343
+ const serialIdToStop = serialId ?? (await getSerialIdAsync({ deviceName: resolvedDeviceName, env })) ?? undefined;
344
+ if (serialIdToStop) {
345
+ await stopAsync({ serialId: serialIdToStop, env });
346
+ }
347
+ await (0, turtle_spawn_1.default)('avdmanager', ['delete', 'avd', '-n', resolvedDeviceName], { env });
319
348
  }
320
349
  AndroidEmulatorUtils.deleteAsync = deleteAsync;
321
350
  async function startScreenRecordingAsync({ serialId, env, }) {
@@ -1,9 +1,9 @@
1
1
  import { bunyan } from '@expo/logger';
2
- import { StepMetricsCollection } from '@expo/steps';
2
+ import { StepMetric } from '@expo/steps';
3
3
  export declare function uploadStepMetricsToWwwAsync({ workflowJobId, robotAccessToken, expoApiV2BaseUrl, stepMetrics, logger, }: {
4
4
  workflowJobId: string;
5
5
  robotAccessToken: string;
6
6
  expoApiV2BaseUrl: string;
7
- stepMetrics: StepMetricsCollection;
7
+ stepMetrics: StepMetric[];
8
8
  logger: bunyan;
9
9
  }): Promise<void>;
@@ -3,22 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadStepMetricsToWwwAsync = uploadStepMetricsToWwwAsync;
4
4
  const turtleFetch_1 = require("./turtleFetch");
5
5
  async function uploadStepMetricsToWwwAsync({ workflowJobId, robotAccessToken, expoApiV2BaseUrl, stepMetrics, logger, }) {
6
- if (stepMetrics.length === 0) {
7
- logger.debug('No step metrics to upload');
8
- return;
9
- }
10
6
  try {
11
7
  await (0, turtleFetch_1.turtleFetch)(new URL(`workflows/${workflowJobId}/metrics`, expoApiV2BaseUrl).toString(), 'POST', {
12
8
  json: { stepMetrics },
13
- headers: {
14
- Authorization: `Bearer ${robotAccessToken}`,
15
- },
16
- timeout: 20000,
9
+ headers: { Authorization: `Bearer ${robotAccessToken}` },
10
+ timeout: 5000,
11
+ retries: 2,
17
12
  logger,
18
13
  });
19
- logger.info(`Uploaded ${stepMetrics.length} step metrics`);
20
14
  }
21
15
  catch {
22
- // Don't display unactionable error to the user, let's send it to Sentry in the future
16
+ // Don't fail the build for metrics silently give up
23
17
  }
24
18
  }