diff --git a/__e2e__/__snapshots__/config.test.ts.snap b/__e2e__/__snapshots__/config.test.ts.snap index b5ac0aaf8..ab8784d6d 100644 --- a/__e2e__/__snapshots__/config.test.ts.snap +++ b/__e2e__/__snapshots__/config.test.ts.snap @@ -20,6 +20,16 @@ exports[`shows up current config without unnecessary output 1`] = ` "<>" ] }, + { + "name": "build-ios", + "description": "builds your app on iOS simulator", + "examples": [ + "<>" + ], + "options": [ + "<>" + ] + }, { "name": "log-android", "description": "starts logkitty" diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index 527225bb2..477a38984 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -37,7 +37,7 @@ function createCorruptedSetupEnvScript() { beforeAll(() => { // Register all packages to be linked - for (const pkg of ['platform-ios', 'platform-android']) { + for (const pkg of ['cli-platform-ios', 'cli-platform-android']) { spawnScript('yarn', ['link'], { cwd: path.join(__dirname, `../packages/${pkg}`), }); diff --git a/__e2e__/root.test.ts b/__e2e__/root.test.ts index b93acc5aa..c59b9ae97 100644 --- a/__e2e__/root.test.ts +++ b/__e2e__/root.test.ts @@ -12,7 +12,7 @@ const cwd = getTempDirectory('test_different_roots'); beforeAll(() => { // Register all packages to be linked - for (const pkg of ['platform-ios', 'platform-android']) { + for (const pkg of ['cli-platform-ios', 'cli-platform-android']) { spawnScript('yarn', ['link'], { cwd: path.join(__dirname, `../packages/${pkg}`), }); diff --git a/packages/cli-platform-android/src/commands/buildAndroid/index.ts b/packages/cli-platform-android/src/commands/buildAndroid/index.ts index dae41287d..05622b6d7 100644 --- a/packages/cli-platform-android/src/commands/buildAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/buildAndroid/index.ts @@ -84,10 +84,11 @@ async function buildAndroid( args.mode || args.variant, tasks, 'assemble', + androidProject.sourceDir, ); if (args.extraParams) { - gradleArgs = [...gradleArgs, ...args.extraParams]; + gradleArgs.push(...args.extraParams); } if (args.activeArchOnly) { @@ -183,5 +184,5 @@ export default { name: 'build-android', description: 'builds your app', func: buildAndroid, - options: options, + options, }; diff --git a/packages/cli-platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts b/packages/cli-platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts index d9950eb92..1ad378d9a 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts @@ -11,6 +11,41 @@ import execa from 'execa'; import {Flags} from '..'; import {AndroidProjectConfig} from '@react-native-community/cli-types'; +const gradleTaskOutput = ` +> Task :tasks + +------------------------------------------------------------ +Tasks runnable from root project 'Bar' +------------------------------------------------------------ + +Android tasks +------------- +androidDependencies - Displays the Android dependencies of the project. +signingReport - Displays the signing info for the base and test modules +sourceSets - Prints out all the source sets defined in this project. + +Build tasks +----------- +assemble - Assemble main outputs for all the variants. +assembleAndroidTest - Assembles all the Test applications. +assembleDebug - Assembles main outputs for all Debug variants. +assembleRelease - Assembles main outputs for all Release variants. +build - Assembles and tests this project. +buildDependents - Assembles and tests this project and all projects that depend on it. +buildNeeded - Assembles and tests this project and all projects it depends on. +bundle - Assemble bundles for all the variants. +bundleDebug - Assembles bundles for all Debug variants. +bundleRelease - Assembles bundles for all Release variants. + + +Install tasks +------------- +installDebug - Installs the Debug build. +installDebugAndroidTest - Installs the android (on device) tests for the Debug build. +installRelease - Installs the Release build. +uninstallAll - Uninstall all applications. +`; + jest.mock('execa'); jest.mock('../getAdbPath'); jest.mock('../tryLaunchEmulator'); @@ -35,6 +70,7 @@ describe('--appFolder', () => { }; beforeEach(() => { jest.clearAllMocks(); + (execa.sync as jest.Mock).mockReturnValueOnce({stdout: gradleTaskOutput}); }); it('uses task "install[Variant]" as default task', async () => { diff --git a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts index 07233e395..c08166b23 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/getTaskNames.ts @@ -1,13 +1,45 @@ import {toPascalCase} from './toPascalCase'; import type {BuildFlags} from '../buildAndroid'; +import {getGradleTasks} from './listAndroidTasks'; +import {CLIError, logger} from '@react-native-community/cli-tools'; export function getTaskNames( appName: string, mode: BuildFlags['mode'] = 'debug', tasks: BuildFlags['tasks'], taskPrefix: 'assemble' | 'install', + sourceDir: string, ): Array { - const appTasks = tasks || [taskPrefix + toPascalCase(mode)]; + let appTasks = tasks || [taskPrefix + toPascalCase(mode)]; + + // Check against build flavors for "install" task ("assemble" don't care about it so much and will build all) + if (!tasks && taskPrefix === 'install') { + const actionableInstallTasks = getGradleTasks('install', sourceDir); + if (!actionableInstallTasks.find((t) => t.task.includes(appTasks[0]))) { + const installTasksForMode = actionableInstallTasks.filter((t) => + t.task.toLowerCase().includes(mode), + ); + if (!installTasksForMode.length) { + throw new CLIError( + `Couldn't find "${appTasks + .map((taskName) => taskName.replace(taskPrefix, '')) + .join( + ', ', + )}" build variant. Available variants are: ${actionableInstallTasks + .map((t) => `"${t.task.replace(taskPrefix, '')}"`) + .join(', ')}.`, + ); + } + logger.warn( + `Found multiple tasks for "install" command: ${installTasksForMode + .map((t) => t.task) + .join(', ')}.\nSelecting first available: ${ + installTasksForMode[0].task + }.`, + ); + appTasks = [installTasksForMode[0].task]; + } + } return appName ? appTasks.map((command) => `${appName}:${command}`) diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 90aba8cf0..8071ab25a 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -21,6 +21,7 @@ import chalk from 'chalk'; import path from 'path'; import {build, runPackager, BuildFlags, options} from '../buildAndroid'; import {promptForTaskSelection} from './listAndroidTasks'; +import {getTaskNames} from './getTaskNames'; export interface Flags extends BuildFlags { appId: string; @@ -86,19 +87,19 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { const adbPath = getAdbPath(); - let {tasks} = args; + let selectedTask; if (args.interactive) { - const selectedTask = await promptForTaskSelection( + const task = await promptForTaskSelection( 'install', androidProject.sourceDir, ); - if (selectedTask) { - tasks = [selectedTask]; + if (task) { + selectedTask = task; } } - if (args.listDevices) { + if (args.listDevices || args.interactive) { if (args.deviceId) { logger.warn( 'Both "deviceId" and "list-devices" parameters were passed to "run" command. We will list available devices and let you choose from one', @@ -108,7 +109,9 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { const device = await listAndroidDevices(); if (!device) { throw new CLIError( - 'Failed to select device, please try to run app without "list-devices" command.', + `Failed to select device, please try to run app without ${ + args.listDevices ? 'list-devices' : 'interactive' + } command.`, ); } @@ -117,6 +120,7 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { {...args, deviceId: device.deviceId}, adbPath, androidProject, + selectedTask, ); } @@ -130,16 +134,18 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { {...args, deviceId: emulator}, adbPath, androidProject, + selectedTask, ); } throw new CLIError( `Failed to launch emulator. Reason: ${chalk.dim(result.error || '')}`, ); } + if (args.deviceId) { - return runOnSpecificDevice({...args, tasks}, adbPath, androidProject); + return runOnSpecificDevice(args, adbPath, androidProject, selectedTask); } else { - return runOnAllDevices({...args, tasks}, cmd, adbPath, androidProject); + return runOnAllDevices(args, cmd, adbPath, androidProject); } } @@ -147,16 +153,30 @@ function runOnSpecificDevice( args: Flags, adbPath: string, androidProject: AndroidProject, + selectedTask?: string, ) { const devices = adb.getDevices(adbPath); const {deviceId} = args; + + // if coming from run-android command and we have selected task + // from interactive mode we need to create appropriate build task + // eg 'installRelease' -> 'assembleRelease' + const buildTask = selectedTask?.replace('install', 'assemble') ?? 'build'; + if (devices.length > 0 && deviceId) { if (devices.indexOf(deviceId) !== -1) { - // using '-x lint' in order to ignore linting errors while building the apk - let gradleArgs = ['build', '-x', 'lint']; + let gradleArgs = getTaskNames( + androidProject.appName, + args.mode || args.variant, + args.tasks ?? [buildTask], + 'install', + androidProject.sourceDir, + ); + // using '-x lint' in order to ignore linting errors while building the apk + gradleArgs.push('-x', 'lint'); if (args.extraParams) { - gradleArgs = [...gradleArgs, ...args.extraParams]; + gradleArgs.push(...args.extraParams); } if (args.port) { @@ -179,7 +199,13 @@ function runOnSpecificDevice( build(gradleArgs, androidProject.sourceDir); } - installAndLaunchOnDevice(args, deviceId, adbPath, androidProject); + installAndLaunchOnDevice( + args, + deviceId, + adbPath, + androidProject, + selectedTask, + ); } else { logger.error( `Could not find device with the id: "${deviceId}". Please choose one of the following:`, @@ -196,9 +222,17 @@ function installAndLaunchOnDevice( selectedDevice: string, adbPath: string, androidProject: AndroidProject, + selectedTask?: string, ) { tryRunAdbReverse(args.port, selectedDevice); - tryInstallAppOnDevice(args, adbPath, selectedDevice, androidProject); + + tryInstallAppOnDevice( + args, + adbPath, + selectedDevice, + androidProject, + selectedTask, + ); tryLaunchAppOnDevice( selectedDevice, androidProject.packageName, diff --git a/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts b/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts index fdbfce848..ca6535f9d 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts @@ -28,7 +28,7 @@ export const parseTasksFromGradleFile = ( return instalTasks; }; -export const promptForTaskSelection = async ( +export const getGradleTasks = ( taskType: 'install' | 'build', sourceDir: string, ) => { @@ -37,15 +37,22 @@ export const promptForTaskSelection = async ( const out = execa.sync(cmd, ['tasks'], { cwd: sourceDir, }).stdout; - const installTasks = parseTasksFromGradleFile(taskType, out); - if (!installTasks.length) { + return parseTasksFromGradleFile(taskType, out); +}; + +export const promptForTaskSelection = async ( + taskType: 'install' | 'build', + sourceDir: string, +): Promise => { + const tasks = getGradleTasks(taskType, sourceDir); + if (!tasks.length) { throw new CLIError(`No actionable ${taskType} tasks were found...`); } - const {task} = await prompts({ + const {task}: {task: string} = await prompts({ type: 'select', name: 'task', message: `Select ${taskType} task you want to perform`, - choices: installTasks.map((t: GradleTask) => ({ + choices: tasks.map((t: GradleTask) => ({ title: `${chalk.bold(t.task)} - ${t.description}`, value: t.task, })), diff --git a/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts b/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts index 7ce7b5f22..eafe5d503 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/runOnAllDevices.ts @@ -59,10 +59,11 @@ async function runOnAllDevices( args.mode || args.variant, args.tasks, 'install', + androidProject.sourceDir, ); if (args.extraParams) { - gradleArgs = [...gradleArgs, ...args.extraParams]; + gradleArgs.push(...args.extraParams); } if (args.port != null) { @@ -120,8 +121,7 @@ async function runOnAllDevices( function createInstallError(error: Error & {stderr: string}) { const stderr = (error.stderr || '').toString(); - let message = ''; - + let message = error.message ?? ''; // Pass the error message from the command to stdout because we pipe it to // parent process so it's not visible logger.log(stderr); diff --git a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts index 05d00d589..fdff32aeb 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/tryInstallAppOnDevice.ts @@ -10,19 +10,33 @@ function tryInstallAppOnDevice( adbPath: string, device: string, androidProject: AndroidProject, + selectedTask?: string, ) { try { // "app" is usually the default value for Android apps with only 1 app const {appName, sourceDir} = androidProject; - const variant = (args.mode || 'debug').toLowerCase(); + + const defaultVariant = (args.mode || 'debug').toLowerCase(); + + // handle if selected task from interactive mode includes build flavour as well, eg. installProductionDebug should create ['production','debug'] array + const variantFromSelectedTask = selectedTask + ?.replace('install', '') + .split(/(?=[A-Z])/); + + // create path to output file, eg. `production/debug` + const variantPath = + variantFromSelectedTask?.join('/')?.toLowerCase() ?? defaultVariant; + // create output file name, eg. `production-debug` + const variantAppName = + variantFromSelectedTask?.join('-')?.toLowerCase() ?? defaultVariant; let pathToApk; if (!args.binaryPath) { - const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variant}`; + const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variantPath}`; const apkFile = getInstallApkName( appName, adbPath, - variant, + variantAppName, device, buildDirectory, ); diff --git a/packages/cli-platform-ios/src/commands/buildIOS/index.ts b/packages/cli-platform-ios/src/commands/buildIOS/index.ts index d6cd90ad1..aa1e1edd9 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/buildIOS/index.ts @@ -255,11 +255,6 @@ export const iosBuildOptions = [ description: 'Location for iOS build artifacts. Corresponds to Xcode\'s "-derivedDataPath".', }, - { - name: '--interactive', - description: - 'Explicitly select which scheme and configuration to use before running a build', - }, { name: '--extra-params ', description: 'Custom params that will be passed to xcodebuild command.', @@ -285,5 +280,12 @@ export default { cmd: 'react-native build-ios --simulator "IPhone 11"', }, ], - options: iosBuildOptions, + options: [ + ...iosBuildOptions, + { + name: '--interactive', + description: + 'Explicitly select which scheme and configuration to use before running a build', + }, + ], }; diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index 2cc142d4b..1fcf427f7 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -100,7 +100,7 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { const modifiedArgs = {...args, scheme, mode}; modifiedArgs.mode = getConfigurationScheme( - {scheme: args.scheme, mode: args.mode}, + {scheme: modifiedArgs.scheme, mode: modifiedArgs.mode}, sourceDir, ); @@ -111,7 +111,7 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { ); const availableDevices = await listIOSDevices(); - if (modifiedArgs.listDevices) { + if (modifiedArgs.listDevices || modifiedArgs.interactive) { if (modifiedArgs.device || modifiedArgs.udid) { logger.warn( `Both ${ @@ -122,7 +122,9 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { const selectedDevice = await promptForDeviceSelection(availableDevices); if (!selectedDevice) { throw new CLIError( - 'Failed to select device, please try to run app without "list-devices" command.', + `Failed to select device, please try to run app without ${ + args.listDevices ? 'list-devices' : 'interactive' + } command.`, ); } if (selectedDevice.type === 'simulator') { @@ -597,5 +599,10 @@ export default { description: 'List all available iOS devices and simulators and let you choose one to run the app. ', }, + { + name: '--interactive', + description: + 'Explicitly select which scheme and configuration to use before running a build and select device to run the application.', + }, ], };