@expo/build-tools 21.7.1 → 21.8.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.
@@ -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, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.7.1",
3
+ "version": "21.8.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "60032049ec947177bbcacf8802c22e1d5f24845f"
103
+ "gitHead": "b8cff2008966a6c7202736577cd231025ee47f21"
104
104
  }