From 41b1ec62a3b4bbe928c3c30e171b43d8c0430be3 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 5 May 2026 17:46:26 -0400 Subject: [PATCH 1/2] fix(android): respect --device on run-android when multiple devices connected When `react-native run-android --device ` is invoked with more than one device or emulator attached, Gradle's `installDebug` task silently installs on a device of its own choosing instead of the one named by `--device`. The flag is honored only for the post-Gradle `adb install` step in `tryInstallAppOnDevice`, which is too late: the APK has already been pushed to the wrong device, and on a fresh emulator the second adb install can fail outright due to a debug-keystore mismatch left behind by the Gradle install. Root cause: `runOnSpecificDevice` builds the Gradle task list with the `'install'` prefix (yielding `app:installDebug`) but neither sets `ANDROID_SERIAL` on the gradle spawn nor passes `-Pandroid.injected.serial=`, so AGP picks a device by its own logic. The interactive path already side-steps this by swapping the build task to `assemble*` (line ~230), but the non-interactive path keeps using `install*`. This change mirrors that swap for the non-interactive path: Gradle builds `app:assembleDebug` (or the corresponding flavored variant) and `tryInstallAppOnDevice` then runs `adb -s install -r -d ` as it always has, so the install lands on the device the user asked for. No environment variables, no AGP-injected properties, and no behavior change when only one device is connected. `runOnAllDevices` is unchanged: it still uses `install*` because it intentionally fans the install across every connected device. --- .../src/commands/runAndroid/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 7e43d2d22..71f24ade4 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -232,11 +232,22 @@ function runOnSpecificDevice( if (devices.length > 0 && device) { if (devices.indexOf(device) !== -1) { + // Build the APK only — Gradle's installDebug task has no way to + // know which device the CLI selected (we don't pass + // -Pandroid.injected.serial or set ANDROID_SERIAL on the spawn), + // so when more than one device is connected it picks one on its + // own and silently ignores --device. tryInstallAppOnDevice (called + // below via installAndLaunchOnDevice) already runs + // `adb -s install -r -d ` against the correct + // device, so handing the install off to it produces the right + // result without changing any other behavior. The interactive + // path on line ~230 already does this swap; this just mirrors + // it for the non-interactive path. let gradleArgs = getTaskNames( androidProject.appName, args.mode, args.tasks ?? buildTask, - 'install', + 'assemble', ); // using '-x lint' in order to ignore linting errors while building the apk From 6e6e0911bc442979947ae97b7968f0e6d8598367 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 5 May 2026 17:49:12 -0400 Subject: [PATCH 2/2] drop inline comment, explained in PR description --- .../src/commands/runAndroid/index.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 71f24ade4..8b839496e 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -232,17 +232,6 @@ function runOnSpecificDevice( if (devices.length > 0 && device) { if (devices.indexOf(device) !== -1) { - // Build the APK only — Gradle's installDebug task has no way to - // know which device the CLI selected (we don't pass - // -Pandroid.injected.serial or set ANDROID_SERIAL on the spawn), - // so when more than one device is connected it picks one on its - // own and silently ignores --device. tryInstallAppOnDevice (called - // below via installAndLaunchOnDevice) already runs - // `adb -s install -r -d ` against the correct - // device, so handing the install off to it produces the right - // result without changing any other behavior. The interactive - // path on line ~230 already does this swap; this just mirrors - // it for the non-interactive path. let gradleArgs = getTaskNames( androidProject.appName, args.mode,