@expo/build-tools 18.12.0 → 18.12.3

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.
package/dist/index.d.ts CHANGED
@@ -7,3 +7,4 @@ export { PackageManager } from './utils/packageManager';
7
7
  export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
8
8
  export { Hook, runHookIfPresent } from './utils/hooks';
9
9
  export * from './generic';
10
+ export { Sentry } from './sentry';
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.GCSLoggerStream = exports.GCS = exports.Builders = void 0;
42
+ exports.Sentry = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.GCSLoggerStream = exports.GCS = exports.Builders = void 0;
43
43
  const Builders = __importStar(require("./builders"));
44
44
  exports.Builders = Builders;
45
45
  const LoggerStream_1 = __importDefault(require("./gcs/LoggerStream"));
@@ -57,3 +57,5 @@ var hooks_1 = require("./utils/hooks");
57
57
  Object.defineProperty(exports, "Hook", { enumerable: true, get: function () { return hooks_1.Hook; } });
58
58
  Object.defineProperty(exports, "runHookIfPresent", { enumerable: true, get: function () { return hooks_1.runHookIfPresent; } });
59
59
  __exportStar(require("./generic"), exports);
60
+ var sentry_1 = require("./sentry");
61
+ Object.defineProperty(exports, "Sentry", { enumerable: true, get: function () { return sentry_1.Sentry; } });
@@ -0,0 +1,19 @@
1
+ import * as sentryNode from '@sentry/node';
2
+ type CaptureOptions = {
3
+ tags?: Record<string, string>;
4
+ extras?: Record<string, unknown>;
5
+ level?: sentryNode.SeverityLevel;
6
+ };
7
+ type SentryAPI = {
8
+ setup(opts: {
9
+ dsn: string | null;
10
+ environment: string;
11
+ tags?: Record<string, string>;
12
+ }): void;
13
+ capture(msg: string, options?: CaptureOptions): void;
14
+ capture(msg: string, err: Error | undefined, options?: CaptureOptions): void;
15
+ capture(err: Error, options?: CaptureOptions): void;
16
+ flush(timeoutMs?: number): Promise<boolean>;
17
+ };
18
+ export declare const Sentry: SentryAPI;
19
+ export {};
package/dist/sentry.js ADDED
@@ -0,0 +1,102 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Sentry = void 0;
37
+ const sentryNode = __importStar(require("@sentry/node"));
38
+ exports.Sentry = {
39
+ setup({ dsn, environment, tags }) {
40
+ if (dsn) {
41
+ sentryNode.init({
42
+ dsn,
43
+ environment,
44
+ ...(tags ? { initialScope: { tags } } : {}),
45
+ });
46
+ }
47
+ },
48
+ capture(arg1, arg2, arg3) {
49
+ let msg;
50
+ let err;
51
+ let options = {};
52
+ if (arg1 instanceof Error) {
53
+ err = arg1;
54
+ options = arg2 ?? {};
55
+ }
56
+ else {
57
+ msg = arg1;
58
+ if (arg3 !== undefined) {
59
+ // 3-arg form: arg2 unambiguously means err (null/undefined → no err)
60
+ options = arg3;
61
+ if (arg2 !== undefined && arg2 !== null) {
62
+ err = arg2 instanceof Error ? arg2 : new Error(String(arg2));
63
+ }
64
+ }
65
+ else if (arg2 instanceof Error) {
66
+ err = arg2;
67
+ }
68
+ else if (arg2 !== undefined && arg2 !== null) {
69
+ // 2-arg form: non-Error object → options; primitive → coerced err
70
+ if (typeof arg2 === 'object') {
71
+ options = arg2;
72
+ }
73
+ else {
74
+ err = new Error(String(arg2));
75
+ }
76
+ }
77
+ }
78
+ sentryNode.withScope(scope => {
79
+ if (options.tags) {
80
+ scope.setTags(options.tags);
81
+ }
82
+ if (options.extras) {
83
+ scope.setExtras(options.extras);
84
+ }
85
+ if (options.level) {
86
+ scope.setLevel(options.level);
87
+ }
88
+ if (err) {
89
+ if (msg && err.message !== msg) {
90
+ scope.setExtra('message', msg);
91
+ }
92
+ sentryNode.captureException(err);
93
+ }
94
+ else if (msg) {
95
+ sentryNode.captureMessage(msg);
96
+ }
97
+ });
98
+ },
99
+ flush(timeoutMs = 2000) {
100
+ return sentryNode.flush(timeoutMs);
101
+ },
102
+ };
@@ -41,6 +41,7 @@ const startAgentDeviceRemoteSession_1 = require("./functions/startAgentDeviceRem
41
41
  const startAndroidEmulator_1 = require("./functions/startAndroidEmulator");
42
42
  const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
43
43
  const startIosSimulator_1 = require("./functions/startIosSimulator");
44
+ const startServeSimRemoteSession_1 = require("./functions/startServeSimRemoteSession");
44
45
  const uploadArtifact_1 = require("./functions/uploadArtifact");
45
46
  const uploadToAsc_1 = require("./functions/uploadToAsc");
46
47
  const useNpmToken_1 = require("./functions/useNpmToken");
@@ -79,6 +80,7 @@ function getEasFunctions(ctx) {
79
80
  (0, startAndroidEmulator_1.createStartAndroidEmulatorBuildFunction)(),
80
81
  (0, startCuttlefishDevice_1.createStartCuttlefishDeviceBuildFunction)(),
81
82
  (0, startIosSimulator_1.createStartIosSimulatorBuildFunction)(),
83
+ (0, startServeSimRemoteSession_1.createStartServeSimRemoteSessionBuildFunction)(ctx),
82
84
  (0, installMaestro_1.createInstallMaestroBuildFunction)(),
83
85
  (0, installPods_1.createInstallPodsBuildFunction)(),
84
86
  (0, sendSlackMessage_1.createSendSlackMessageFunction)(),
@@ -4,37 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDeviceRemoteSessionBuildFunction;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
8
  const steps_1 = require("@expo/steps");
8
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
- const gql_tada_1 = require("gql.tada");
10
- const node_child_process_1 = __importDefault(require("node:child_process"));
11
10
  const node_fs_1 = __importDefault(require("node:fs"));
12
11
  const node_os_1 = __importDefault(require("node:os"));
13
12
  const node_path_1 = __importDefault(require("node:path"));
14
13
  const retry_1 = require("../../utils/retry");
15
- const eas_build_job_1 = require("@expo/eas-build-job");
14
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
16
15
  const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
17
16
  const SRC_DIR = '/tmp/agent-device-src';
18
- const RUN_DIR = '/tmp/agent-device';
19
- const DAEMON_LOG = node_path_1.default.join(RUN_DIR, 'daemon.log');
20
- const TUNNEL_LOG = node_path_1.default.join(RUN_DIR, 'cloudflared.log');
21
17
  const DAEMON_JSON_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device', 'daemon.json');
22
18
  const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
23
19
  const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
24
20
  const STARTUP_TIMEOUT_MS = 60_000;
25
- const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
26
- mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
27
- deviceRunSession {
28
- startDeviceRunSession(
29
- deviceRunSessionId: $deviceRunSessionId
30
- remoteConfig: $remoteConfig
31
- ) {
32
- id
33
- status
34
- }
35
- }
36
- }
37
- `);
38
21
  function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
39
22
  return new steps_1.BuildFunction({
40
23
  namespace: 'eas',
@@ -52,17 +35,10 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
52
35
  // Fail fast before any expensive setup if the orchestrator-injected
53
36
  // DEVICE_RUN_SESSION_ID env var is missing — without it we cannot
54
37
  // report the remote config back to the API server.
55
- const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
56
- if (!deviceRunSessionId) {
57
- throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
58
- 'This step must run as part of a device run session created by the API server, ' +
59
- 'which injects DEVICE_RUN_SESSION_ID into the job environment.');
60
- }
38
+ const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
61
39
  const packageVersion = inputs.package_version.value;
62
40
  const { runtimePlatform } = global;
63
41
  logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
64
- logger.info(`Preparing runtime directory at ${RUN_DIR}.`);
65
- await node_fs_1.default.promises.mkdir(RUN_DIR, { recursive: true });
66
42
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
67
43
  logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
68
44
  await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
@@ -83,13 +59,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
83
59
  env,
84
60
  logger,
85
61
  });
86
- logger.info(`Launching agent-device daemon (log file: ${DAEMON_LOG}).`);
87
- await spawnDetachedAsync({
88
- command: 'bun run src/daemon.ts',
62
+ logger.info('Launching agent-device daemon.');
63
+ (0, remoteDeviceRunSession_1.spawnDetached)({
64
+ command: 'bun',
65
+ args: ['run', 'src/daemon.ts'],
89
66
  cwd: SRC_DIR,
90
- logFile: DAEMON_LOG,
91
67
  env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
92
- logger,
93
68
  });
94
69
  logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
95
70
  await waitForFileAsync({
@@ -99,31 +74,42 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
99
74
  });
100
75
  const { port: daemonPort, token: daemonToken } = readDaemonInfo(DAEMON_JSON_PATH);
101
76
  logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
102
- logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort} (log file: ${TUNNEL_LOG}).`);
103
- await spawnDetachedAsync({
104
- command: `${cloudflaredCommand} tunnel --url "http://localhost:${daemonPort}"`,
105
- logFile: TUNNEL_LOG,
77
+ logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort}.`);
78
+ const cloudflared = (0, remoteDeviceRunSession_1.spawnDetached)({
79
+ command: cloudflaredCommand,
80
+ args: ['tunnel', '--url', `http://localhost:${daemonPort}`],
106
81
  env,
107
- logger,
108
82
  });
109
83
  logger.info('Waiting for a public tunnel URL.');
110
- const tunnelUrl = await waitForMatchInLogAsync({
111
- logFile: TUNNEL_LOG,
84
+ const agentDeviceRemoteSessionUrl = await waitForMatchInOutputAsync({
85
+ process: cloudflared,
112
86
  pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
113
87
  timeoutMs: STARTUP_TIMEOUT_MS,
114
88
  description: 'cloudflared tunnel',
115
89
  });
116
- logger.info(`Tunnel is ready at ${tunnelUrl}.`);
117
- logger.info(`Reporting agent-device remote config to the API server (device run session: ${deviceRunSessionId}).`);
118
- const result = await ctx.graphqlClient
119
- .mutation(START_DEVICE_RUN_SESSION_MUTATION, {
120
- deviceRunSessionId,
121
- remoteConfig: { url: tunnelUrl, token: daemonToken },
122
- })
123
- .toPromise();
124
- if (result.error) {
125
- throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
90
+ logger.info(`Tunnel is ready at ${agentDeviceRemoteSessionUrl}.`);
91
+ // serve-sim is iOS-only only launch it (and report a webPreviewUrl)
92
+ // on Darwin. Android sessions go without a preview URL.
93
+ let webPreviewUrl;
94
+ if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
95
+ const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
96
+ env,
97
+ logger,
98
+ timeoutMs: STARTUP_TIMEOUT_MS,
99
+ });
100
+ webPreviewUrl = previewUrl;
101
+ logger.info(`Web preview URL: ${webPreviewUrl}`);
126
102
  }
103
+ await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
104
+ ctx,
105
+ deviceRunSessionId,
106
+ remoteConfig: {
107
+ agentDeviceRemoteSessionUrl,
108
+ agentDeviceRemoteSessionToken: daemonToken,
109
+ ...(webPreviewUrl ? { webPreviewUrl } : {}),
110
+ },
111
+ logger,
112
+ });
127
113
  logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
128
114
  // Keep the turtle job alive so the daemon and tunnel stay reachable
129
115
  // until stopDeviceRunSession cancels the run.
@@ -133,7 +119,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
133
119
  }
134
120
  async function ensureCloudflaredInstalledAsync({ runtimePlatform, env, logger, }) {
135
121
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
136
- await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
122
+ await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
137
123
  return 'cloudflared';
138
124
  }
139
125
  if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
@@ -158,10 +144,7 @@ function cloudflaredLinuxArchForNodeArch(arch) {
158
144
  if (arch === 'arm64') {
159
145
  return 'arm64';
160
146
  }
161
- throw new Error(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
162
- }
163
- async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
164
- 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 });
147
+ throw new eas_build_job_1.SystemError(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
165
148
  }
166
149
  async function isCommandAvailableAsync({ command, env, }) {
167
150
  try {
@@ -179,40 +162,16 @@ async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
179
162
  logger,
180
163
  });
181
164
  }
182
- async function spawnDetachedAsync({ command, cwd, logFile, env, logger, }) {
183
- // Launch the process fully detached so this function returns immediately and
184
- // the grandchild survives the step. Stdio goes to a log file so the daemon
185
- // output can be polled, and we unref so Node doesn't wait on it.
186
- const fd = node_fs_1.default.openSync(logFile, 'a');
187
- try {
188
- const child = node_child_process_1.default.spawn('bash', ['-c', command], {
189
- cwd,
190
- env,
191
- detached: true,
192
- stdio: ['ignore', fd, fd],
193
- });
194
- if (!child.pid) {
195
- throw new Error(`Failed to spawn detached process: ${command}`);
196
- }
197
- child.unref();
198
- logger.info(`Started detached process (pid ${child.pid}).`);
199
- }
200
- finally {
201
- node_fs_1.default.closeSync(fd);
202
- }
203
- }
204
- async function waitForMatchInLogAsync({ logFile, pattern, timeoutMs, description, }) {
165
+ async function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }) {
205
166
  const deadline = Date.now() + timeoutMs;
206
167
  while (Date.now() < deadline) {
207
- const content = await readFileOrEmptyAsync(logFile);
208
- const match = pattern.exec(content);
168
+ const match = pattern.exec(process.getOutput());
209
169
  if (match) {
210
170
  return match[1] ?? match[0];
211
171
  }
212
172
  await (0, retry_1.sleepAsync)(1_000);
213
173
  }
214
- const tail = await readFileOrEmptyAsync(logFile);
215
- throw new Error(`Timed out waiting for ${description} to start. Last log contents:\n${tail || '<empty>'}`);
174
+ throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`);
216
175
  }
217
176
  async function waitForFileAsync({ filePath, timeoutMs, description, }) {
218
177
  const deadline = Date.now() + timeoutMs;
@@ -226,15 +185,7 @@ async function waitForFileAsync({ filePath, timeoutMs, description, }) {
226
185
  }
227
186
  await (0, retry_1.sleepAsync)(1_000);
228
187
  }
229
- throw new Error(`Timed out waiting for ${description} to write ${filePath}.`);
230
- }
231
- async function readFileOrEmptyAsync(filePath) {
232
- try {
233
- return await node_fs_1.default.promises.readFile(filePath, 'utf8');
234
- }
235
- catch {
236
- return '';
237
- }
188
+ throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to write ${filePath}.`);
238
189
  }
239
190
  function readDaemonInfo(filePath) {
240
191
  const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
@@ -243,7 +194,7 @@ function readDaemonInfo(filePath) {
243
194
  typeof parsed !== 'object' ||
244
195
  typeof parsed.httpPort !== 'number' ||
245
196
  typeof parsed.token !== 'string') {
246
- throw new Error(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
197
+ throw new eas_build_job_1.SystemError(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
247
198
  }
248
199
  const { httpPort, token } = parsed;
249
200
  return { port: httpPort, token };
@@ -0,0 +1,3 @@
1
+ import { BuildFunction } from '@expo/steps';
2
+ import { CustomBuildContext } from '../../customBuildContext';
3
+ export declare function createStartServeSimRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
@@ -0,0 +1,45 @@
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.createStartServeSimRemoteSessionBuildFunction = createStartServeSimRemoteSessionBuildFunction;
7
+ const steps_1 = require("@expo/steps");
8
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
10
+ const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
11
+ const STARTUP_TIMEOUT_MS = 60_000;
12
+ function createStartServeSimRemoteSessionBuildFunction(ctx) {
13
+ return new steps_1.BuildFunction({
14
+ namespace: 'eas',
15
+ id: 'start_serve_sim_remote_session',
16
+ name: 'Start serve-sim remote session',
17
+ __metricsId: 'eas/start_serve_sim_remote_session',
18
+ supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
19
+ fn: async ({ logger }, { env }) => {
20
+ const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
21
+ logger.info('Starting serve-sim remote session.');
22
+ logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
23
+ 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
+ const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
27
+ env,
28
+ logger,
29
+ timeoutMs: STARTUP_TIMEOUT_MS,
30
+ });
31
+ logger.info(`Preview URL: ${previewUrl}`);
32
+ logger.info(`Stream URL: ${streamUrl}`);
33
+ await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
34
+ ctx,
35
+ deviceRunSessionId,
36
+ remoteConfig: { previewUrl, streamUrl },
37
+ logger,
38
+ });
39
+ logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
40
+ // Keep the turtle job alive so the serve-sim tunnel stays reachable
41
+ // until stopDeviceRunSession cancels the run.
42
+ await new Promise(() => { });
43
+ },
44
+ });
45
+ }
@@ -1,11 +1,9 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { z } from 'zod';
3
3
  /**
4
- * Download xclogparser, parse xcactivitylog from derived data, and log a
5
- * compile metrics report. Never throws all errors are logged at debug level.
6
- *
7
- * Can be called from both the step-based flow (BuildFunction) and the
8
- * traditional builder flow (runBuildPhase).
4
+ * Never throws best-effort observability that does not affect build status.
5
+ * Failures route to Sentry via `Sentry.capture` for engineering triage;
6
+ * users see only a generic skip message.
9
7
  */
10
8
  export declare function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, }: {
11
9
  derivedDataPath: string;
@@ -15,33 +15,38 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
15
15
  const os_1 = __importDefault(require("os"));
16
16
  const path_1 = __importDefault(require("path"));
17
17
  const zod_1 = require("zod");
18
+ const sentry_1 = require("../../../sentry");
18
19
  const DEFAULT_XCLOGPARSER_VERSION = 'v0.2.47';
19
20
  const XCLOGPARSER_DOWNLOAD_URL = 'https://storage.googleapis.com/turtle-v2/xclogparser';
20
21
  const XCLOGPARSER_DOWNLOAD_TIMEOUT_MS = 20_000;
21
22
  const XCLOGPARSER_OUTPUT_FILENAME = 'xcactivitylog.json';
22
23
  /**
23
- * Download xclogparser, parse xcactivitylog from derived data, and log a
24
- * compile metrics report. Never throws all errors are logged at debug level.
25
- *
26
- * Can be called from both the step-based flow (BuildFunction) and the
27
- * traditional builder flow (runBuildPhase).
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.
28
27
  */
29
28
  async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion = DEFAULT_XCLOGPARSER_VERSION, logger, proxyBaseUrl, }) {
30
29
  let tempDir;
30
+ let phase = 'creating_temp_directory';
31
31
  try {
32
32
  tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'xclogparser-'));
33
+ phase = 'downloading_xclogparser';
33
34
  const xclogparserPath = await downloadXclogparser(tempDir, xclogparserVersion, logger, proxyBaseUrl);
35
+ phase = 'running_xclogparser';
34
36
  const jsonOutputPath = await runXclogparser({
35
37
  binaryPath: xclogparserPath,
36
38
  derivedDataPath,
37
39
  workspacePath,
38
40
  outputDir: tempDir,
39
41
  });
42
+ phase = 'parsing_xclogparser_output';
40
43
  const data = XcactivitylogDataSchemaZ.parse(JSON.parse(await fs_extra_1.default.readFile(jsonOutputPath, 'utf8')));
41
44
  logger.info(formatReport(data));
42
45
  }
43
46
  catch (err) {
44
- logger.debug({ err }, 'Failed to analyze build performance; continuing without a report');
47
+ logger.info('Build performance analysis skipped.');
48
+ const msg = `Build performance analysis failed during "${phase}"`;
49
+ sentry_1.Sentry.capture(msg, err, { tags: { phase } });
45
50
  }
46
51
  finally {
47
52
  if (tempDir) {
@@ -0,0 +1,32 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildStepEnv } from '@expo/steps';
3
+ import { CustomBuildContext } from '../../customBuildContext';
4
+ export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
5
+ export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
6
+ ctx: CustomBuildContext;
7
+ deviceRunSessionId: string;
8
+ remoteConfig: Record<string, unknown>;
9
+ logger: bunyan;
10
+ }): Promise<void>;
11
+ export declare function ensureBrewPackageInstalledAsync({ name, env, logger, }: {
12
+ name: string;
13
+ env: BuildStepEnv;
14
+ logger: bunyan;
15
+ }): Promise<void>;
16
+ export type DetachedProcessHandle = {
17
+ getOutput: () => string;
18
+ };
19
+ export declare function spawnDetached({ command, args, cwd, env, }: {
20
+ command: string;
21
+ args: string[];
22
+ cwd?: string;
23
+ env: BuildStepEnv;
24
+ }): DetachedProcessHandle;
25
+ export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }: {
26
+ env: BuildStepEnv;
27
+ logger: bunyan;
28
+ timeoutMs: number;
29
+ }): Promise<{
30
+ previewUrl: string;
31
+ streamUrl: string;
32
+ }>;
@@ -0,0 +1,93 @@
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.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
7
+ exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
8
+ exports.ensureBrewPackageInstalledAsync = ensureBrewPackageInstalledAsync;
9
+ exports.spawnDetached = spawnDetached;
10
+ exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
11
+ const eas_build_job_1 = require("@expo/eas-build-job");
12
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
13
+ const gql_tada_1 = require("gql.tada");
14
+ const retry_1 = require("../../utils/retry");
15
+ const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
16
+ const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
17
+ mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
18
+ deviceRunSession {
19
+ startDeviceRunSession(
20
+ deviceRunSessionId: $deviceRunSessionId
21
+ remoteConfig: $remoteConfig
22
+ ) {
23
+ id
24
+ status
25
+ }
26
+ }
27
+ }
28
+ `);
29
+ function getDeviceRunSessionIdOrThrow(env) {
30
+ const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
31
+ if (!deviceRunSessionId) {
32
+ throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
33
+ 'This step must run as part of a device run session created by the API server, ' +
34
+ 'which injects DEVICE_RUN_SESSION_ID into the job environment.');
35
+ }
36
+ return deviceRunSessionId;
37
+ }
38
+ async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
39
+ logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
40
+ const result = await ctx.graphqlClient
41
+ .mutation(START_DEVICE_RUN_SESSION_MUTATION, { deviceRunSessionId, remoteConfig })
42
+ .toPromise();
43
+ if (result.error) {
44
+ throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
45
+ }
46
+ }
47
+ async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
48
+ 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 });
49
+ }
50
+ function spawnDetached({ command, args, cwd, env, }) {
51
+ const promise = (0, turtle_spawn_1.default)(command, args, {
52
+ cwd,
53
+ env,
54
+ stdio: ['ignore', 'pipe', 'pipe'],
55
+ detached: true,
56
+ });
57
+ // We don't await the process — it should outlive this step. Failures show
58
+ // up in the captured output; suppress unhandled rejections here.
59
+ promise.catch(() => { });
60
+ promise.child.unref();
61
+ let output = '';
62
+ const appendChunk = (chunk) => {
63
+ output += chunk.toString();
64
+ };
65
+ promise.child.stdout?.on('data', appendChunk);
66
+ promise.child.stderr?.on('data', appendChunk);
67
+ return { getOutput: () => output };
68
+ }
69
+ async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
70
+ logger.info('Launching serve-sim with tunnel.');
71
+ const serveSim = spawnDetached({
72
+ command: 'npx',
73
+ args: ['serve-sim-szdziedzic@latest', '--tunnel'],
74
+ env,
75
+ });
76
+ logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
77
+ const deadline = Date.now() + timeoutMs;
78
+ while (Date.now() < deadline) {
79
+ const output = serveSim.getOutput();
80
+ const previewUrl = matchLabeledUrl(output, 'Tunnel');
81
+ const streamUrl = matchLabeledUrl(output, 'Stream');
82
+ if (previewUrl && streamUrl) {
83
+ return { previewUrl, streamUrl };
84
+ }
85
+ await (0, retry_1.sleepAsync)(1_000);
86
+ }
87
+ 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>'}`);
88
+ }
89
+ function matchLabeledUrl(content, label) {
90
+ const labelPattern = new RegExp(`${label}:\\s*(${TRYCLOUDFLARE_URL_PATTERN.source})`);
91
+ const match = labelPattern.exec(content);
92
+ return match ? match[1] : null;
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "18.12.0",
3
+ "version": "18.12.3",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -50,6 +50,7 @@
50
50
  "@expo/turtle-spawn": "18.5.0",
51
51
  "@expo/xcpretty": "^4.3.1",
52
52
  "@google-cloud/storage": "^7.11.2",
53
+ "@sentry/node": "7.77.0",
53
54
  "@urql/core": "^6.0.1",
54
55
  "bplist-parser": "0.3.2",
55
56
  "fast-glob": "^3.3.2",
@@ -98,5 +99,5 @@
98
99
  "typescript": "^5.5.4",
99
100
  "uuid": "^9.0.1"
100
101
  },
101
- "gitHead": "53d294330de9c63eb792f646c0603224def9a1b0"
102
+ "gitHead": "985cd9d52f4de333cf563cc7fe92f4e2d1e1c8e5"
102
103
  }