From 4b6cd8e8c91facf648d60b6844cf1fa27fd7afa2 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:07:39 +0300 Subject: [PATCH] fix(android): preserve device profiles, task paths and command context --- .../src/commands/buildAndroid/index.ts | 3 +- .../src/commands/runAndroid/adb.ts | 6 +- .../src/commands/runAndroid/getTaskNames.ts | 4 +- .../src/commands/runAndroid/index.ts | 98 +++++++++---------- .../commands/runAndroid/listAndroidDevices.ts | 42 ++++---- .../commands/runAndroid/runOnAllDevices.ts | 4 +- .../runAndroid/tryInstallAppOnDevice.ts | 3 +- .../runAndroid/tryLaunchAppOnDevice.ts | 4 + 8 files changed, 84 insertions(+), 80 deletions(-) diff --git a/packages/cli-platform-android/src/commands/buildAndroid/index.ts b/packages/cli-platform-android/src/commands/buildAndroid/index.ts index 1441978ef..75cbb5d84 100644 --- a/packages/cli-platform-android/src/commands/buildAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/buildAndroid/index.ts @@ -80,7 +80,6 @@ async function buildAndroid( } export function build(gradleArgs: string[], sourceDir: string) { - process.chdir(sourceDir); const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; logger.info('Building the app...'); logger.debug(`Running command "${cmd} ${gradleArgs.join(' ')}"`); @@ -103,7 +102,7 @@ export const options = [ { name: '--tasks ', description: - 'Run custom Gradle tasks. By default it\'s "assembleDebug". Will override passed mode and variant arguments.', + "Run custom Gradle tasks instead of the command's default tasks. Will override passed mode and variant arguments.", parse: (val: string) => val.split(','), }, { diff --git a/packages/cli-platform-android/src/commands/runAndroid/adb.ts b/packages/cli-platform-android/src/commands/runAndroid/adb.ts index 8cd6995de..3e00c69f2 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/adb.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/adb.ts @@ -6,7 +6,7 @@ * */ -import {execSync, execFileSync} from 'child_process'; +import {execFileSync} from 'child_process'; /** * Parses the output of the 'adb devices' command @@ -34,7 +34,7 @@ function parseDevicesResult(result: string): Array { */ function getDevices(adbPath: string): Array { try { - const devicesResult = execSync(`"${adbPath}" devices`); + const devicesResult = execFileSync(adbPath, ['devices']); return parseDevicesResult(devicesResult.toString()); } catch (e) { return []; @@ -61,7 +61,7 @@ function getAvailableCPUs(adbPath: string, device: string): Array { ).toString(); } - return (cpus || '').trim().split(','); + return cpus.trim().split(',').filter(Boolean); } catch (e) { return []; } diff --git a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts index 23c022977..681654dbb 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts @@ -11,6 +11,8 @@ export function getTaskNames( tasks && tasks.length ? tasks : [taskPrefix + toPascalCase(mode)]; return appName - ? appTasks.map((command) => `${appName}:${command}`) + ? appTasks.map((command) => + command.includes(':') ? command : `${appName}:${command}`, + ) : appTasks; } diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 8b839496e..619f4f36c 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -55,6 +55,21 @@ async function runAndroid(_argv: Array, config: Config, args: Flags) { let {packager, port} = args; + if (args.binaryPath) { + if (args.tasks) { + throw new CLIError( + 'binary-path and tasks were specified, but they are not compatible. Specify only one', + ); + } + + args.binaryPath = path.resolve(config.root, args.binaryPath); + if (!fs.existsSync(args.binaryPath)) { + throw new CLIError( + 'binary-path was specified, but the file was not found.', + ); + } + } + if (packager) { const {port: newPort, startPackager} = await findDevServerPort( port, @@ -80,25 +95,7 @@ async function runAndroid(_argv: Array, config: Config, args: Flags) { link.setVersion(config.reactNativeVersion); } - if (args.binaryPath) { - if (args.tasks) { - throw new CLIError( - 'binary-path and tasks were specified, but they are not compatible. Specify only one', - ); - } - - args.binaryPath = path.isAbsolute(args.binaryPath) - ? args.binaryPath - : path.join(config.root, args.binaryPath); - - if (args.binaryPath && !fs.existsSync(args.binaryPath)) { - throw new CLIError( - 'binary-path was specified, but the file was not found.', - ); - } - } - - let androidProject = getAndroidProject(config); + const androidProject = {...getAndroidProject(config)}; if (args.mainActivity) { androidProject.mainActivity = args.mainActivity; @@ -107,7 +104,7 @@ async function runAndroid(_argv: Array, config: Config, args: Flags) { return buildAndRun(args, androidProject); } -const defaultPort = 5552; +const defaultPort = 5554; async function getAvailableDevicePort( port: number = defaultPort, ): Promise { @@ -119,7 +116,7 @@ async function getAvailableDevicePort( if (port > 5682) { throw new CLIError('Failed to launch emulator...'); } - if (devices.some((d) => d.includes(port.toString()))) { + if (devices.includes(`emulator-${port}`)) { return await getAvailableDevicePort(port + 2); } return port; @@ -134,7 +131,6 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { args.device = args.deviceId; } - process.chdir(androidProject.sourceDir); const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; const adbPath = getAdbPath(); @@ -169,8 +165,26 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { ); } + let deviceId = device.deviceId; + if (!device.connected) { + const port = await getAvailableDevicePort(); + deviceId = `emulator-${port}`; + logger.info('Launching emulator...'); + const result = await tryLaunchEmulator( + adbPath, + device.readableName, + port, + ); + if (!result.success) { + throw new CLIError( + `Failed to launch emulator. Reason: ${pico.dim(result.error || '')}`, + ); + } + logger.info('Successfully launched emulator.'); + } + if (args.interactive) { - const users = checkUsers(device.deviceId as string, adbPath); + const users = checkUsers(deviceId as string, adbPath); if (users && users.length > 1) { const user = await promptForUser(users); @@ -180,30 +194,11 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { } } - if (device.connected) { - return runOnSpecificDevice( - {...args, device: device.deviceId}, - adbPath, - androidProject, - selectedTask, - ); - } - - const port = await getAvailableDevicePort(); - const emulator = `emulator-${port}`; - logger.info('Launching emulator...'); - const result = await tryLaunchEmulator(adbPath, device.readableName, port); - if (result.success) { - logger.info('Successfully launched emulator.'); - return runOnSpecificDevice( - {...args, device: emulator}, - adbPath, - androidProject, - selectedTask, - ); - } - throw new CLIError( - `Failed to launch emulator. Reason: ${pico.dim(result.error || '')}`, + return runOnSpecificDevice( + {...args, device: deviceId}, + adbPath, + androidProject, + selectedTask, ); } @@ -273,13 +268,14 @@ function runOnSpecificDevice( selectedTask, ); } else { - logger.error( - `Could not find device: "${device}". Please choose one of the following:`, - ...devices, + throw new CLIError( + `Could not find device: "${device}". Please choose one of the following: ${devices.join( + ', ', + )}`, ); } } else { - logger.error('No Android device or emulator connected.'); + throw new CLIError('No Android device or emulator connected.'); } } diff --git a/packages/cli-platform-android/src/commands/runAndroid/listAndroidDevices.ts b/packages/cli-platform-android/src/commands/runAndroid/listAndroidDevices.ts index 8d1266d3c..f4b6e57e6 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/listAndroidDevices.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/listAndroidDevices.ts @@ -1,9 +1,8 @@ -import {execSync} from 'child_process'; +import {execFileSync} from 'child_process'; import adb from './adb'; import getAdbPath from './getAdbPath'; import {getEmulators} from './tryLaunchEmulator'; import {toPascalCase} from './toPascalCase'; -import os from 'os'; import pico from 'picocolors'; import {CLIError, prompt} from '@react-native-community/cli-tools'; @@ -21,14 +20,10 @@ type DeviceData = { */ function getEmulatorName(deviceId: string) { const adbPath = getAdbPath(); - const buffer = execSync(`${adbPath} -s ${deviceId} emu avd name`); + const buffer = execFileSync(adbPath, ['-s', deviceId, 'emu', 'avd', 'name']); // 1st line should get us emu name - return buffer - .toString() - .split(os.EOL)[0] - .replace(/(\r\n|\n|\r)/gm, '') - .trim(); + return buffer.toString().split(/\r?\n/)[0].trim(); } /** @@ -38,13 +33,14 @@ function getEmulatorName(deviceId: string) { */ function getPhoneName(deviceId: string) { const adbPath = getAdbPath(); - const buffer = execSync( - `${adbPath} -s ${deviceId} shell getprop | grep ro.product.model`, - ); - return buffer - .toString() - .replace(/\[ro\.product\.model\]:\s*\[(.*)\]/, '$1') - .trim(); + const buffer = execFileSync(adbPath, [ + '-s', + deviceId, + 'shell', + 'getprop', + 'ro.product.model', + ]); + return buffer.toString().trim(); } async function promptForDeviceSelection( @@ -75,7 +71,7 @@ async function listAndroidDevices() { const adbPath = getAdbPath(); const devices = adb.getDevices(adbPath); - let allDevices: Array = []; + const allDevices: Array = []; devices.forEach((deviceId) => { if (deviceId.includes('emulator')) { @@ -85,7 +81,7 @@ async function listAndroidDevices() { connected: true, type: 'emulator', }; - allDevices = [...allDevices, emulatorData]; + allDevices.push(emulatorData); } else { const phoneData: DeviceData = { deviceId, @@ -93,16 +89,21 @@ async function listAndroidDevices() { type: 'phone', connected: true, }; - allDevices = [...allDevices, phoneData]; + allDevices.push(phoneData); } }); const emulators = getEmulators(); + const emulatorNames = new Set( + allDevices + .filter((device) => device.type === 'emulator') + .map((device) => device.readableName), + ); // Find not booted ones: emulators.forEach((emulatorName) => { // skip those already booted - if (allDevices.some((device) => device.readableName === emulatorName)) { + if (emulatorNames.has(emulatorName)) { return; } const emulatorData: DeviceData = { @@ -111,7 +112,8 @@ async function listAndroidDevices() { type: 'emulator', connected: false, }; - allDevices = [...allDevices, emulatorData]; + allDevices.push(emulatorData); + emulatorNames.add(emulatorName); }); const selectedDevice = await promptForDeviceSelection(allDevices); diff --git a/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts b/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts index 73c13952c..157c55926 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts @@ -54,7 +54,7 @@ async function runOnAllDevices( androidProject.appName, args.mode, args.tasks, - 'install', + args.user !== undefined ? 'assemble' : 'install', ); if (args.extraParams) { @@ -101,7 +101,7 @@ async function runOnAllDevices( (devices.length > 0 ? devices : [undefined]).forEach( (device: string | void) => { tryRunAdbReverse(args.port, device); - if (args.binaryPath && device) { + if ((args.binaryPath || args.user !== undefined) && device) { tryInstallAppOnDevice(args, adbPath, device, androidProject); } tryLaunchAppOnDevice(device, androidProject, adbPath, args); diff --git a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts index f601b4450..f1f031491 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts @@ -20,7 +20,8 @@ function tryInstallAppOnDevice( // handle if selected task from interactive mode, or mode from arguments, includes build flavour as well, eg. installProductionDebug should create ['production','debug'] array const variantFromSelectedTask = (selectedTask ?? args.mode) - ?.replace('install', '') + ?.replace(/^install/, '') + .replace(/^./, (letter) => letter.toLowerCase()) .split(/(?=[A-Z])/); // create path to output file, eg. `production/debug` diff --git a/packages/cli-platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts b/packages/cli-platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts index dfb53e9d9..4eabe2715 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts @@ -46,6 +46,10 @@ function tryLaunchAppOnDevice( 'android.intent.category.LAUNCHER', ]; + if (args.user !== undefined) { + adbArgs.push('--user', `${args.user}`); + } + if (device) { adbArgs.unshift('-s', device); logger.info(`Starting the app on "${device}"...`);