@expo/build-tools 19.1.0 → 20.1.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.
@@ -22,6 +22,7 @@ const saveBuildCache_1 = require("../steps/functions/saveBuildCache");
22
22
  const gradleConfig_1 = require("../steps/utils/android/gradleConfig");
23
23
  const artifacts_1 = require("../utils/artifacts");
24
24
  const expoUpdates_1 = require("../utils/expoUpdates");
25
+ const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
25
26
  const hooks_1 = require("../utils/hooks");
26
27
  const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
27
28
  async function androidBuilder(ctx) {
@@ -177,6 +178,11 @@ async function buildAsync(ctx) {
177
178
  logger: ctx.logger,
178
179
  });
179
180
  });
181
+ if (ctx.env.EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE) {
182
+ await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_EMBEDDED_BUNDLE, async () => {
183
+ await (0, expoUpdatesEmbedded_1.uploadEmbeddedBundleAsync)(ctx);
184
+ });
185
+ }
180
186
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.SAVE_CACHE, async () => {
181
187
  if (ctx.isLocal) {
182
188
  ctx.logger.info('Local builds do not support saving cache.');
@@ -26,6 +26,7 @@ const restoreBuildCache_1 = require("../steps/functions/restoreBuildCache");
26
26
  const saveBuildCache_1 = require("../steps/functions/saveBuildCache");
27
27
  const artifacts_1 = require("../utils/artifacts");
28
28
  const expoUpdates_1 = require("../utils/expoUpdates");
29
+ const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
29
30
  const hooks_1 = require("../utils/hooks");
30
31
  const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
31
32
  const processes_1 = require("../utils/processes");
@@ -166,13 +167,16 @@ async function buildAsync(ctx) {
166
167
  }
167
168
  try {
168
169
  const { derivedDataPath, workspacePath } = (0, nullthrows_1.default)(fastlaneResult);
169
- await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
170
+ const { skipped } = await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
170
171
  derivedDataPath,
171
172
  workspacePath,
172
173
  logger: ctx.logger,
173
174
  proxyBaseUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
174
175
  env: ctx.env,
175
176
  });
177
+ if (skipped) {
178
+ ctx.markBuildPhaseSkipped();
179
+ }
176
180
  }
177
181
  catch (err) {
178
182
  sentry_1.Sentry.capture('Failed to parse xcactivitylog', err);
@@ -189,6 +193,11 @@ async function buildAsync(ctx) {
189
193
  logger: ctx.logger,
190
194
  });
191
195
  });
196
+ if (ctx.env.EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE) {
197
+ await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_EMBEDDED_BUNDLE, async () => {
198
+ await (0, expoUpdatesEmbedded_1.uploadEmbeddedBundleAsync)(ctx);
199
+ });
200
+ }
192
201
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.SAVE_CACHE, async () => {
193
202
  if (ctx.isLocal) {
194
203
  ctx.logger.info('Local builds do not support saving cache.');
package/dist/datadog.d.ts CHANGED
@@ -1,11 +1,19 @@
1
+ type MetricsTarget = {
2
+ kind: 'build';
3
+ turtleBuildId: string;
4
+ } | {
5
+ kind: 'jobRun';
6
+ turtleJobRunId: string;
7
+ };
1
8
  type DatadogSetupOptions = {
2
9
  expoApiV2BaseUrl: string;
3
- turtleBuildId: string;
4
10
  robotAccessToken: string;
11
+ target: MetricsTarget;
5
12
  };
6
13
  export declare const Datadog: {
7
14
  setup(opts: DatadogSetupOptions | null): void;
8
15
  distribution(name: string, value: number, tags?: Record<string, string>): void;
16
+ log(message: string, tags?: Record<string, string>): void;
9
17
  flushAsync(): Promise<void>;
10
18
  };
11
19
  export {};
package/dist/datadog.js CHANGED
@@ -4,7 +4,7 @@ exports.Datadog = void 0;
4
4
  const sentry_1 = require("./sentry");
5
5
  const turtleFetch_1 = require("./utils/turtleFetch");
6
6
  let setupOptions = null;
7
- let pendingMetricUploads = [];
7
+ let pendingUploads = [];
8
8
  exports.Datadog = {
9
9
  setup(opts) {
10
10
  setupOptions = opts;
@@ -13,7 +13,7 @@ exports.Datadog = {
13
13
  if (!setupOptions) {
14
14
  return;
15
15
  }
16
- const { expoApiV2BaseUrl, turtleBuildId, robotAccessToken } = setupOptions;
16
+ const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
17
17
  const metrics = [
18
18
  {
19
19
  name,
@@ -22,20 +22,49 @@ exports.Datadog = {
22
22
  tags,
23
23
  },
24
24
  ];
25
- const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(`turtle-builds/${turtleBuildId}/metrics`, expoApiV2BaseUrl).toString(), 'POST', {
25
+ const metricsPath = target.kind === 'build'
26
+ ? `turtle-builds/${target.turtleBuildId}/metrics`
27
+ : `turtle-job-runs/${target.turtleJobRunId}/metrics`;
28
+ const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(metricsPath, expoApiV2BaseUrl).toString(), 'POST', {
26
29
  json: { metrics },
27
30
  headers: {
28
31
  Authorization: `Bearer ${robotAccessToken}`,
29
32
  },
30
33
  retries: 2,
31
34
  }).catch(err => {
32
- sentry_1.Sentry.capture('Failed to report turtle build metric', err, {
35
+ sentry_1.Sentry.capture(`Failed to report turtle ${target.kind} metric`, err, {
33
36
  extras: { metrics },
34
37
  });
35
38
  });
36
- pendingMetricUploads.push(uploadPromise);
39
+ pendingUploads.push(uploadPromise);
40
+ },
41
+ log(message, tags = {}) {
42
+ if (!setupOptions) {
43
+ return;
44
+ }
45
+ const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
46
+ const log = target.kind === 'build'
47
+ ? { buildId: target.turtleBuildId, message, tags }
48
+ : { jobRunId: target.turtleJobRunId, message, tags };
49
+ const logsPath = target.kind === 'build' ? 'turtle-builds/logs' : 'turtle-job-runs/logs';
50
+ const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(logsPath, expoApiV2BaseUrl).toString(), 'POST', {
51
+ json: log,
52
+ headers: {
53
+ Authorization: `Bearer ${robotAccessToken}`,
54
+ },
55
+ shouldThrowOnNotOk: false,
56
+ }).catch(err => {
57
+ sentry_1.Sentry.capture(`Failed to report turtle ${target.kind} log`, err, {
58
+ extras: { log },
59
+ });
60
+ });
61
+ pendingUploads.push(uploadPromise);
37
62
  },
38
63
  async flushAsync() {
39
- await Promise.allSettled(pendingMetricUploads);
64
+ // Rotate so each flush only awaits its own batch; uploads enqueued during the
65
+ // await land in the fresh array for a later flush.
66
+ const uploads = pendingUploads;
67
+ pendingUploads = [];
68
+ await Promise.allSettled(uploads);
40
69
  },
41
70
  };
package/dist/ios/pod.js CHANGED
@@ -38,6 +38,10 @@ async function installPods(ctx, { infoCallbackFn }) {
38
38
  };
39
39
  }
40
40
  async function resolvePrecompiledModulesPodInstallEnvAsync(ctx) {
41
+ if (ctx.env.EXPO_USE_PRECOMPILED_MODULES === '0') {
42
+ ctx.logger.info('EXPO_USE_PRECOMPILED_MODULES=0 is set; not enabling precompiled modules use.');
43
+ return {};
44
+ }
41
45
  if (ctx.job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES !== '1') {
42
46
  return {};
43
47
  }
@@ -18,6 +18,7 @@ const path_1 = __importDefault(require("path"));
18
18
  const ccacheStats_1 = require("./ccacheStats");
19
19
  const restoreCache_1 = require("./restoreCache");
20
20
  const cacheKey_1 = require("../../utils/cacheKey");
21
+ const datadog_1 = require("../../datadog");
21
22
  const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
22
23
  const turtleFetch_1 = require("../../utils/turtleFetch");
23
24
  function createRestoreBuildCacheFunction() {
@@ -216,23 +217,10 @@ async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets,
216
217
  });
217
218
  const hitType = matchedKey === cacheKey ? 'direct_hit' : 'prefix_match';
218
219
  logger.info(`Gradle cache restored to ${gradleCachesPath} (${hitType === 'direct_hit' ? 'direct hit' : 'prefix match'})`);
219
- try {
220
- await (0, turtleFetch_1.turtleFetch)(new URL('v2/turtle-builds/logs', expoApiServerURL).toString(), 'POST', {
221
- json: {
222
- buildId: jobId,
223
- message: `Gradle cache restored (${hitType})`,
224
- tags: {
225
- event: 'gradle_cache_restored',
226
- cache_hit_type: hitType,
227
- },
228
- },
229
- headers: {
230
- Authorization: `Bearer ${robotAccessToken}`,
231
- },
232
- shouldThrowOnNotOk: false,
233
- });
234
- }
235
- catch { }
220
+ datadog_1.Datadog.log(`Gradle cache restored (${hitType})`, {
221
+ event: 'gradle_cache_restored',
222
+ cache_hit_type: hitType,
223
+ });
236
224
  }
237
225
  catch (err) {
238
226
  if (err instanceof turtleFetch_1.TurtleFetchError && err.response?.status === 404) {
@@ -29,10 +29,13 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
29
29
  }),
30
30
  ],
31
31
  fn: async ({ logger, global }, { inputs, env }) => {
32
- // Fail fast before any expensive setup if the orchestrator-injected
33
- // DEVICE_RUN_SESSION_ID env var is missing without it we cannot
34
- // report the remote config back to the API server.
32
+ // Fail fast before any expensive setup if the injected env
33
+ // vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
34
+ // back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
35
+ // for our ngrok tunnels), and NGROK_AUTHTOKEN (to authenticate them).
35
36
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
37
+ const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
38
+ const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
36
39
  const packageVersion = inputs.package_version.value;
37
40
  const { runtimePlatform } = global;
38
41
  logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
@@ -40,12 +43,6 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
40
43
  logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
41
44
  await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
42
45
  }
43
- logger.info('Ensuring cloudflared is installed.');
44
- const cloudflaredCommand = await (0, remoteDeviceRunSession_1.ensureCloudflaredInstalledAsync)({
45
- runtimePlatform,
46
- env,
47
- logger,
48
- });
49
46
  logger.info(packageVersion
50
47
  ? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
51
48
  : `Cloning agent-device (latest) into ${SRC_DIR}.`);
@@ -71,18 +68,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
71
68
  parse: parseDaemonInfo,
72
69
  });
73
70
  logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
74
- logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort}.`);
75
- const cloudflared = (0, remoteDeviceRunSession_1.spawnDetached)({
76
- command: cloudflaredCommand,
77
- args: ['tunnel', '--url', `http://localhost:${daemonPort}`],
78
- env,
79
- });
80
- logger.info('Waiting for a public tunnel URL.');
81
- const agentDeviceRemoteSessionUrl = await (0, remoteDeviceRunSession_1.waitForMatchInOutputAsync)({
82
- process: cloudflared,
83
- pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
84
- timeoutMs: STARTUP_TIMEOUT_MS,
85
- description: 'cloudflared tunnel',
71
+ const agentDeviceRemoteSessionUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
72
+ port: daemonPort,
73
+ subdomainPrefix: 'agent-device',
74
+ baseDomain: ngrokTunnelDomain,
75
+ authtoken: ngrokAuthtoken,
76
+ logger,
86
77
  });
87
78
  logger.info(`Tunnel is ready at ${agentDeviceRemoteSessionUrl}.`);
88
79
  // serve-sim is iOS-only — only launch it (and report a webPreviewUrl)
@@ -90,6 +81,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
90
81
  let webPreviewUrl;
91
82
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
92
83
  const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
84
+ baseDomain: ngrokTunnelDomain,
93
85
  env,
94
86
  logger,
95
87
  timeoutMs: STARTUP_TIMEOUT_MS,
@@ -31,10 +31,13 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
31
31
  }),
32
32
  ],
33
33
  fn: async ({ logger, global }, { inputs, env }) => {
34
- // Fail fast before any expensive setup if the orchestrator-injected
35
- // DEVICE_RUN_SESSION_ID env var is missing without it we cannot
36
- // report the remote config back to the API server.
34
+ // Fail fast before any expensive setup if the injected env
35
+ // vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
36
+ // back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
37
+ // for our ngrok tunnels), and NGROK_AUTHTOKEN (to authenticate them).
37
38
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
39
+ const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
40
+ const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
38
41
  const packageVersion = inputs.package_version.value;
39
42
  const versionSpec = packageVersion ?? 'latest';
40
43
  const { runtimePlatform } = global;
@@ -43,12 +46,6 @@ function createStartArgentRemoteSessionBuildFunction(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('Ensuring cloudflared is installed.');
47
- const cloudflaredCommand = await (0, remoteDeviceRunSession_1.ensureCloudflaredInstalledAsync)({
48
- runtimePlatform,
49
- env,
50
- logger,
51
- });
52
49
  // Stale state from a previous run would mask the new server's port.
53
50
  await node_fs_1.default.promises.rm(ARGENT_STATE_FILE, { force: true });
54
51
  logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} via bunx.`);
@@ -69,24 +66,19 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
69
66
  parse: parseArgentToolServerState,
70
67
  });
71
68
  logger.info(`Argent tool-server is listening on port ${toolServerPort}.`);
72
- logger.info(`Starting cloudflared tunnel to http://localhost:${toolServerPort}.`);
73
- const cloudflared = (0, remoteDeviceRunSession_1.spawnDetached)({
74
- command: cloudflaredCommand,
75
- args: ['tunnel', '--url', `http://localhost:${toolServerPort}`],
76
- env,
77
- });
78
- logger.info('Waiting for a public tunnel URL.');
79
- const toolsUrl = await (0, remoteDeviceRunSession_1.waitForMatchInOutputAsync)({
80
- process: cloudflared,
81
- pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
82
- timeoutMs: STARTUP_TIMEOUT_MS,
83
- description: 'cloudflared tunnel',
69
+ const toolsUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
70
+ port: toolServerPort,
71
+ subdomainPrefix: 'argent',
72
+ baseDomain: ngrokTunnelDomain,
73
+ authtoken: ngrokAuthtoken,
74
+ logger,
84
75
  });
85
76
  logger.info(`Tunnel is ready at ${toolsUrl}.`);
86
77
  // serve-sim is iOS-only — Android sessions go without a preview URL.
87
78
  let webPreviewUrl;
88
79
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
89
80
  const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
81
+ baseDomain: ngrokTunnelDomain,
90
82
  env,
91
83
  logger,
92
84
  timeoutMs: STARTUP_TIMEOUT_MS,
@@ -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,
@@ -18,12 +18,12 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
18
18
  supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
19
19
  fn: async ({ logger }, { env }) => {
20
20
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
21
+ const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
21
22
  logger.info('Starting serve-sim remote session.');
22
23
  logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
23
24
  await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
24
- logger.info('Ensuring cloudflared is installed.');
25
- await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
26
25
  const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
26
+ baseDomain: ngrokTunnelDomain,
27
27
  env,
28
28
  logger,
29
29
  timeoutMs: STARTUP_TIMEOUT_MS,
@@ -2,9 +2,9 @@ import { bunyan } from '@expo/logger';
2
2
  import { BuildStepEnv } from '@expo/steps';
3
3
  import { z } from 'zod';
4
4
  /**
5
- * Never throws best-effort observability that does not affect build status.
6
- * Failures route to Sentry via `Sentry.capture` for engineering triage;
7
- * users see only a generic skip message.
5
+ * Catches all internal failures (logged + routed to Sentry); returns
6
+ * `{ skipped: true }` when analysis did not produce a report so callers can
7
+ * mark the build phase skipped.
8
8
  */
9
9
  export declare function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, env, }: {
10
10
  derivedDataPath: string;
@@ -13,7 +13,9 @@ export declare function parseAndReportXcactivitylog({ derivedDataPath, workspace
13
13
  logger: bunyan;
14
14
  proxyBaseUrl?: string;
15
15
  env: BuildStepEnv;
16
- }): Promise<void>;
16
+ }): Promise<{
17
+ skipped: boolean;
18
+ }>;
17
19
  declare const XcactivitylogStepSchemaZ: z.ZodObject<{
18
20
  title: z.ZodOptional<z.ZodString>;
19
21
  detailStepType: z.ZodOptional<z.ZodString>;
@@ -21,14 +21,40 @@ const XCLOGPARSER_DOWNLOAD_URL = 'https://storage.googleapis.com/turtle-v2/xclog
21
21
  const XCLOGPARSER_DOWNLOAD_TIMEOUT_MS = 20_000;
22
22
  const XCLOGPARSER_OUTPUT_FILENAME = 'xcactivitylog.json';
23
23
  /**
24
- * Never throws best-effort observability that does not affect build status.
25
- * Failures route to Sentry via `Sentry.capture` for engineering triage;
26
- * users see only a generic skip message.
24
+ * Catches all internal failures (logged + routed to Sentry); returns
25
+ * `{ skipped: true }` when analysis did not produce a report so callers can
26
+ * mark the build phase skipped.
27
27
  */
28
28
  async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, env, }) {
29
29
  let tempDir;
30
- let phase = 'creating_temp_directory';
30
+ let phase = 'checking_xcactivitylog_existence';
31
31
  try {
32
+ const logsBuildDir = path_1.default.join(derivedDataPath, 'Logs', 'Build');
33
+ let buildLogEntries;
34
+ try {
35
+ buildLogEntries = await fs_extra_1.default.readdir(logsBuildDir);
36
+ }
37
+ catch (err) {
38
+ if (err?.code !== 'ENOENT') {
39
+ throw err;
40
+ }
41
+ buildLogEntries = [];
42
+ }
43
+ const hasActivityLog = buildLogEntries.some(entry => entry.endsWith('.xcactivitylog'));
44
+ if (!hasActivityLog) {
45
+ logger.info([
46
+ `Build performance analysis skipped: no .xcactivitylog files found at ${logsBuildDir}.`,
47
+ '',
48
+ 'This typically happens when your project has a custom ios/Gymfile. To enable',
49
+ 'build performance analysis, add the following lines to your Gymfile:',
50
+ '',
51
+ ' derived_data_path("./build")',
52
+ ' result_bundle(true)',
53
+ ' result_bundle_path("./build/result-bundle.xcresult")',
54
+ ].join('\n'));
55
+ return { skipped: true };
56
+ }
57
+ phase = 'creating_temp_directory';
32
58
  tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'xclogparser-'));
33
59
  phase = 'resolving_xclogparser';
34
60
  const preinstalledVersion = await detectPreinstalledXclogparserVersion(env);
@@ -63,6 +89,7 @@ async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xcl
63
89
  phase = 'parsing_xclogparser_output';
64
90
  const data = XcactivitylogDataSchemaZ.parse(JSON.parse(await fs_extra_1.default.readFile(jsonOutputPath, 'utf8')));
65
91
  logger.info(formatReport(data));
92
+ return { skipped: false };
66
93
  }
67
94
  catch (err) {
68
95
  logger.info('Build performance analysis skipped.');
@@ -76,6 +103,7 @@ async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xcl
76
103
  stdout: err?.stdout?.slice(-4000),
77
104
  },
78
105
  });
106
+ return { skipped: true };
79
107
  }
80
108
  finally {
81
109
  if (tempDir) {
@@ -1,18 +1,15 @@
1
1
  import { bunyan } from '@expo/logger';
2
- import { BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
2
+ import { BuildStepEnv } from '@expo/steps';
3
3
  import { CustomBuildContext } from '../../customBuildContext';
4
4
  export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
5
+ export declare function getNgrokTunnelDomainOrThrow(env: BuildStepEnv): string;
6
+ export declare function getNgrokAuthtokenOrThrow(env: BuildStepEnv): string;
5
7
  export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
6
8
  ctx: CustomBuildContext;
7
9
  deviceRunSessionId: string;
8
10
  remoteConfig: Record<string, unknown>;
9
11
  logger: bunyan;
10
12
  }): Promise<void>;
11
- export declare function ensureBrewPackageInstalledAsync({ name, env, logger, }: {
12
- name: string;
13
- env: BuildStepEnv;
14
- logger: bunyan;
15
- }): Promise<void>;
16
13
  export type DetachedProcessHandle = {
17
14
  getOutput: () => string;
18
15
  };
@@ -22,7 +19,8 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
22
19
  cwd?: string;
23
20
  env: BuildStepEnv;
24
21
  }): DetachedProcessHandle;
25
- export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }: {
22
+ export declare function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }: {
23
+ baseDomain: string;
26
24
  env: BuildStepEnv;
27
25
  logger: bunyan;
28
26
  timeoutMs: number;
@@ -30,17 +28,13 @@ export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }
30
28
  previewUrl: string;
31
29
  streamUrl: string;
32
30
  }>;
33
- export declare function ensureCloudflaredInstalledAsync({ runtimePlatform, env, logger, }: {
34
- runtimePlatform: BuildRuntimePlatform;
35
- env: BuildStepEnv;
31
+ export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, logger, }: {
32
+ port: number;
33
+ subdomainPrefix: string;
34
+ baseDomain: string;
35
+ authtoken: string;
36
36
  logger: bunyan;
37
37
  }): Promise<string>;
38
- export declare function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }: {
39
- process: DetachedProcessHandle;
40
- pattern: RegExp;
41
- timeoutMs: number;
42
- description: string;
43
- }): Promise<string>;
44
38
  export declare function waitForFileAsync<T>({ filePath, timeoutMs, description, parse, }: {
45
39
  filePath: string;
46
40
  timeoutMs: number;
@@ -1,25 +1,56 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
40
+ exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
41
+ exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
7
42
  exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
8
- exports.ensureBrewPackageInstalledAsync = ensureBrewPackageInstalledAsync;
9
43
  exports.spawnDetached = spawnDetached;
10
44
  exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
11
- exports.ensureCloudflaredInstalledAsync = ensureCloudflaredInstalledAsync;
12
- exports.waitForMatchInOutputAsync = waitForMatchInOutputAsync;
45
+ exports.startNgrokTunnelAsync = startNgrokTunnelAsync;
13
46
  exports.waitForFileAsync = waitForFileAsync;
14
47
  const eas_build_job_1 = require("@expo/eas-build-job");
15
- const steps_1 = require("@expo/steps");
16
48
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
49
+ const ngrok = __importStar(require("@ngrok/ngrok"));
17
50
  const gql_tada_1 = require("gql.tada");
51
+ const node_crypto_1 = require("node:crypto");
18
52
  const node_fs_1 = __importDefault(require("node:fs"));
19
- const node_os_1 = __importDefault(require("node:os"));
20
53
  const retry_1 = require("../../utils/retry");
21
- const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
22
- const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
23
54
  const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
24
55
  mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
25
56
  deviceRunSession {
@@ -37,11 +68,29 @@ function getDeviceRunSessionIdOrThrow(env) {
37
68
  const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
38
69
  if (!deviceRunSessionId) {
39
70
  throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
40
- 'This step must run as part of a device run session created by the API server, ' +
71
+ 'This step must run as part of a device run session ' +
41
72
  'which injects DEVICE_RUN_SESSION_ID into the job environment.');
42
73
  }
43
74
  return deviceRunSessionId;
44
75
  }
76
+ function getNgrokTunnelDomainOrThrow(env) {
77
+ const baseDomain = env.EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN;
78
+ if (!baseDomain) {
79
+ throw new eas_build_job_1.SystemError('EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN is not set. ' +
80
+ 'This step must run as part of a device run session ' +
81
+ 'which injects EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN into the job environment.');
82
+ }
83
+ return baseDomain;
84
+ }
85
+ function getNgrokAuthtokenOrThrow(env) {
86
+ const authtoken = env.NGROK_AUTHTOKEN;
87
+ if (!authtoken) {
88
+ throw new eas_build_job_1.SystemError('NGROK_AUTHTOKEN is not set. ' +
89
+ 'This step must run as part of a device run session ' +
90
+ 'which injects NGROK_AUTHTOKEN into the job environment.');
91
+ }
92
+ return authtoken;
93
+ }
45
94
  async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
46
95
  logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
47
96
  const result = await ctx.graphqlClient
@@ -51,9 +100,6 @@ async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteC
51
100
  throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
52
101
  }
53
102
  }
54
- async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
55
- await (0, turtle_spawn_1.default)('bash', ['-c', `command -v ${name} >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install ${name}`], { env, logger });
56
- }
57
103
  function spawnDetached({ command, args, cwd, env, }) {
58
104
  const promise = (0, turtle_spawn_1.default)(command, args, {
59
105
  cwd,
@@ -73,19 +119,23 @@ function spawnDetached({ command, args, cwd, env, }) {
73
119
  promise.child.stderr?.on('data', appendChunk);
74
120
  return { getOutput: () => output };
75
121
  }
76
- async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
122
+ async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }) {
77
123
  logger.info('Launching serve-sim with tunnel.');
78
124
  const serveSim = spawnDetached({
79
125
  command: 'npx',
80
126
  args: [
81
127
  'serve-sim-szdziedzic@latest',
82
128
  '--tunnel',
83
- '--tunnel-protocol',
84
- 'quic',
129
+ '--tunnel-provider',
130
+ 'ngrok',
131
+ '--tunnel-domain',
132
+ baseDomain,
85
133
  '--stream-max-dimension',
86
134
  '1280',
87
135
  '--stream-quality',
88
136
  '0.55',
137
+ '--codec',
138
+ 'webrtc',
89
139
  ],
90
140
  env,
91
141
  });
@@ -93,8 +143,8 @@ async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
93
143
  const deadline = Date.now() + timeoutMs;
94
144
  while (Date.now() < deadline) {
95
145
  const output = serveSim.getOutput();
96
- const previewUrl = matchLabeledUrl(output, 'Tunnel');
97
- const streamUrl = matchLabeledUrl(output, 'Stream');
146
+ const previewUrl = matchLabeledUrl({ output, label: 'Tunnel', baseDomain });
147
+ const streamUrl = matchLabeledUrl({ output, label: 'Stream', baseDomain });
98
148
  if (previewUrl && streamUrl) {
99
149
  return { previewUrl, streamUrl };
100
150
  }
@@ -102,59 +152,25 @@ async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
102
152
  }
103
153
  throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim to report Tunnel and Stream URLs. Last output:\n${serveSim.getOutput() || '<empty>'}`);
104
154
  }
105
- function matchLabeledUrl(content, label) {
106
- const labelPattern = new RegExp(`${label}:\\s*(${TRYCLOUDFLARE_URL_PATTERN.source})`);
107
- const match = labelPattern.exec(content);
155
+ function matchLabeledUrl({ output, label, baseDomain, }) {
156
+ const labelPattern = new RegExp(`${label}:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`);
157
+ const match = labelPattern.exec(output);
108
158
  return match ? match[1] : null;
109
159
  }
110
- async function ensureCloudflaredInstalledAsync({ runtimePlatform, env, logger, }) {
111
- if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
112
- await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
113
- return 'cloudflared';
114
- }
115
- if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
116
- return 'cloudflared';
117
- }
118
- const cloudflaredArch = cloudflaredLinuxArchForNodeArch(node_os_1.default.arch());
119
- const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cloudflaredArch}`;
120
- logger.info(`Downloading cloudflared from ${downloadUrl} to ${CLOUDFLARED_LINUX_INSTALL_PATH}.`);
121
- await (0, turtle_spawn_1.default)('sudo', ['curl', '-fsSL', '-o', CLOUDFLARED_LINUX_INSTALL_PATH, downloadUrl], {
122
- env,
123
- logger,
124
- });
125
- await (0, turtle_spawn_1.default)('sudo', ['chmod', '+x', CLOUDFLARED_LINUX_INSTALL_PATH], { env, logger });
126
- // Return the absolute install path so the tunnel command works even when
127
- // /usr/local/bin is not on the step's PATH.
128
- return CLOUDFLARED_LINUX_INSTALL_PATH;
129
- }
130
- function cloudflaredLinuxArchForNodeArch(arch) {
131
- if (arch === 'x64') {
132
- return 'amd64';
133
- }
134
- if (arch === 'arm64') {
135
- return 'arm64';
136
- }
137
- throw new eas_build_job_1.SystemError(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
138
- }
139
- async function isCommandAvailableAsync({ command, env, }) {
140
- try {
141
- await (0, turtle_spawn_1.default)('bash', ['-c', `command -v ${command}`], { env, ignoreStdio: true });
142
- return true;
143
- }
144
- catch {
145
- return false;
160
+ async function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, logger, }) {
161
+ const domain = `${subdomainPrefix}-${(0, node_crypto_1.randomBytes)(8).toString('hex')}.${baseDomain}`;
162
+ logger.info(`Starting ngrok tunnel ${domain} -> http://localhost:${port}.`);
163
+ // Run the ngrok agent in-process via the SDK; it keeps the session alive until
164
+ // the process exits, and the step blocks forever to hold it open.
165
+ const listener = await ngrok.forward({ addr: port, authtoken, domain });
166
+ const url = listener.url();
167
+ if (!url) {
168
+ throw new eas_build_job_1.SystemError(`ngrok tunnel for ${domain} did not return a public URL.`);
146
169
  }
170
+ return url;
147
171
  }
148
- async function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }) {
149
- const deadline = Date.now() + timeoutMs;
150
- while (Date.now() < deadline) {
151
- const match = pattern.exec(process.getOutput());
152
- if (match) {
153
- return match[1] ?? match[0];
154
- }
155
- await (0, retry_1.sleepAsync)(1_000);
156
- }
157
- throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`);
172
+ function escapeRegExp(value) {
173
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
158
174
  }
159
175
  async function waitForFileAsync({ filePath, timeoutMs, description, parse, }) {
160
176
  const deadline = Date.now() + timeoutMs;
@@ -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;
@@ -49,8 +49,9 @@ var IosSimulatorUtils;
49
49
  }
50
50
  IosSimulatorUtils.startAsync = startAsync;
51
51
  async function waitForReadyAsync({ udid, env, }) {
52
+ const readinessScreenshotPath = node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-simulator-readiness.png');
52
53
  await (0, retry_1.retryAsync)(async () => {
53
- await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'screenshot', '/dev/null'], {
54
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'screenshot', readinessScreenshotPath], {
54
55
  env,
55
56
  });
56
57
  }, {
@@ -60,6 +61,7 @@ var IosSimulatorUtils;
60
61
  retryIntervalMs: 1_000,
61
62
  },
62
63
  });
64
+ await node_fs_1.default.promises.rm(readinessScreenshotPath, { force: true });
63
65
  // Wait for data migration to complete before declaring the simulator ready
64
66
  // Based on WebKit's approach: https://trac.webkit.org/changeset/231452/webkit
65
67
  await (0, retry_1.retryAsync)(async () => {
@@ -75,6 +77,11 @@ var IosSimulatorUtils;
75
77
  });
76
78
  }
77
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;
78
85
  async function collectLogsAsync({ deviceIdentifier, env, }) {
79
86
  const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-simulator-logs-'));
80
87
  const outputPath = node_path_1.default.join(outputDir, `${deviceIdentifier}.logarchive`);
@@ -13,21 +13,15 @@ const fast_glob_1 = __importDefault(require("fast-glob"));
13
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const promise_limit_1 = __importDefault(require("promise-limit"));
16
- const sentry_1 = require("../sentry");
17
16
  class FindArtifactsError extends Error {
18
17
  }
19
18
  exports.FindArtifactsError = FindArtifactsError;
20
19
  async function findArtifacts({ rootDir, patternOrPath, logger, }) {
21
- const files = path_1.default.isAbsolute(patternOrPath)
20
+ const files = path_1.default.isAbsolute(patternOrPath) && !fast_glob_1.default.isDynamicPattern(patternOrPath)
22
21
  ? (await fs_extra_1.default.pathExists(patternOrPath))
23
22
  ? [patternOrPath]
24
23
  : []
25
24
  : await (0, fast_glob_1.default)(patternOrPath, { cwd: rootDir, onlyFiles: false });
26
- await maybeReportAbsoluteGlobDryRunMismatchAsync({
27
- rootDir,
28
- patternOrPath,
29
- files,
30
- });
31
25
  if (files.length === 0) {
32
26
  if (fast_glob_1.default.isDynamicPattern(patternOrPath)) {
33
27
  throw new FindArtifactsError(`There are no files matching pattern "${patternOrPath}"`);
@@ -50,60 +44,6 @@ async function findArtifacts({ rootDir, patternOrPath, logger, }) {
50
44
  return path_1.default.join(rootDir, filePath);
51
45
  });
52
46
  }
53
- async function findArtifactsWithAbsoluteGlobSupportDryRunAsync(rootDir, patternOrPath) {
54
- return path_1.default.isAbsolute(patternOrPath) && !fast_glob_1.default.isDynamicPattern(patternOrPath)
55
- ? (await fs_extra_1.default.pathExists(patternOrPath))
56
- ? [patternOrPath]
57
- : []
58
- : await (0, fast_glob_1.default)(patternOrPath, { cwd: rootDir, onlyFiles: false });
59
- }
60
- async function maybeReportAbsoluteGlobDryRunMismatchAsync({ rootDir, patternOrPath, files, }) {
61
- if (!path_1.default.isAbsolute(patternOrPath)) {
62
- return;
63
- }
64
- try {
65
- const filesWithAbsoluteGlobSupport = await findArtifactsWithAbsoluteGlobSupportDryRunAsync(rootDir, patternOrPath);
66
- if (areArtifactListsEqual(files, filesWithAbsoluteGlobSupport)) {
67
- return;
68
- }
69
- sentry_1.Sentry.capture(new Error('findArtifacts output changed for an absolute path'), {
70
- tags: {
71
- source: 'find-artifacts',
72
- reason: 'absolute_path',
73
- },
74
- extras: {
75
- rootDir,
76
- patternOrPath,
77
- currentCount: files.length,
78
- dryRunCount: filesWithAbsoluteGlobSupport.length,
79
- currentSample: files.slice(0, 20),
80
- dryRunSample: filesWithAbsoluteGlobSupport.slice(0, 20),
81
- },
82
- });
83
- }
84
- catch (err) {
85
- sentry_1.Sentry.capture(err, {
86
- tags: {
87
- source: 'find-artifacts',
88
- reason: 'absolute_path_dry_run_failed',
89
- },
90
- extras: {
91
- rootDir,
92
- patternOrPath,
93
- currentCount: files.length,
94
- currentSample: files.slice(0, 20),
95
- },
96
- });
97
- }
98
- }
99
- function areArtifactListsEqual(first, second) {
100
- if (first.length !== second.length) {
101
- return false;
102
- }
103
- const sortedFirst = [...first].sort();
104
- const sortedSecond = [...second].sort();
105
- return sortedFirst.every((artifactPath, index) => artifactPath === sortedSecond[index]);
106
- }
107
47
  async function logMissingFileError(artifactPath, buildLogger) {
108
48
  let currentPath = artifactPath;
109
49
  while (!(await fs_extra_1.default.pathExists(currentPath))) {
@@ -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": "19.1.0",
3
+ "version": "20.1.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -37,19 +37,20 @@
37
37
  "dependencies": {
38
38
  "@expo/config": "55.0.10",
39
39
  "@expo/config-plugins": "55.0.7",
40
- "@expo/downloader": "19.0.0",
41
- "@expo/eas-build-job": "19.1.0",
40
+ "@expo/downloader": "20.0.0",
41
+ "@expo/eas-build-job": "20.1.0",
42
42
  "@expo/env": "^0.4.0",
43
- "@expo/logger": "19.0.0",
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": "19.1.0",
49
- "@expo/template-file": "19.0.0",
50
- "@expo/turtle-spawn": "19.0.0",
48
+ "@expo/steps": "20.1.0",
49
+ "@expo/template-file": "20.0.0",
50
+ "@expo/turtle-spawn": "20.0.0",
51
51
  "@expo/xcpretty": "^4.3.1",
52
52
  "@google-cloud/storage": "^7.11.2",
53
+ "@ngrok/ngrok": "1.7.0",
53
54
  "@sentry/node": "7.77.0",
54
55
  "@urql/core": "^6.0.1",
55
56
  "bplist-parser": "0.3.2",
@@ -99,5 +100,5 @@
99
100
  "typescript": "^5.5.4",
100
101
  "uuid": "^9.0.1"
101
102
  },
102
- "gitHead": "27a2e2abc752aa0123075958651a796fe045d601"
103
+ "gitHead": "33225d2b73a34db5cbdfeaa537aedb6c6b4a84af"
103
104
  }