/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ 'use strict'; const adb = require('./adb'); const chalk = require('chalk'); const child_process = require('child_process'); const fs = require('fs'); const isPackagerRunning = require('../util/isPackagerRunning'); const findReactNativeScripts = require('../util/findReactNativeScripts'); const isString = require('lodash/isString'); const path = require('path'); const Promise = require('promise'); // Verifies this is an Android project function checkAndroid(root) { return fs.existsSync(path.join(root, 'android/gradlew')); } /** * Starts the app on a connected Android emulator or device. */ function runAndroid(argv, config, args) { if (!checkAndroid(args.root)) { const reactNativeScriptsPath = findReactNativeScripts(); if (reactNativeScriptsPath) { child_process.spawnSync( reactNativeScriptsPath, ['android'].concat(process.argv.slice(1)), {stdio: 'inherit'}, ); } else { console.log( chalk.red( 'Android project not found. Maybe run react-native android first?', ), ); } return; } if (!args.packager) { return buildAndRun(args); } return isPackagerRunning(args.port).then(result => { if (result === 'running') { console.log(chalk.bold('JS server already running.')); } else if (result === 'unrecognized') { console.warn( chalk.yellow('JS server not recognized, continuing with build...'), ); } else { // result == 'not_running' console.log(chalk.bold('Starting JS server...')); startServerInNewWindow(args.port, args.terminal); } return buildAndRun(args); }); } function getAdbPath() { return process.env.ANDROID_HOME ? process.env.ANDROID_HOME + '/platform-tools/adb' : 'adb'; } // Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager function tryRunAdbReverse(packagerPort, device) { try { const adbPath = getAdbPath(); const adbArgs = ['reverse', `tcp:${packagerPort}`, `tcp:${packagerPort}`]; // If a device is specified then tell adb to use it if (device) { adbArgs.unshift('-s', device); } console.log(chalk.bold(`Running ${adbPath} ${adbArgs.join(' ')}`)); child_process.execFileSync(adbPath, adbArgs, { stdio: [process.stdin, process.stdout, process.stderr], }); } catch (e) { console.log(chalk.yellow(`Could not run adb reverse: ${e.message}`)); } } function getPackageNameWithSuffix(appId, appIdSuffix, packageName) { if (appId) { return appId; } else if (appIdSuffix) { return packageName + '.' + appIdSuffix; } return packageName; } // Builds the app and runs it on a connected emulator / device. function buildAndRun(args) { process.chdir(path.join(args.root, 'android')); const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; const packageName = fs .readFileSync(`${args.appFolder}/src/main/AndroidManifest.xml`, 'utf8') .match(/package="(.+?)"/)[1]; const packageNameWithSuffix = getPackageNameWithSuffix( args.appId, args.appIdSuffix, packageName, ); const adbPath = getAdbPath(); if (args.deviceId) { if (isString(args.deviceId)) { return runOnSpecificDevice( args, cmd, packageNameWithSuffix, packageName, adbPath, ); } else { console.log(chalk.red('Argument missing for parameter --deviceId')); } } else { return runOnAllDevices( args, cmd, packageNameWithSuffix, packageName, adbPath, ); } } function runOnSpecificDevice( args, gradlew, packageNameWithSuffix, packageName, adbPath, ) { let devices = adb.getDevices(); if (devices && devices.length > 0) { if (devices.indexOf(args.deviceId) !== -1) { buildApk(gradlew); installAndLaunchOnDevice( args, args.deviceId, packageNameWithSuffix, packageName, adbPath, ); } else { console.log( 'Could not find device with the id: "' + args.deviceId + '".', ); console.log('Choose one of the following:'); console.log(devices); } } else { console.log('No Android devices connected.'); } } function buildApk(gradlew) { try { console.log(chalk.bold('Building the app...')); // using '-x lint' in order to ignore linting errors while building the apk child_process.execFileSync(gradlew, ['build', '-x', 'lint'], { stdio: [process.stdin, process.stdout, process.stderr], }); } catch (e) { console.log( chalk.red('Could not build the app, read the error above for details.\n'), ); } } function tryInstallAppOnDevice(args, device) { try { const pathToApk = `${args.appFolder}/build/outputs/apk/${ args.appFolder }-debug.apk`; const adbPath = getAdbPath(); const adbArgs = ['-s', device, 'install', pathToApk]; console.log( chalk.bold( `Installing the app on the device (cd android && adb -s ${device} install ${pathToApk}`, ), ); child_process.execFileSync(adbPath, adbArgs, { stdio: [process.stdin, process.stdout, process.stderr], }); } catch (e) { console.log(e.message); console.log( chalk.red( 'Could not install the app on the device, read the error above for details.\n', ), ); } } function tryLaunchAppOnDevice( device, packageNameWithSuffix, packageName, adbPath, mainActivity, ) { try { const adbArgs = [ '-s', device, 'shell', 'am', 'start', '-n', packageNameWithSuffix + '/' + packageName + '.' + mainActivity, ]; console.log( chalk.bold( `Starting the app on ${device} (${adbPath} ${adbArgs.join(' ')})...`, ), ); child_process.spawnSync(adbPath, adbArgs, {stdio: 'inherit'}); } catch (e) { console.log( chalk.red('adb invocation failed. Do you have adb in your PATH?'), ); } } function installAndLaunchOnDevice( args, selectedDevice, packageNameWithSuffix, packageName, adbPath, ) { tryRunAdbReverse(args.port, selectedDevice); tryInstallAppOnDevice(args, selectedDevice); tryLaunchAppOnDevice( selectedDevice, packageNameWithSuffix, packageName, adbPath, args.mainActivity, ); } function runOnAllDevices( args, cmd, packageNameWithSuffix, packageName, adbPath, ) { try { const gradleArgs = []; if (args.variant) { gradleArgs.push( 'install' + args.variant[0].toUpperCase() + args.variant.slice(1), ); } else if (args.flavor) { console.warn( chalk.yellow('--flavor has been deprecated. Use --variant instead'), ); gradleArgs.push( 'install' + args.flavor[0].toUpperCase() + args.flavor.slice(1), ); } else { gradleArgs.push('installDebug'); } if (args.installDebug) { gradleArgs.push(args.installDebug); } console.log( chalk.bold( `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join( ' ', )})...`, ), ); child_process.execFileSync(cmd, gradleArgs, { stdio: [process.stdin, process.stdout, process.stderr], }); } catch (e) { console.log( chalk.red( '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 // `console.log(e.stderr)` return Promise.reject(e); } const devices = adb.getDevices(); 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', ]; console.log( chalk.bold( `Starting the app (${adbPath} ${fallbackAdbArgs.join(' ')}...`, ), ); child_process.spawnSync(adbPath, fallbackAdbArgs, {stdio: 'inherit'}); } catch (e) { console.log( chalk.red('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 // `console.log(e.stderr)` return Promise.reject(e); } } } function startServerInNewWindow(port, terminal = process.env.REACT_TERMINAL) { // set up OS-specific filenames and commands const isWindows = /^win/.test(process.platform); const scriptFile = isWindows ? 'launchPackager.bat' : 'launchPackager.command'; const packagerEnvFilename = isWindows ? '.packager.bat' : '.packager.env'; const portExportContent = isWindows ? `set RCT_METRO_PORT=${port}` : `export RCT_METRO_PORT=${port}`; // set up the launchpackager.(command|bat) file const scriptsDir = path.resolve(__dirname, '..', '..', 'scripts'); const launchPackagerScript = path.resolve(scriptsDir, scriptFile); const procConfig = {cwd: scriptsDir}; // set up the .packager.(env|bat) file to ensure the packager starts on the right port const packagerEnvFile = path.join( __dirname, '..', '..', 'scripts', packagerEnvFilename, ); // ensure we overwrite file by passing the 'w' flag fs.writeFileSync(packagerEnvFile, portExportContent, { encoding: 'utf8', flag: 'w', }); if (process.platform === 'darwin') { if (terminal) { return child_process.spawnSync( 'open', ['-a', terminal, launchPackagerScript], procConfig, ); } return child_process.spawnSync('open', [launchPackagerScript], procConfig); } else if (process.platform === 'linux') { if (terminal) { procConfig.detached = true; return child_process.spawn( terminal, ['-e', 'sh ' + launchPackagerScript], procConfig, ); } // By default, the child shell process will be attached to the parent procConfig.detached = false; return child_process.spawn('sh', [launchPackagerScript], procConfig); } else if (/^win/.test(process.platform)) { procConfig.detached = true; procConfig.stdio = 'ignore'; return child_process.spawn( 'cmd.exe', ['/C', launchPackagerScript], procConfig, ); } else { console.log( chalk.red( `Cannot start the packager. Unknown platform ${process.platform}`, ), ); } } module.exports = { name: 'run-android', description: 'builds your app and starts it on a connected Android emulator or device', func: runAndroid, options: [ { command: '--install-debug', }, { command: '--root [string]', description: 'Override the root directory for the android build (which contains the android directory)', default: '', }, { command: '--flavor [string]', description: '--flavor has been deprecated. Use --variant instead', }, { command: '--variant [string]', }, { command: '--appFolder [string]', description: 'Specify a different application folder name for the android source.', default: 'app', }, { command: '--appId [string]', description: 'Specify an applicationId to launch after build.', default: '', }, { command: '--appIdSuffix [string]', description: 'Specify an applicationIdSuffix to launch after build.', default: '', }, { command: '--main-activity [string]', description: 'Name of the activity to start', default: 'MainActivity', }, { command: '--deviceId [string]', description: 'builds your app and starts it on a specific device/simulator with the ' + 'given device id (listed by running "adb devices" on the command line).', }, { command: '--no-packager', description: 'Do not launch packager while building', }, { command: '--port [number]', default: process.env.RCT_METRO_PORT || 8081, parse: (val: string) => Number(val), }, { command: '--terminal [string]', description: 'Launches the Metro Bundler in a new window using the specified terminal path.', default: '', }, ], };