Ely Alvarado 0fab27cbac Allow specifying iOS version for run-ios with simulator option (#19079)
Summary:
Fixes #19069

The --simulator option for the run-ios command now can take an optional
iOS version between parenthesis to further match the desired simulator.
This is useful if you have installed simulators for different iOS
versions and you want to run the app in an especific one. Example:

react-native run-ios --simulator "iPhone 6s (9.3)"

Thank you for sending the PR! We appreciate you spending the time to work on these changes.
Help us understand your motivation by explaining why you decided to make this change.

Updated tests for the findMatchingSimulator function to include test cases specifying iOS version, and tested on the command line in my app to make sure it has the expected behavior.

[CLI] [ENHANCEMENT] [{/runIOS/findMatchingSimulator.js}] - run-ios command with the --simulator option now allows specifying the iOS version to run an specific simulator if you have multiple versions of the simulator installed. Example: `react-native run-ios --simulator "iPhone 6s (9.3)"`.
Pull Request resolved: https://github.com/facebook/react-native/pull/19079

Differential Revision: D10432487

Pulled By: hramos

fbshipit-source-id: efa50d798b79d83bfe357ee17967a56c7c003bee
2018-10-17 13:25:39 -07:00

456 lines
12 KiB
JavaScript

/**
* 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 child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const findXcodeProject = require('./findXcodeProject');
const findReactNativeScripts = require('../util/findReactNativeScripts');
const parseIOSDevicesList = require('./parseIOSDevicesList');
const findMatchingSimulator = require('./findMatchingSimulator');
const getBuildPath = function(configuration = 'Debug', appName, isDevice) {
let device;
if (isDevice) {
device = 'iphoneos';
} else if (appName.toLowerCase().includes('tvos')) {
device = 'appletvsimulator';
} else {
device = 'iphonesimulator';
}
return `build/Build/Products/${configuration}-${device}/${appName}.app`;
};
const xcprettyAvailable = function() {
try {
child_process.execSync('xcpretty --version', {
stdio: [0, 'pipe', 'ignore'],
});
} catch (error) {
return false;
}
return true;
};
function runIOS(argv, config, args) {
if (!fs.existsSync(args.projectPath)) {
const reactNativeScriptsPath = findReactNativeScripts();
if (reactNativeScriptsPath) {
child_process.spawnSync(
reactNativeScriptsPath,
['ios'].concat(process.argv.slice(1)),
{stdio: 'inherit'},
);
return;
} else {
throw new Error(
'iOS project folder not found. Are you sure this is a React Native project?',
);
}
}
process.chdir(args.projectPath);
const xcodeProject = findXcodeProject(fs.readdirSync('.'));
if (!xcodeProject) {
throw new Error('Could not find Xcode project files in ios folder');
}
const inferredSchemeName = path.basename(
xcodeProject.name,
path.extname(xcodeProject.name),
);
const scheme = args.scheme || inferredSchemeName;
console.log(
`Found Xcode ${xcodeProject.isWorkspace ? 'workspace' : 'project'} ${
xcodeProject.name
}`,
);
const devices = parseIOSDevicesList(
child_process.execFileSync('xcrun', ['instruments', '-s'], {
encoding: 'utf8',
}),
);
if (args.device) {
const selectedDevice = matchingDevice(devices, args.device);
if (selectedDevice) {
return runOnDevice(
selectedDevice,
scheme,
xcodeProject,
args.configuration,
args.packager,
args.verbose,
args.port,
);
} else {
if (devices && devices.length > 0) {
console.log(
'Could not find device with the name: "' + args.device + '".',
);
console.log('Choose one of the following:');
printFoundDevices(devices);
} else {
console.log('No iOS devices connected.');
}
}
} else if (args.udid) {
return runOnDeviceByUdid(args, scheme, xcodeProject, devices);
} else {
return runOnSimulator(xcodeProject, args, scheme);
}
}
function runOnDeviceByUdid(args, scheme, xcodeProject, devices) {
const selectedDevice = matchingDeviceByUdid(devices, args.udid);
if (selectedDevice) {
return runOnDevice(
selectedDevice,
scheme,
xcodeProject,
args.configuration,
args.packager,
args.verbose,
args.port,
);
} else {
if (devices && devices.length > 0) {
console.log('Could not find device with the udid: "' + args.udid + '".');
console.log('Choose one of the following:');
printFoundDevices(devices);
} else {
console.log('No iOS devices connected.');
}
}
}
function runOnSimulator(xcodeProject, args, scheme) {
return new Promise(resolve => {
try {
var simulators = JSON.parse(
child_process.execFileSync(
'xcrun',
['simctl', 'list', '--json', 'devices'],
{encoding: 'utf8'},
),
);
} catch (e) {
throw new Error('Could not parse the simulator list output');
}
const selectedSimulator = findMatchingSimulator(simulators, args.simulator);
if (!selectedSimulator) {
throw new Error(`Could not find ${args.simulator} simulator`);
}
/**
* Booting simulator through `xcrun simctl boot` will boot it in the `headless` mode
* (running in the background).
*
* In order for user to see the app and the simulator itself, we have to make sure
* that the Simulator.app is running.
*
* We also pass it `-CurrentDeviceUDID` so that when we launch it for the first time,
* it will not boot the "default" device, but the one we set. If the app is already running,
* this flag has no effect.
*/
const activeDeveloperDir = child_process
.execFileSync('xcode-select', ['-p'], {encoding: 'utf8'})
.trim();
child_process.execFileSync('open', [
`${activeDeveloperDir}/Applications/Simulator.app`,
'--args',
'-CurrentDeviceUDID',
selectedSimulator.udid,
]);
if (!selectedSimulator.booted) {
const simulatorFullName = formattedDeviceName(selectedSimulator);
console.log(`Launching ${simulatorFullName}...`);
try {
child_process.spawnSync('xcrun', [
'instruments',
'-w',
selectedSimulator.udid,
]);
} catch (e) {
// instruments always fail with 255 because it expects more arguments,
// but we want it to only launch the simulator
}
}
buildProject(
xcodeProject,
selectedSimulator.udid,
scheme,
args.configuration,
args.packager,
args.verbose,
args.port,
).then(appName => resolve({udid: selectedSimulator.udid, appName}));
}).then(({udid, appName}) => {
if (!appName) {
appName = scheme;
}
let appPath = getBuildPath(args.configuration, appName);
console.log(`Installing ${appPath}`);
child_process.spawnSync('xcrun', ['simctl', 'install', udid, appPath], {
stdio: 'inherit',
});
const bundleID = child_process
.execFileSync(
'/usr/libexec/PlistBuddy',
['-c', 'Print:CFBundleIdentifier', path.join(appPath, 'Info.plist')],
{encoding: 'utf8'},
)
.trim();
console.log(`Launching ${bundleID}`);
child_process.spawnSync('xcrun', ['simctl', 'launch', udid, bundleID], {
stdio: 'inherit',
});
});
}
function runOnDevice(
selectedDevice,
scheme,
xcodeProject,
configuration,
launchPackager,
verbose,
port,
) {
return buildProject(
xcodeProject,
selectedDevice.udid,
scheme,
configuration,
launchPackager,
verbose,
port,
).then(appName => {
if (!appName) {
appName = scheme;
}
const iosDeployInstallArgs = [
'--bundle',
getBuildPath(configuration, appName, true),
'--id',
selectedDevice.udid,
'--justlaunch',
];
console.log(
`installing and launching your app on ${selectedDevice.name}...`,
);
const iosDeployOutput = child_process.spawnSync(
'ios-deploy',
iosDeployInstallArgs,
{encoding: 'utf8'},
);
if (iosDeployOutput.error) {
console.log('');
console.log('** INSTALLATION FAILED **');
console.log('Make sure you have ios-deploy installed globally.');
console.log('(e.g "npm install -g ios-deploy")');
} else {
console.log('** INSTALLATION SUCCEEDED **');
}
});
}
function buildProject(
xcodeProject,
udid,
scheme,
configuration = 'Debug',
launchPackager = false,
verbose,
port,
) {
return new Promise((resolve, reject) => {
var xcodebuildArgs = [
xcodeProject.isWorkspace ? '-workspace' : '-project',
xcodeProject.name,
'-configuration',
configuration,
'-scheme',
scheme,
'-destination',
`id=${udid}`,
'-derivedDataPath',
'build',
];
console.log(`Building using "xcodebuild ${xcodebuildArgs.join(' ')}"`);
let xcpretty;
if (!verbose) {
xcpretty =
xcprettyAvailable() &&
child_process.spawn('xcpretty', [], {
stdio: ['pipe', process.stdout, process.stderr],
});
}
const buildProcess = child_process.spawn(
'xcodebuild',
xcodebuildArgs,
getProcessOptions(launchPackager, port),
);
let buildOutput = '';
buildProcess.stdout.on('data', function(data) {
buildOutput += data.toString();
if (xcpretty) {
xcpretty.stdin.write(data);
} else {
console.log(data.toString());
}
});
buildProcess.stderr.on('data', function(data) {
console.error(data.toString());
});
buildProcess.on('close', function(code) {
if (xcpretty) {
xcpretty.stdin.end();
}
//FULL_PRODUCT_NAME is the actual file name of the app, which actually comes from the Product Name in the build config, which does not necessary match a scheme name, example output line: export FULL_PRODUCT_NAME="Super App Dev.app"
let productNameMatch = /export FULL_PRODUCT_NAME="?(.+).app"?$/m.exec(
buildOutput,
);
if (
productNameMatch &&
productNameMatch.length &&
productNameMatch.length > 1
) {
return resolve(productNameMatch[1]); //0 is the full match, 1 is the app name
}
return buildProcess.error ? reject(buildProcess.error) : resolve();
});
});
}
function matchingDevice(devices, deviceName) {
if (deviceName === true && devices.length === 1) {
console.log(
`Using first available device ${
devices[0].name
} due to lack of name supplied.`,
);
return devices[0];
}
for (let i = devices.length - 1; i >= 0; i--) {
if (
devices[i].name === deviceName ||
formattedDeviceName(devices[i]) === deviceName
) {
return devices[i];
}
}
}
function matchingDeviceByUdid(devices, udid) {
for (let i = devices.length - 1; i >= 0; i--) {
if (devices[i].udid === udid) {
return devices[i];
}
}
}
function formattedDeviceName(simulator) {
return `${simulator.name} (${simulator.version})`;
}
function printFoundDevices(devices) {
for (let i = devices.length - 1; i >= 0; i--) {
console.log(devices[i].name + ' Udid: ' + devices[i].udid);
}
}
function getProcessOptions(launchPackager, port) {
if (launchPackager) {
return {
env: {...process.env, RCT_METRO_PORT: port},
};
}
return {
env: {...process.env, RCT_NO_LAUNCH_PACKAGER: true},
};
}
module.exports = {
name: 'run-ios',
description: 'builds your app and starts it on iOS simulator',
func: runIOS,
examples: [
{
desc: 'Run on a different simulator, e.g. iPhone 5',
cmd: 'react-native run-ios --simulator "iPhone 5"',
},
{
desc: 'Pass a non-standard location of iOS directory',
cmd: 'react-native run-ios --project-path "./app/ios"',
},
{
desc: "Run on a connected device, e.g. Max's iPhone",
cmd: 'react-native run-ios --device "Max\'s iPhone"',
},
{
desc: 'Run on the AppleTV simulator',
cmd:
'react-native run-ios --simulator "Apple TV" --scheme "helloworld-tvOS"',
},
],
options: [
{
command: '--simulator [string]',
description:
'Explicitly set simulator to use. Optionally include iOS version between' +
'parenthesis at the end to match an exact version: "iPhone 6 (10.0)"',
default: 'iPhone X',
},
{
command: '--configuration [string]',
description: 'Explicitly set the scheme configuration to use',
},
{
command: '--scheme [string]',
description: 'Explicitly set Xcode scheme to use',
},
{
command: '--project-path [string]',
description:
'Path relative to project root where the Xcode project ' +
"(.xcodeproj) lives. The default is 'ios'.",
default: 'ios',
},
{
command: '--device [string]',
description:
'Explicitly set device to use by name. The value is not required if you have a single device connected.',
},
{
command: '--udid [string]',
description: 'Explicitly set device to use by udid',
},
{
command: '--no-packager',
description: 'Do not launch packager while building',
},
{
command: '--verbose',
description: 'Do not use xcpretty even if installed',
},
{
command: '--port [number]',
default: process.env.RCT_METRO_PORT || 8081,
parse: (val: string) => Number(val),
},
],
};