diff --git a/docs/commands.md b/docs/commands.md index 7ba65c626..d6ae064bd 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -262,8 +262,6 @@ Builds your app and starts it on a connected Android emulator or device. #### Options -#### `--install-debug` - #### `--root [string]` Override the root directory for the Android build (which contains the android directory)'. @@ -272,6 +270,8 @@ Override the root directory for the Android build (which contains the android di > default: 'debug' +Specify your app's build variant. + #### `--appFolder [string]` > default: 'app' @@ -302,7 +302,7 @@ Do not launch packager while building. #### `--port [number]` -> default: process.env.RCT_METRO_PORT || 8081, +> default: process.env.RCT_METRO_PORT || 8081 #### `--terminal [string]` @@ -310,6 +310,13 @@ Do not launch packager while building. Launches the Metro Bundler in a new window using the specified terminal path. +#### `--tasks [list]` + +> default: 'installDebug' + +Run custom gradle tasks. If this argument is provided, then `--variant` option is ignored. +Example: `yarn react-native run-android --tasks clean,installDebug`. + ### `run-ios` Usage: diff --git a/packages/cli/src/tools/copyFiles.js b/packages/cli/src/tools/copyFiles.js index 795f6862c..6f8206a05 100644 --- a/packages/cli/src/tools/copyFiles.js +++ b/packages/cli/src/tools/copyFiles.js @@ -53,7 +53,7 @@ function copyFile(srcPath: string, destPath: string) { */ function copyBinaryFile(srcPath, destPath, cb) { let cbCalled = false; - // const {mode} = fs.statSync(srcPath); + const {mode} = fs.statSync(srcPath); const readStream = fs.createReadStream(srcPath); const writeStream = fs.createWriteStream(destPath); readStream.on('error', err => { @@ -64,10 +64,7 @@ function copyBinaryFile(srcPath, destPath, cb) { }); readStream.on('close', () => { done(); - // We may revisit setting mode to original later, however this fn is used - // before "replace placeholder in template" step, which expects files to be - // writable. - // fs.chmodSync(destPath, mode); + fs.chmodSync(destPath, mode); }); readStream.pipe(writeStream); function done(err) { diff --git a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.js b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.js index 840e5cb54..86cd9948b 100644 --- a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.js +++ b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.js @@ -21,8 +21,10 @@ describe('--appFolder', () => { jest.clearAllMocks(); }); - it('uses installDebug as default if no arguments', () => { - runOnAllDevices({}); + it('uses task "install[Variant]" as default task', () => { + runOnAllDevices({ + variant: 'debug', + }); expect(execFileSync.mock.calls[0][1]).toContain('installDebug'); }); @@ -30,59 +32,51 @@ describe('--appFolder', () => { it('uses appFolder and default variant', () => { runOnAllDevices({ appFolder: 'someApp', + variant: 'debug', }); expect(execFileSync.mock.calls[0][1]).toContain('someApp:installDebug'); }); - it('uses appFolder and variant', () => { - runOnAllDevices({ - appFolder: 'app', - variant: 'debug', - }); - - expect(execFileSync.mock.calls[0][1]).toContain('app:installDebug'); - - runOnAllDevices({ - appFolder: 'anotherApp', - variant: 'debug', - }); - - expect(execFileSync.mock.calls[1][1]).toContain('anotherApp:installDebug'); - + it('uses appFolder and custom variant', () => { runOnAllDevices({ appFolder: 'anotherApp', variant: 'staging', }); - expect(execFileSync.mock.calls[2][1]).toContain( + expect(execFileSync.mock.calls[0][1]).toContain( 'anotherApp:installStaging', ); }); - it('uses appFolder and flavor', () => { + it('uses only task argument', () => { runOnAllDevices({ - appFolder: 'app', - flavor: 'someFlavor', + tasks: ['someTask'], + variant: 'debug', }); - expect(execFileSync.mock.calls[0][1]).toContain('app:installSomeFlavor'); + expect(execFileSync.mock.calls[0][1]).toContain('someTask'); }); - it('uses only installDebug argument', () => { + it('uses appFolder and custom task argument', () => { runOnAllDevices({ - installDebug: 'someCommand', + appFolder: 'anotherApp', + tasks: ['someTask'], + variant: 'debug', }); - expect(execFileSync.mock.calls[0][1]).toContain('someCommand'); + expect(execFileSync.mock.calls[0][1]).toContain('anotherApp:someTask'); }); - it('uses appFolder and custom installDebug argument', () => { + it('uses multiple tasks', () => { runOnAllDevices({ - appFolder: 'anotherApp', - installDebug: 'someCommand', + appFolder: 'app', + tasks: ['clean', 'someTask'], }); - expect(execFileSync.mock.calls[0][1]).toContain('anotherApp:someCommand'); + expect(execFileSync.mock.calls[0][1]).toContain( + 'app:clean', + 'app:someTask', + ); }); }); diff --git a/packages/platform-android/src/commands/runAndroid/adb.js b/packages/platform-android/src/commands/runAndroid/adb.js index d526829bc..c67f467c6 100644 --- a/packages/platform-android/src/commands/runAndroid/adb.js +++ b/packages/platform-android/src/commands/runAndroid/adb.js @@ -70,7 +70,6 @@ function getAvailableCPUs(adbPath: string, device: string): Array { } export default { - parseDevicesResult, getDevices, getAvailableCPUs, }; diff --git a/packages/platform-android/src/commands/runAndroid/index.js b/packages/platform-android/src/commands/runAndroid/index.js index 09c7f71c5..096da3991 100644 --- a/packages/platform-android/src/commands/runAndroid/index.js +++ b/packages/platform-android/src/commands/runAndroid/index.js @@ -30,10 +30,24 @@ function checkAndroid(root) { return fs.existsSync(path.join(root, 'android/gradlew')); } +export type FlagsT = {| + tasks?: Array, + root: string, + variant: string, + appFolder: string, + appId: string, + appIdSuffix: string, + mainActivity: string, + deviceId?: string, + packager: boolean, + port: number, + terminal: string, +|}; + /** * Starts the app on a connected Android emulator or device. */ -function runAndroid(argv: Array, ctx: ConfigT, args: Object) { +function runAndroid(argv: Array, config: ConfigT, args: FlagsT) { if (!checkAndroid(args.root)) { logger.error( 'Android project not found. Are you sure this is a React Native project?', @@ -53,7 +67,7 @@ function runAndroid(argv: Array, ctx: ConfigT, args: Object) { } else { // result == 'not_running' logger.info('Starting JS server...'); - startServerInNewWindow(args.port, args.terminal, ctx.reactNativePath); + startServerInNewWindow(args.port, args.terminal, config.reactNativePath); } return buildAndRun(args); }); @@ -90,16 +104,13 @@ function buildAndRun(args) { const adbPath = getAdbPath(); if (args.deviceId) { - if (typeof args.deviceId === 'string') { - return runOnSpecificDevice( - args, - cmd, - packageNameWithSuffix, - packageName, - adbPath, - ); - } - logger.error('Argument missing for parameter --deviceId'); + return runOnSpecificDevice( + args, + cmd, + packageNameWithSuffix, + packageName, + adbPath, + ); } else { return runOnAllDevices( args, @@ -119,21 +130,20 @@ function runOnSpecificDevice( adbPath, ) { const devices = adb.getDevices(adbPath); - if (devices && devices.length > 0) { - if (devices.indexOf(args.deviceId) !== -1) { + const {deviceId} = args; + if (devices.length > 0 && deviceId) { + if (devices.indexOf(deviceId) !== -1) { buildApk(gradlew); installAndLaunchOnDevice( args, - args.deviceId, + deviceId, packageNameWithSuffix, packageName, adbPath, ); } else { logger.error( - `Could not find device with the id: "${ - args.deviceId - }". Choose one of the following:`, + `Could not find device with the id: "${deviceId}". Please choose one of the following:`, ...devices, ); } @@ -144,17 +154,13 @@ function runOnSpecificDevice( function buildApk(gradlew) { try { - logger.info('Building the app...'); - // using '-x lint' in order to ignore linting errors while building the apk - execFileSync(gradlew, ['build', '-x', 'lint'], { - stdio: [process.stdin, process.stdout, process.stderr], - }); + const gradleArgs = ['build', '-x', 'lint']; + logger.info('Building the app...'); + logger.debug(`Running command "${gradlew} ${gradleArgs.join(' ')}"`); + execFileSync(gradlew, gradleArgs, {stdio: 'inherit'}); } catch (error) { - throw new CLIError( - 'Could not build the app, read the error above for details', - error, - ); + throw new CLIError('Failed to build the app.', error); } } @@ -174,18 +180,13 @@ function tryInstallAppOnDevice(args, adbPath, device) { const pathToApk = `${buildDirectory}/${apkFile}`; const adbArgs = ['-s', device, 'install', '-r', '-d', pathToApk]; - logger.info( - `Installing the app on the device (cd android && adb -s ${device} install -r -d ${pathToApk}`, - ); - execFileSync(adbPath, adbArgs, { - stdio: [process.stdin, process.stdout, process.stderr], - }); - } catch (e) { - logger.error( - `${ - e.message - }\nCould not install the app on the device, read the error above for details.`, + logger.info(`Installing the app on the device "${device}"...`); + logger.debug( + `Running command "cd android && adb -s ${device} install -r -d ${pathToApk}"`, ); + execFileSync(adbPath, adbArgs, {stdio: 'inherit'}); + } catch (error) { + throw new CLIError('Failed to install the app on the device.', error); } } @@ -305,21 +306,15 @@ export default { 'builds your app and starts it on a connected Android emulator or device', func: runAndroid, options: [ - { - name: '--install-debug', - }, { name: '--root [string]', description: 'Override the root directory for the android build (which contains the android directory)', default: '', }, - { - name: '--flavor [string]', - description: '--flavor has been deprecated. Use --variant instead', - }, { name: '--variant [string]', + description: "Specify your app's build variant", default: 'debug', }, { @@ -364,5 +359,10 @@ export default { 'Launches the Metro Bundler in a new window using the specified terminal path.', default: getDefaultUserTerminal, }, + { + name: '--tasks [list]', + description: 'Run custom Gradle tasks. By default it\'s "installDebug"', + parse: (val: string) => val.split(','), + }, ], }; diff --git a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.js b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.js index 3eb59ae44..a4e9b2fff 100644 --- a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.js +++ b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.js @@ -7,103 +7,87 @@ * @flow */ -import {spawnSync, execFileSync} from 'child_process'; -import {logger} from '@react-native-community/cli-tools'; +import chalk from 'chalk'; +import {execFileSync} from 'child_process'; +import {logger, CLIError} from '@react-native-community/cli-tools'; import adb from './adb'; import tryRunAdbReverse from './tryRunAdbReverse'; import tryLaunchAppOnDevice from './tryLaunchAppOnDevice'; +import type {FlagsT} from '.'; -function getCommand(appFolder, command) { - return appFolder ? `${appFolder}:${command}` : command; +function getTaskNames( + appFolder: string, + commands: Array, +): Array { + return appFolder + ? commands.map(command => `${appFolder}:${command}`) + : commands; +} + +function toPascalCase(value: string) { + return value[0].toUpperCase() + value.slice(1); } function runOnAllDevices( - args: Object, + args: FlagsT, cmd: string, packageNameWithSuffix: string, packageName: string, adbPath: string, ) { try { - const gradleArgs = []; - - if (args.installDebug) { - gradleArgs.push(getCommand(args.appFolder, args.installDebug)); - } else if (args.variant) { - gradleArgs.push( - `${getCommand( - args.appFolder, - 'install', - )}${args.variant[0].toUpperCase()}${args.variant.slice(1)}`, - ); - } else if (args.flavor) { - logger.warn('--flavor has been deprecated. Use --variant instead'); - gradleArgs.push( - `${getCommand( - args.appFolder, - 'install', - )}${args.flavor[0].toUpperCase()}${args.flavor.slice(1)}`, - ); - } else { - gradleArgs.push(getCommand(args.appFolder, 'installDebug')); - } + const tasks = args.tasks || ['install' + toPascalCase(args.variant)]; + const gradleArgs = getTaskNames(args.appFolder, tasks); - logger.info( - `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join( - ' ', - )})...`, + logger.info('Installing the app...'); + logger.debug( + `Running command "cd android && ${cmd} ${gradleArgs.join(' ')}"`, ); - execFileSync(cmd, gradleArgs, { - stdio: [process.stdin, process.stdout, process.stderr], - }); - } catch (e) { - logger.error( - 'Could not install the app on the device, read the error above for details.\n' + - 'Make sure you have an Android emulator running or a device connected and have\n' + - 'set up your Android development environment:\n' + - 'https://facebook.github.io/react-native/docs/getting-started.html', - ); - // stderr is automatically piped from the gradle process, so the user - // should see the error already, there is no need to do - // `logger.info(e.stderr)` - return Promise.reject(e); + execFileSync(cmd, gradleArgs, {stdio: ['inherit', 'inherit', 'pipe']}); + } catch (error) { + throw createInstallError(error); } const devices = adb.getDevices(adbPath); - if (devices && devices.length > 0) { - devices.forEach(device => { - tryRunAdbReverse(args.port, device); - tryLaunchAppOnDevice( - device, - packageNameWithSuffix, - packageName, - adbPath, - args.mainActivity, - ); - }); - } else { - try { - // If we cannot execute based on adb devices output, fall back to - // shell am start - const fallbackAdbArgs = [ - 'shell', - 'am', - 'start', - '-n', - `${packageNameWithSuffix}/${packageName}.MainActivity`, - ]; - logger.info( - `Starting the app (${adbPath} ${fallbackAdbArgs.join(' ')}...`, - ); - spawnSync(adbPath, fallbackAdbArgs, {stdio: 'inherit'}); - } catch (e) { - logger.error('adb invocation failed. Do you have adb in your PATH?'); - // stderr is automatically piped from the gradle process, so the user - // should see the error already, there is no need to do - // `logger.info(e.stderr)` - return Promise.reject(e); - } + + (devices.length > 0 ? devices : [undefined]).forEach(device => { + tryRunAdbReverse(args.port, device); + tryLaunchAppOnDevice( + device, + packageNameWithSuffix, + packageName, + adbPath, + args.mainActivity, + ); + }); +} + +function createInstallError(error) { + const stderr = (error.stderr || '').toString(); + const docs = + 'https://facebook.github.io/react-native/docs/getting-started.html#android-development-environment'; + let message = `Make sure you have the Android development environment set up: ${chalk.underline.dim( + docs, + )}`; + + // Pass the error message from the command to stdout because we pipe it to + // parent process so it's not visible + logger.log(stderr); + + // Handle some common failures and make the errors more helpful + if (stderr.includes('No connected devices')) { + message = + 'Make sure you have an Android emulator running or a device connected'; + } else if ( + stderr.includes('licences have not been accepted') || + stderr.includes('accept the SDK license') + ) { + message = `Please accept all necessary SDK licenses using SDK Manager: "${chalk.bold( + '$ANDROID_HOME/tools/bin/sdkmanager --licenses', + )}"`; } + + return new CLIError(`Failed to install the app. ${message}.`, error); } export default runOnAllDevices; diff --git a/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.js b/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.js index a2b88d021..4fd39fbec 100644 --- a/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.js +++ b/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.js @@ -8,10 +8,10 @@ */ import {spawnSync} from 'child_process'; -import {logger} from '@react-native-community/cli-tools'; +import {logger, CLIError} from '@react-native-community/cli-tools'; function tryLaunchAppOnDevice( - device: string, + device?: string, packageNameWithSuffix: string, packageName: string, adbPath: string, @@ -19,20 +19,22 @@ function tryLaunchAppOnDevice( ) { try { const adbArgs = [ - '-s', - device, 'shell', 'am', 'start', '-n', `${packageNameWithSuffix}/${packageName}.${mainActivity}`, ]; - logger.info( - `Starting the app on ${device} (${adbPath} ${adbArgs.join(' ')})...`, - ); + if (device) { + adbArgs.unshift('-s', device); + logger.info(`Starting the app on "${device}"...`); + } else { + logger.info('Starting the app...'); + } + logger.debug(`Running command "${adbPath} ${adbArgs.join(' ')}"`); spawnSync(adbPath, adbArgs, {stdio: 'inherit'}); - } catch (e) { - logger.error('adb invocation failed. Do you have adb in your PATH?'); + } catch (error) { + throw new CLIError('Failed to start the app.', error); } } diff --git a/packages/platform-android/src/commands/runAndroid/tryRunAdbReverse.js b/packages/platform-android/src/commands/runAndroid/tryRunAdbReverse.js index 58c443b67..ffb021540 100644 --- a/packages/platform-android/src/commands/runAndroid/tryRunAdbReverse.js +++ b/packages/platform-android/src/commands/runAndroid/tryRunAdbReverse.js @@ -12,7 +12,7 @@ import {logger} from '@react-native-community/cli-tools'; import getAdbPath from './getAdbPath'; // Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager -function tryRunAdbReverse(packagerPort: number | string, device: string) { +function tryRunAdbReverse(packagerPort: number | string, device?: string) { try { const adbPath = getAdbPath(); const adbArgs = ['reverse', `tcp:${packagerPort}`, `tcp:${packagerPort}`]; @@ -22,13 +22,16 @@ function tryRunAdbReverse(packagerPort: number | string, device: string) { adbArgs.unshift('-s', device); } - logger.info(`Running ${adbPath} ${adbArgs.join(' ')}`); + logger.info('Connecting to the development server...'); + logger.debug(`Running command "${adbPath} ${adbArgs.join(' ')}"`); - execFileSync(adbPath, adbArgs, { - stdio: [process.stdin, process.stdout, process.stderr], - }); + execFileSync(adbPath, adbArgs, {stdio: 'inherit'}); } catch (e) { - logger.info(`Could not run adb reverse: ${e.message}`); + logger.warn( + `Failed to connect to development server using "adb reverse": ${ + e.message + }`, + ); } }