@expo/build-tools 21.7.1 → 22.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.
@@ -5,6 +5,7 @@ const eas_build_job_1 = require("@expo/eas-build-job");
5
5
  const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
6
6
  const artifacts_1 = require("../utils/artifacts");
7
7
  const hooks_1 = require("../utils/hooks");
8
+ const sourceMaps_1 = require("../utils/sourceMaps");
8
9
  async function runBuilderWithHooksAsync(ctx, builderAsync) {
9
10
  try {
10
11
  let buildSuccess = true;
@@ -35,6 +36,9 @@ async function runBuilderWithHooksAsync(ctx, builderAsync) {
35
36
  });
36
37
  }
37
38
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_BUILD_ARTIFACTS, async () => {
39
+ if (buildSuccess) {
40
+ await (0, sourceMaps_1.maybeUploadSourceMapAsync)(ctx);
41
+ }
38
42
  await (0, artifacts_1.maybeFindAndUploadBuildArtifacts)(ctx, {
39
43
  logger: ctx.logger,
40
44
  });
@@ -30,6 +30,7 @@ const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
30
30
  const hooks_1 = require("../utils/hooks");
31
31
  const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
32
32
  const processes_1 = require("../utils/processes");
33
+ const sourceMaps_1 = require("../utils/sourceMaps");
33
34
  const INSTALL_PODS_WARN_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
34
35
  const INSTALL_PODS_KILL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
35
36
  class InstallPodsTimeoutError extends Error {
@@ -151,19 +152,23 @@ async function buildInnerAsync(ctx, jobHooksRef) {
151
152
  fastlaneResult = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.RUN_FASTLANE, async () => {
152
153
  const scheme = (0, resolve_1.resolveScheme)(ctx);
153
154
  const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
155
+ const sourceMapPath = (0, sourceMaps_1.isSourceMapUploadEnabled)(ctx)
156
+ ? await (0, sourceMaps_1.resolveIosSourceMapPathAsync)(ctx)
157
+ : null;
154
158
  return await (0, fastlane_1.runFastlaneGym)(ctx, {
155
159
  credentials,
156
160
  scheme,
157
161
  buildConfiguration,
158
162
  entitlements,
159
- ...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
160
- ? {
161
- extraEnv: {
163
+ extraEnv: {
164
+ ...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
165
+ ? {
162
166
  EXPO_UPDATES_FINGERPRINT_OVERRIDE: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
163
167
  EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
164
- },
165
- }
166
- : null),
168
+ }
169
+ : null),
170
+ ...(sourceMapPath ? { SOURCEMAP_FILE: sourceMapPath } : null),
171
+ },
167
172
  });
168
173
  });
169
174
  }
@@ -11,6 +11,7 @@ const assert_1 = __importDefault(require("assert"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const os_1 = __importDefault(require("os"));
13
13
  const path_1 = __importDefault(require("path"));
14
+ const maestroBackend_1 = require("./maestroBackend");
14
15
  const datadog_1 = require("../../datadog");
15
16
  function createInstallMaestroBuildFunction() {
16
17
  return new steps_1.BuildFunction({
@@ -24,6 +25,11 @@ function createInstallMaestroBuildFunction() {
24
25
  required: false,
25
26
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
26
27
  }),
28
+ steps_1.BuildStepInput.createProvider({
29
+ id: 'backend',
30
+ required: false,
31
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
32
+ }),
27
33
  ],
28
34
  outputProviders: [
29
35
  steps_1.BuildStepOutput.createProvider({
@@ -32,35 +38,43 @@ function createInstallMaestroBuildFunction() {
32
38
  }),
33
39
  ],
34
40
  fn: async ({ logger, global }, { inputs, env, outputs }) => {
35
- const requestedMaestroVersion = inputs.maestro_version.value;
36
- const { value: currentMaestroVersion } = await (0, results_1.asyncResult)(getMaestroVersion({ env }));
41
+ const backend = (0, maestroBackend_1.resolveMaestroBackend)({
42
+ input: inputs.backend.value,
43
+ env,
44
+ });
45
+ const requestedVersion = inputs.maestro_version.value;
46
+ const { value: currentMaestroVersion } = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
37
47
  // When not running in EAS Build VM, do not modify local environment.
38
48
  if (env.EAS_BUILD_RUNNER !== 'eas-build') {
39
- const currentIsJavaInstalled = await isJavaInstalled({ env });
40
- const currentIsIdbInstalled = await isIdbInstalled({ env });
41
- if (!currentIsJavaInstalled) {
49
+ const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
50
+ const needsToInstallIdb = backend === 'maestro' && !(await isIdbInstalled({ env }));
51
+ if (needsToInstallJava) {
42
52
  logger.warn('It seems Java is not installed. It is required to run Maestro. If the job fails, this may be the reason.');
43
53
  logger.info('');
44
54
  }
45
- if (!currentIsIdbInstalled) {
55
+ if (needsToInstallIdb) {
46
56
  logger.warn('It seems IDB is not installed. Maestro requires it to run flows on iOS Simulator. If the job fails, this may be the reason.');
47
57
  logger.info('');
48
58
  }
49
59
  if (!currentMaestroVersion) {
50
- logger.warn('It seems Maestro is not installed. Please install Maestro manually and rerun the job.');
60
+ logger.warn(`It seems ${backend} is not installed. Please install it manually and rerun the job.`);
51
61
  logger.info('');
52
62
  }
53
63
  // Guide is helpful in these two cases, it doesn't mention Java.
54
- if (!currentIsIdbInstalled || !currentMaestroVersion) {
64
+ if (backend === 'maestro' && (needsToInstallIdb || !currentMaestroVersion)) {
55
65
  logger.warn('For more info, check out Maestro installation guide: https://maestro.mobile.dev/getting-started/installing-maestro');
56
66
  }
67
+ else if (backend === 'maestro-runner' && !currentMaestroVersion) {
68
+ logger.warn('For more info, check out maestro-runner installation guide: https://github.com/devicelab-dev/maestro-runner#install');
69
+ }
57
70
  if (currentMaestroVersion) {
58
71
  outputs.maestro_version.set(currentMaestroVersion);
59
- logger.info(`Maestro ${currentMaestroVersion} is ready.`);
72
+ logger.info(`${backend} ${currentMaestroVersion} is ready.`);
60
73
  }
61
74
  return;
62
75
  }
63
- if (!(await isJavaInstalled({ env }))) {
76
+ const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
77
+ if (needsToInstallJava) {
64
78
  if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
65
79
  logger.info('Installing Java');
66
80
  await installJavaFromGcs({ logger, env });
@@ -73,45 +87,83 @@ function createInstallMaestroBuildFunction() {
73
87
  }
74
88
  }
75
89
  // IDB is only a requirement on macOS.
76
- if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
77
- !(await isIdbInstalled({ env }))) {
90
+ const needsToInstallIdb = backend === 'maestro' &&
91
+ global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
92
+ !(await isIdbInstalled({ env }));
93
+ if (needsToInstallIdb) {
78
94
  logger.info('Installing IDB');
79
95
  await installIdbFromBrew({ logger, env });
80
96
  }
81
97
  // Skip installing if the input sets a specific Maestro version to install
82
98
  // and it is already installed which happens when developing on a local computer.
83
99
  if (!currentMaestroVersion ||
84
- (requestedMaestroVersion && requestedMaestroVersion !== currentMaestroVersion)) {
85
- await installMaestro({
86
- version: requestedMaestroVersion,
87
- global,
88
- logger,
89
- env,
90
- });
100
+ (requestedVersion && requestedVersion !== currentMaestroVersion)) {
101
+ switch (backend) {
102
+ case 'maestro':
103
+ await installMaestro({ version: requestedVersion, global, logger, env });
104
+ break;
105
+ case 'maestro-runner':
106
+ await installMaestroRunner({ version: requestedVersion, global, logger, env });
107
+ break;
108
+ }
91
109
  }
92
- const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env }));
110
+ const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
93
111
  if (!maestroVersionResult.ok) {
94
112
  logger.error(maestroVersionResult.reason, 'Failed to get Maestro version.');
95
- throw new Error('Failed to ensure Maestro is installed.');
113
+ throw new Error(`Failed to ensure ${backend} is installed.`);
96
114
  }
97
- logger.info(`Maestro ${maestroVersionResult.value} is ready.`);
115
+ logger.info(`${backend} ${maestroVersionResult.value} is ready.`);
98
116
  outputs.maestro_version.set(maestroVersionResult.value);
99
117
  datadog_1.Datadog.distribution('eas.maestro.install', 1, {
100
118
  maestro_version: maestroVersionResult.value,
119
+ maestro_backend: backend,
101
120
  });
102
121
  },
103
122
  });
104
123
  }
105
- async function getMaestroVersion({ env }) {
106
- const { stdout } = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
107
- // `maestro --version` can print an analytics notice to stdout before the version,
108
- // e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
109
- // Take the last version-looking token: the real version is printed after the notice, so
110
- // this stays correct even if the notice itself contains a version-like string. Keeps the
111
- // step output and the eas.maestro.install metric tag clean. Best-effort only: fall back to
112
- // the raw output if none is found, so we never fail the build over a version string.
113
- const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
114
- return versions?.at(-1) ?? stdout.trim();
124
+ async function getMaestroVersion({ env, backend, }) {
125
+ switch (backend) {
126
+ case 'maestro': {
127
+ const { stdout } = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
128
+ // `maestro --version` can print an analytics notice to stdout before the version,
129
+ // e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
130
+ // Take the last version-looking token: the real version is printed after the notice.
131
+ const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
132
+ return versions?.at(-1) ?? stdout.trim();
133
+ }
134
+ case 'maestro-runner': {
135
+ const { stdout } = await (0, turtle_spawn_1.default)('maestro-runner', ['--version'], { stdio: 'pipe', env });
136
+ // maestro-runner prints build information after its version. The Go runtime version in
137
+ // that output is also semver-shaped, so read only the prefixed runner version.
138
+ return /^maestro-runner\s+(\S+)/m.exec(stdout)?.[1] ?? stdout.trim();
139
+ }
140
+ }
141
+ }
142
+ async function installMaestroRunner({ global, version, logger, env, }) {
143
+ logger.info('Fetching maestro-runner install script');
144
+ const tempDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'install_maestro_runner'));
145
+ try {
146
+ const installMaestroRunnerScriptResponse = await fetch('https://open.devicelab.dev/install/maestro-runner');
147
+ const installMaestroRunnerScript = await installMaestroRunnerScriptResponse.text();
148
+ const scriptPath = path_1.default.join(tempDirectory, 'install_maestro_runner.sh');
149
+ await fs_1.default.promises.writeFile(scriptPath, installMaestroRunnerScript, { mode: 0o777 });
150
+ logger.info('Installing maestro-runner');
151
+ (0, assert_1.default)(env.HOME, 'Failed to infer directory to install maestro-runner in: $HOME environment variable is empty.');
152
+ await (0, turtle_spawn_1.default)(scriptPath, version && version !== 'latest' ? ['--version', version] : [], {
153
+ logger,
154
+ env,
155
+ });
156
+ const binDir = path_1.default.join(env.HOME, '.maestro-runner', 'bin');
157
+ global.updateEnv({
158
+ ...global.env,
159
+ PATH: `${global.env.PATH}:${binDir}`,
160
+ });
161
+ env.PATH = `${env.PATH}:${binDir}`;
162
+ process.env.PATH = `${process.env.PATH}:${binDir}`;
163
+ }
164
+ finally {
165
+ await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
166
+ }
115
167
  }
116
168
  async function installMaestro({ global, version, logger, env, }) {
117
169
  logger.info('Fetching install script');
@@ -0,0 +1,11 @@
1
+ import { BuildStepEnv } from '@expo/steps';
2
+ import { z } from 'zod';
3
+ export declare const MaestroBackendSchema: z.ZodDefault<z.ZodEnum<{
4
+ maestro: "maestro";
5
+ "maestro-runner": "maestro-runner";
6
+ }>>;
7
+ export type MaestroBackend = z.output<typeof MaestroBackendSchema>;
8
+ export declare function resolveMaestroBackend({ input, env, }: {
9
+ input: unknown;
10
+ env: BuildStepEnv;
11
+ }): MaestroBackend;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MaestroBackendSchema = void 0;
4
+ exports.resolveMaestroBackend = resolveMaestroBackend;
5
+ const eas_build_job_1 = require("@expo/eas-build-job");
6
+ const zod_1 = require("zod");
7
+ exports.MaestroBackendSchema = zod_1.z.enum(['maestro', 'maestro-runner']).default('maestro');
8
+ function resolveMaestroBackend({ input, env, }) {
9
+ const result = exports.MaestroBackendSchema.safeParse(input || env.EAS_MAESTRO_BACKEND || undefined);
10
+ if (!result.success) {
11
+ throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', 'backend and EAS_MAESTRO_BACKEND must be either "maestro" or "maestro-runner".', { cause: result.error });
12
+ }
13
+ return result.data;
14
+ }
@@ -18,6 +18,7 @@ const retry_1 = require("../../utils/retry");
18
18
  const FlowPathSchema = zod_1.z.array(zod_1.z.string().min(1)).min(1);
19
19
  const RetriesSchema = zod_1.z.number().int().min(0).default(0);
20
20
  const ShardsSchema = zod_1.z.number().int().min(1).optional();
21
+ const AndroidConnectionModeSchema = zod_1.z.enum(['adb', 'dadb']).default('adb');
21
22
  function parseInput(schema, value, message) {
22
23
  const result = schema.safeParse(value);
23
24
  if (!result.success) {
@@ -109,6 +110,11 @@ function createMaestroTestsBuildFunction(ctx) {
109
110
  required: false,
110
111
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
111
112
  }),
113
+ steps_1.BuildStepInput.createProvider({
114
+ id: 'android_connection_mode',
115
+ required: false,
116
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
117
+ }),
112
118
  ],
113
119
  outputProviders: [
114
120
  steps_1.BuildStepOutput.createProvider({ id: 'junit_report_directory', required: true }),
@@ -153,6 +159,9 @@ function createMaestroTestsBuildFunction(ctx) {
153
159
  const flowPaths = parseInput(FlowPathSchema, inputs.flow_path.value, 'flow_path must be a non-empty array of non-empty strings.');
154
160
  const retries = parseInput(RetriesSchema, inputs.retries.value, 'retries must be a non-negative integer.');
155
161
  const shards = parseInput(ShardsSchema, inputs.shards.value, 'shards must be a positive integer.');
162
+ const androidConnectionMode = parseInput(AndroidConnectionModeSchema, inputs.android_connection_mode.value ||
163
+ env.EAS_MAESTRO_ANDROID_CONNECTION_MODE ||
164
+ undefined, 'android_connection_mode and EAS_MAESTRO_ANDROID_CONNECTION_MODE must be either "adb" or "dadb".');
156
165
  const retryFailedOnly = inputs.retry_failed_only.value;
157
166
  try {
158
167
  await promises_1.default.mkdir(junitReportDirectory, { recursive: true });
@@ -176,6 +185,33 @@ function createMaestroTestsBuildFunction(ctx) {
176
185
  let lastAttemptExitCode = null;
177
186
  const harvested = [];
178
187
  const totalAttempts = retries + 1;
188
+ if (platform === 'android' && androidConnectionMode === 'dadb') {
189
+ try {
190
+ const adbOverrideDirectoryPath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'maestro-tests-adb-override-'));
191
+ await promises_1.default.writeFile(path_1.default.join(adbOverrideDirectoryPath, 'adb'), '#!/bin/sh\nexit 1\n', {
192
+ mode: 0o755,
193
+ });
194
+ // DADB starts an ADB server when it can find an adb binary. Stop the existing
195
+ // server first, then make only the Maestro process find the failing shim.
196
+ try {
197
+ await (0, turtle_spawn_1.default)('adb', ['kill-server'], { env: { ...spawnEnv }, logger, signal });
198
+ logger.info('Using a direct DADB connection for Android Maestro tests after stopping the ADB server.');
199
+ }
200
+ catch (err) {
201
+ logger.warn({ err }, 'Using a direct DADB connection for Android Maestro tests, but failed to stop the ADB server.');
202
+ }
203
+ spawnEnv.PATH = spawnEnv.PATH
204
+ ? `${adbOverrideDirectoryPath}${path_1.default.delimiter}${spawnEnv.PATH}`
205
+ : adbOverrideDirectoryPath;
206
+ }
207
+ catch (err) {
208
+ // Intentionally skip cleanup because the worker is disposable.
209
+ throw new eas_build_job_1.SystemError('Failed to enable direct DADB connection for Maestro', {
210
+ cause: err,
211
+ });
212
+ }
213
+ // Do not restart ADB or remove the override. This keeps Maestro in direct DADB mode.
214
+ }
179
215
  for (let attempt = 0; attempt <= retries; attempt++) {
180
216
  const outputPath = outputFormat === 'junit'
181
217
  ? path_1.default.join(junitReportDirectory, `${platform}-maestro-junit-attempt-${attempt}.xml`)
@@ -8,6 +8,9 @@ const eas_build_job_1 = require("@expo/eas-build-job");
8
8
  const results_1 = require("@expo/results");
9
9
  const steps_1 = require("@expo/steps");
10
10
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ const node_path_1 = __importDefault(require("node:path"));
11
14
  const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
12
15
  const retry_1 = require("../../utils/retry");
13
16
  const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
@@ -43,7 +46,15 @@ function createStartAndroidEmulatorBuildFunction() {
43
46
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
47
  }),
45
48
  ],
46
- fn: async ({ logger }, { inputs, env }) => {
49
+ outputProviders: [
50
+ steps_1.BuildStepOutput.createProvider({
51
+ id: 'logcat_directory',
52
+ required: true,
53
+ }),
54
+ ],
55
+ fn: async ({ logger }, { inputs, outputs, env }) => {
56
+ const logcatDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-android-emulator-logcat-'));
57
+ outputs.logcat_directory.set(logcatDirectory);
47
58
  if (env.EAS_NO_EMULATOR_HOST_SUPPORT_CHECK !== '1') {
48
59
  await assertAndroidEmulatorHostSupportAsync({ env });
49
60
  }
@@ -103,6 +114,7 @@ function createStartAndroidEmulatorBuildFunction() {
103
114
  const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
104
115
  deviceName,
105
116
  env,
117
+ logcatDirectory,
106
118
  });
107
119
  attemptSerialId = startResult.serialId;
108
120
  await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
@@ -184,6 +196,7 @@ function createStartAndroidEmulatorBuildFunction() {
184
196
  const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
185
197
  deviceName: cloneIdentifier,
186
198
  env,
199
+ logcatDirectory,
187
200
  });
188
201
  cloneSerialId = startResult.serialId;
189
202
  logger.info('Waiting for emulator to become ready');
@@ -34,12 +34,14 @@ export declare namespace AndroidEmulatorUtils {
34
34
  env: NodeJS.ProcessEnv;
35
35
  logger: bunyan;
36
36
  }): Promise<void>;
37
- function startAsync({ deviceName, env, }: {
37
+ function startAsync({ deviceName, env, logcatDirectory, }: {
38
38
  deviceName: AndroidVirtualDeviceName;
39
39
  env: NodeJS.ProcessEnv;
40
+ logcatDirectory: string;
40
41
  }): Promise<{
41
42
  emulatorPromise: SpawnPromise<SpawnResult>;
42
43
  serialId: AndroidDeviceSerialId;
44
+ logcatOutputPath: string;
43
45
  }>;
44
46
  function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
45
47
  serialId: AndroidDeviceSerialId;
@@ -4,10 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AndroidEmulatorUtils = void 0;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
8
  const results_1 = require("@expo/results");
8
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
10
  const assert_1 = __importDefault(require("assert"));
10
11
  const fast_glob_1 = __importDefault(require("fast-glob"));
12
+ const node_crypto_1 = require("node:crypto");
11
13
  const node_fs_1 = __importDefault(require("node:fs"));
12
14
  const node_os_1 = __importDefault(require("node:os"));
13
15
  const node_path_1 = __importDefault(require("node:path"));
@@ -212,13 +214,32 @@ var AndroidEmulatorUtils;
212
214
  }
213
215
  }
214
216
  AndroidEmulatorUtils.cloneAsync = cloneAsync;
215
- async function startAsync({ deviceName, env, }) {
217
+ async function startAsync({ deviceName, env, logcatDirectory, }) {
218
+ let logcatOutputPath;
219
+ try {
220
+ await node_fs_1.default.promises.mkdir(logcatDirectory, { recursive: true });
221
+ const safeDeviceName = deviceName.replace(/[^a-zA-Z0-9_.-]/g, '_');
222
+ const timestamp = Math.floor(Date.now() / 1000)
223
+ .toString(16)
224
+ .padStart(8, '0');
225
+ logcatOutputPath = node_path_1.default.join(logcatDirectory, `${safeDeviceName}-${timestamp}-${(0, node_crypto_1.randomBytes)(2).toString('hex')}.log`);
226
+ await node_fs_1.default.promises.writeFile(logcatOutputPath, '');
227
+ }
228
+ catch (err) {
229
+ throw new eas_build_job_1.SystemError(`Failed to prepare Android emulator logcat output for ${deviceName}.`, {
230
+ cause: err,
231
+ });
232
+ }
216
233
  const emulatorPromise = (0, turtle_spawn_1.default)(`${process.env.ANDROID_HOME}/emulator/emulator`, [
217
234
  '-no-window',
218
235
  '-no-boot-anim',
219
236
  '-writable-system',
220
237
  '-noaudio',
221
238
  '-no-snapshot-save',
239
+ '-logcat',
240
+ '*:v',
241
+ '-logcat-output',
242
+ logcatOutputPath,
222
243
  '-avd',
223
244
  deviceName,
224
245
  '-accel',
@@ -255,7 +276,7 @@ var AndroidEmulatorUtils;
255
276
  });
256
277
  // We don't want to await the SpawnPromise here.
257
278
  // eslint-disable-next-line @typescript-eslint/return-await
258
- return { emulatorPromise, serialId };
279
+ return { emulatorPromise, serialId, logcatOutputPath };
259
280
  }
260
281
  AndroidEmulatorUtils.startAsync = startAsync;
261
282
  async function waitForReadyAsync({ serialId, env, timeoutMs = 3 * 60 * 1_000, logger, }) {
@@ -0,0 +1,5 @@
1
+ import { BuildJob } from '@expo/eas-build-job';
2
+ import { BuildContext } from '../context';
3
+ export declare function isSourceMapUploadEnabled(ctx: BuildContext<BuildJob>): boolean;
4
+ export declare function resolveIosSourceMapPathAsync(ctx: BuildContext<BuildJob>): Promise<string>;
5
+ export declare function maybeUploadSourceMapAsync(ctx: BuildContext<BuildJob>): Promise<void>;
@@ -0,0 +1,105 @@
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.isSourceMapUploadEnabled = isSourceMapUploadEnabled;
7
+ exports.resolveIosSourceMapPathAsync = resolveIosSourceMapPathAsync;
8
+ exports.maybeUploadSourceMapAsync = maybeUploadSourceMapAsync;
9
+ const eas_build_job_1 = require("@expo/eas-build-job");
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 ANDROID_SOURCE_MAP_PATTERN = 'android/**/build/generated/sourcemaps/react/**/*.map';
14
+ const SOURCE_MAP_UPLOAD_DIRECTORY = 'observe-source-maps';
15
+ function isSourceMapUploadEnabled(ctx) {
16
+ return !ctx.isLocal && ctx.job.experimental?.uploadSourceMaps === true;
17
+ }
18
+ async function resolveIosSourceMapPathAsync(ctx) {
19
+ const configuredPath = ctx.env.SOURCEMAP_FILE;
20
+ const sourceMapPath = configuredPath
21
+ ? path_1.default.resolve(ctx.getReactNativeProjectDirectory(), 'ios', configuredPath)
22
+ : path_1.default.join(ctx.workingdir, SOURCE_MAP_UPLOAD_DIRECTORY, 'main.jsbundle.map');
23
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(sourceMapPath));
24
+ return sourceMapPath;
25
+ }
26
+ async function maybeUploadSourceMapAsync(ctx) {
27
+ if (!isSourceMapUploadEnabled(ctx)) {
28
+ return;
29
+ }
30
+ try {
31
+ const sourceMapPath = await resolveSourceMapPathAsync(ctx);
32
+ const strippedSourceMapPath = await stripSourcesContentAsync(ctx, sourceMapPath);
33
+ ctx.logger.info(`Uploading source map: ${sourceMapPath}`);
34
+ await ctx.uploadArtifact({
35
+ artifact: {
36
+ type: eas_build_job_1.ManagedArtifactType.SOURCE_MAP,
37
+ paths: [strippedSourceMapPath],
38
+ },
39
+ logger: ctx.logger,
40
+ });
41
+ }
42
+ catch (err) {
43
+ ctx.logger.warn({ err }, 'Failed to upload source map.');
44
+ ctx.markBuildPhaseHasWarnings();
45
+ }
46
+ }
47
+ async function resolveSourceMapPathAsync(ctx) {
48
+ if (ctx.job.platform === eas_build_job_1.Platform.IOS) {
49
+ const sourceMapPath = await resolveIosSourceMapPathAsync(ctx);
50
+ if (!(await fs_extra_1.default.pathExists(sourceMapPath))) {
51
+ throw new eas_build_job_1.SystemError(`The iOS source map was not generated at ${sourceMapPath}.`);
52
+ }
53
+ return sourceMapPath;
54
+ }
55
+ if (ctx.job.platform === eas_build_job_1.Platform.ANDROID) {
56
+ const projectDir = ctx.getReactNativeProjectDirectory();
57
+ const sourceMapPaths = (await (0, fast_glob_1.default)(ANDROID_SOURCE_MAP_PATTERN, {
58
+ absolute: true,
59
+ cwd: projectDir,
60
+ onlyFiles: true,
61
+ })).filter(sourceMapPath => !isIntermediateAndroidSourceMap(sourceMapPath));
62
+ if (sourceMapPaths.length === 0) {
63
+ throw new eas_build_job_1.SystemError('The Android build did not generate a final composed source map.');
64
+ }
65
+ if (sourceMapPaths.length > 1) {
66
+ throw new eas_build_job_1.SystemError(`Found multiple final Android source maps: ${sourceMapPaths.join(', ')}. ` +
67
+ 'Refusing to upload a source map that may not match the application archive.');
68
+ }
69
+ return sourceMapPaths[0];
70
+ }
71
+ throw new eas_build_job_1.SystemError('Source-map upload is not supported for this build platform.');
72
+ }
73
+ function isIntermediateAndroidSourceMap(sourceMapPath) {
74
+ return sourceMapPath.endsWith('.packager.map') || sourceMapPath.endsWith('.compiler.map');
75
+ }
76
+ async function stripSourcesContentAsync(ctx, sourceMapPath) {
77
+ const sourceMap = JSON.parse(await fs_extra_1.default.readFile(sourceMapPath, 'utf8'));
78
+ if (!isRecord(sourceMap) || sourceMap.version !== 3) {
79
+ throw new eas_build_job_1.SystemError(`Invalid source map at ${sourceMapPath}.`);
80
+ }
81
+ removeSourcesContent(sourceMap);
82
+ const uploadDirectory = path_1.default.join(ctx.workingdir, SOURCE_MAP_UPLOAD_DIRECTORY);
83
+ const uploadPath = path_1.default.join(uploadDirectory, `${ctx.job.platform}.map`);
84
+ await fs_extra_1.default.ensureDir(uploadDirectory);
85
+ await fs_extra_1.default.writeFile(uploadPath, JSON.stringify(sourceMap), 'utf8');
86
+ return uploadPath;
87
+ }
88
+ function removeSourcesContent(value) {
89
+ if (Array.isArray(value)) {
90
+ for (const child of value) {
91
+ removeSourcesContent(child);
92
+ }
93
+ return;
94
+ }
95
+ if (!isRecord(value)) {
96
+ return;
97
+ }
98
+ delete value.sourcesContent;
99
+ for (const child of Object.values(value)) {
100
+ removeSourcesContent(child);
101
+ }
102
+ }
103
+ function isRecord(value) {
104
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.7.1",
3
+ "version": "22.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>",
@@ -38,17 +38,17 @@
38
38
  "dependencies": {
39
39
  "@expo/config": "55.0.10",
40
40
  "@expo/config-plugins": "55.0.7",
41
- "@expo/downloader": "21.0.0",
42
- "@expo/eas-build-job": "21.6.0",
41
+ "@expo/downloader": "22.0.0",
42
+ "@expo/eas-build-job": "22.0.0",
43
43
  "@expo/env": "^0.4.0",
44
- "@expo/logger": "21.0.0",
44
+ "@expo/logger": "22.0.0",
45
45
  "@expo/package-manager": "1.9.10",
46
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.7.1",
50
- "@expo/template-file": "21.0.2",
51
- "@expo/turtle-spawn": "21.0.0",
49
+ "@expo/steps": "22.0.0",
50
+ "@expo/template-file": "22.0.0",
51
+ "@expo/turtle-spawn": "22.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
53
53
  "@ngrok/ngrok": "1.7.0",
54
54
  "@sentry/node": "7.77.0",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "60032049ec947177bbcacf8802c22e1d5f24845f"
103
+ "gitHead": "b92a9cc5a718f133966e27b9fedcedb569f9cd17"
104
104
  }