@expo/build-tools 21.0.1 → 21.0.2

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/context.js CHANGED
@@ -17,26 +17,6 @@ const packageManager_1 = require("./utils/packageManager");
17
17
  class SkipNativeBuildError extends Error {
18
18
  }
19
19
  exports.SkipNativeBuildError = SkipNativeBuildError;
20
- function logEnvComparison({ oldMergeOrderEnv, newMergeOrderEnv, }) {
21
- const envNames = new Set([...Object.keys(oldMergeOrderEnv), ...Object.keys(newMergeOrderEnv)]);
22
- const sameEnvNames = [];
23
- const differentEnvNames = [];
24
- for (const key of envNames) {
25
- const hasOldValue = key in oldMergeOrderEnv;
26
- const hasNewValue = key in newMergeOrderEnv;
27
- if (hasOldValue === hasNewValue && oldMergeOrderEnv[key] === newMergeOrderEnv[key]) {
28
- sameEnvNames.push(key);
29
- }
30
- else {
31
- differentEnvNames.push(key);
32
- }
33
- }
34
- if (differentEnvNames.length === 0) {
35
- datadog_1.Datadog.log('BuildContext.updateEnv merge order produced same env');
36
- return;
37
- }
38
- datadog_1.Datadog.log(`BuildContext.updateEnv merge order produced different env: compared_env_count=${envNames.size} different_env_count=${differentEnvNames.length} different_env_names=${differentEnvNames.join(',')} same_env_count=${sameEnvNames.length} same_env_names=${sameEnvNames.join(',')}`);
39
- }
40
20
  class BuildContext {
41
21
  workingdir;
42
22
  logger;
@@ -177,21 +157,11 @@ class BuildContext {
177
157
  if (this._job.triggeredBy !== common_1.BuildTrigger.GIT_BASED_INTEGRATION) {
178
158
  throw new Error('Updating environment variables is only allowed when build was triggered by a git-based integration.');
179
159
  }
180
- const oldMergeOrderEnv = {
181
- ...env,
182
- ...this._env,
183
- __EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
184
- };
185
- const newMergeOrderEnv = {
160
+ this._env = {
186
161
  ...this._env,
187
162
  ...env,
188
163
  __EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
189
164
  };
190
- logEnvComparison({
191
- oldMergeOrderEnv,
192
- newMergeOrderEnv,
193
- });
194
- this._env = newMergeOrderEnv;
195
165
  this._env.PATH = this._env.PATH
196
166
  ? [this.buildExecutablesDirectory, this._env.PATH].join(':')
197
167
  : this.buildExecutablesDirectory;
@@ -1,6 +1,6 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { Writable } from 'stream';
3
- import GCS from './client';
3
+ import { GCS } from './client';
4
4
  declare class GCSLoggerStream extends Writable {
5
5
  writable: boolean;
6
6
  private readonly logger;
@@ -37,10 +37,6 @@ declare namespace GCSLoggerStream {
37
37
  compress?: CompressionMethod;
38
38
  }
39
39
  type UploadMethod = {
40
- client: GCS;
41
- key: string;
42
- customTime: Date | null;
43
- } | {
44
40
  signedUrl: GCS.SignedUrl;
45
41
  };
46
42
  interface Config {
@@ -11,7 +11,7 @@ const path_1 = __importDefault(require("path"));
11
11
  const stream_1 = require("stream");
12
12
  const util_1 = require("util");
13
13
  const zlib_1 = __importDefault(require("zlib"));
14
- const client_1 = __importDefault(require("./client"));
14
+ const client_1 = require("./client");
15
15
  const pipe = (0, util_1.promisify)(stream_1.pipeline);
16
16
  class GCSLoggerStream extends stream_1.Writable {
17
17
  writable = true;
@@ -38,7 +38,7 @@ class GCSLoggerStream extends stream_1.Writable {
38
38
  this.temporaryCompressedLogsPath = `${this.temporaryLogsPath}.compressed`;
39
39
  }
40
40
  findNormalizedHeader(name) {
41
- if (!this.uploadMethod || 'client' in this.uploadMethod) {
41
+ if (!this.uploadMethod) {
42
42
  return null;
43
43
  }
44
44
  const normalizedName = name.toLowerCase().replace('-', '');
@@ -145,24 +145,10 @@ class GCSLoggerStream extends stream_1.Writable {
145
145
  const srcGeneratorAsync = async () => {
146
146
  return await this.createCompressedStream(fs_extra_1.default.createReadStream(this.temporaryLogsPath, { end: size }));
147
147
  };
148
- if ('signedUrl' in this.uploadMethod) {
149
- return await client_1.default.uploadWithSignedUrl({
150
- signedUrl: this.uploadMethod.signedUrl,
151
- srcGeneratorAsync,
152
- });
153
- }
154
- const { Location } = await this.uploadMethod.client.uploadFile({
155
- key: this.uploadMethod.key,
156
- src: await srcGeneratorAsync(),
157
- streamOptions: {
158
- metadata: {
159
- contentType: 'text/plain;charset=utf-8',
160
- ...(this.compress !== null ? { contentEncoding: this.compress } : {}),
161
- customTime: this.uploadMethod.customTime,
162
- },
163
- },
148
+ return await client_1.GCS.uploadWithSignedUrl({
149
+ signedUrl: this.uploadMethod.signedUrl,
150
+ srcGeneratorAsync,
164
151
  });
165
- return Location;
166
152
  }
167
153
  async createCompressedStream(src) {
168
154
  if (!this.compress) {
@@ -1,58 +1,15 @@
1
- import { CreateWriteStreamOptions, File } from '@google-cloud/storage';
2
1
  import { Readable } from 'stream';
3
- import { RetryOptions } from './retry';
4
- interface SignedUrlParams {
5
- key: string;
6
- expirationTime: number;
7
- contentType?: string;
8
- extensionHeaders?: {
9
- [key: string]: number | string | string[];
10
- };
11
- }
12
2
  interface UploadWithSignedUrlParams {
13
3
  signedUrl: GCS.SignedUrl;
14
4
  srcGeneratorAsync: () => Promise<Readable>;
15
- retryIntervalMs?: RetryOptions['retryIntervalMs'];
16
- retries?: RetryOptions['retries'];
5
+ retryIntervalMs?: number;
6
+ retries?: number;
17
7
  }
18
- declare class GCS {
19
- private readonly bucket;
20
- private readonly client;
21
- constructor(bucket: string);
22
- static uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries, retryIntervalMs, }: UploadWithSignedUrlParams): Promise<string>;
23
- formatHttpUrl(key: string): string;
24
- uploadFile({ key, src, streamOptions, }: {
25
- key: string;
26
- src: Readable;
27
- streamOptions?: CreateWriteStreamOptions;
28
- }): Promise<{
29
- Location: string;
30
- }>;
31
- deleteFile(key: string): Promise<void>;
32
- createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders, }: SignedUrlParams): Promise<GCS.SignedUrl>;
33
- createSignedDownloadUrl({ key, expirationTime, }: {
34
- key: string;
35
- expirationTime: number;
36
- }): Promise<string>;
37
- checkIfFileExists(key: string, fileHash?: string): Promise<boolean>;
38
- listDirectory(prefix: string): Promise<string[]>;
39
- moveFile(src: string, dest: string): Promise<void>;
40
- deleteFiles(keys: string[]): Promise<void>;
41
- downloadFile(key: string, destinationPath: string): Promise<void>;
42
- getFile(key: string): File;
43
- }
44
- declare namespace GCS {
45
- interface SignedUrl {
8
+ export declare namespace GCS {
9
+ type SignedUrl = {
46
10
  url: string;
47
- headers: {
48
- [key: string]: string;
49
- };
50
- }
51
- interface Config {
52
- accessKeyId: string;
53
- secretAccessKey: string;
54
- region: string;
55
- bucket: string;
56
- }
11
+ headers: Record<string, string>;
12
+ };
13
+ function uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries, retryIntervalMs, }: UploadWithSignedUrlParams): Promise<string>;
57
14
  }
58
- export default GCS;
15
+ export {};
@@ -32,26 +32,14 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
- const storage_1 = require("@google-cloud/storage");
40
- const fs_extra_1 = __importDefault(require("fs-extra"));
36
+ exports.GCS = void 0;
41
37
  const node_fetch_1 = __importStar(require("node-fetch"));
42
- const node_stream_1 = __importDefault(require("node:stream"));
43
- const node_util_1 = require("node:util");
44
38
  const url_1 = require("url");
45
39
  const retry_1 = require("./retry");
46
- const pipeline = (0, node_util_1.promisify)(node_stream_1.default.pipeline);
47
- class GCS {
48
- bucket;
49
- client = new storage_1.Storage();
50
- constructor(bucket) {
51
- this.bucket = bucket;
52
- this.bucket = bucket;
53
- }
54
- static async uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries = 2, retryIntervalMs = 30_000, }) {
40
+ var GCS;
41
+ (function (GCS) {
42
+ async function uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries = 2, retryIntervalMs = 30_000, }) {
55
43
  let resp;
56
44
  try {
57
45
  resp = await (0, retry_1.retryOnGCSUploadFailure)(async () => {
@@ -61,18 +49,13 @@ class GCS {
61
49
  headers: signedUrl.headers,
62
50
  body: src,
63
51
  });
64
- }, {
65
- retries,
66
- retryIntervalMs,
67
- });
52
+ }, { retries, retryIntervalMs });
68
53
  }
69
54
  catch (err) {
70
55
  if (err instanceof node_fetch_1.FetchError) {
71
56
  throw new Error(`Failed to upload the file, reason: ${err.code}`);
72
57
  }
73
- else {
74
- throw err;
75
- }
58
+ throw err;
76
59
  }
77
60
  if (!resp.ok) {
78
61
  let body;
@@ -83,91 +66,7 @@ class GCS {
83
66
  throw new Error(`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`);
84
67
  }
85
68
  const url = new url_1.URL(signedUrl.url);
86
- return `${url.protocol}//${url.host}${url.pathname}`; // strip query string
87
- }
88
- formatHttpUrl(key) {
89
- return this.client.bucket(this.bucket).file(key).publicUrl();
90
- }
91
- async uploadFile({ key, src, streamOptions, }) {
92
- const file = this.client.bucket(this.bucket).file(key);
93
- await new Promise((res, rej) => {
94
- src.pipe(file
95
- .createWriteStream(streamOptions)
96
- .on('error', err => {
97
- rej(err);
98
- })
99
- .on('finish', () => {
100
- res();
101
- }));
102
- });
103
- return { Location: file.publicUrl() };
104
- }
105
- async deleteFile(key) {
106
- try {
107
- await this.client.bucket(this.bucket).file(key).delete();
108
- }
109
- catch (err) {
110
- if (err.response?.statusCode === 404) {
111
- return;
112
- }
113
- throw err;
114
- }
115
- }
116
- async createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders = {}, }) {
117
- const config = {
118
- version: 'v4',
119
- action: 'write',
120
- expires: Date.now() + expirationTime,
121
- contentType,
122
- extensionHeaders,
123
- };
124
- const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
125
- return {
126
- url,
127
- headers: {
128
- ...(contentType ? { 'content-type': contentType } : {}),
129
- ...extensionHeaders,
130
- },
131
- };
132
- }
133
- async createSignedDownloadUrl({ key, expirationTime, }) {
134
- const options = {
135
- version: 'v4',
136
- action: 'read',
137
- expires: Date.now() + expirationTime,
138
- };
139
- const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
140
- return url;
141
- }
142
- async checkIfFileExists(key, fileHash) {
143
- let metadata;
144
- try {
145
- [metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
146
- }
147
- catch (error) {
148
- if (error.code === 404) {
149
- return false;
150
- }
151
- throw error;
152
- }
153
- return fileHash ? metadata.etag === fileHash : true;
154
- }
155
- async listDirectory(prefix) {
156
- const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
157
- return files.map(x => this.formatHttpUrl(x.name));
158
- }
159
- async moveFile(src, dest) {
160
- await this.client.bucket(this.bucket).file(src).move(dest);
161
- }
162
- async deleteFiles(keys) {
163
- await Promise.all(keys.map(key => this.deleteFile(key)));
164
- }
165
- async downloadFile(key, destinationPath) {
166
- const stream = this.client.bucket(this.bucket).file(key).createReadStream();
167
- await pipeline(stream, fs_extra_1.default.createWriteStream(destinationPath));
168
- }
169
- getFile(key) {
170
- return this.client.bucket(this.bucket).file(key);
69
+ return `${url.protocol}//${url.host}${url.pathname}`;
171
70
  }
172
- }
173
- exports.default = GCS;
71
+ GCS.uploadWithSignedUrl = uploadWithSignedUrl;
72
+ })(GCS || (exports.GCS = GCS = {}));
@@ -1,11 +1,12 @@
1
1
  import { Response } from 'node-fetch';
2
- export interface RetryOptions {
2
+ type RetryOptions = {
3
3
  retries: number;
4
4
  retryIntervalMs: number;
5
- shouldRetryOnError: (error: any) => boolean;
5
+ shouldRetryOnError: (error: unknown) => boolean;
6
6
  shouldRetryOnResponse: (response: Response) => boolean;
7
- }
7
+ };
8
8
  export declare function retryOnGCSUploadFailure(fn: (attemptCount: number) => Promise<Response>, { retries, retryIntervalMs, }: {
9
9
  retries: RetryOptions['retries'];
10
10
  retryIntervalMs: RetryOptions['retryIntervalMs'];
11
11
  }): Promise<Response>;
12
+ export {};
package/dist/gcs/retry.js CHANGED
@@ -6,20 +6,24 @@ async function retryOnGCSUploadFailure(fn, { retries, retryIntervalMs, }) {
6
6
  return await retry(fn, {
7
7
  retries,
8
8
  retryIntervalMs,
9
- shouldRetryOnError: e => {
10
- return (e.code === 'ENOTFOUND' ||
11
- e.code === 'EAI_AGAIN' ||
12
- e.code === 'ECONNRESET' ||
13
- e.code === 'ETIMEDOUT' ||
14
- e.code === 'EPIPE');
9
+ shouldRetryOnError: err => {
10
+ return (isErrorWithCode(err) &&
11
+ (err.code === 'ENOTFOUND' ||
12
+ err.code === 'EAI_AGAIN' ||
13
+ err.code === 'ECONNRESET' ||
14
+ err.code === 'ETIMEDOUT' ||
15
+ err.code === 'EPIPE'));
15
16
  },
16
17
  shouldRetryOnResponse: resp => {
17
18
  return [408, 429, 500, 502, 503, 504].includes(resp.status);
18
19
  },
19
20
  });
20
21
  }
22
+ function isErrorWithCode(error) {
23
+ return typeof error === 'object' && error !== null && 'code' in error;
24
+ }
21
25
  /**
22
- * Wrapper used to execute an inner function and possibly retry it if it throws and error
26
+ * Wrapper used to execute an inner function and possibly retry it if it throws an error
23
27
  * @param fn Function to be executed and retried in case of error
24
28
  * @param retries How many times at most should the function be retried
25
29
  * @param retryIntervalMs Time interval between the retries
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Builders from './builders';
2
2
  import GCSLoggerStream from './gcs/LoggerStream';
3
- import GCS from './gcs/client';
3
+ import { GCS } from './gcs/client';
4
4
  export { Builders, GCS, GCSLoggerStream };
5
5
  export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
6
6
  export { PackageManager } from './utils/packageManager';
package/dist/index.js CHANGED
@@ -44,8 +44,8 @@ const Builders = __importStar(require("./builders"));
44
44
  exports.Builders = Builders;
45
45
  const LoggerStream_1 = __importDefault(require("./gcs/LoggerStream"));
46
46
  exports.GCSLoggerStream = LoggerStream_1.default;
47
- const client_1 = __importDefault(require("./gcs/client"));
48
- exports.GCS = client_1.default;
47
+ const client_1 = require("./gcs/client");
48
+ Object.defineProperty(exports, "GCS", { enumerable: true, get: function () { return client_1.GCS; } });
49
49
  var context_1 = require("./context");
50
50
  Object.defineProperty(exports, "BuildContext", { enumerable: true, get: function () { return context_1.BuildContext; } });
51
51
  Object.defineProperty(exports, "SkipNativeBuildError", { enumerable: true, get: function () { return context_1.SkipNativeBuildError; } });
@@ -16,18 +16,17 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
16
16
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
17
17
  logger.info('Starting serve-sim remote session.');
18
18
  await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
19
- const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
19
+ const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
20
20
  baseDomain: ngrokTunnelDomain,
21
21
  env,
22
22
  logger,
23
23
  timeoutMs: STARTUP_TIMEOUT_MS,
24
24
  });
25
25
  logger.info(`Preview URL: ${previewUrl}`);
26
- logger.info(`Stream URL: ${streamUrl}`);
27
26
  await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
28
27
  ctx,
29
28
  deviceRunSessionId,
30
- remoteConfig: { previewUrl, streamUrl },
29
+ remoteConfig: { previewUrl },
31
30
  logger,
32
31
  });
33
32
  await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
@@ -25,8 +25,22 @@ const RecordingManifestSchema = zod_1.z.object({
25
25
  firstFrameWallClock: zod_1.z.object({
26
26
  iso8601: zod_1.z.string(),
27
27
  }),
28
+ width: zod_1.z.number().int().positive(),
29
+ height: zod_1.z.number().int().positive(),
28
30
  recording: zod_1.z.string(),
29
31
  });
32
+ const recordingStartTimeFormatter = new Intl.DateTimeFormat('en-US', {
33
+ year: 'numeric',
34
+ month: 'short',
35
+ day: 'numeric',
36
+ hour: '2-digit',
37
+ minute: '2-digit',
38
+ second: '2-digit',
39
+ fractionalSecondDigits: 3,
40
+ hourCycle: 'h23',
41
+ timeZone: 'UTC',
42
+ timeZoneName: 'short',
43
+ });
30
44
  function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
31
45
  return new steps_1.BuildFunction({
32
46
  namespace: 'eas',
@@ -56,9 +70,11 @@ function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
56
70
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
57
71
  const limit = (0, promise_limit_1.default)(5);
58
72
  await Promise.all(recordings.map(recording => limit(async () => {
59
- const displayName = `${recording.deviceName} screen recording`;
60
73
  try {
61
74
  const metadata = RecordingManifestSchema.parse(JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(recording.directory, 'session.json'), 'utf-8')));
75
+ const startedAt = recordingStartTimeFormatter.format(new Date(metadata.firstFrameWallClock.iso8601));
76
+ const shortUdid = `${recording.udid.slice(0, 8)}-…`;
77
+ const displayName = `${recording.deviceName} screen recording (${shortUdid}, started at ${startedAt})`;
62
78
  const recordingPath = node_path_1.default.join(recording.directory, metadata.recording);
63
79
  const { size } = await (0, promises_1.stat)(recordingPath);
64
80
  const recordingId = node_path_1.default.basename(recording.directory);
@@ -70,11 +86,14 @@ function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
70
86
  filename: `${recordingId}.mp4`,
71
87
  kind: 'screen-recording',
72
88
  metadata: {
89
+ __eas_type: 'screen-recording',
73
90
  __eas_screen_recording: '1',
74
91
  udid: recording.udid,
75
92
  deviceName: recording.deviceName,
76
93
  runtimeDisplayName: recording.runtimeDisplayName,
77
94
  firstFrameAt: metadata.firstFrameWallClock.iso8601,
95
+ width: metadata.width,
96
+ height: metadata.height,
78
97
  },
79
98
  size,
80
99
  stream: (0, node_fs_1.createReadStream)(recordingPath),
@@ -13,7 +13,7 @@ const promises_2 = require("node:timers/promises");
13
13
  const sentry_1 = require("../../sentry");
14
14
  const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
15
15
  const IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS = 2_000;
16
- const RECORD_SIM_FINISH_TIMEOUT_MS = 30_000;
16
+ const RECORD_SIM_FINISH_TIMEOUT_MS = 70_000;
17
17
  const RECORD_SIM_FORCE_STOP_TIMEOUT_MS = 5_000;
18
18
  const RECORD_SIM_MAX_ATTEMPTS_PER_BOOT = 3;
19
19
  const RECORD_SIM_COMMAND = 'record-sim';
@@ -70,7 +70,11 @@ var IosSimulatorRecordingUtils;
70
70
  if (finished) {
71
71
  return;
72
72
  }
73
- logger.warn(`Forcing iOS Simulator recording process for ${recording.deviceName} to stop.`);
73
+ const recordSimOutput = recording.getOutput().trim();
74
+ const finishTimeoutSeconds = Math.round(RECORD_SIM_FINISH_TIMEOUT_MS / 1_000);
75
+ logger.warn({ recordSimOutput }, `Screen recording for ${recording.deviceName} did not finish within ${finishTimeoutSeconds} seconds and will be stopped.${recordSimOutput
76
+ ? `\nRecent recorder messages:\n${recordSimOutput}`
77
+ : '\nNo recorder messages were captured.'}`);
74
78
  recording.recordingProcess.kill('SIGKILL');
75
79
  const killed = await Promise.race([
76
80
  recording.completionPromise.then(() => true),
@@ -65,7 +65,6 @@ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, {
65
65
  timeoutMs: number;
66
66
  }): Promise<{
67
67
  previewUrl: string;
68
- streamUrl: string;
69
68
  }>;
70
69
  export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, rewriteHostHeader, logger, }: {
71
70
  port: number;
@@ -289,21 +289,20 @@ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, time
289
289
  ],
290
290
  env,
291
291
  });
292
- logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
292
+ logger.info('Waiting for serve-sim to report tunnel URL.');
293
293
  const deadline = Date.now() + timeoutMs;
294
294
  while (Date.now() < deadline) {
295
295
  const output = serveSim.getOutput();
296
- const previewUrl = matchLabeledUrl({ output, label: 'Tunnel', baseDomain });
297
- const streamUrl = matchLabeledUrl({ output, label: 'Stream', baseDomain });
298
- if (previewUrl && streamUrl) {
299
- return { previewUrl, streamUrl };
296
+ const previewUrl = matchTunnelUrl({ output, baseDomain });
297
+ if (previewUrl) {
298
+ return { previewUrl };
300
299
  }
301
300
  await (0, retry_1.sleepAsync)(1_000);
302
301
  }
303
- 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>'}`);
302
+ throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim to report Tunnel URL. Last output:\n${serveSim.getOutput() || '<empty>'}`);
304
303
  }
305
- function matchLabeledUrl({ output, label, baseDomain, }) {
306
- const labelPattern = new RegExp(`${label}:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`);
304
+ function matchTunnelUrl({ output, baseDomain, }) {
305
+ const labelPattern = new RegExp(`Tunnel:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`);
307
306
  const match = labelPattern.exec(output);
308
307
  return match ? match[1] : null;
309
308
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.0.1",
3
+ "version": "21.0.2",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -39,18 +39,17 @@
39
39
  "@expo/config": "55.0.10",
40
40
  "@expo/config-plugins": "55.0.7",
41
41
  "@expo/downloader": "21.0.0",
42
- "@expo/eas-build-job": "21.0.1",
42
+ "@expo/eas-build-job": "21.0.2",
43
43
  "@expo/env": "^0.4.0",
44
44
  "@expo/logger": "21.0.0",
45
45
  "@expo/package-manager": "1.9.10",
46
- "@expo/plist": "^0.2.0",
46
+ "@expo/plist": "^0.3.5",
47
47
  "@expo/results": "^1.0.0",
48
48
  "@expo/spawn-async": "1.7.2",
49
- "@expo/steps": "21.0.1",
50
- "@expo/template-file": "21.0.0",
49
+ "@expo/steps": "21.0.2",
50
+ "@expo/template-file": "21.0.2",
51
51
  "@expo/turtle-spawn": "21.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
53
- "@google-cloud/storage": "^7.11.2",
54
53
  "@ngrok/ngrok": "1.7.0",
55
54
  "@sentry/node": "7.77.0",
56
55
  "@urql/core": "^6.0.1",
@@ -61,7 +60,7 @@
61
60
  "gql.tada": "^1.8.13",
62
61
  "joi": "^17.13.1",
63
62
  "jose": "5.10.0",
64
- "lodash": "^4.17.21",
63
+ "lodash": "^4.18.1",
65
64
  "node-fetch": "^2.7.0",
66
65
  "node-forge": "^1.3.1",
67
66
  "node-stream-zip": "1.15.0",
@@ -101,5 +100,5 @@
101
100
  "typescript": "^5.5.4",
102
101
  "uuid": "^9.0.1"
103
102
  },
104
- "gitHead": "a62289b9888b71fe90e3022840643b182b732048"
103
+ "gitHead": "e9ebc41413ae2b5c7b4b92e91f45e9f65e3dbd23"
105
104
  }
@@ -59,8 +59,15 @@ public struct FirstFrameWallClock: Codable, Sendable {
59
59
  public let iso8601: String
60
60
  }
61
61
 
62
+ public enum RecordingFinalizationStage: Sendable {
63
+ case captureStopped
64
+ case videoSaved
65
+ }
66
+
62
67
  public struct RecordingManifest: Codable, Sendable {
63
68
  public let firstFrameWallClock: FirstFrameWallClock
69
+ public let width: Int
70
+ public let height: Int
64
71
  public let hlsVersion: Int?
65
72
  public let hlsTargetDurationSeconds: Int?
66
73
  public let hlsMediaSequence: Int?
@@ -100,7 +100,9 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
100
100
  @discardableResult
101
101
  func writeManifest(
102
102
  configuration: SimulatorRecordingConfiguration,
103
- firstFrameWallClock: Date
103
+ firstFrameWallClock: Date,
104
+ width: Int,
105
+ height: Int
104
106
  ) throws -> RecordingManifest {
105
107
  try stateQueue.sync {
106
108
  if let firstError {
@@ -121,6 +123,8 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
121
123
  unixMs: unixMs(firstFrameWallClock),
122
124
  iso8601: iso8601(firstFrameWallClock)
123
125
  ),
126
+ width: width,
127
+ height: height,
124
128
  hlsVersion: segmented ? 7 : nil,
125
129
  hlsTargetDurationSeconds: targetDuration,
126
130
  hlsMediaSequence: segmented ? 0 : nil,
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
7
7
  public final class SimulatorRecorder {
8
8
  public var onSegment: ((SegmentOutput) -> Void)?
9
9
  public var onSimulatorStopped: ((String) -> Void)?
10
+ public var onFinalizationStage: ((RecordingFinalizationStage) -> Void)?
10
11
 
11
12
  private static let callbackStalenessTimeout: TimeInterval = 5
12
13
  private static let firstFrameRewireInterval: TimeInterval = 1
@@ -26,6 +27,8 @@ public final class SimulatorRecorder {
26
27
  private var monotonicClock = MonotonicClock()
27
28
  private var firstAcceptedCaptureTime: CMTime?
28
29
  private var firstAcceptedWallClock: Date?
30
+ private var recordingWidth: Int?
31
+ private var recordingHeight: Int?
29
32
  private var lastPTS: CMTime?
30
33
  private var lastSeed: UInt32?
31
34
  private var lastFrameCallbackElapsed: TimeInterval?
@@ -113,6 +116,7 @@ public final class SimulatorRecorder {
113
116
  displaySource?.stop()
114
117
  displaySource = nil
115
118
  }
119
+ onFinalizationStage?(.captureStopped)
116
120
 
117
121
  return try writerQueue.sync {
118
122
  if let firstError {
@@ -139,12 +143,18 @@ public final class SimulatorRecorder {
139
143
  if let error = outputWriter.error {
140
144
  throw error
141
145
  }
146
+ onFinalizationStage?(.videoSaved)
142
147
  guard let firstAcceptedWallClock else {
143
148
  throw RecorderError.make(25, "Missing first frame wall-clock timestamp")
144
149
  }
150
+ guard let recordingWidth, let recordingHeight else {
151
+ throw RecorderError.make(27, "Missing recording dimensions")
152
+ }
145
153
  return try outputWriter.writeManifest(
146
154
  configuration: configuration,
147
- firstFrameWallClock: firstAcceptedWallClock
155
+ firstFrameWallClock: firstAcceptedWallClock,
156
+ width: recordingWidth,
157
+ height: recordingHeight
148
158
  )
149
159
  }
150
160
  }
@@ -590,6 +600,8 @@ public final class SimulatorRecorder {
590
600
  self.writer = writer
591
601
  self.input = input
592
602
  self.adaptor = adaptor
603
+ recordingWidth = width
604
+ recordingHeight = height
593
605
  }
594
606
 
595
607
  private func assetWriterSettings(width: Int, height: Int) -> [String: Any] {
@@ -10,15 +10,15 @@ struct CLIOptions {
10
10
  var codec: RecorderCodec = .h264
11
11
  }
12
12
 
13
- private var stopRequested: CInt = 0
13
+ private var requestedStopSignal: CInt = 0
14
14
  private var simulatorStopped: CInt = 0
15
15
 
16
- private func handleStopSignal(_: CInt) {
17
- stopRequested = 1
16
+ private func handleStopSignal(_ signal: CInt) {
17
+ requestedStopSignal = signal
18
18
  }
19
19
 
20
20
  private func isStopRequested() -> Bool {
21
- stopRequested != 0 || simulatorStopped != 0
21
+ requestedStopSignal != 0 || simulatorStopped != 0
22
22
  }
23
23
 
24
24
  func usage() -> String {
@@ -112,6 +112,14 @@ do {
112
112
  simulatorStopped = 1
113
113
  fputs("[record-sim] simulator stopped state=\(state); finalizing recording\n", stderr)
114
114
  }
115
+ recorder.onFinalizationStage = { stage in
116
+ switch stage {
117
+ case .captureStopped:
118
+ fputs("[record-sim] screen capture stopped; saving final video\n", stderr)
119
+ case .videoSaved:
120
+ fputs("[record-sim] final video saved; writing recording details\n", stderr)
121
+ }
122
+ }
115
123
  recorder.onSegment = { segment in
116
124
  switch segment.kind {
117
125
  case .initialization:
@@ -130,6 +138,14 @@ do {
130
138
  Thread.sleep(forTimeInterval: 0.1)
131
139
  }
132
140
 
141
+ if requestedStopSignal != 0 {
142
+ let signalName = switch requestedStopSignal {
143
+ case SIGINT: "SIGINT"
144
+ case SIGTERM: "SIGTERM"
145
+ default: "signal \(requestedStopSignal)"
146
+ }
147
+ fputs("[record-sim] received \(signalName); finishing recording\n", stderr)
148
+ }
133
149
  let manifest = try recorder.stop()
134
150
  if let recording = manifest.recording {
135
151
  fputs("[record-sim] done recording=\(recording)\n", stderr)
@@ -1 +0,0 @@
1
- export {};
@@ -1,335 +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
- const crypto_1 = require("crypto");
7
- const fs_extra_1 = __importDefault(require("fs-extra"));
8
- const node_fetch_1 = __importDefault(require("node-fetch"));
9
- const path_1 = __importDefault(require("path"));
10
- const stream_1 = require("stream");
11
- const client_1 = __importDefault(require("../client"));
12
- jest.mock('node-fetch');
13
- class ErrorWithCode extends Error {
14
- _code;
15
- constructor(message, code) {
16
- super(message);
17
- if (code) {
18
- this._code = code;
19
- }
20
- }
21
- get code() {
22
- return this._code;
23
- }
24
- }
25
- class DNSError extends ErrorWithCode {
26
- _code = 'ENOTFOUND';
27
- }
28
- const TEST_BUCKET = 'turtle-v2-test';
29
- let googleApplicationCredentials;
30
- beforeAll(() => {
31
- googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
32
- process.env.GOOGLE_APPLICATION_CREDENTIALS = path_1.default.join(__dirname, 'mock-credentials.json');
33
- });
34
- afterAll(() => {
35
- process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
36
- });
37
- describe('GCS client', () => {
38
- describe('uploadWithPresignedURL function', () => {
39
- it('should throw an error if upload fails', async () => {
40
- const fetchMock = jest.mocked(node_fetch_1.default);
41
- const res = {
42
- ok: false,
43
- status: 500,
44
- };
45
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
46
- const key = 'cat.jpg';
47
- const gcs = new client_1.default(TEST_BUCKET);
48
- const signedUrl = await gcs.createSignedUploadUrl({
49
- key,
50
- expirationTime: 30000,
51
- contentType: 'image/jpeg',
52
- });
53
- fetchMock.mockImplementation(async () => res);
54
- await expect(client_1.default.uploadWithSignedUrl({
55
- signedUrl,
56
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
57
- retryIntervalMs: 500,
58
- })).rejects.toThrow();
59
- });
60
- it('should return stripped URL if successful', async () => {
61
- const fetchMock = jest.mocked(node_fetch_1.default);
62
- const res = {
63
- ok: true,
64
- status: 200,
65
- };
66
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
67
- const key = 'cat.jpg';
68
- const gcs = new client_1.default(TEST_BUCKET);
69
- const signedUrl = await gcs.createSignedUploadUrl({
70
- key,
71
- expirationTime: 30000,
72
- contentType: 'image/jpeg',
73
- });
74
- fetchMock.mockImplementation(async () => res);
75
- const result = await client_1.default.uploadWithSignedUrl({
76
- signedUrl,
77
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
78
- retryIntervalMs: 500,
79
- });
80
- expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
81
- });
82
- it('should retry upload on DNS error up to 2 times', async () => {
83
- const fetchMock = jest.mocked(node_fetch_1.default);
84
- const res = {
85
- ok: true,
86
- status: 200,
87
- };
88
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
89
- const key = 'cat.jpg';
90
- const gcs = new client_1.default(TEST_BUCKET);
91
- const signedUrl = await gcs.createSignedUploadUrl({
92
- key,
93
- expirationTime: 30000,
94
- contentType: 'image/jpeg',
95
- });
96
- fetchMock
97
- .mockImplementationOnce(async () => {
98
- throw new DNSError('failed once');
99
- })
100
- .mockImplementationOnce(async () => {
101
- throw new DNSError('failed twice', 'EAI_AGAIN');
102
- })
103
- .mockImplementation(async () => res);
104
- const result = await client_1.default.uploadWithSignedUrl({
105
- signedUrl,
106
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
107
- retryIntervalMs: 500,
108
- });
109
- expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
110
- expect(fetchMock).toHaveBeenCalledTimes(3);
111
- });
112
- it('should retry upload on retriable status codes error up to 2 times', async () => {
113
- const fetchMock = jest.mocked(node_fetch_1.default);
114
- const res = {
115
- ok: true,
116
- status: 200,
117
- };
118
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
119
- const key = 'cat.jpg';
120
- const gcs = new client_1.default(TEST_BUCKET);
121
- const signedUrl = await gcs.createSignedUploadUrl({
122
- key,
123
- expirationTime: 30000,
124
- contentType: 'image/jpeg',
125
- });
126
- fetchMock
127
- .mockImplementationOnce(async () => {
128
- return {
129
- ok: false,
130
- status: 503,
131
- };
132
- })
133
- .mockImplementationOnce(async () => {
134
- return {
135
- ok: false,
136
- status: 408,
137
- };
138
- })
139
- .mockImplementation(async () => res);
140
- const result = await client_1.default.uploadWithSignedUrl({
141
- signedUrl,
142
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
143
- retryIntervalMs: 500,
144
- });
145
- expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
146
- expect(fetchMock).toHaveBeenCalledTimes(3);
147
- });
148
- it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {
149
- const fetchMock = jest.mocked(node_fetch_1.default);
150
- const res = {
151
- ok: true,
152
- status: 200,
153
- };
154
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
155
- const key = 'cat.jpg';
156
- const gcs = new client_1.default(TEST_BUCKET);
157
- const signedUrl = await gcs.createSignedUploadUrl({
158
- key,
159
- expirationTime: 30000,
160
- contentType: 'image/jpeg',
161
- });
162
- fetchMock
163
- .mockImplementationOnce(async () => {
164
- return {
165
- ok: false,
166
- status: 503,
167
- };
168
- })
169
- .mockImplementationOnce(async () => {
170
- throw new DNSError('failed once');
171
- })
172
- .mockImplementation(async () => res);
173
- const result = await client_1.default.uploadWithSignedUrl({
174
- signedUrl,
175
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
176
- retryIntervalMs: 500,
177
- });
178
- expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
179
- expect(fetchMock).toHaveBeenCalledTimes(3);
180
- });
181
- it('should not retry upload on DNS error more than 2 times', async () => {
182
- const fetchMock = jest.mocked(node_fetch_1.default);
183
- const res = {
184
- ok: true,
185
- status: 200,
186
- };
187
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
188
- const key = 'cat.jpg';
189
- const gcs = new client_1.default(TEST_BUCKET);
190
- const signedUrl = await gcs.createSignedUploadUrl({
191
- key,
192
- expirationTime: 30000,
193
- contentType: 'image/jpeg',
194
- });
195
- const lastDNSError = new DNSError('failed thrice');
196
- fetchMock
197
- .mockImplementationOnce(async () => {
198
- throw new DNSError('failed once');
199
- })
200
- .mockImplementationOnce(async () => {
201
- throw new DNSError('failed twice', 'EAI_AGAIN');
202
- })
203
- .mockImplementationOnce(async () => {
204
- throw lastDNSError;
205
- })
206
- .mockImplementation(async () => res);
207
- await expect(client_1.default.uploadWithSignedUrl({
208
- signedUrl,
209
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
210
- retryIntervalMs: 500,
211
- })).rejects.toThrow(lastDNSError);
212
- expect(fetchMock).toHaveBeenCalledTimes(3);
213
- });
214
- it('should not retry upload on retriable status code more than 2 times', async () => {
215
- const fetchMock = jest.mocked(node_fetch_1.default);
216
- const res = {
217
- ok: true,
218
- status: 200,
219
- };
220
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
221
- const key = 'cat.jpg';
222
- const gcs = new client_1.default(TEST_BUCKET);
223
- const signedUrl = await gcs.createSignedUploadUrl({
224
- key,
225
- expirationTime: 30000,
226
- contentType: 'image/jpeg',
227
- });
228
- fetchMock
229
- .mockImplementationOnce(async () => {
230
- return {
231
- ok: false,
232
- status: 503,
233
- };
234
- })
235
- .mockImplementationOnce(async () => {
236
- return {
237
- ok: false,
238
- status: 408,
239
- };
240
- })
241
- .mockImplementationOnce(async () => {
242
- return {
243
- ok: false,
244
- status: 504,
245
- };
246
- })
247
- .mockImplementation(async () => res);
248
- await expect(client_1.default.uploadWithSignedUrl({
249
- signedUrl,
250
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
251
- retryIntervalMs: 500,
252
- })).rejects.toThrow();
253
- expect(fetchMock).toHaveBeenCalledTimes(3);
254
- });
255
- it('should not retry upload on other error codes', async () => {
256
- const fetchMock = jest.mocked(node_fetch_1.default);
257
- const res = {
258
- ok: true,
259
- status: 200,
260
- };
261
- const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
262
- const key = 'cat.jpg';
263
- const gcs = new client_1.default(TEST_BUCKET);
264
- const signedUrl = await gcs.createSignedUploadUrl({
265
- key,
266
- expirationTime: 30000,
267
- contentType: 'image/jpeg',
268
- });
269
- const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
270
- fetchMock
271
- .mockImplementationOnce(async () => {
272
- throw nonDNSError;
273
- })
274
- .mockImplementation(async () => res);
275
- await expect(client_1.default.uploadWithSignedUrl({
276
- signedUrl,
277
- srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
278
- retryIntervalMs: 500,
279
- })).rejects.toThrow(nonDNSError);
280
- expect(fetchMock).toHaveBeenCalledTimes(1);
281
- });
282
- it('retries upload with full stream', async () => {
283
- const fetchMock = jest.mocked(node_fetch_1.default);
284
- const receivedBodies = [];
285
- const recordBody = async (request) => {
286
- let body = Buffer.from([]);
287
- for await (const chunk of request.body) {
288
- body = Buffer.concat([body, chunk]);
289
- }
290
- receivedBodies.push(body);
291
- };
292
- fetchMock
293
- .mockImplementationOnce(async (_path, request) => {
294
- await recordBody(request);
295
- return {
296
- ok: false,
297
- status: 503,
298
- };
299
- })
300
- .mockImplementationOnce(async (_path, request) => {
301
- await recordBody(request);
302
- throw new DNSError('failed once');
303
- })
304
- .mockImplementation(async (_path, request) => {
305
- await recordBody(request);
306
- return {
307
- ok: true,
308
- status: 201,
309
- };
310
- });
311
- const gcs = new client_1.default(TEST_BUCKET);
312
- const signedUrl = await gcs.createSignedUploadUrl({
313
- key: 'text.txt',
314
- expirationTime: 30000,
315
- contentType: 'text/plain',
316
- });
317
- const bufferToUpload = (0, crypto_1.randomBytes)(16);
318
- const result = await client_1.default.uploadWithSignedUrl({
319
- signedUrl,
320
- srcGeneratorAsync: async () => stream_1.Readable.from(bufferToUpload),
321
- retryIntervalMs: 500,
322
- });
323
- expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
324
- expect(fetchMock).toHaveBeenCalledTimes(3);
325
- expect(receivedBodies.length).toBe(3);
326
- for (const body of receivedBodies) {
327
- // Here we're testing that each body received in every retry is the same.
328
- // If we passed the same body instance to each of the retries
329
- // each retry would consume a chunk of the same stream,
330
- // causing the uploaded file to be corrupted (missing first bytes).
331
- expect(body).toStrictEqual(bufferToUpload);
332
- }
333
- });
334
- });
335
- });