486 lines
13 KiB
JavaScript
486 lines
13 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* 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)) {
|
|
runOnSpecificDevice(
|
|
args,
|
|
cmd,
|
|
packageNameWithSuffix,
|
|
packageName,
|
|
adbPath,
|
|
);
|
|
} else {
|
|
console.log(chalk.red('Argument missing for parameter --deviceId'));
|
|
}
|
|
} else {
|
|
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();
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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: '',
|
|
},
|
|
],
|
|
};
|