@expo/build-tools 19.0.6 → 20.0.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.
@@ -166,13 +166,16 @@ async function buildAsync(ctx) {
166
166
  }
167
167
  try {
168
168
  const { derivedDataPath, workspacePath } = (0, nullthrows_1.default)(fastlaneResult);
169
- await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
169
+ const { skipped } = await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
170
170
  derivedDataPath,
171
171
  workspacePath,
172
172
  logger: ctx.logger,
173
173
  proxyBaseUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
174
174
  env: ctx.env,
175
175
  });
176
+ if (skipped) {
177
+ ctx.markBuildPhaseSkipped();
178
+ }
176
179
  }
177
180
  catch (err) {
178
181
  sentry_1.Sentry.capture('Failed to parse xcactivitylog', err);
@@ -28,6 +28,10 @@ async function runEasBuildInternalAsync({ job, logger, env, cwd, projectRootOver
28
28
  else if (githubTriggerOptions?.autoSubmit) {
29
29
  autoSubmitArgs.push('--auto-submit');
30
30
  }
31
+ const refreshAdHocProvisioningProfileArgs = [];
32
+ if (job.platform === eas_build_job_1.Platform.IOS && job.refreshAdHocProvisioningProfile === true) {
33
+ refreshAdHocProvisioningProfileArgs.push('--refresh-ad-hoc-provisioning-profile');
34
+ }
31
35
  try {
32
36
  const result = await (0, turtle_spawn_1.default)(cmd, [
33
37
  ...args,
@@ -37,6 +41,7 @@ async function runEasBuildInternalAsync({ job, logger, env, cwd, projectRootOver
37
41
  '--profile',
38
42
  buildProfile,
39
43
  ...autoSubmitArgs,
44
+ ...refreshAdHocProvisioningProfileArgs,
40
45
  ], {
41
46
  cwd,
42
47
  env: {
@@ -106,6 +111,9 @@ function validateEasBuildInternalResult({ oldJob, result, }) {
106
111
  // We want to retain values that we have set on the job.
107
112
  appId: oldJob.appId,
108
113
  initiatingUserId: oldJob.initiatingUserId,
114
+ ...(oldJob.platform === eas_build_job_1.Platform.IOS && oldJob.refreshAdHocProvisioningProfile === true
115
+ ? { refreshAdHocProvisioningProfile: true }
116
+ : null),
109
117
  });
110
118
  (0, assert_1.default)(newJob.platform === oldJob.platform, 'eas-cli returned a job for a wrong platform');
111
119
  const newMetadata = (0, eas_build_job_1.sanitizeMetadata)(value.metadata);
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ExpoConfig } from '@expo/config';
2
- import { BuildPhase, BuildPhaseStats, Env, GenericArtifactType, Job, ManagedArtifactType, Metadata } from '@expo/eas-build-job';
2
+ import { BuildPhase, Env, GenericArtifactType, Job, ManagedArtifactType, Metadata } from '@expo/eas-build-job';
3
3
  import { bunyan } from '@expo/logger';
4
4
  import { Client } from '@urql/core';
5
5
  import { PackageManager } from './utils/packageManager';
@@ -44,7 +44,6 @@ export interface BuildContextOptions {
44
44
  tags?: Record<string, string>;
45
45
  extras?: Record<string, string>;
46
46
  }) => void;
47
- reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
48
47
  skipNativeBuild?: boolean;
49
48
  metadata?: Metadata;
50
49
  expoApiV2BaseUrl?: string;
@@ -73,7 +72,6 @@ export declare class BuildContext<TJob extends Job = Job> {
73
72
  private buildPhaseSkipped;
74
73
  private buildPhaseHasWarnings;
75
74
  private _appConfig?;
76
- private readonly reportBuildPhaseStats?;
77
75
  readonly graphqlClient: Client;
78
76
  constructor(job: TJob, options: BuildContextOptions);
79
77
  get job(): TJob;
package/dist/context.js CHANGED
@@ -36,7 +36,6 @@ class BuildContext {
36
36
  buildPhaseSkipped = false;
37
37
  buildPhaseHasWarnings = false;
38
38
  _appConfig;
39
- reportBuildPhaseStats;
40
39
  graphqlClient;
41
40
  constructor(job, options) {
42
41
  this.workingdir = options.workingdir;
@@ -50,7 +49,6 @@ class BuildContext {
50
49
  this._metadata = options.metadata;
51
50
  this.skipNativeBuild = options.skipNativeBuild;
52
51
  this.expoApiV2BaseUrl = options.expoApiV2BaseUrl;
53
- this.reportBuildPhaseStats = options.reportBuildPhaseStats;
54
52
  const environmentSecrets = this.getEnvironmentSecrets(job);
55
53
  this._env = {
56
54
  ...options.env,
@@ -235,7 +233,6 @@ class BuildContext {
235
233
  return;
236
234
  }
237
235
  await this.collectAndUpdateEnvVariablesAsync();
238
- this.reportBuildPhaseStats?.({ buildPhase: this.buildPhase, result, durationMs });
239
236
  if (this.job.platform) {
240
237
  datadog_1.Datadog.distribution('eas.build.phase_duration', durationMs, {
241
238
  build_phase: this.buildPhase.toLowerCase(),
package/dist/datadog.d.ts CHANGED
@@ -6,6 +6,7 @@ type DatadogSetupOptions = {
6
6
  export declare const Datadog: {
7
7
  setup(opts: DatadogSetupOptions | null): void;
8
8
  distribution(name: string, value: number, tags?: Record<string, string>): void;
9
+ log(message: string, tags?: Record<string, string>): void;
9
10
  flushAsync(): Promise<void>;
10
11
  };
11
12
  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;
@@ -33,9 +33,32 @@ exports.Datadog = {
33
33
  extras: { metrics },
34
34
  });
35
35
  });
36
- pendingMetricUploads.push(uploadPromise);
36
+ pendingUploads.push(uploadPromise);
37
+ },
38
+ log(message, tags = {}) {
39
+ if (!setupOptions) {
40
+ return;
41
+ }
42
+ const { expoApiV2BaseUrl, turtleBuildId, robotAccessToken } = setupOptions;
43
+ const log = {
44
+ buildId: turtleBuildId,
45
+ message,
46
+ tags,
47
+ };
48
+ const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL('turtle-builds/logs', expoApiV2BaseUrl).toString(), 'POST', {
49
+ json: log,
50
+ headers: {
51
+ Authorization: `Bearer ${robotAccessToken}`,
52
+ },
53
+ shouldThrowOnNotOk: false,
54
+ }).catch(err => {
55
+ sentry_1.Sentry.capture('Failed to report turtle build log', err, {
56
+ extras: { log },
57
+ });
58
+ });
59
+ pendingUploads.push(uploadPromise);
37
60
  },
38
61
  async flushAsync() {
39
- await Promise.allSettled(pendingMetricUploads);
62
+ await Promise.allSettled(pendingUploads);
40
63
  },
41
64
  };
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,
@@ -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,11 +89,21 @@ 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.');
69
96
  const msg = `Build performance analysis failed during "${phase}"`;
70
- sentry_1.Sentry.capture(msg, err, { tags: { phase } });
97
+ sentry_1.Sentry.capture(msg, err, {
98
+ tags: { phase },
99
+ extras: {
100
+ exitStatus: err?.status,
101
+ signal: err?.signal,
102
+ stderr: err?.stderr?.slice(-4000),
103
+ stdout: err?.stdout?.slice(-4000),
104
+ },
105
+ });
106
+ return { skipped: true };
71
107
  }
72
108
  finally {
73
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
+ 'h264',
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;
@@ -1 +1 @@
1
- export declare const FastfileResignTemplate = "lane :do_resign do\n resign(\n ipa: \"<%- IPA_PATH %>\",\n signing_identity: \"<%- SIGNING_IDENTITY %>\",\n provisioning_profile: {<% _.forEach(PROFILES, function(profile) { %>\n \"<%- profile.BUNDLE_ID %>\" => \"<%- profile.PATH %>\",<% }); %>\n },\n keychain_path: \"<%- KEYCHAIN_PATH %>\"\n )\nend\n";
1
+ export declare const FastfileResignTemplate = "lane :do_resign do\n resign(\n ipa: \"<%- IPA_PATH %>\",\n signing_identity: \"<%- SIGNING_IDENTITY %>\",\n provisioning_profile: {<% _.forEach(PROFILES, function(profile) { %>\n \"<%- profile.BUNDLE_ID %>\" => \"<%- profile.PATH %>\",<% }); %>\n },\n use_app_entitlements: true,\n keychain_path: \"<%- KEYCHAIN_PATH %>\"\n )\nend\n";
@@ -8,6 +8,7 @@ exports.FastfileResignTemplate = `lane :do_resign do
8
8
  provisioning_profile: {<% _.forEach(PROFILES, function(profile) { %>
9
9
  "<%- profile.BUNDLE_ID %>" => "<%- profile.PATH %>",<% }); %>
10
10
  },
11
+ use_app_entitlements: true,
11
12
  keychain_path: "<%- KEYCHAIN_PATH %>"
12
13
  )
13
14
  end
@@ -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 () => {
@@ -17,7 +17,7 @@ class FindArtifactsError extends Error {
17
17
  }
18
18
  exports.FindArtifactsError = FindArtifactsError;
19
19
  async function findArtifacts({ rootDir, patternOrPath, logger, }) {
20
- const files = path_1.default.isAbsolute(patternOrPath)
20
+ const files = path_1.default.isAbsolute(patternOrPath) && !fast_glob_1.default.isDynamicPattern(patternOrPath)
21
21
  ? (await fs_extra_1.default.pathExists(patternOrPath))
22
22
  ? [patternOrPath]
23
23
  : []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "19.0.6",
3
+ "version": "20.0.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.0.0",
40
+ "@expo/downloader": "20.0.0",
41
+ "@expo/eas-build-job": "20.0.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.0.0",
49
- "@expo/template-file": "19.0.0",
50
- "@expo/turtle-spawn": "19.0.0",
48
+ "@expo/steps": "20.0.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": "1e9a0981a5a707543d7686f71540aa5d8f2d098b"
103
+ "gitHead": "890387b3b62c2ed2a946ec0e1197ff69f3e29342"
103
104
  }