@expo/build-tools 18.12.0 → 18.12.1

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.
@@ -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,26 @@ 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 tunnelUrl = 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
90
  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, {
91
+ await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
92
+ ctx,
120
93
  deviceRunSessionId,
121
94
  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}`);
126
- }
95
+ logger,
96
+ });
127
97
  logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
128
98
  // Keep the turtle job alive so the daemon and tunnel stay reachable
129
99
  // until stopDeviceRunSession cancels the run.
@@ -133,7 +103,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
133
103
  }
134
104
  async function ensureCloudflaredInstalledAsync({ runtimePlatform, env, logger, }) {
135
105
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
136
- await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
106
+ await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
137
107
  return 'cloudflared';
138
108
  }
139
109
  if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
@@ -158,10 +128,7 @@ function cloudflaredLinuxArchForNodeArch(arch) {
158
128
  if (arch === 'arm64') {
159
129
  return 'arm64';
160
130
  }
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 });
131
+ throw new eas_build_job_1.SystemError(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
165
132
  }
166
133
  async function isCommandAvailableAsync({ command, env, }) {
167
134
  try {
@@ -179,40 +146,16 @@ async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
179
146
  logger,
180
147
  });
181
148
  }
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, }) {
149
+ async function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }) {
205
150
  const deadline = Date.now() + timeoutMs;
206
151
  while (Date.now() < deadline) {
207
- const content = await readFileOrEmptyAsync(logFile);
208
- const match = pattern.exec(content);
152
+ const match = pattern.exec(process.getOutput());
209
153
  if (match) {
210
154
  return match[1] ?? match[0];
211
155
  }
212
156
  await (0, retry_1.sleepAsync)(1_000);
213
157
  }
214
- const tail = await readFileOrEmptyAsync(logFile);
215
- throw new Error(`Timed out waiting for ${description} to start. Last log contents:\n${tail || '<empty>'}`);
158
+ throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`);
216
159
  }
217
160
  async function waitForFileAsync({ filePath, timeoutMs, description, }) {
218
161
  const deadline = Date.now() + timeoutMs;
@@ -226,15 +169,7 @@ async function waitForFileAsync({ filePath, timeoutMs, description, }) {
226
169
  }
227
170
  await (0, retry_1.sleepAsync)(1_000);
228
171
  }
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
- }
172
+ throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to write ${filePath}.`);
238
173
  }
239
174
  function readDaemonInfo(filePath) {
240
175
  const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
@@ -243,7 +178,7 @@ function readDaemonInfo(filePath) {
243
178
  typeof parsed !== 'object' ||
244
179
  typeof parsed.httpPort !== 'number' ||
245
180
  typeof parsed.token !== 'string') {
246
- throw new Error(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
181
+ throw new eas_build_job_1.SystemError(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
247
182
  }
248
183
  const { httpPort, token } = parsed;
249
184
  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,72 @@
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 eas_build_job_1 = require("@expo/eas-build-job");
8
+ const steps_1 = require("@expo/steps");
9
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
10
+ const retry_1 = require("../../utils/retry");
11
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
12
+ const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
13
+ const STARTUP_TIMEOUT_MS = 60_000;
14
+ const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
15
+ function createStartServeSimRemoteSessionBuildFunction(ctx) {
16
+ return new steps_1.BuildFunction({
17
+ namespace: 'eas',
18
+ id: 'start_serve_sim_remote_session',
19
+ name: 'Start serve-sim remote session',
20
+ __metricsId: 'eas/start_serve_sim_remote_session',
21
+ supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
22
+ fn: async ({ logger }, { env }) => {
23
+ const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
24
+ logger.info('Starting serve-sim remote session.');
25
+ logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
26
+ await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
27
+ logger.info('Ensuring cloudflared is installed.');
28
+ await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
29
+ logger.info('Launching serve-sim with tunnel.');
30
+ const serveSim = (0, remoteDeviceRunSession_1.spawnDetached)({
31
+ command: 'npx',
32
+ args: ['serve-sim-szdziedzic@latest', '--tunnel'],
33
+ env,
34
+ });
35
+ logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
36
+ const { previewUrl, streamUrl } = await waitForServeSimUrlsAsync({
37
+ serveSim,
38
+ timeoutMs: STARTUP_TIMEOUT_MS,
39
+ });
40
+ logger.info(`Preview URL: ${previewUrl}`);
41
+ logger.info(`Stream URL: ${streamUrl}`);
42
+ await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
43
+ ctx,
44
+ deviceRunSessionId,
45
+ remoteConfig: { previewUrl, streamUrl },
46
+ logger,
47
+ });
48
+ logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
49
+ // Keep the turtle job alive so the serve-sim tunnel stays reachable
50
+ // until stopDeviceRunSession cancels the run.
51
+ await new Promise(() => { });
52
+ },
53
+ });
54
+ }
55
+ async function waitForServeSimUrlsAsync({ serveSim, timeoutMs, }) {
56
+ const deadline = Date.now() + timeoutMs;
57
+ while (Date.now() < deadline) {
58
+ const output = serveSim.getOutput();
59
+ const previewUrl = matchLabeledUrl(output, 'Tunnel');
60
+ const streamUrl = matchLabeledUrl(output, 'Stream');
61
+ if (previewUrl && streamUrl) {
62
+ return { previewUrl, streamUrl };
63
+ }
64
+ await (0, retry_1.sleepAsync)(1_000);
65
+ }
66
+ 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>'}`);
67
+ }
68
+ function matchLabeledUrl(content, label) {
69
+ const labelPattern = new RegExp(`${label}:\\s*(${TRYCLOUDFLARE_URL_PATTERN.source})`);
70
+ const match = labelPattern.exec(content);
71
+ return match ? match[1] : null;
72
+ }
@@ -0,0 +1,24 @@
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;
@@ -0,0 +1,65 @@
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
+ const eas_build_job_1 = require("@expo/eas-build-job");
11
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
12
+ const gql_tada_1 = require("gql.tada");
13
+ const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
14
+ mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
15
+ deviceRunSession {
16
+ startDeviceRunSession(
17
+ deviceRunSessionId: $deviceRunSessionId
18
+ remoteConfig: $remoteConfig
19
+ ) {
20
+ id
21
+ status
22
+ }
23
+ }
24
+ }
25
+ `);
26
+ function getDeviceRunSessionIdOrThrow(env) {
27
+ const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
28
+ if (!deviceRunSessionId) {
29
+ throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
30
+ 'This step must run as part of a device run session created by the API server, ' +
31
+ 'which injects DEVICE_RUN_SESSION_ID into the job environment.');
32
+ }
33
+ return deviceRunSessionId;
34
+ }
35
+ async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
36
+ logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
37
+ const result = await ctx.graphqlClient
38
+ .mutation(START_DEVICE_RUN_SESSION_MUTATION, { deviceRunSessionId, remoteConfig })
39
+ .toPromise();
40
+ if (result.error) {
41
+ throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
42
+ }
43
+ }
44
+ async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
45
+ 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 });
46
+ }
47
+ function spawnDetached({ command, args, cwd, env, }) {
48
+ const promise = (0, turtle_spawn_1.default)(command, args, {
49
+ cwd,
50
+ env,
51
+ stdio: ['ignore', 'pipe', 'pipe'],
52
+ detached: true,
53
+ });
54
+ // We don't await the process — it should outlive this step. Failures show
55
+ // up in the captured output; suppress unhandled rejections here.
56
+ promise.catch(() => { });
57
+ promise.child.unref();
58
+ let output = '';
59
+ const appendChunk = (chunk) => {
60
+ output += chunk.toString();
61
+ };
62
+ promise.child.stdout?.on('data', appendChunk);
63
+ promise.child.stderr?.on('data', appendChunk);
64
+ return { getOutput: () => output };
65
+ }
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.1",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -98,5 +98,5 @@
98
98
  "typescript": "^5.5.4",
99
99
  "uuid": "^9.0.1"
100
100
  },
101
- "gitHead": "53d294330de9c63eb792f646c0603224def9a1b0"
101
+ "gitHead": "f46b469267f4f87be6a6d1acff22464c8cf89b26"
102
102
  }