@expo/build-tools 20.0.0 → 20.2.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/builders/android.js +6 -0
  2. package/dist/builders/custom.js +23 -0
  3. package/dist/builders/ios.js +6 -0
  4. package/dist/common/installDependencies.d.ts +10 -1
  5. package/dist/common/installDependencies.js +95 -1
  6. package/dist/common/prebuild.js +2 -3
  7. package/dist/{ios → common}/xcpretty.d.ts +1 -0
  8. package/dist/{ios → common}/xcpretty.js +18 -3
  9. package/dist/datadog.d.ts +8 -1
  10. package/dist/datadog.js +23 -13
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/ios/fastlane.js +1 -1
  14. package/dist/ios/pod.js +1 -1
  15. package/dist/runtimeSettings.d.ts +12 -0
  16. package/dist/runtimeSettings.js +120 -0
  17. package/dist/steps/easFunctions.js +1 -1
  18. package/dist/steps/functions/downloadBuild.d.ts +5 -3
  19. package/dist/steps/functions/downloadBuild.js +34 -4
  20. package/dist/steps/functions/findAndUploadBuildArtifacts.js +2 -2
  21. package/dist/steps/functions/installMaestro.js +13 -2
  22. package/dist/steps/functions/maestroResultParser.d.ts +18 -0
  23. package/dist/steps/functions/maestroResultParser.js +132 -3
  24. package/dist/steps/functions/maestroTests.js +26 -13
  25. package/dist/steps/functions/repack.d.ts +3 -1
  26. package/dist/steps/functions/repack.js +15 -1
  27. package/dist/steps/functions/reportMaestroTestResults.js +39 -20
  28. package/dist/steps/functions/restoreBuildCache.js +9 -6
  29. package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +1 -1
  30. package/dist/steps/functions/startAgentDeviceRemoteSession.js +101 -22
  31. package/dist/steps/functions/startArgentRemoteSession.js +1 -1
  32. package/dist/steps/functions/startIosSimulator.js +12 -0
  33. package/dist/steps/functions/startServeSimRemoteSession.js +1 -1
  34. package/dist/steps/functions/uploadArtifact.js +1 -1
  35. package/dist/steps/utils/ios/fastlane.js +1 -1
  36. package/dist/steps/utils/remoteDeviceRunSession.d.ts +29 -1
  37. package/dist/steps/utils/remoteDeviceRunSession.js +76 -2
  38. package/dist/utils/IosSimulatorUtils.d.ts +4 -0
  39. package/dist/utils/IosSimulatorUtils.js +5 -0
  40. package/dist/utils/expoUpdatesEmbedded.d.ts +3 -0
  41. package/dist/utils/expoUpdatesEmbedded.js +109 -0
  42. package/package.json +5 -5
  43. package/dist/steps/utils/ios/xcpretty.d.ts +0 -15
  44. package/dist/steps/utils/ios/xcpretty.js +0 -92
@@ -7,9 +7,12 @@ exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDevic
7
7
  const eas_build_job_1 = require("@expo/eas-build-job");
8
8
  const steps_1 = require("@expo/steps");
9
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
10
11
  const node_os_1 = __importDefault(require("node:os"));
11
12
  const node_path_1 = __importDefault(require("node:path"));
13
+ const sentry_1 = require("../../sentry");
12
14
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
15
+ const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
13
16
  const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
14
17
  const SRC_DIR = '/tmp/agent-device-src';
15
18
  const DAEMON_JSON_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device', 'daemon.json');
@@ -43,29 +46,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
43
46
  logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
44
47
  await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
45
48
  }
46
- logger.info(packageVersion
47
- ? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
48
- : `Cloning agent-device (latest) into ${SRC_DIR}.`);
49
- await cloneAgentDeviceAsync({ packageVersion, env, logger });
50
- logger.info('Installing agent-device dependencies.');
51
- await (0, turtle_spawn_1.default)('bun', ['install', '--production'], {
52
- cwd: SRC_DIR,
53
- env,
54
- logger,
55
- });
56
49
  logger.info('Launching agent-device daemon.');
57
- (0, remoteDeviceRunSession_1.spawnDetached)({
58
- command: 'bun',
59
- args: ['run', 'src/daemon.ts'],
60
- cwd: SRC_DIR,
61
- env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
62
- });
50
+ const daemonProcess = await startAgentDeviceDaemonAsync({ packageVersion, env, logger });
63
51
  logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
64
- const { port: daemonPort, token: daemonToken } = await (0, remoteDeviceRunSession_1.waitForFileAsync)({
65
- filePath: DAEMON_JSON_PATH,
66
- timeoutMs: STARTUP_TIMEOUT_MS,
67
- description: 'agent-device daemon credentials',
68
- parse: parseDaemonInfo,
52
+ const { port: daemonPort, token: daemonToken } = await waitForDaemonInfoAsync({
53
+ daemonProcess,
69
54
  });
70
55
  logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
71
56
  const agentDeviceRemoteSessionUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
@@ -80,7 +65,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
80
65
  // on Darwin. Android sessions go without a preview URL.
81
66
  let webPreviewUrl;
82
67
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
83
- const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
68
+ const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
84
69
  baseDomain: ngrokTunnelDomain,
85
70
  env,
86
71
  logger,
@@ -106,6 +91,64 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
106
91
  },
107
92
  });
108
93
  }
94
+ async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
95
+ const packageSpec = createAgentDevicePackageSpec(packageVersion);
96
+ try {
97
+ logger.info(`Installing ${packageSpec} globally with Bun.`);
98
+ await (0, turtle_spawn_1.default)('bun', ['add', '--global', packageSpec], {
99
+ env,
100
+ logger,
101
+ });
102
+ const daemonPath = getGlobalAgentDeviceDaemonPath(env);
103
+ if (!node_fs_1.default.existsSync(daemonPath)) {
104
+ throw new eas_build_job_1.SystemError(`Expected agent-device daemon entry at ${daemonPath}.`);
105
+ }
106
+ logger.info(`Launching daemon from ${daemonPath}.`);
107
+ return (0, remoteDeviceRunSession_1.spawnDetached)({
108
+ command: 'node',
109
+ args: [daemonPath],
110
+ env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
111
+ });
112
+ }
113
+ catch (err) {
114
+ const error = err instanceof Error ? err : new Error(String(err));
115
+ const bunVersion = await getBunVersionForDiagnosticsAsync(env);
116
+ sentry_1.Sentry.capture('Failed to start agent-device daemon from global Bun package; falling back to git clone', error, {
117
+ level: 'warning',
118
+ tags: {
119
+ phase: 'agent-device-daemon-start',
120
+ fallback: 'git-clone',
121
+ },
122
+ extras: {
123
+ packageSpec,
124
+ packageVersion: packageVersion ?? 'latest',
125
+ bunVersion,
126
+ bunInstallConfigured: Boolean(env.BUN_INSTALL?.trim()),
127
+ },
128
+ });
129
+ logger.warn(`Failed to start daemon from global ${packageSpec}; falling back to git clone: ${error.message}`);
130
+ return await startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger });
131
+ }
132
+ }
133
+ async function startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger, }) {
134
+ logger.info(packageVersion
135
+ ? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
136
+ : `Cloning agent-device (latest) into ${SRC_DIR}.`);
137
+ await cloneAgentDeviceAsync({ packageVersion, env, logger });
138
+ logger.info('Installing agent-device dependencies.');
139
+ await (0, turtle_spawn_1.default)('bun', ['install', '--production'], {
140
+ cwd: SRC_DIR,
141
+ env,
142
+ logger,
143
+ });
144
+ logger.info('Launching daemon from cloned agent-device source.');
145
+ return (0, remoteDeviceRunSession_1.spawnDetached)({
146
+ command: 'bun',
147
+ args: ['run', 'src/daemon.ts'],
148
+ cwd: SRC_DIR,
149
+ env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
150
+ });
151
+ }
109
152
  async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
110
153
  const branchArgs = packageVersion ? ['--branch', `v${packageVersion}`] : [];
111
154
  await (0, turtle_spawn_1.default)('git', ['clone', '--depth', '1', ...branchArgs, AGENT_DEVICE_REPO_URL, SRC_DIR], {
@@ -113,6 +156,42 @@ async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
113
156
  logger,
114
157
  });
115
158
  }
159
+ async function waitForDaemonInfoAsync({ daemonProcess, }) {
160
+ try {
161
+ return await (0, remoteDeviceRunSession_1.waitForFileAsync)({
162
+ filePath: DAEMON_JSON_PATH,
163
+ timeoutMs: STARTUP_TIMEOUT_MS,
164
+ description: 'agent-device daemon credentials',
165
+ parse: parseDaemonInfo,
166
+ });
167
+ }
168
+ catch (err) {
169
+ const output = daemonProcess.getOutput();
170
+ throw new eas_build_job_1.SystemError(`${err instanceof Error
171
+ ? err.message
172
+ : `Timed out waiting for agent-device daemon credentials.`}${output ? `\nagent-device daemon output:\n${output}` : ''}`);
173
+ }
174
+ }
175
+ async function getBunVersionForDiagnosticsAsync(env) {
176
+ try {
177
+ const result = await (0, turtle_spawn_1.default)('bun', ['--version'], { stdio: 'pipe', env, cwd: node_os_1.default.tmpdir() });
178
+ return result.stdout.trim() || 'unknown';
179
+ }
180
+ catch {
181
+ return 'unknown';
182
+ }
183
+ }
184
+ function createAgentDevicePackageSpec(packageVersion) {
185
+ const versionSpec = packageVersion ? packageVersion.replace(/^v(?=\d)/, '') : 'latest';
186
+ return `${AGENT_DEVICE_PACKAGE_NAME}@${versionSpec}`;
187
+ }
188
+ function getGlobalAgentDeviceDaemonPath(env) {
189
+ return node_path_1.default.join(getBunInstallDirectory(env), 'install', 'global', 'node_modules', AGENT_DEVICE_PACKAGE_NAME, 'dist', 'src', 'internal', 'daemon.js');
190
+ }
191
+ function getBunInstallDirectory(env) {
192
+ const bunInstall = env.BUN_INSTALL?.trim();
193
+ return bunInstall ? bunInstall : node_path_1.default.join(node_os_1.default.homedir(), '.bun');
194
+ }
116
195
  function parseDaemonInfo(raw) {
117
196
  const parsed = JSON.parse(raw);
118
197
  if (!parsed ||
@@ -77,7 +77,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
77
77
  // serve-sim is iOS-only — Android sessions go without a preview URL.
78
78
  let webPreviewUrl;
79
79
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
80
- const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
80
+ const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
81
81
  baseDomain: ngrokTunnelDomain,
82
82
  env,
83
83
  logger,
@@ -52,6 +52,12 @@ function createStartIosSimulatorBuildFunction() {
52
52
  deviceIdentifier: originalDeviceIdentifier,
53
53
  env,
54
54
  });
55
+ try {
56
+ await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid, env });
57
+ }
58
+ catch (err) {
59
+ logger.warn({ err }, 'Failed to disable apsd in the Simulator.');
60
+ }
55
61
  await IosSimulatorUtils_1.IosSimulatorUtils.waitForReadyAsync({ udid, env });
56
62
  logger.info('');
57
63
  const device = await IosSimulatorUtils_1.IosSimulatorUtils.getDeviceAsync({ udid, env });
@@ -76,6 +82,12 @@ function createStartIosSimulatorBuildFunction() {
76
82
  deviceIdentifier: cloneDeviceName,
77
83
  env,
78
84
  });
85
+ try {
86
+ await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid: cloneUdid, env });
87
+ }
88
+ catch (err) {
89
+ logger.warn({ err }, 'Failed to disable apsd in the Simulator.');
90
+ }
79
91
  await IosSimulatorUtils_1.IosSimulatorUtils.waitForReadyAsync({
80
92
  udid: cloneUdid,
81
93
  env,
@@ -22,7 +22,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
22
22
  logger.info('Starting serve-sim remote session.');
23
23
  logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
24
24
  await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
25
- const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
25
+ const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
26
26
  baseDomain: ngrokTunnelDomain,
27
27
  env,
28
28
  logger,
@@ -112,7 +112,7 @@ function createUploadArtifactBuildFunction(ctx) {
112
112
  }
113
113
  catch (error) {
114
114
  if (inputs.ignore_error.value) {
115
- logger.error(`Failed to upload ${artifact.type}. Ignoring error.`, error);
115
+ logger.error({ err: error }, `Failed to upload ${artifact.type}. Ignoring error.`);
116
116
  // Ignoring error.
117
117
  return;
118
118
  }
@@ -7,8 +7,8 @@ exports.runFastlaneGym = runFastlaneGym;
7
7
  exports.runFastlane = runFastlane;
8
8
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
9
  const path_1 = __importDefault(require("path"));
10
- const xcpretty_1 = require("./xcpretty");
11
10
  const fastlane_1 = require("../../../common/fastlane");
11
+ const xcpretty_1 = require("../../../common/xcpretty");
12
12
  async function runFastlaneGym({ workingDir, logger, buildLogsDirectory, env, extraEnv, }) {
13
13
  const buildLogger = new xcpretty_1.XcodeBuildLogger(logger, workingDir);
14
14
  void buildLogger.watchLogFiles(buildLogsDirectory);
@@ -1,9 +1,36 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { BuildStepEnv } from '@expo/steps';
3
+ import { z } from 'zod';
3
4
  import { CustomBuildContext } from '../../customBuildContext';
4
5
  export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
5
6
  export declare function getNgrokTunnelDomainOrThrow(env: BuildStepEnv): string;
6
7
  export declare function getNgrokAuthtokenOrThrow(env: BuildStepEnv): string;
8
+ declare const TurnIceServersSchema: z.ZodArray<z.ZodObject<{
9
+ urls: z.ZodArray<z.ZodString>;
10
+ username: z.ZodOptional<z.ZodString>;
11
+ credential: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>>;
13
+ export type TurnIceServers = z.infer<typeof TurnIceServersSchema>;
14
+ /**
15
+ * Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
16
+ * credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
17
+ * (the entry carrying the short-lived credentials).
18
+ */
19
+ export declare function turnIceServersToServeSimArgs(iceServers: TurnIceServers): string[];
20
+ /**
21
+ * Fetch short-lived Cloudflare TURN ICE servers for this job run from www
22
+ * (minted on demand, mirroring how the worker fetches project clone URLs) and
23
+ * translate them into serve-sim CLI flags.
24
+ *
25
+ * Best-effort: on any failure we log and return [] so serve-sim falls back to
26
+ * its built-in P2P/STUN behavior. The credential is passed to serve-sim as a
27
+ * process arg and deliberately not logged (turtle-spawn never logs argv and the
28
+ * worker is single-tenant).
29
+ */
30
+ export declare function fetchServeSimTurnArgsAsync(ctx: CustomBuildContext, { env, logger }: {
31
+ env: BuildStepEnv;
32
+ logger: bunyan;
33
+ }): Promise<string[]>;
7
34
  export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
8
35
  ctx: CustomBuildContext;
9
36
  deviceRunSessionId: string;
@@ -19,7 +46,7 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
19
46
  cwd?: string;
20
47
  env: BuildStepEnv;
21
48
  }): DetachedProcessHandle;
22
- export declare function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }: {
49
+ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, }: {
23
50
  baseDomain: string;
24
51
  env: BuildStepEnv;
25
52
  logger: bunyan;
@@ -41,3 +68,4 @@ export declare function waitForFileAsync<T>({ filePath, timeoutMs, description,
41
68
  description: string;
42
69
  parse: (raw: string) => T;
43
70
  }): Promise<T>;
71
+ export {};
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
40
40
  exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
41
41
  exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
42
+ exports.turnIceServersToServeSimArgs = turnIceServersToServeSimArgs;
43
+ exports.fetchServeSimTurnArgsAsync = fetchServeSimTurnArgsAsync;
42
44
  exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
43
45
  exports.spawnDetached = spawnDetached;
44
46
  exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
@@ -48,9 +50,13 @@ const eas_build_job_1 = require("@expo/eas-build-job");
48
50
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
49
51
  const ngrok = __importStar(require("@ngrok/ngrok"));
50
52
  const gql_tada_1 = require("gql.tada");
53
+ const nullthrows_1 = __importDefault(require("nullthrows"));
54
+ const zod_1 = require("zod");
51
55
  const node_crypto_1 = require("node:crypto");
52
56
  const node_fs_1 = __importDefault(require("node:fs"));
57
+ const sentry_1 = require("../../sentry");
53
58
  const retry_1 = require("../../utils/retry");
59
+ const turtleFetch_1 = require("../../utils/turtleFetch");
54
60
  const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
55
61
  mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
56
62
  deviceRunSession {
@@ -91,6 +97,72 @@ function getNgrokAuthtokenOrThrow(env) {
91
97
  }
92
98
  return authtoken;
93
99
  }
100
+ const TurnIceServersSchema = zod_1.z.array(zod_1.z.object({
101
+ urls: zod_1.z.array(zod_1.z.string()),
102
+ username: zod_1.z.string().optional(),
103
+ credential: zod_1.z.string().optional(),
104
+ }));
105
+ const TurnIceServersResponseSchema = zod_1.z.object({
106
+ data: zod_1.z.object({
107
+ iceServers: TurnIceServersSchema,
108
+ }),
109
+ });
110
+ /**
111
+ * Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
112
+ * credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
113
+ * (the entry carrying the short-lived credentials).
114
+ */
115
+ function turnIceServersToServeSimArgs(iceServers) {
116
+ const stunUrls = iceServers
117
+ .filter(server => !server.username && !server.credential)
118
+ .flatMap(server => server.urls);
119
+ const turnServer = iceServers.find(server => server.username && server.credential);
120
+ const args = [];
121
+ if (stunUrls.length > 0) {
122
+ args.push('--stun-url', stunUrls.join(','));
123
+ }
124
+ if (turnServer?.username && turnServer.credential && turnServer.urls.length > 0) {
125
+ args.push('--turn-url', turnServer.urls.join(','), '--turn-username', turnServer.username, '--turn-credential', turnServer.credential);
126
+ }
127
+ return args;
128
+ }
129
+ /**
130
+ * Fetch short-lived Cloudflare TURN ICE servers for this job run from www
131
+ * (minted on demand, mirroring how the worker fetches project clone URLs) and
132
+ * translate them into serve-sim CLI flags.
133
+ *
134
+ * Best-effort: on any failure we log and return [] so serve-sim falls back to
135
+ * its built-in P2P/STUN behavior. The credential is passed to serve-sim as a
136
+ * process arg and deliberately not logged (turtle-spawn never logs argv and the
137
+ * worker is single-tenant).
138
+ */
139
+ async function fetchServeSimTurnArgsAsync(ctx, { env, logger }) {
140
+ try {
141
+ const deviceRunSessionId = getDeviceRunSessionIdOrThrow(env);
142
+ const expoApiServerUrl = (0, nullthrows_1.default)(ctx.env.__API_SERVER_URL, '__API_SERVER_URL is not set');
143
+ const robotAccessToken = (0, nullthrows_1.default)(ctx.job.secrets?.robotAccessToken, 'robot access token is not set');
144
+ const response = await (0, turtleFetch_1.turtleFetch)(new URL(`/v2/device-run-sessions/${deviceRunSessionId}/turn-ice-servers`, expoApiServerUrl).toString(), 'POST', {
145
+ headers: {
146
+ Authorization: `Bearer ${robotAccessToken}`,
147
+ },
148
+ timeout: 5000,
149
+ retries: 1,
150
+ logger,
151
+ });
152
+ const { data } = TurnIceServersResponseSchema.parse(await response.json());
153
+ const args = turnIceServersToServeSimArgs(data.iceServers);
154
+ if (args.length > 0) {
155
+ logger.info('Configured serve-sim with Cloudflare TURN ICE servers.');
156
+ }
157
+ return args;
158
+ }
159
+ catch (err) {
160
+ const error = err instanceof Error ? err : new Error(String(err));
161
+ sentry_1.Sentry.capture('Could not fetch Cloudflare TURN ICE servers', error, { level: 'warning' });
162
+ logger.warn({ err: error }, 'Could not fetch Cloudflare TURN ICE servers; serve-sim will fall back to P2P/STUN.');
163
+ return [];
164
+ }
165
+ }
94
166
  async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
95
167
  logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
96
168
  const result = await ctx.graphqlClient
@@ -119,8 +191,9 @@ function spawnDetached({ command, args, cwd, env, }) {
119
191
  promise.child.stderr?.on('data', appendChunk);
120
192
  return { getOutput: () => output };
121
193
  }
122
- async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }) {
194
+ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
123
195
  logger.info('Launching serve-sim with tunnel.');
196
+ const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
124
197
  const serveSim = spawnDetached({
125
198
  command: 'npx',
126
199
  args: [
@@ -135,7 +208,8 @@ async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs
135
208
  '--stream-quality',
136
209
  '0.55',
137
210
  '--codec',
138
- 'h264',
211
+ 'webrtc',
212
+ ...turnArgs,
139
213
  ],
140
214
  env,
141
215
  });
@@ -49,6 +49,10 @@ export declare namespace IosSimulatorUtils {
49
49
  udid: IosSimulatorUuid;
50
50
  env: NodeJS.ProcessEnv;
51
51
  }): Promise<void>;
52
+ export function disableApsdAsync({ udid, env, }: {
53
+ udid: IosSimulatorUuid;
54
+ env: NodeJS.ProcessEnv;
55
+ }): Promise<void>;
52
56
  export function collectLogsAsync({ deviceIdentifier, env, }: {
53
57
  deviceIdentifier: IosSimulatorName | IosSimulatorUuid;
54
58
  env: NodeJS.ProcessEnv;
@@ -77,6 +77,11 @@ var IosSimulatorUtils;
77
77
  });
78
78
  }
79
79
  IosSimulatorUtils.waitForReadyAsync = waitForReadyAsync;
80
+ async function disableApsdAsync({ udid, env, }) {
81
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'disable', 'system/com.apple.apsd'], { env });
82
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'bootout', 'system/com.apple.apsd'], { env });
83
+ }
84
+ IosSimulatorUtils.disableApsdAsync = disableApsdAsync;
80
85
  async function collectLogsAsync({ deviceIdentifier, env, }) {
81
86
  const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-simulator-logs-'));
82
87
  const outputPath = node_path_1.default.join(outputDir, `${deviceIdentifier}.logarchive`);
@@ -0,0 +1,3 @@
1
+ import { BuildJob } from '@expo/eas-build-job';
2
+ import { BuildContext } from '../context';
3
+ export declare function uploadEmbeddedBundleAsync(ctx: BuildContext<BuildJob>): Promise<void>;
@@ -0,0 +1,109 @@
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.uploadEmbeddedBundleAsync = uploadEmbeddedBundleAsync;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
8
+ const logger_1 = require("@expo/logger");
9
+ const results_1 = require("@expo/results");
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const node_stream_zip_1 = __importDefault(require("node-stream-zip"));
14
+ const artifacts_1 = require("./artifacts");
15
+ const easCli_1 = require("./easCli");
16
+ const resolve_1 = require("../ios/resolve");
17
+ const expoUpdates_1 = require("./expoUpdates");
18
+ async function uploadEmbeddedBundleAsync(ctx) {
19
+ if (!(await (0, expoUpdates_1.isEASUpdateConfigured)(ctx))) {
20
+ ctx.markBuildPhaseSkipped();
21
+ return;
22
+ }
23
+ const { platform } = ctx.job;
24
+ if (platform === eas_build_job_1.Platform.IOS && ctx.job.simulator) {
25
+ ctx.markBuildPhaseSkipped();
26
+ return;
27
+ }
28
+ const channel = ctx.job.updates?.channel;
29
+ if (!channel) {
30
+ ctx.logger.warn('Skipping embedded bundle upload: no channel configured for this build profile.');
31
+ ctx.markBuildPhaseHasWarnings();
32
+ return;
33
+ }
34
+ const projectDir = ctx.getReactNativeProjectDirectory();
35
+ let archivePattern;
36
+ if (platform === eas_build_job_1.Platform.IOS) {
37
+ archivePattern = (0, resolve_1.resolveArtifactPath)(ctx);
38
+ }
39
+ else if (platform === eas_build_job_1.Platform.ANDROID) {
40
+ archivePattern =
41
+ ctx.job.applicationArchivePath ??
42
+ 'android/app/build/outputs/**/*.{apk,aab}';
43
+ }
44
+ else {
45
+ throw new Error(`Uploading embedded updates is not supported for the ${platform} platform.`);
46
+ }
47
+ const [archivePath] = await (0, artifacts_1.findArtifacts)({
48
+ rootDir: projectDir,
49
+ patternOrPath: archivePattern,
50
+ logger: null,
51
+ }).catch(() => []);
52
+ if (!archivePath) {
53
+ ctx.logger.warn('Skipping embedded bundle upload: build archive not found.');
54
+ ctx.markBuildPhaseHasWarnings();
55
+ return;
56
+ }
57
+ const tmpDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'eas-embedded-bundle-'));
58
+ const bundleName = platform === eas_build_job_1.Platform.IOS ? 'main.jsbundle' : 'index.android.bundle';
59
+ const bundlePath = path_1.default.join(tmpDir, bundleName);
60
+ const manifestPath = path_1.default.join(tmpDir, 'app.manifest');
61
+ const zip = new node_stream_zip_1.default.async({ file: archivePath });
62
+ try {
63
+ const entries = Object.values(await zip.entries());
64
+ const bundleEntry = entries.find(e => platform === eas_build_job_1.Platform.IOS
65
+ ? e.name.endsWith('/main.jsbundle')
66
+ : e.name.endsWith('assets/index.android.bundle'));
67
+ const manifestEntry = entries.find(e => platform === eas_build_job_1.Platform.IOS
68
+ ? e.name.includes('EXUpdates.bundle/app.manifest')
69
+ : e.name.endsWith('assets/app.manifest'));
70
+ if (!bundleEntry || !manifestEntry) {
71
+ ctx.logger.warn('Skipping embedded bundle upload: bundle or manifest not found in archive.');
72
+ ctx.markBuildPhaseHasWarnings();
73
+ return;
74
+ }
75
+ await zip.extract(bundleEntry.name, bundlePath);
76
+ await zip.extract(manifestEntry.name, manifestPath);
77
+ const args = [
78
+ 'update:embedded:upload',
79
+ '--platform',
80
+ platform,
81
+ '--bundle',
82
+ bundlePath,
83
+ '--manifest',
84
+ manifestPath,
85
+ '--channel',
86
+ channel,
87
+ '--non-interactive',
88
+ ];
89
+ if (ctx.env.EAS_BUILD_ID) {
90
+ args.push('--build-id', ctx.env.EAS_BUILD_ID);
91
+ }
92
+ await (0, easCli_1.runEasCliCommand)({
93
+ args,
94
+ options: {
95
+ cwd: projectDir,
96
+ env: ctx.env,
97
+ logger: ctx.logger,
98
+ mode: logger_1.PipeMode.STDERR_ONLY_AS_STDOUT,
99
+ },
100
+ });
101
+ }
102
+ catch (err) {
103
+ ctx.logger.warn({ err }, 'Failed to upload embedded bundle.');
104
+ ctx.markBuildPhaseHasWarnings();
105
+ }
106
+ finally {
107
+ await (0, results_1.asyncResult)(zip.close());
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "20.0.0",
3
+ "version": "20.2.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -38,14 +38,14 @@
38
38
  "@expo/config": "55.0.10",
39
39
  "@expo/config-plugins": "55.0.7",
40
40
  "@expo/downloader": "20.0.0",
41
- "@expo/eas-build-job": "20.0.0",
41
+ "@expo/eas-build-job": "20.1.0",
42
42
  "@expo/env": "^0.4.0",
43
43
  "@expo/logger": "20.0.0",
44
44
  "@expo/package-manager": "1.9.10",
45
45
  "@expo/plist": "^0.2.0",
46
46
  "@expo/results": "^1.0.0",
47
47
  "@expo/spawn-async": "1.7.2",
48
- "@expo/steps": "20.0.0",
48
+ "@expo/steps": "20.1.0",
49
49
  "@expo/template-file": "20.0.0",
50
50
  "@expo/turtle-spawn": "20.0.0",
51
51
  "@expo/xcpretty": "^4.3.1",
@@ -77,7 +77,7 @@
77
77
  "zod": "^4.3.5"
78
78
  },
79
79
  "devDependencies": {
80
- "@expo/repack-app": "~0.4.1",
80
+ "@expo/repack-app": "~0.6.1",
81
81
  "@types/fs-extra": "^11.0.4",
82
82
  "@types/jest": "^29.5.12",
83
83
  "@types/lodash": "^4.17.4",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "890387b3b62c2ed2a946ec0e1197ff69f3e29342"
103
+ "gitHead": "203e9997eebafcbddc600285f6a809afb4a93a74"
104
104
  }
@@ -1,15 +0,0 @@
1
- import { bunyan } from '@expo/logger';
2
- export declare class XcodeBuildLogger {
3
- private readonly logger;
4
- private readonly projectRoot;
5
- private loggerError?;
6
- private flushing;
7
- private logReaderPromise?;
8
- private logsPath?;
9
- constructor(logger: bunyan, projectRoot: string);
10
- watchLogFiles(logsDirectory: string): Promise<void>;
11
- flush(): Promise<void>;
12
- private getBuildLogFilename;
13
- private startBuildLogger;
14
- private findBundlerErrors;
15
- }