@expo/build-tools 20.1.0 → 20.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/builders/custom.js +23 -0
  2. package/dist/common/installDependencies.d.ts +10 -1
  3. package/dist/common/installDependencies.js +95 -1
  4. package/dist/common/prebuild.js +2 -3
  5. package/dist/{ios → common}/xcpretty.d.ts +1 -0
  6. package/dist/{ios → common}/xcpretty.js +18 -3
  7. package/dist/datadog.js +7 -3
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/ios/fastlane.js +1 -1
  11. package/dist/ios/pod.js +1 -1
  12. package/dist/runtimeSettings.d.ts +12 -0
  13. package/dist/runtimeSettings.js +120 -0
  14. package/dist/steps/easFunctions.js +1 -1
  15. package/dist/steps/functions/downloadBuild.d.ts +5 -3
  16. package/dist/steps/functions/downloadBuild.js +34 -4
  17. package/dist/steps/functions/findAndUploadBuildArtifacts.js +2 -2
  18. package/dist/steps/functions/installMaestro.js +13 -2
  19. package/dist/steps/functions/maestroResultParser.d.ts +18 -0
  20. package/dist/steps/functions/maestroResultParser.js +132 -3
  21. package/dist/steps/functions/maestroTests.js +26 -13
  22. package/dist/steps/functions/repack.d.ts +3 -1
  23. package/dist/steps/functions/repack.js +15 -1
  24. package/dist/steps/functions/reportMaestroTestResults.js +39 -20
  25. package/dist/steps/functions/restoreBuildCache.js +9 -6
  26. package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +1 -1
  27. package/dist/steps/functions/startAgentDeviceRemoteSession.js +101 -22
  28. package/dist/steps/functions/startArgentRemoteSession.js +1 -1
  29. package/dist/steps/functions/startServeSimRemoteSession.js +1 -1
  30. package/dist/steps/functions/uploadArtifact.js +1 -1
  31. package/dist/steps/utils/ios/fastlane.js +1 -1
  32. package/dist/steps/utils/remoteDeviceRunSession.d.ts +29 -1
  33. package/dist/steps/utils/remoteDeviceRunSession.js +75 -1
  34. package/package.json +3 -3
  35. package/dist/steps/utils/ios/xcpretty.d.ts +0 -15
  36. package/dist/steps/utils/ios/xcpretty.js +0 -92
@@ -1,9 +1,36 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { BuildStepEnv } from '@expo/steps';
3
+ import { z } from 'zod';
3
4
  import { CustomBuildContext } from '../../customBuildContext';
4
5
  export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
5
6
  export declare function getNgrokTunnelDomainOrThrow(env: BuildStepEnv): string;
6
7
  export declare function getNgrokAuthtokenOrThrow(env: BuildStepEnv): string;
8
+ declare const TurnIceServersSchema: z.ZodArray<z.ZodObject<{
9
+ urls: z.ZodArray<z.ZodString>;
10
+ username: z.ZodOptional<z.ZodString>;
11
+ credential: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>>;
13
+ export type TurnIceServers = z.infer<typeof TurnIceServersSchema>;
14
+ /**
15
+ * Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
16
+ * credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
17
+ * (the entry carrying the short-lived credentials).
18
+ */
19
+ export declare function turnIceServersToServeSimArgs(iceServers: TurnIceServers): string[];
20
+ /**
21
+ * Fetch short-lived Cloudflare TURN ICE servers for this job run from www
22
+ * (minted on demand, mirroring how the worker fetches project clone URLs) and
23
+ * translate them into serve-sim CLI flags.
24
+ *
25
+ * Best-effort: on any failure we log and return [] so serve-sim falls back to
26
+ * its built-in P2P/STUN behavior. The credential is passed to serve-sim as a
27
+ * process arg and deliberately not logged (turtle-spawn never logs argv and the
28
+ * worker is single-tenant).
29
+ */
30
+ export declare function fetchServeSimTurnArgsAsync(ctx: CustomBuildContext, { env, logger }: {
31
+ env: BuildStepEnv;
32
+ logger: bunyan;
33
+ }): Promise<string[]>;
7
34
  export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
8
35
  ctx: CustomBuildContext;
9
36
  deviceRunSessionId: string;
@@ -19,7 +46,7 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
19
46
  cwd?: string;
20
47
  env: BuildStepEnv;
21
48
  }): DetachedProcessHandle;
22
- export declare function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }: {
49
+ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, }: {
23
50
  baseDomain: string;
24
51
  env: BuildStepEnv;
25
52
  logger: bunyan;
@@ -41,3 +68,4 @@ export declare function waitForFileAsync<T>({ filePath, timeoutMs, description,
41
68
  description: string;
42
69
  parse: (raw: string) => T;
43
70
  }): Promise<T>;
71
+ export {};
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
40
40
  exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
41
41
  exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
42
+ exports.turnIceServersToServeSimArgs = turnIceServersToServeSimArgs;
43
+ exports.fetchServeSimTurnArgsAsync = fetchServeSimTurnArgsAsync;
42
44
  exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
43
45
  exports.spawnDetached = spawnDetached;
44
46
  exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
@@ -48,9 +50,13 @@ const eas_build_job_1 = require("@expo/eas-build-job");
48
50
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
49
51
  const ngrok = __importStar(require("@ngrok/ngrok"));
50
52
  const gql_tada_1 = require("gql.tada");
53
+ const nullthrows_1 = __importDefault(require("nullthrows"));
54
+ const zod_1 = require("zod");
51
55
  const node_crypto_1 = require("node:crypto");
52
56
  const node_fs_1 = __importDefault(require("node:fs"));
57
+ const sentry_1 = require("../../sentry");
53
58
  const retry_1 = require("../../utils/retry");
59
+ const turtleFetch_1 = require("../../utils/turtleFetch");
54
60
  const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
55
61
  mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
56
62
  deviceRunSession {
@@ -91,6 +97,72 @@ function getNgrokAuthtokenOrThrow(env) {
91
97
  }
92
98
  return authtoken;
93
99
  }
100
+ const TurnIceServersSchema = zod_1.z.array(zod_1.z.object({
101
+ urls: zod_1.z.array(zod_1.z.string()),
102
+ username: zod_1.z.string().optional(),
103
+ credential: zod_1.z.string().optional(),
104
+ }));
105
+ const TurnIceServersResponseSchema = zod_1.z.object({
106
+ data: zod_1.z.object({
107
+ iceServers: TurnIceServersSchema,
108
+ }),
109
+ });
110
+ /**
111
+ * Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
112
+ * credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
113
+ * (the entry carrying the short-lived credentials).
114
+ */
115
+ function turnIceServersToServeSimArgs(iceServers) {
116
+ const stunUrls = iceServers
117
+ .filter(server => !server.username && !server.credential)
118
+ .flatMap(server => server.urls);
119
+ const turnServer = iceServers.find(server => server.username && server.credential);
120
+ const args = [];
121
+ if (stunUrls.length > 0) {
122
+ args.push('--stun-url', stunUrls.join(','));
123
+ }
124
+ if (turnServer?.username && turnServer.credential && turnServer.urls.length > 0) {
125
+ args.push('--turn-url', turnServer.urls.join(','), '--turn-username', turnServer.username, '--turn-credential', turnServer.credential);
126
+ }
127
+ return args;
128
+ }
129
+ /**
130
+ * Fetch short-lived Cloudflare TURN ICE servers for this job run from www
131
+ * (minted on demand, mirroring how the worker fetches project clone URLs) and
132
+ * translate them into serve-sim CLI flags.
133
+ *
134
+ * Best-effort: on any failure we log and return [] so serve-sim falls back to
135
+ * its built-in P2P/STUN behavior. The credential is passed to serve-sim as a
136
+ * process arg and deliberately not logged (turtle-spawn never logs argv and the
137
+ * worker is single-tenant).
138
+ */
139
+ async function fetchServeSimTurnArgsAsync(ctx, { env, logger }) {
140
+ try {
141
+ const deviceRunSessionId = getDeviceRunSessionIdOrThrow(env);
142
+ const expoApiServerUrl = (0, nullthrows_1.default)(ctx.env.__API_SERVER_URL, '__API_SERVER_URL is not set');
143
+ const robotAccessToken = (0, nullthrows_1.default)(ctx.job.secrets?.robotAccessToken, 'robot access token is not set');
144
+ const response = await (0, turtleFetch_1.turtleFetch)(new URL(`/v2/device-run-sessions/${deviceRunSessionId}/turn-ice-servers`, expoApiServerUrl).toString(), 'POST', {
145
+ headers: {
146
+ Authorization: `Bearer ${robotAccessToken}`,
147
+ },
148
+ timeout: 5000,
149
+ retries: 1,
150
+ logger,
151
+ });
152
+ const { data } = TurnIceServersResponseSchema.parse(await response.json());
153
+ const args = turnIceServersToServeSimArgs(data.iceServers);
154
+ if (args.length > 0) {
155
+ logger.info('Configured serve-sim with Cloudflare TURN ICE servers.');
156
+ }
157
+ return args;
158
+ }
159
+ catch (err) {
160
+ const error = err instanceof Error ? err : new Error(String(err));
161
+ sentry_1.Sentry.capture('Could not fetch Cloudflare TURN ICE servers', error, { level: 'warning' });
162
+ logger.warn({ err: error }, 'Could not fetch Cloudflare TURN ICE servers; serve-sim will fall back to P2P/STUN.');
163
+ return [];
164
+ }
165
+ }
94
166
  async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
95
167
  logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
96
168
  const result = await ctx.graphqlClient
@@ -119,8 +191,9 @@ function spawnDetached({ command, args, cwd, env, }) {
119
191
  promise.child.stderr?.on('data', appendChunk);
120
192
  return { getOutput: () => output };
121
193
  }
122
- async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }) {
194
+ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
123
195
  logger.info('Launching serve-sim with tunnel.');
196
+ const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
124
197
  const serveSim = spawnDetached({
125
198
  command: 'npx',
126
199
  args: [
@@ -136,6 +209,7 @@ async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs
136
209
  '0.55',
137
210
  '--codec',
138
211
  'webrtc',
212
+ ...turnArgs,
139
213
  ],
140
214
  env,
141
215
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "20.1.0",
3
+ "version": "20.2.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -77,7 +77,7 @@
77
77
  "zod": "^4.3.5"
78
78
  },
79
79
  "devDependencies": {
80
- "@expo/repack-app": "~0.4.1",
80
+ "@expo/repack-app": "~0.6.1",
81
81
  "@types/fs-extra": "^11.0.4",
82
82
  "@types/jest": "^29.5.12",
83
83
  "@types/lodash": "^4.17.4",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "33225d2b73a34db5cbdfeaa537aedb6c6b4a84af"
103
+ "gitHead": "203e9997eebafcbddc600285f6a809afb4a93a74"
104
104
  }
@@ -1,15 +0,0 @@
1
- import { bunyan } from '@expo/logger';
2
- export declare class XcodeBuildLogger {
3
- private readonly logger;
4
- private readonly projectRoot;
5
- private loggerError?;
6
- private flushing;
7
- private logReaderPromise?;
8
- private logsPath?;
9
- constructor(logger: bunyan, projectRoot: string);
10
- watchLogFiles(logsDirectory: string): Promise<void>;
11
- flush(): Promise<void>;
12
- private getBuildLogFilename;
13
- private startBuildLogger;
14
- private findBundlerErrors;
15
- }
@@ -1,92 +0,0 @@
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.XcodeBuildLogger = void 0;
7
- const spawn_async_1 = __importDefault(require("@expo/spawn-async"));
8
- const xcpretty_1 = require("@expo/xcpretty");
9
- const assert_1 = __importDefault(require("assert"));
10
- const fast_glob_1 = __importDefault(require("fast-glob"));
11
- const fs_extra_1 = __importDefault(require("fs-extra"));
12
- const path_1 = __importDefault(require("path"));
13
- const CHECK_FILE_INTERVAL_MS = 1000;
14
- class XcodeBuildLogger {
15
- logger;
16
- projectRoot;
17
- loggerError;
18
- flushing = false;
19
- logReaderPromise;
20
- logsPath;
21
- constructor(logger, projectRoot) {
22
- this.logger = logger;
23
- this.projectRoot = projectRoot;
24
- }
25
- async watchLogFiles(logsDirectory) {
26
- while (!this.flushing) {
27
- const logsFilename = await this.getBuildLogFilename(logsDirectory);
28
- if (logsFilename) {
29
- this.logsPath = path_1.default.join(logsDirectory, logsFilename);
30
- void this.startBuildLogger(this.logsPath);
31
- return;
32
- }
33
- await new Promise(res => setTimeout(res, CHECK_FILE_INTERVAL_MS));
34
- }
35
- }
36
- async flush() {
37
- this.flushing = true;
38
- if (this.loggerError) {
39
- throw this.loggerError;
40
- }
41
- if (this.logReaderPromise) {
42
- this.logReaderPromise.child.kill('SIGINT');
43
- try {
44
- await this.logReaderPromise;
45
- }
46
- catch { }
47
- }
48
- if (this.logsPath) {
49
- await this.findBundlerErrors(this.logsPath);
50
- }
51
- }
52
- async getBuildLogFilename(logsDirectory) {
53
- const paths = await (0, fast_glob_1.default)('*.log', { cwd: logsDirectory });
54
- return paths.length >= 1 ? paths[0] : undefined;
55
- }
56
- async startBuildLogger(logsPath) {
57
- try {
58
- const formatter = xcpretty_1.ExpoRunFormatter.create(this.projectRoot, {
59
- // TODO: Can provide xcode project name for better parsing
60
- isDebug: false,
61
- });
62
- this.logReaderPromise = (0, spawn_async_1.default)('tail', ['-n', '+0', '-f', logsPath], { stdio: 'pipe' });
63
- (0, assert_1.default)(this.logReaderPromise.child.stdout, 'stdout is not available');
64
- this.logReaderPromise.child.stdout.on('data', (data) => {
65
- const lines = formatter.pipe(data.toString());
66
- for (const line of lines) {
67
- this.logger.info(line);
68
- }
69
- });
70
- await this.logReaderPromise;
71
- this.logger.info(formatter.getBuildSummary());
72
- }
73
- catch (err) {
74
- if (!this.flushing) {
75
- this.loggerError = err;
76
- }
77
- }
78
- }
79
- async findBundlerErrors(logsPath) {
80
- try {
81
- const logFile = await fs_extra_1.default.readFile(logsPath, 'utf-8');
82
- const match = logFile.match(/Welcome to Metro!\s* Fast - Scalable - Integrated\s*([\s\S]*)Run CLI with --verbose flag for more details.\nCommand PhaseScriptExecution failed with a nonzero exit code/);
83
- if (match) {
84
- this.logger.info(match[1]);
85
- }
86
- }
87
- catch (err) {
88
- this.logger.error({ err }, 'Failed to read Xcode logs');
89
- }
90
- }
91
- }
92
- exports.XcodeBuildLogger = XcodeBuildLogger;