diff --git a/packages/cli-platform-android/src/commands/buildAndroid/index.ts b/packages/cli-platform-android/src/commands/buildAndroid/index.ts index 160f18acc..2f4523159 100644 --- a/packages/cli-platform-android/src/commands/buildAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/buildAndroid/index.ts @@ -11,6 +11,7 @@ import adb from '../runAndroid/adb'; import getAdbPath from '../runAndroid/getAdbPath'; import {startServerInNewWindow} from './startServerInNewWindow'; import {getTaskNames} from '../runAndroid/getTaskNames'; +import {promptForTaskSelection} from '../runAndroid/listAndroidTasks'; export interface BuildFlags { mode?: string; @@ -21,6 +22,7 @@ export interface BuildFlags { terminal: string; tasks?: Array; extraParams?: Array; + interactive?: boolean; } export async function runPackager(args: BuildFlags, config: Config) { @@ -64,10 +66,22 @@ async function buildAndroid( ); } + let {tasks} = args; + + if (args.interactive) { + const selectedTask = await promptForTaskSelection( + 'build', + androidProject.sourceDir, + ); + if (selectedTask) { + tasks = [selectedTask]; + } + } + let gradleArgs = getTaskNames( androidProject.appName, args.mode || args.variant, - args.tasks, + tasks, 'assemble', ); @@ -156,6 +170,11 @@ export const options = [ description: 'Custom properties passed to gradle build command', parse: (val: string) => val.split(' '), }, + { + name: '--interactive', + description: + 'Explicitly select build type and flavour to use before running a build', + }, ]; export default { diff --git a/packages/cli-platform-android/src/commands/runAndroid/__tests__/listAndroidTasks.test.ts b/packages/cli-platform-android/src/commands/runAndroid/__tests__/listAndroidTasks.test.ts new file mode 100644 index 000000000..d35f9a6ef --- /dev/null +++ b/packages/cli-platform-android/src/commands/runAndroid/__tests__/listAndroidTasks.test.ts @@ -0,0 +1,144 @@ +import chalk from 'chalk'; +import execa from 'execa'; +import prompts from 'prompts'; +import { + parseTasksFromGradleFile, + promptForTaskSelection, +} from '../listAndroidTasks'; + +const gradleTaskOutput = ` +> Task :tasks + +------------------------------------------------------------ +Tasks runnable from root project 'com.bananas' +------------------------------------------------------------ + +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. +assembleProduction - Assembles main outputs for all Production variants. +assembleRelease - Assembles main outputs for all Release variants. +assembleUat - Assembles main outputs for all Uat 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. +bundleProduction - Assembles bundles for all Production variants. +bundleRelease - Assembles bundles for all Release variants. +bundleUat - Assembles bundles for all Uat variants. +clean - Deletes the build directory. +compileProductionDebugAndroidTestSources +compileProductionDebugSources +compileProductionDebugUnitTestSources +compileProductionReleaseSources +compileProductionReleaseUnitTestSources +compileUatDebugAndroidTestSources +compileUatDebugSources +compileUatDebugUnitTestSources +compileUatReleaseSources +compileUatReleaseUnitTestSources + +Build Setup tasks +----------------- +init - Initializes a new Gradle build. +wrapper - Generates Gradle wrapper files. + +Help tasks +---------- +buildEnvironment - Displays all buildscript dependencies declared in root project 'com.bananas'. +dependencies - Displays all dependencies declared in root project 'com.bananas'. +dependencyInsight - Displays the insight into a specific dependency in root project 'com.bananas'. +help - Displays a help message. +javaToolchains - Displays the detected java toolchains. +outgoingVariants - Displays the outgoing variants of root project 'com.bananas'. +projects - Displays the sub-projects of root project 'com.bananas'. +properties - Displays the properties of root project 'com.bananas'. +resolvableConfigurations - Displays the configurations that can be resolved in root project 'com.bananas'. +tasks - Displays the tasks runnable from root project 'com.bananas' (some of the displayed tasks may belong to subprojects). + +Install tasks +------------- +installProductionDebug - Installs the DebugProductionDebug build. +installProductionDebugAndroidTest - Installs the android (on device) tests for the ProductionDebug build. +installProductionRelease - Installs the ReleaseProductionRelease build. +installUatDebug - Installs the DebugUatDebug build. +installUatDebugAndroidTest - Installs the android (on device) tests for the UatDebug build. +installUatRelease - Installs the ReleaseUatRelease build. +uninstallAll - Uninstall all applications. + +`; + +const tasksList = [ + { + description: 'Installs the DebugProductionDebug build.', + task: 'installProductionDebug', + }, + { + description: 'Installs the ReleaseProductionRelease build.', + task: 'installProductionRelease', + }, + { + description: 'Installs the DebugUatDebug build.', + task: 'installUatDebug', + }, + { + description: 'Installs the ReleaseUatRelease build.', + task: 'installUatRelease', + }, +]; + +jest.mock('execa', () => { + return {sync: jest.fn()}; +}); + +jest.mock('prompts', () => jest.fn()); + +describe('promptForTaskSelection', () => { + it('should prompt with correct tasks', () => { + (execa.sync as jest.Mock).mockReturnValueOnce({stdout: gradleTaskOutput}); + prompts.mockReturnValue({task: []}); + + promptForTaskSelection('install', 'sourceDir'); + + expect(prompts).toHaveBeenCalledWith({ + choices: tasksList.map((t) => ({ + title: `${chalk.bold(t.task)} - ${t.description}`, + value: t.task, + })), + message: 'Select install task you want to perform', + min: 1, + name: 'task', + type: 'select', + }); + }); +}); + +describe('parseTasksFromGradleFile', () => { + it('should correctly parse gradle tasks output for "install" taskType', () => { + const tasks = parseTasksFromGradleFile('install', gradleTaskOutput); + + expect(tasks).toEqual(tasksList); + }); + it('should correctly parse gradle tasks output for "build" taskType', () => { + const buildTasks = parseTasksFromGradleFile('build', gradleTaskOutput); + + expect(buildTasks).toContainEqual({ + description: 'Assemble main outputs for all the variants.', + task: 'assemble', + }); + + expect(buildTasks).not.toContainEqual({ + description: 'Assembles all the Test applications.', + task: 'assembleAndroidTest', + }); + }); +}); diff --git a/packages/cli-platform-android/src/commands/runAndroid/index.ts b/packages/cli-platform-android/src/commands/runAndroid/index.ts index 66ef070df..90aba8cf0 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/index.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/index.ts @@ -20,6 +20,7 @@ import tryLaunchEmulator from './tryLaunchEmulator'; import chalk from 'chalk'; import path from 'path'; import {build, runPackager, BuildFlags, options} from '../buildAndroid'; +import {promptForTaskSelection} from './listAndroidTasks'; export interface Flags extends BuildFlags { appId: string; @@ -84,6 +85,19 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; const adbPath = getAdbPath(); + + let {tasks} = args; + + if (args.interactive) { + const selectedTask = await promptForTaskSelection( + 'install', + androidProject.sourceDir, + ); + if (selectedTask) { + tasks = [selectedTask]; + } + } + if (args.listDevices) { if (args.deviceId) { logger.warn( @@ -123,9 +137,9 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) { ); } if (args.deviceId) { - return runOnSpecificDevice(args, adbPath, androidProject); + return runOnSpecificDevice({...args, tasks}, adbPath, androidProject); } else { - return runOnAllDevices(args, cmd, adbPath, androidProject); + return runOnAllDevices({...args, tasks}, cmd, adbPath, androidProject); } } diff --git a/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts b/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts new file mode 100644 index 000000000..fdbfce848 --- /dev/null +++ b/packages/cli-platform-android/src/commands/runAndroid/listAndroidTasks.ts @@ -0,0 +1,55 @@ +import {CLIError} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import execa from 'execa'; +import prompts from 'prompts'; + +type GradleTask = { + task: string; + description: string; +}; + +export const parseTasksFromGradleFile = ( + taskType: 'install' | 'build', + text: string, +): Array => { + const instalTasks: Array = []; + const taskRegex = new RegExp( + taskType === 'build' ? '^assemble|^bundle' : '^install', + ); + text.split('\n').forEach((line) => { + if (taskRegex.test(line) && /(?!.*?Test)^.*$/.test(line)) { + const metadata = line.split(' - '); + instalTasks.push({ + task: metadata[0], + description: metadata[1], + }); + } + }); + return instalTasks; +}; + +export const promptForTaskSelection = async ( + taskType: 'install' | 'build', + sourceDir: string, +) => { + const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; + + const out = execa.sync(cmd, ['tasks'], { + cwd: sourceDir, + }).stdout; + const installTasks = parseTasksFromGradleFile(taskType, out); + if (!installTasks.length) { + throw new CLIError(`No actionable ${taskType} tasks were found...`); + } + const {task} = await prompts({ + type: 'select', + name: 'task', + message: `Select ${taskType} task you want to perform`, + choices: installTasks.map((t: GradleTask) => ({ + title: `${chalk.bold(t.task)} - ${t.description}`, + value: t.task, + })), + min: 1, + }); + return task; +};