@expo/build-tools 22.0.0 → 22.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/builders/android.js +2 -2
  2. package/dist/builders/ios.js +2 -2
  3. package/dist/steps/easFunctions.js +6 -0
  4. package/dist/steps/functions/downloadBuild.d.ts +9 -2
  5. package/dist/steps/functions/downloadBuild.js +83 -14
  6. package/dist/steps/functions/installBuild.d.ts +12 -0
  7. package/dist/steps/functions/installBuild.js +85 -0
  8. package/dist/steps/functions/installMaestro.d.ts +6 -1
  9. package/dist/steps/functions/installMaestro.js +107 -33
  10. package/dist/steps/functions/launchApplication.d.ts +10 -0
  11. package/dist/steps/functions/launchApplication.js +67 -0
  12. package/dist/steps/functions/maestroResultParser.d.ts +30 -0
  13. package/dist/steps/functions/maestroResultParser.js +78 -5
  14. package/dist/steps/functions/maestroScreenshots.d.ts +6 -0
  15. package/dist/steps/functions/maestroScreenshots.js +123 -0
  16. package/dist/steps/functions/maestroTests.js +187 -69
  17. package/dist/steps/functions/readIpaInfo.d.ts +1 -0
  18. package/dist/steps/functions/readIpaInfo.js +2 -0
  19. package/dist/steps/functions/repack.d.ts +5 -2
  20. package/dist/steps/functions/repack.js +50 -2
  21. package/dist/steps/functions/restoreBuildCache.d.ts +3 -3
  22. package/dist/steps/functions/restoreBuildCache.js +21 -7
  23. package/dist/steps/functions/saveBuildCache.d.ts +3 -3
  24. package/dist/steps/functions/saveBuildCache.js +21 -6
  25. package/dist/steps/functions/saveCache.js +3 -0
  26. package/dist/steps/functions/startAgentDeviceRemoteSession.js +7 -0
  27. package/dist/steps/functions/startAppiumRemoteSession.d.ts +17 -0
  28. package/dist/steps/functions/startAppiumRemoteSession.js +244 -0
  29. package/dist/steps/functions/startArgentRemoteSession.js +7 -0
  30. package/dist/steps/functions/startServeSimRemoteSession.js +17 -1
  31. package/dist/steps/functions/uploadToAsc.js +11 -1
  32. package/dist/steps/utils/appiumEvents.d.ts +10 -0
  33. package/dist/steps/utils/appiumEvents.js +175 -0
  34. package/dist/steps/utils/ios/AscApiClient.d.ts +1 -0
  35. package/dist/steps/utils/ios/AscApiUtils.d.ts +11 -2
  36. package/dist/steps/utils/ios/AscApiUtils.js +36 -2
  37. package/dist/steps/utils/remoteDeviceRunSession.d.ts +6 -3
  38. package/dist/steps/utils/remoteDeviceRunSession.js +81 -52
  39. package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
  40. package/dist/utils/AndroidEmulatorUtils.js +46 -4
  41. package/dist/utils/cacheKey.d.ts +8 -2
  42. package/dist/utils/cacheKey.js +12 -8
  43. package/package.json +6 -4
@@ -264,6 +264,7 @@ export type AscApiClientPostApi = {
264
264
  response: z.output<(typeof PostApi)[Path]['response']>;
265
265
  };
266
266
  };
267
+ export type AscPlatform = AscApiClientPostApi['/v1/buildUploads']['request']['data']['attributes']['platform'];
267
268
  export type AscApiClientPatchApi = {
268
269
  [Path in keyof typeof PatchApi]: {
269
270
  request: z.input<(typeof PatchApi)[Path]['request']>;
@@ -1,14 +1,23 @@
1
- import { AscApiClient, AscApiClientGetApi, AscApiClientPostApi } from './AscApiClient';
1
+ import { AscApiClient, AscApiClientGetApi, AscApiClientPostApi, AscPlatform } from './AscApiClient';
2
2
  export declare namespace AscApiUtils {
3
+ /**
4
+ * Maps a bundle's `DTPlatformName` (from its Info.plist) to the App Store
5
+ * Connect platform used for a build upload. Unknown or missing values fall
6
+ * back to `IOS` to preserve the previous default.
7
+ */
8
+ function ascPlatformFromDtPlatformName(dtPlatformName: string | null): AscPlatform;
9
+ /** The App Store Connect TestFlight URL path segment for a platform. */
10
+ function testFlightPlatformPathSegment(platform: AscPlatform): string;
3
11
  function getAppInfoAsync({ client, appleAppIdentifier, }: {
4
12
  client: Pick<AscApiClient, 'getAsync'>;
5
13
  appleAppIdentifier: string;
6
14
  }): Promise<AscApiClientGetApi['/v1/apps/:id']['response']>;
7
- function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }: {
15
+ function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, platform, }: {
8
16
  client: Pick<AscApiClient, 'postAsync'>;
9
17
  appleAppIdentifier: string;
10
18
  bundleShortVersion: string;
11
19
  bundleVersion: string;
20
+ platform: AscPlatform;
12
21
  }): Promise<AscApiClientPostApi['/v1/buildUploads']['response']>;
13
22
  function getAppsAsync({ client, limit, }: {
14
23
  client: Pick<AscApiClient, 'getAsync'>;
@@ -5,6 +5,40 @@ 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) {
8
+ /**
9
+ * Maps a bundle's `DTPlatformName` (from its Info.plist) to the App Store
10
+ * Connect platform used for a build upload. Unknown or missing values fall
11
+ * back to `IOS` to preserve the previous default.
12
+ */
13
+ function ascPlatformFromDtPlatformName(dtPlatformName) {
14
+ switch (dtPlatformName) {
15
+ case 'appletvos':
16
+ return 'TV_OS';
17
+ case 'macosx':
18
+ return 'MAC_OS';
19
+ case 'xros':
20
+ return 'VISION_OS';
21
+ case 'iphoneos':
22
+ default:
23
+ return 'IOS';
24
+ }
25
+ }
26
+ AscApiUtils.ascPlatformFromDtPlatformName = ascPlatformFromDtPlatformName;
27
+ /** The App Store Connect TestFlight URL path segment for a platform. */
28
+ function testFlightPlatformPathSegment(platform) {
29
+ switch (platform) {
30
+ case 'TV_OS':
31
+ return 'tvos';
32
+ case 'MAC_OS':
33
+ return 'macos';
34
+ case 'VISION_OS':
35
+ return 'visionos';
36
+ case 'IOS':
37
+ default:
38
+ return 'ios';
39
+ }
40
+ }
41
+ AscApiUtils.testFlightPlatformPathSegment = testFlightPlatformPathSegment;
8
42
  async function getAppInfoAsync({ client, appleAppIdentifier, }) {
9
43
  try {
10
44
  return await client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appleAppIdentifier });
@@ -37,13 +71,13 @@ var AscApiUtils;
37
71
  }
38
72
  }
39
73
  AscApiUtils.getAppInfoAsync = getAppInfoAsync;
40
- async function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }) {
74
+ async function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, platform, }) {
41
75
  try {
42
76
  return await client.postAsync('/v1/buildUploads', {
43
77
  data: {
44
78
  type: 'buildUploads',
45
79
  attributes: {
46
- platform: 'IOS',
80
+ platform,
47
81
  cfBundleShortVersionString: bundleShortVersion,
48
82
  cfBundleVersion: bundleVersion,
49
83
  },
@@ -25,10 +25,11 @@ export type DeviceRunSessionIdleTimeout = {
25
25
  */
26
26
  getLastEventObservedAt: () => Date | undefined;
27
27
  };
28
- export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }: {
28
+ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, maxDurationSeconds, signal: cancelSignal, idleTimeout, }: {
29
29
  ctx: CustomBuildContext;
30
30
  deviceRunSessionId: string;
31
31
  logger: bunyan;
32
+ maxDurationSeconds?: number;
32
33
  signal?: AbortSignal;
33
34
  idleTimeout?: DeviceRunSessionIdleTimeout;
34
35
  }): Promise<void>;
@@ -91,10 +92,11 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
91
92
  env: BuildStepEnv;
92
93
  }): DetachedProcessHandle;
93
94
  export declare function metricsCorsOriginToServeSimArgs(env: BuildStepEnv): string[];
94
- export declare function createServeSimArgs({ port, turnArgs, metricsCorsArgs, }: {
95
+ export declare function createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion, }: {
95
96
  port: number;
96
97
  turnArgs?: string[];
97
98
  metricsCorsArgs?: string[];
99
+ packageVersion?: string;
98
100
  }): string[];
99
101
  export declare function waitForServeSimReadyAsync({ serveSim, port, timeoutMs, }: {
100
102
  serveSim: Pick<DetachedProcessHandle, 'pid' | 'getOutput'>;
@@ -105,11 +107,12 @@ export type ServeSimPreviewHandle = {
105
107
  previewUrl: string;
106
108
  stopAsync: () => Promise<void>;
107
109
  };
108
- export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, }: {
110
+ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, packageVersion, }: {
109
111
  baseDomain: string;
110
112
  env: BuildStepEnv;
111
113
  logger: bunyan;
112
114
  timeoutMs: number;
115
+ packageVersion?: string;
113
116
  }): Promise<ServeSimPreviewHandle>;
114
117
  export type NgrokTunnelHandle = {
115
118
  url: string;
@@ -63,12 +63,13 @@ const zod_1 = require("zod");
63
63
  const node_crypto_1 = require("node:crypto");
64
64
  const node_fs_1 = __importDefault(require("node:fs"));
65
65
  const node_net_1 = require("node:net");
66
+ const node_timers_1 = require("node:timers");
66
67
  const promises_1 = require("node:timers/promises");
67
68
  const sentry_1 = require("../../sentry");
68
69
  const retry_1 = require("../../utils/retry");
69
70
  const turtleFetch_1 = require("../../utils/turtleFetch");
70
71
  const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
71
- const SERVE_SIM_PACKAGE_SPEC = '@expo/serve-sim@latest';
72
+ const SERVE_SIM_PACKAGE_NAME = '@expo/serve-sim';
72
73
  const SERVE_SIM_HOST = '127.0.0.1';
73
74
  const SERVE_SIM_MAX_DIMENSION = '1280';
74
75
  const SERVE_SIM_MJPEG_QUALITY = '0.55';
@@ -152,58 +153,83 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
152
153
  stdio: ['ignore', 'pipe', 'pipe'],
153
154
  });
154
155
  }
155
- async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }) {
156
- logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
157
- if (idleTimeout) {
158
- logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
159
- }
160
- let pollErrorCount = 0;
161
- let lastActivityAt = new Date();
162
- while (!signal?.aborted) {
156
+ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, maxDurationSeconds, signal: cancelSignal, idleTimeout, }) {
157
+ const durationAbortController = new AbortController();
158
+ const signal = cancelSignal
159
+ ? AbortSignal.any([cancelSignal, durationAbortController.signal])
160
+ : durationAbortController.signal;
161
+ // Nothing to wait for if the step was already aborted before we started;
162
+ // return before logging so we don't claim to be polling a session we never poll.
163
+ if (signal.aborted) {
164
+ return;
165
+ }
166
+ const durationTimeout = maxDurationSeconds === undefined
167
+ ? undefined
168
+ : (0, node_timers_1.setTimeout)(() => {
169
+ logger.info(`Device run session ${deviceRunSessionId} reached its maximum duration.`);
170
+ durationAbortController.abort();
171
+ }, maxDurationSeconds * 1_000);
172
+ try {
173
+ logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
174
+ if (durationTimeout !== undefined) {
175
+ logger.info(`The device run session will stop automatically after ${maxDurationSeconds} seconds.`);
176
+ }
163
177
  if (idleTimeout) {
164
- const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
165
- if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
166
- lastActivityAt = lastEventObservedAt;
167
- }
168
- if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
169
- logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
170
- `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
171
- await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
172
- return;
173
- }
178
+ logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
174
179
  }
175
- try {
176
- const result = await ctx.graphqlClient
177
- .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
178
- .toPromise();
179
- if (result.error) {
180
- throw result.error;
181
- }
182
- const status = result.data?.deviceRunSessions?.byId?.status;
183
- if (!status) {
184
- throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
180
+ let pollErrorCount = 0;
181
+ let lastActivityAt = new Date();
182
+ while (!signal.aborted) {
183
+ if (idleTimeout) {
184
+ const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
185
+ if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
186
+ lastActivityAt = lastEventObservedAt;
187
+ }
188
+ if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
189
+ logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
190
+ `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
191
+ await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
192
+ return;
193
+ }
185
194
  }
186
- pollErrorCount = 0;
187
- if (status === 'STOPPED') {
188
- logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
189
- return;
195
+ try {
196
+ const result = await ctx.graphqlClient
197
+ .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
198
+ .toPromise();
199
+ if (result.error) {
200
+ throw result.error;
201
+ }
202
+ const status = result.data?.deviceRunSessions?.byId?.status;
203
+ if (!status) {
204
+ throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
205
+ }
206
+ pollErrorCount = 0;
207
+ if (status === 'STOPPED') {
208
+ logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
209
+ return;
210
+ }
211
+ if (status === 'ERRORED') {
212
+ throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
213
+ }
190
214
  }
191
- if (status === 'ERRORED') {
192
- throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
215
+ catch (err) {
216
+ if (err instanceof eas_build_job_1.SystemError) {
217
+ throw err;
218
+ }
219
+ const error = err instanceof Error ? err : new Error(String(err));
220
+ pollErrorCount += 1;
221
+ if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
222
+ sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
223
+ logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
224
+ }
193
225
  }
226
+ await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
194
227
  }
195
- catch (err) {
196
- if (err instanceof eas_build_job_1.SystemError) {
197
- throw err;
198
- }
199
- const error = err instanceof Error ? err : new Error(String(err));
200
- pollErrorCount += 1;
201
- if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
202
- sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
203
- logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
204
- }
228
+ }
229
+ finally {
230
+ if (durationTimeout !== undefined) {
231
+ (0, node_timers_1.clearTimeout)(durationTimeout);
205
232
  }
206
- await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
207
233
  }
208
234
  }
209
235
  // Best effort: when this fails, the caller still tears the session down and the
@@ -447,10 +473,13 @@ function metricsCorsOriginToServeSimArgs(env) {
447
473
  }
448
474
  return args;
449
475
  }
450
- function createServeSimArgs({ port, turnArgs = [], metricsCorsArgs = [], }) {
476
+ function createServeSimPackageSpec(packageVersion) {
477
+ return `${SERVE_SIM_PACKAGE_NAME}@${packageVersion ?? 'latest'}`;
478
+ }
479
+ function createServeSimArgs({ port, turnArgs = [], metricsCorsArgs = [], packageVersion, }) {
451
480
  return [
452
481
  '--yes',
453
- SERVE_SIM_PACKAGE_SPEC,
482
+ createServeSimPackageSpec(packageVersion),
454
483
  '--port',
455
484
  String(port),
456
485
  '--host',
@@ -514,14 +543,14 @@ async function waitForServeSimReadyAsync({ serveSim, port, timeoutMs, }) {
514
543
  }
515
544
  throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim readiness at ${readyUrl}${lastError instanceof Error ? `: ${lastError.message}` : ''}. Last output:\n${serveSim.getOutput() || '<empty>'}`);
516
545
  }
517
- async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
546
+ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, packageVersion, }) {
518
547
  const port = await findAvailablePortAsync();
519
- logger.info(`Launching ${SERVE_SIM_PACKAGE_SPEC} on ${SERVE_SIM_HOST}:${port}.`);
548
+ logger.info(`Launching ${createServeSimPackageSpec(packageVersion)} on ${SERVE_SIM_HOST}:${port}.`);
520
549
  const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
521
550
  const metricsCorsArgs = metricsCorsOriginToServeSimArgs(env);
522
551
  const serveSim = spawnDetached({
523
552
  command: 'npx',
524
- args: createServeSimArgs({ port, turnArgs, metricsCorsArgs }),
553
+ args: createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion }),
525
554
  env,
526
555
  });
527
556
  try {
@@ -529,7 +558,7 @@ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, time
529
558
  await waitForServeSimReadyAsync({ serveSim, port, timeoutMs });
530
559
  const tunnel = await startNgrokTunnelAsync({
531
560
  port,
532
- subdomainPrefix: 'serve-sim',
561
+ subdomainPrefix: 'web-preview',
533
562
  baseDomain,
534
563
  authtoken: getNgrokAuthtokenOrThrow(env),
535
564
  logger,
@@ -42,6 +42,7 @@ export declare namespace AndroidEmulatorUtils {
42
42
  emulatorPromise: SpawnPromise<SpawnResult>;
43
43
  serialId: AndroidDeviceSerialId;
44
44
  logcatOutputPath: string;
45
+ emulatorOutputPath: string;
45
46
  }>;
46
47
  function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
47
48
  serialId: AndroidDeviceSerialId;
@@ -11,9 +11,11 @@ const assert_1 = __importDefault(require("assert"));
11
11
  const fast_glob_1 = __importDefault(require("fast-glob"));
12
12
  const node_crypto_1 = require("node:crypto");
13
13
  const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_net_1 = require("node:net");
14
15
  const node_os_1 = __importDefault(require("node:os"));
15
16
  const node_path_1 = __importDefault(require("node:path"));
16
17
  const promises_1 = require("node:timers/promises");
18
+ const sentry_1 = require("../sentry");
17
19
  const retry_1 = require("./retry");
18
20
  var AndroidEmulatorUtils;
19
21
  (function (AndroidEmulatorUtils) {
@@ -216,17 +218,21 @@ var AndroidEmulatorUtils;
216
218
  AndroidEmulatorUtils.cloneAsync = cloneAsync;
217
219
  async function startAsync({ deviceName, env, logcatDirectory, }) {
218
220
  let logcatOutputPath;
221
+ let emulatorOutputPath;
219
222
  try {
220
223
  await node_fs_1.default.promises.mkdir(logcatDirectory, { recursive: true });
221
224
  const safeDeviceName = deviceName.replace(/[^a-zA-Z0-9_.-]/g, '_');
222
225
  const timestamp = Math.floor(Date.now() / 1000)
223
226
  .toString(16)
224
227
  .padStart(8, '0');
225
- logcatOutputPath = node_path_1.default.join(logcatDirectory, `${safeDeviceName}-${timestamp}-${(0, node_crypto_1.randomBytes)(2).toString('hex')}.log`);
228
+ const outputName = `${safeDeviceName}-${timestamp}-${(0, node_crypto_1.randomBytes)(2).toString('hex')}`;
229
+ logcatOutputPath = node_path_1.default.join(logcatDirectory, `${outputName}-logcat.log`);
230
+ emulatorOutputPath = node_path_1.default.join(logcatDirectory, `${outputName}-emulator.log`);
226
231
  await node_fs_1.default.promises.writeFile(logcatOutputPath, '');
232
+ await node_fs_1.default.promises.writeFile(emulatorOutputPath, '');
227
233
  }
228
234
  catch (err) {
229
- throw new eas_build_job_1.SystemError(`Failed to prepare Android emulator logcat output for ${deviceName}.`, {
235
+ throw new eas_build_job_1.SystemError(`Failed to prepare Android emulator output for ${deviceName}.`, {
230
236
  cause: err,
231
237
  });
232
238
  }
@@ -249,13 +255,49 @@ var AndroidEmulatorUtils;
249
255
  : []),
250
256
  ], {
251
257
  detached: true,
252
- stdio: 'inherit',
258
+ stdio: ['ignore', 'pipe', 'pipe'],
259
+ ignoreStdio: true,
253
260
  env: {
254
261
  ...env,
255
262
  // We don't need to wait for emulator to exit gracefully.
256
263
  ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: '1',
257
264
  },
258
265
  });
266
+ const emulatorOutputStream = node_fs_1.default.createWriteStream(emulatorOutputPath, { flags: 'a' });
267
+ let reportedEmulatorOutputError = false;
268
+ emulatorOutputStream.on('error', err => {
269
+ process.stderr.write(`Failed to write Android emulator output to ${emulatorOutputPath}: ${err}\n`);
270
+ if (!reportedEmulatorOutputError) {
271
+ reportedEmulatorOutputError = true;
272
+ sentry_1.Sentry.capture('Failed to write Android emulator process output', err, {
273
+ level: 'warning',
274
+ tags: {
275
+ errorCode: err.code ?? 'unknown',
276
+ },
277
+ extras: {
278
+ deviceName,
279
+ emulatorOutputPath,
280
+ },
281
+ });
282
+ }
283
+ });
284
+ // Only into the log file -- this process' stdout/stderr is not watched or uploaded.
285
+ emulatorPromise.child.stdout?.pipe(emulatorOutputStream, { end: false });
286
+ emulatorPromise.child.stderr?.pipe(emulatorOutputStream, { end: false });
287
+ emulatorPromise.child.once('close', () => {
288
+ emulatorOutputStream.end();
289
+ });
290
+ // Piped stdio creates socket handles in this process which, unlike inherited
291
+ // file descriptors, keep the event loop alive for as long as the emulator runs.
292
+ // We never stop the emulator explicitly, so without unref-ing these the process
293
+ // would never exit on its own -- defeating the `detached` + `unref()` below.
294
+ // Output is still captured for as long as this process lives.
295
+ if (emulatorPromise.child.stdout instanceof node_net_1.Socket) {
296
+ emulatorPromise.child.stdout.unref();
297
+ }
298
+ if (emulatorPromise.child.stderr instanceof node_net_1.Socket) {
299
+ emulatorPromise.child.stderr.unref();
300
+ }
259
301
  // If emulator fails to start, throw its error.
260
302
  if (!emulatorPromise.child.pid) {
261
303
  await emulatorPromise;
@@ -276,7 +318,7 @@ var AndroidEmulatorUtils;
276
318
  });
277
319
  // We don't want to await the SpawnPromise here.
278
320
  // eslint-disable-next-line @typescript-eslint/return-await
279
- return { emulatorPromise, serialId, logcatOutputPath };
321
+ return { emulatorPromise, serialId, logcatOutputPath, emulatorOutputPath };
280
322
  }
281
323
  AndroidEmulatorUtils.startAsync = startAsync;
282
324
  async function waitForReadyAsync({ serialId, env, timeoutMs = 3 * 60 * 1_000, logger, }) {
@@ -1,5 +1,11 @@
1
1
  import { Platform } from '@expo/eas-build-job';
2
- export declare const CACHE_KEY_PREFIX_BY_PLATFORM: Record<Platform, string>;
2
+ export type CcacheBuildTarget = {
3
+ platform: Platform.ANDROID;
4
+ } | {
5
+ platform: Platform.IOS;
6
+ simulator: boolean;
7
+ };
8
+ export declare function getCcacheKeyPrefix(target: CcacheBuildTarget): string;
3
9
  export declare const PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM: Record<Platform, string>;
4
10
  export declare function getCcachePath(env: Record<string, string | undefined>): string;
5
- export declare function generateDefaultBuildCacheKeyAsync(workingDirectory: string, platform: Platform): Promise<string>;
11
+ export declare function generateDefaultBuildCacheKeyAsync(workingDirectory: string, target: CcacheBuildTarget): Promise<string>;
@@ -36,7 +36,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = exports.CACHE_KEY_PREFIX_BY_PLATFORM = void 0;
39
+ exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = void 0;
40
+ exports.getCcacheKeyPrefix = getCcacheKeyPrefix;
40
41
  exports.getCcachePath = getCcachePath;
41
42
  exports.generateDefaultBuildCacheKeyAsync = generateDefaultBuildCacheKeyAsync;
42
43
  const eas_build_job_1 = require("@expo/eas-build-job");
@@ -46,16 +47,19 @@ const assert_1 = __importDefault(require("assert"));
46
47
  const os_1 = __importDefault(require("os"));
47
48
  const path_1 = __importDefault(require("path"));
48
49
  const packageManager_1 = require("./packageManager");
49
- const IOS_CACHE_KEY_PREFIX = 'ios-ccache-';
50
+ const IOS_DEVICE_CACHE_KEY_PREFIX = 'ios-device-ccache-';
51
+ const IOS_SIMULATOR_CACHE_KEY_PREFIX = 'ios-simulator-ccache-';
50
52
  const ANDROID_CACHE_KEY_PREFIX = 'android-ccache-';
51
53
  const PUBLIC_IOS_CACHE_KEY_PREFIX = 'public-ios-ccache-';
52
54
  const PUBLIC_ANDROID_CACHE_KEY_PREFIX = 'public-android-ccache-';
53
55
  const DARWIN_CACHE_PATH = 'Library/Caches/ccache';
54
56
  const LINUX_CACHE_PATH = '.cache/ccache';
55
- exports.CACHE_KEY_PREFIX_BY_PLATFORM = {
56
- [eas_build_job_1.Platform.ANDROID]: ANDROID_CACHE_KEY_PREFIX,
57
- [eas_build_job_1.Platform.IOS]: IOS_CACHE_KEY_PREFIX,
58
- };
57
+ function getCcacheKeyPrefix(target) {
58
+ if (target.platform === eas_build_job_1.Platform.IOS) {
59
+ return target.simulator ? IOS_SIMULATOR_CACHE_KEY_PREFIX : IOS_DEVICE_CACHE_KEY_PREFIX;
60
+ }
61
+ return ANDROID_CACHE_KEY_PREFIX;
62
+ }
59
63
  exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = {
60
64
  [eas_build_job_1.Platform.ANDROID]: PUBLIC_ANDROID_CACHE_KEY_PREFIX,
61
65
  [eas_build_job_1.Platform.IOS]: PUBLIC_IOS_CACHE_KEY_PREFIX,
@@ -68,14 +72,14 @@ function getCcachePath(env) {
68
72
  (0, assert_1.default)(env.HOME, 'Failed to infer directory: $HOME environment variable is empty.');
69
73
  return path_1.default.join(env.HOME, PATH_BY_PLATFORM[os_1.default.platform()]);
70
74
  }
71
- async function generateDefaultBuildCacheKeyAsync(workingDirectory, platform) {
75
+ async function generateDefaultBuildCacheKeyAsync(workingDirectory, target) {
72
76
  // This will resolve which package manager and use the relevant lock file
73
77
  // The lock file hash is the key and ensures cache is fresh
74
78
  const packagerRunDir = (0, packageManager_1.findPackagerRootDir)(workingDirectory);
75
79
  const manager = PackageManagerUtils.createForProject(packagerRunDir);
76
80
  const lockPath = path_1.default.join(packagerRunDir, manager.lockFile);
77
81
  try {
78
- return `${exports.CACHE_KEY_PREFIX_BY_PLATFORM[platform]}${(0, steps_1.hashFiles)([lockPath])}`;
82
+ return `${getCcacheKeyPrefix(target)}${(0, steps_1.hashFiles)([lockPath])}`;
79
83
  }
80
84
  catch (err) {
81
85
  throw new Error(`Failed to read lockfile for cache key generation: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "22.0.0",
3
+ "version": "22.3.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -46,7 +46,7 @@
46
46
  "@expo/plist": "^0.3.5",
47
47
  "@expo/results": "^1.0.0",
48
48
  "@expo/spawn-async": "1.7.2",
49
- "@expo/steps": "22.0.0",
49
+ "@expo/steps": "22.1.0",
50
50
  "@expo/template-file": "22.0.0",
51
51
  "@expo/turtle-spawn": "22.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
@@ -54,6 +54,7 @@
54
54
  "@sentry/node": "7.77.0",
55
55
  "@urql/core": "^6.0.1",
56
56
  "bplist-parser": "0.3.2",
57
+ "content-disposition": "1.0.1",
57
58
  "fast-glob": "^3.3.2",
58
59
  "fast-xml-parser": "^4.4.1",
59
60
  "fs-extra": "^11.2.0",
@@ -77,7 +78,8 @@
77
78
  "zod": "^4.3.5"
78
79
  },
79
80
  "devDependencies": {
80
- "@expo/repack-app": "~0.6.1",
81
+ "@expo/repack-app": "~0.9.0",
82
+ "@types/content-disposition": "0.5.9",
81
83
  "@types/fs-extra": "^11.0.4",
82
84
  "@types/jest": "^29.5.12",
83
85
  "@types/lodash": "^4.17.4",
@@ -100,5 +102,5 @@
100
102
  "typescript": "^5.5.4",
101
103
  "uuid": "^9.0.1"
102
104
  },
103
- "gitHead": "b92a9cc5a718f133966e27b9fedcedb569f9cd17"
105
+ "gitHead": "70066fc8beedabdd31081f18c18928461d400806"
104
106
  }