@expo/build-tools 20.5.1 → 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/android/gradle.d.ts +5 -0
  2. package/dist/android/gradle.js +13 -33
  3. package/dist/common/installDependencies.js +21 -18
  4. package/dist/context.js +31 -1
  5. package/dist/steps/easFunctions.js +18 -0
  6. package/dist/steps/functions/capturePosthogEvent.d.ts +2 -0
  7. package/dist/steps/functions/capturePosthogEvent.js +79 -0
  8. package/dist/steps/functions/createPosthogAnnotation.d.ts +2 -0
  9. package/dist/steps/functions/createPosthogAnnotation.js +75 -0
  10. package/dist/steps/functions/finishIosSimulatorRecordings.d.ts +2 -0
  11. package/dist/steps/functions/finishIosSimulatorRecordings.js +24 -0
  12. package/dist/steps/functions/rolloutPosthogFlag.d.ts +2 -0
  13. package/dist/steps/functions/rolloutPosthogFlag.js +208 -0
  14. package/dist/steps/functions/startAgentDeviceRemoteSession.js +21 -8
  15. package/dist/steps/functions/startArgentRemoteSession.d.ts +14 -0
  16. package/dist/steps/functions/startArgentRemoteSession.js +105 -44
  17. package/dist/steps/functions/startIosSimulatorRecordings.d.ts +2 -0
  18. package/dist/steps/functions/startIosSimulatorRecordings.js +20 -0
  19. package/dist/steps/functions/startServeSimRemoteSession.js +7 -5
  20. package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.d.ts +3 -0
  21. package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.js +91 -0
  22. package/dist/steps/functions/uploadPosthogSourcemaps.d.ts +2 -0
  23. package/dist/steps/functions/uploadPosthogSourcemaps.js +83 -0
  24. package/dist/steps/functions/waitForPosthogMetric.d.ts +2 -0
  25. package/dist/steps/functions/waitForPosthogMetric.js +135 -0
  26. package/dist/steps/functions/waitForPosthogQuery.d.ts +2 -0
  27. package/dist/steps/functions/waitForPosthogQuery.js +95 -0
  28. package/dist/steps/utils/IosSimulatorRecordingUtils.d.ts +17 -0
  29. package/dist/steps/utils/IosSimulatorRecordingUtils.js +218 -0
  30. package/dist/steps/utils/PosthogClient.d.ts +46 -0
  31. package/dist/steps/utils/PosthogClient.js +156 -0
  32. package/dist/steps/utils/PosthogUtils.d.ts +41 -0
  33. package/dist/steps/utils/PosthogUtils.js +65 -0
  34. package/dist/steps/utils/agentDeviceArtifacts.d.ts +27 -0
  35. package/dist/steps/utils/agentDeviceArtifacts.js +133 -0
  36. package/dist/steps/utils/android/gradle.d.ts +5 -0
  37. package/dist/steps/utils/android/gradle.js +13 -62
  38. package/dist/steps/utils/argentArtifacts.d.ts +5 -3
  39. package/dist/steps/utils/argentArtifacts.js +95 -23
  40. package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +3 -1
  41. package/dist/steps/utils/deviceRunSessionArtifacts.js +6 -2
  42. package/dist/steps/utils/remoteDeviceRunSession.d.ts +8 -0
  43. package/dist/steps/utils/remoteDeviceRunSession.js +62 -1
  44. package/dist/utils/AndroidEmulatorUtils.js +3 -0
  45. package/dist/utils/IosSimulatorUtils.d.ts +1 -0
  46. package/dist/utils/IosSimulatorUtils.js +41 -2
  47. package/dist/utils/expoUpdatesEmbedded.js +4 -0
  48. package/dist/utils/processes.d.ts +1 -0
  49. package/dist/utils/processes.js +23 -0
  50. package/package.json +9 -8
  51. package/resources/record-sim/Package.swift +28 -0
  52. package/resources/record-sim/README.md +79 -0
  53. package/resources/record-sim/Sources/RecordSim/FramebufferDisplaySource.swift +192 -0
  54. package/resources/record-sim/Sources/RecordSim/PrivateSimulatorServices.swift +96 -0
  55. package/resources/record-sim/Sources/RecordSim/RecordingModels.swift +70 -0
  56. package/resources/record-sim/Sources/RecordSim/RecordingOutputWriter.swift +136 -0
  57. package/resources/record-sim/Sources/RecordSim/SimulatorRecorder.swift +632 -0
  58. package/resources/record-sim/Sources/RecordSim/Utilities.swift +60 -0
  59. package/resources/record-sim/Sources/record-sim/main.swift +143 -0
@@ -13,12 +13,14 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
13
13
  const node_os_1 = __importDefault(require("node:os"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const promises_2 = require("node:stream/promises");
16
+ const promises_3 = require("node:timers/promises");
16
17
  const zod_1 = require("zod");
17
18
  const sentry_1 = require("../../sentry");
18
19
  const artifacts_1 = require("../../utils/artifacts");
19
- const retry_1 = require("../../utils/retry");
20
20
  const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
21
21
  const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
22
+ const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
23
+ const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
22
24
  const ArgentArtifactSchema = zod_1.z.object({
23
25
  id: zod_1.z.string(),
24
26
  filename: zod_1.z.string(),
@@ -28,46 +30,73 @@ const ArgentArtifactSchema = zod_1.z.object({
28
30
  const ArgentArtifactsListResponseSchema = zod_1.z.object({
29
31
  artifacts: zod_1.z.array(ArgentArtifactSchema),
30
32
  });
31
- async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, }) {
33
+ async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, signal, }) {
32
34
  logger.info('Started polling Argent tool-server for artifacts.');
33
35
  const seenArtifactIds = new Set();
36
+ const pendingUploads = new Set();
34
37
  let listArtifactsErrorCount = 0;
35
- for (;;) {
38
+ const queueArtifactUpload = (artifact) => {
39
+ if (seenArtifactIds.has(artifact.id)) {
40
+ return;
41
+ }
42
+ seenArtifactIds.add(artifact.id);
43
+ const uploadPromise = uploadArgentArtifactAsync(ctx, {
44
+ deviceRunSessionId,
45
+ toolsUrl,
46
+ toolsAuthToken,
47
+ artifact,
48
+ logger,
49
+ }).catch(err => {
50
+ const error = err instanceof Error ? err : new Error(String(err));
51
+ sentry_1.Sentry.capture('Could not upload Argent remote session artifact', error);
52
+ logger.warn({ err: error }, 'Could not upload Argent remote session artifact.');
53
+ });
54
+ pendingUploads.add(uploadPromise);
55
+ void uploadPromise.finally(() => {
56
+ pendingUploads.delete(uploadPromise);
57
+ });
58
+ };
59
+ const listArtifactsForUploadAsync = async ({ signal, } = {}) => {
36
60
  try {
37
- const artifacts = await listArgentArtifactsAsync({ toolsUrl, toolsAuthToken });
61
+ const artifacts = await listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, signal });
38
62
  listArtifactsErrorCount = 0;
39
- for (const artifact of artifacts) {
40
- if (seenArtifactIds.has(artifact.id)) {
41
- continue;
42
- }
43
- seenArtifactIds.add(artifact.id);
44
- void uploadArgentArtifactAsync(ctx, {
45
- deviceRunSessionId,
46
- toolsUrl,
47
- toolsAuthToken,
48
- artifact,
49
- logger,
50
- }).catch(err => {
51
- const error = err instanceof Error ? err : new Error(String(err));
52
- sentry_1.Sentry.capture('Could not upload Argent remote session artifact', error);
53
- logger.warn({ err: error }, 'Could not upload Argent remote session artifact.');
54
- });
55
- }
63
+ return artifacts;
56
64
  }
57
65
  catch (err) {
66
+ if (signal?.aborted) {
67
+ return [];
68
+ }
58
69
  const error = err instanceof Error ? err : new Error(String(err));
59
70
  listArtifactsErrorCount += 1;
60
71
  if (listArtifactsErrorCount === 1 || listArtifactsErrorCount % 5 === 0) {
61
72
  sentry_1.Sentry.capture('Could not list Argent remote session artifacts', error);
62
73
  logger.warn({ err: error, failedArtifactListCount: listArtifactsErrorCount }, 'Could not list Argent remote session artifacts.');
63
74
  }
75
+ return [];
64
76
  }
65
- await (0, retry_1.sleepAsync)(ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS);
77
+ };
78
+ const listAndQueueArtifactUploadsAsync = async ({ signal, } = {}) => {
79
+ const artifacts = await listArtifactsForUploadAsync({ signal });
80
+ for (const artifact of artifacts) {
81
+ queueArtifactUpload(artifact);
82
+ }
83
+ };
84
+ while (!signal.aborted) {
85
+ await listAndQueueArtifactUploadsAsync({ signal });
86
+ await sleepUntilAbortedAsync(ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS, signal);
66
87
  }
88
+ logger.info('Argent artifact polling stopped; draining remaining artifacts.');
89
+ await listAndQueueArtifactUploadsAsync();
90
+ await waitForPendingUploadsAsync({
91
+ pendingUploads,
92
+ timeoutMs: ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS,
93
+ logger,
94
+ });
67
95
  }
68
- async function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, }) {
96
+ async function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, signal, }) {
69
97
  const response = await (0, node_fetch_1.default)(new URL('/artifacts', toolsUrl).toString(), {
70
98
  headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
99
+ signal: createTimeoutSignal({ signal, timeoutMs: ARGENT_ARTIFACT_FETCH_TIMEOUT_MS }),
71
100
  });
72
101
  if (!response.ok) {
73
102
  throw new eas_build_job_1.SystemError(`Failed to list Argent artifacts: ${response.status} ${response.statusText}`);
@@ -97,6 +126,7 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
97
126
  artifactId: artifact.id,
98
127
  name: `${filename} (${artifact.id})`,
99
128
  filename,
129
+ kind: undefined,
100
130
  size,
101
131
  stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
102
132
  });
@@ -108,6 +138,10 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
108
138
  async function downloadArgentArtifactToFileAsync({ artifact, toolsUrl, toolsAuthToken, destinationPath, }) {
109
139
  const response = await (0, node_fetch_1.default)(new URL(`/artifacts/${artifact.id}`, toolsUrl).toString(), {
110
140
  headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
141
+ signal: createTimeoutSignal({
142
+ signal: undefined,
143
+ timeoutMs: ARGENT_ARTIFACT_FETCH_TIMEOUT_MS,
144
+ }),
111
145
  });
112
146
  if (!response.ok) {
113
147
  throw new eas_build_job_1.SystemError(`Failed to download Argent artifact ${artifact.id}: ${response.status} ${response.statusText}`);
@@ -117,3 +151,41 @@ async function downloadArgentArtifactToFileAsync({ artifact, toolsUrl, toolsAuth
117
151
  }
118
152
  await (0, promises_2.pipeline)(response.body, (0, node_fs_1.createWriteStream)(destinationPath));
119
153
  }
154
+ async function sleepUntilAbortedAsync(timeoutMs, signal) {
155
+ try {
156
+ await (0, promises_3.setTimeout)(timeoutMs, undefined, { signal });
157
+ }
158
+ catch (err) {
159
+ if (!signal.aborted) {
160
+ throw err;
161
+ }
162
+ }
163
+ }
164
+ function createTimeoutSignal({ signal, timeoutMs, }) {
165
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
166
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
167
+ }
168
+ async function waitForPendingUploadsAsync({ pendingUploads, timeoutMs, logger, }) {
169
+ if (pendingUploads.size === 0) {
170
+ return;
171
+ }
172
+ logger.info(`Waiting for ${pendingUploads.size} Argent artifact upload(s) to finish.`);
173
+ let timeout;
174
+ try {
175
+ await Promise.race([
176
+ Promise.allSettled([...pendingUploads]),
177
+ new Promise((_resolve, reject) => {
178
+ timeout = setTimeout(() => {
179
+ reject(new eas_build_job_1.SystemError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for Argent artifact uploads.`, {
180
+ trackingCode: 'SIMULATOR_ARTIFACT_SLOW_UPLOAD',
181
+ }));
182
+ }, timeoutMs);
183
+ }),
184
+ ]);
185
+ }
186
+ finally {
187
+ if (timeout) {
188
+ clearTimeout(timeout);
189
+ }
190
+ }
191
+ }
@@ -1,9 +1,11 @@
1
1
  import { CustomBuildContext } from '../../customBuildContext';
2
- export declare function uploadDeviceRunSessionArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, artifactId, name, filename, size, stream, }: {
2
+ export declare function uploadDeviceRunSessionArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, stream, }: {
3
3
  deviceRunSessionId: string;
4
4
  artifactId: string;
5
5
  name: string;
6
6
  filename: string;
7
+ kind: string | undefined;
8
+ metadata?: Record<string, unknown>;
7
9
  size: number;
8
10
  stream: NodeJS.ReadableStream;
9
11
  }): Promise<void>;
@@ -52,12 +52,14 @@ const CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION = (0, gql_tada_
52
52
  }
53
53
  }
54
54
  `);
55
- async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, stream, }) {
55
+ async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, stream, }) {
56
56
  const uploadSession = await createDeviceRunSessionArtifactUploadSessionAsync(ctx, {
57
57
  deviceRunSessionId,
58
58
  artifactId,
59
59
  name,
60
60
  filename,
61
+ kind,
62
+ metadata,
61
63
  size,
62
64
  });
63
65
  const response = await (0, node_fetch_1.default)(uploadSession.url, {
@@ -69,13 +71,15 @@ async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, ar
69
71
  throw new eas_build_job_1.SystemError(`Failed to upload device run session artifact ${artifactId}: ${response.status} ${response.statusText}`, { cause: response });
70
72
  }
71
73
  }
72
- async function createDeviceRunSessionArtifactUploadSessionAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, }) {
74
+ async function createDeviceRunSessionArtifactUploadSessionAsync(ctx, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, }) {
73
75
  const result = await ctx.graphqlClient
74
76
  .mutation(CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION, {
75
77
  deviceRunSessionId,
76
78
  input: {
77
79
  name,
78
80
  filename,
81
+ ...(kind !== undefined ? { kind } : {}),
82
+ ...(metadata !== undefined ? { metadata } : {}),
79
83
  size,
80
84
  },
81
85
  })
@@ -15,6 +15,12 @@ export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
15
15
  env: BuildStepEnv;
16
16
  logger: bunyan;
17
17
  }): Promise<void>;
18
+ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }: {
19
+ ctx: CustomBuildContext;
20
+ deviceRunSessionId: string;
21
+ logger: bunyan;
22
+ signal?: AbortSignal;
23
+ }): Promise<void>;
18
24
  /**
19
25
  * Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
20
26
  * credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
@@ -42,6 +48,8 @@ export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId
42
48
  logger: bunyan;
43
49
  }): Promise<void>;
44
50
  export type DetachedProcessHandle = {
51
+ /** PID of the directly spawned process, if the OS assigned one. */
52
+ pid: number | undefined;
45
53
  getOutput: () => string;
46
54
  };
47
55
  export declare function spawnDetached({ command, args, cwd, env, }: {
@@ -40,6 +40,7 @@ exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
40
40
  exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
41
41
  exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
42
42
  exports.selectXcodeDeveloperDirectoryAsync = selectXcodeDeveloperDirectoryAsync;
43
+ exports.waitForDeviceRunSessionStoppedAsync = waitForDeviceRunSessionStoppedAsync;
43
44
  exports.turnIceServersToServeSimArgs = turnIceServersToServeSimArgs;
44
45
  exports.fetchServeSimTurnArgsAsync = fetchServeSimTurnArgsAsync;
45
46
  exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
@@ -56,6 +57,7 @@ const nullthrows_1 = __importDefault(require("nullthrows"));
56
57
  const zod_1 = require("zod");
57
58
  const node_crypto_1 = require("node:crypto");
58
59
  const node_fs_1 = __importDefault(require("node:fs"));
60
+ const promises_1 = require("node:timers/promises");
59
61
  const sentry_1 = require("../../sentry");
60
62
  const retry_1 = require("../../utils/retry");
61
63
  const turtleFetch_1 = require("../../utils/turtleFetch");
@@ -73,6 +75,17 @@ const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
73
75
  }
74
76
  }
75
77
  `);
78
+ const DEVICE_RUN_SESSION_STATUS_QUERY = (0, gql_tada_1.graphql)(`
79
+ query DeviceRunSessionStatus($deviceRunSessionId: ID!) {
80
+ deviceRunSessions {
81
+ byId(deviceRunSessionId: $deviceRunSessionId) {
82
+ id
83
+ status
84
+ }
85
+ }
86
+ }
87
+ `);
88
+ const DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS = 5_000;
76
89
  function getDeviceRunSessionIdOrThrow(env) {
77
90
  const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
78
91
  if (!deviceRunSessionId) {
@@ -117,6 +130,54 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
117
130
  stdio: ['ignore', 'pipe', 'pipe'],
118
131
  });
119
132
  }
133
+ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }) {
134
+ logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
135
+ let pollErrorCount = 0;
136
+ while (!signal?.aborted) {
137
+ try {
138
+ const result = await ctx.graphqlClient
139
+ .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
140
+ .toPromise();
141
+ if (result.error) {
142
+ throw result.error;
143
+ }
144
+ const status = result.data?.deviceRunSessions?.byId?.status;
145
+ if (!status) {
146
+ throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
147
+ }
148
+ pollErrorCount = 0;
149
+ if (status === 'STOPPED') {
150
+ logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
151
+ return;
152
+ }
153
+ if (status === 'ERRORED') {
154
+ throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
155
+ }
156
+ }
157
+ catch (err) {
158
+ if (err instanceof eas_build_job_1.SystemError) {
159
+ throw err;
160
+ }
161
+ const error = err instanceof Error ? err : new Error(String(err));
162
+ pollErrorCount += 1;
163
+ if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
164
+ sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
165
+ logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
166
+ }
167
+ }
168
+ await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
169
+ }
170
+ }
171
+ async function sleepUntilAbortedAsync(timeoutMs, signal) {
172
+ try {
173
+ await (0, promises_1.setTimeout)(timeoutMs, undefined, signal ? { signal } : undefined);
174
+ }
175
+ catch (err) {
176
+ if (!signal?.aborted) {
177
+ throw err;
178
+ }
179
+ }
180
+ }
120
181
  const TurnIceServersResponseSchema = zod_1.z.object({
121
182
  data: zod_1.z.object({
122
183
  iceServers: TurnIceServersSchema,
@@ -204,7 +265,7 @@ function spawnDetached({ command, args, cwd, env, }) {
204
265
  };
205
266
  promise.child.stdout?.on('data', appendChunk);
206
267
  promise.child.stderr?.on('data', appendChunk);
207
- return { getOutput: () => output };
268
+ return { pid: promise.child.pid, getOutput: () => output };
208
269
  }
209
270
  async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
210
271
  logger.info('Launching serve-sim with tunnel.');
@@ -240,6 +240,9 @@ var AndroidEmulatorUtils;
240
240
  await emulatorPromise;
241
241
  }
242
242
  emulatorPromise.child.unref();
243
+ // The emulator is detached and managed through adb. Observe its process promise
244
+ // immediately so expected exits during retry cleanup do not become unhandled rejections.
245
+ void (0, results_1.asyncResult)(emulatorPromise);
243
246
  const serialId = await (0, retry_1.retryAsync)(async () => {
244
247
  const serialId = await getSerialIdAsync({ deviceName, env });
245
248
  (0, assert_1.default)(serialId, `Failed to configure emulator (${serialId}): emulator with required ID not found.`);
@@ -24,6 +24,7 @@ export declare namespace IosSimulatorUtils {
24
24
  };
25
25
  type SimulatorDevice = XcrunSimctlDevice & {
26
26
  runtime: string;
27
+ runtimeDisplayName: string;
27
28
  displayName: string;
28
29
  };
29
30
  export function getAvailableDevicesAsync({ env, filter, }: {
@@ -21,6 +21,7 @@ var IosSimulatorUtils;
21
21
  allAvailableDevices.push(...devices.map(device => ({
22
22
  ...device,
23
23
  runtime,
24
+ runtimeDisplayName: formatRuntimeDisplayName(runtime),
24
25
  displayName: `${device.name} (${device.udid}) on ${runtime}`,
25
26
  })));
26
27
  }
@@ -118,8 +119,30 @@ var IosSimulatorUtils;
118
119
  }
119
120
  IosSimulatorUtils.waitForReadyAsync = waitForReadyAsync;
120
121
  async function disableApsdAsync({ udid, env, }) {
121
- await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'disable', 'system/com.apple.apsd'], { env });
122
- await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'bootout', 'system/com.apple.apsd'], { env });
122
+ const launchctlDomains = ['user/foreground', 'system'];
123
+ let lastError;
124
+ for (const domain of launchctlDomains) {
125
+ const service = `${domain}/com.apple.apsd`;
126
+ try {
127
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'disable', service], { env });
128
+ try {
129
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'bootout', service], {
130
+ env,
131
+ });
132
+ }
133
+ catch (err) {
134
+ // bootout can fail when apsd is already gone; verify the service state below.
135
+ lastError = err;
136
+ }
137
+ if (!(await isLaunchctlServiceLoadedAsync({ udid, env, serviceLabel: 'com.apple.apsd' }))) {
138
+ return;
139
+ }
140
+ }
141
+ catch (err) {
142
+ lastError = err;
143
+ }
144
+ }
145
+ throw lastError ?? new eas_build_job_1.SystemError('Unable to disable apsd in the Simulator.');
123
146
  }
124
147
  IosSimulatorUtils.disableApsdAsync = disableApsdAsync;
125
148
  async function collectLogsAsync({ deviceIdentifier, env, }) {
@@ -196,6 +219,22 @@ var IosSimulatorUtils;
196
219
  }
197
220
  IosSimulatorUtils.isDataMigratorProcessRunning = isDataMigratorProcessRunning;
198
221
  })(IosSimulatorUtils || (exports.IosSimulatorUtils = IosSimulatorUtils = {}));
222
+ function formatRuntimeDisplayName(runtimeIdentifier) {
223
+ const match = /^com\.apple\.CoreSimulator\.SimRuntime\.([^-]+)-(.+)$/.exec(runtimeIdentifier);
224
+ return match ? `${match[1]} ${match[2].replaceAll('-', '.')}` : runtimeIdentifier;
225
+ }
226
+ async function isLaunchctlServiceLoadedAsync({ udid, env, serviceLabel, }) {
227
+ try {
228
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'list', serviceLabel], { env });
229
+ return true;
230
+ }
231
+ catch (err) {
232
+ if (err instanceof Error && 'status' in err && err.status === 113) {
233
+ return false;
234
+ }
235
+ throw err;
236
+ }
237
+ }
199
238
  function parseUdidFromBootstatusStdout(stdout) {
200
239
  const matches = stdout.match(/^Monitoring boot status for .+ \((.+)\)\.$/m);
201
240
  if (!matches) {
@@ -20,6 +20,10 @@ async function uploadEmbeddedBundleAsync(ctx) {
20
20
  ctx.markBuildPhaseSkipped();
21
21
  return;
22
22
  }
23
+ if (ctx.job.developmentClient) {
24
+ ctx.markBuildPhaseSkipped();
25
+ return;
26
+ }
23
27
  const { platform } = ctx.job;
24
28
  if (platform === eas_build_job_1.Platform.IOS && ctx.job.simulator) {
25
29
  ctx.markBuildPhaseSkipped();
@@ -1 +1,2 @@
1
1
  export declare function getParentAndDescendantProcessPidsAsync(ppid: number): Promise<number[]>;
2
+ export declare function isProcessDescendantOfAsync(pid: number, ancestorPid: number): Promise<boolean>;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getParentAndDescendantProcessPidsAsync = getParentAndDescendantProcessPidsAsync;
7
+ exports.isProcessDescendantOfAsync = isProcessDescendantOfAsync;
7
8
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
8
9
  async function getChildrenPidsAsync(parentPids) {
9
10
  try {
@@ -35,3 +36,25 @@ async function getParentAndDescendantProcessPidsAsync(ppid) {
35
36
  }
36
37
  return [...children];
37
38
  }
39
+ async function isProcessDescendantOfAsync(pid, ancestorPid) {
40
+ let currentPid = pid;
41
+ while (currentPid > 0) {
42
+ if (currentPid === ancestorPid) {
43
+ return true;
44
+ }
45
+ try {
46
+ const result = await (0, turtle_spawn_1.default)('ps', ['-p', String(currentPid), '-o', 'ppid='], {
47
+ stdio: 'pipe',
48
+ });
49
+ const parentPid = Number(result.stdout.toString().trim());
50
+ if (!Number.isInteger(parentPid) || parentPid <= 0 || parentPid === currentPid) {
51
+ return false;
52
+ }
53
+ currentPid = parentPid;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ return false;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "20.5.1",
3
+ "version": "21.0.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -23,6 +23,7 @@
23
23
  "watch": "tsc --watch --preserveWatchOutput",
24
24
  "prebuild": "yarn gql",
25
25
  "build": "tsc",
26
+ "build:record-sim": "mkdir -p bin && record_sim_bin_path=$(swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build --show-bin-path) && swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build && cp \"$record_sim_bin_path/record-sim\" bin/record-sim && chmod +x bin/record-sim",
26
27
  "typecheck": "tsc",
27
28
  "prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
28
29
  "jest-unit": "jest --config jest/unit-config.ts",
@@ -37,17 +38,17 @@
37
38
  "dependencies": {
38
39
  "@expo/config": "55.0.10",
39
40
  "@expo/config-plugins": "55.0.7",
40
- "@expo/downloader": "20.0.0",
41
- "@expo/eas-build-job": "20.5.1",
41
+ "@expo/downloader": "21.0.0",
42
+ "@expo/eas-build-job": "21.0.0",
42
43
  "@expo/env": "^0.4.0",
43
- "@expo/logger": "20.0.0",
44
+ "@expo/logger": "21.0.0",
44
45
  "@expo/package-manager": "1.9.10",
45
46
  "@expo/plist": "^0.2.0",
46
47
  "@expo/results": "^1.0.0",
47
48
  "@expo/spawn-async": "1.7.2",
48
- "@expo/steps": "20.5.1",
49
- "@expo/template-file": "20.0.0",
50
- "@expo/turtle-spawn": "20.0.0",
49
+ "@expo/steps": "21.0.0",
50
+ "@expo/template-file": "21.0.0",
51
+ "@expo/turtle-spawn": "21.0.0",
51
52
  "@expo/xcpretty": "^4.3.1",
52
53
  "@google-cloud/storage": "^7.11.2",
53
54
  "@ngrok/ngrok": "1.7.0",
@@ -100,5 +101,5 @@
100
101
  "typescript": "^5.5.4",
101
102
  "uuid": "^9.0.1"
102
103
  },
103
- "gitHead": "cd47185f29446299b7682d52cd917eb9a01038e7"
104
+ "gitHead": "c72cee372f2d45a252acb014493311de85776497"
104
105
  }
@@ -0,0 +1,28 @@
1
+ // swift-tools-version: 5.9
2
+
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "record-sim",
7
+ platforms: [
8
+ .macOS(.v14),
9
+ ],
10
+ products: [
11
+ .library(name: "RecordSim", targets: ["RecordSim"]),
12
+ .executable(name: "record-sim", targets: ["record-sim"]),
13
+ ],
14
+ targets: [
15
+ .target(
16
+ name: "RecordSim",
17
+ linkerSettings: [
18
+ .linkedFramework("AVFoundation"),
19
+ .linkedFramework("CoreGraphics"),
20
+ .linkedFramework("CoreMedia"),
21
+ .linkedFramework("CoreVideo"),
22
+ .linkedFramework("IOSurface"),
23
+ .linkedFramework("UniformTypeIdentifiers"),
24
+ ]
25
+ ),
26
+ .executableTarget(name: "record-sim", dependencies: ["RecordSim"]),
27
+ ]
28
+ )
@@ -0,0 +1,79 @@
1
+ # RecordSim
2
+
3
+ Small Swift library for non-exclusive iOS Simulator screen recording.
4
+
5
+ It implements one recording path:
6
+
7
+ 1. Listen to SimRenderServer framebuffer callbacks.
8
+ 2. Copy the live `IOSurface` immediately into an owned `CVPixelBuffer`.
9
+ 3. Feed one continuous `AVAssetWriter`.
10
+ 4. Emit Apple HLS/CMAF-style fragmented MP4 segments.
11
+
12
+ The output is suitable for uploading while recording. `session.json` contains
13
+ the ordered segment metadata needed to build a playlist later.
14
+
15
+ ## Output
16
+
17
+ ```text
18
+ session/
19
+ init.mp4
20
+ session.json
21
+ segments/
22
+ segment-000000.m4s
23
+ segment-000001.m4s
24
+ ```
25
+
26
+ `init.mp4` is the fMP4 initialization segment. Individual `.m4s` files are not
27
+ standalone MP4 files. `session.json` stores the wall-clock timestamp for the
28
+ first video frame plus the metadata needed to build a playlist: HLS version,
29
+ target duration, media sequence, `initSegment`, and the ordered `segments` array
30
+ with file path and duration for each media segment.
31
+
32
+ ## CLI
33
+
34
+ ```bash
35
+ swift run record-sim \
36
+ --udid <BOOTED_SIMULATOR_UDID> \
37
+ --output /tmp/sim-recording \
38
+ --segment-duration 120 \
39
+ --fps 30 \
40
+ --bitrate 30000000 \
41
+ --codec h264
42
+ ```
43
+
44
+ For sparse 2h sessions, `--segment-duration 120` is a reasonable default: about
45
+ 60 media objects plus the init segment. The recorder appends a single duplicate
46
+ hold frame near each segment boundary when the simulator is idle, so long idle
47
+ periods still produce uploadable segments without encoding idle frames at 60fps.
48
+ Use `--segment-duration 0` to write one continuous `recording.mp4` instead of
49
+ `init.mp4` plus media segments.
50
+
51
+ Frame timestamps use monotonic host time, not wall-clock time. The recorder also
52
+ probes the framebuffer seed once per second; if the surface changes without
53
+ callbacks for 5 seconds, it captures that frame and rewires the private callback
54
+ registration.
55
+
56
+ ## Library
57
+
58
+ ```swift
59
+ let recorder = SimulatorRecorder(
60
+ configuration: SimulatorRecordingConfiguration(
61
+ deviceUDID: udid,
62
+ outputDirectory: sessionDirectory,
63
+ segmentDuration: 120
64
+ )
65
+ )
66
+
67
+ recorder.onSegment = { segment in
68
+ // Enqueue upload of segment.url.
69
+ }
70
+
71
+ try recorder.start()
72
+ try recorder.waitUntilFirstFrame()
73
+ // ...
74
+ let manifest = try recorder.stop()
75
+ ```
76
+
77
+ `session.json` contains the precise wall-clock time for video PTS zero. In
78
+ segmented mode it also contains the init segment and ordered media segment
79
+ file/duration entries; in single-file mode it points at `recording.mp4`.