cli run-ios on device

Summary:
At the moment the run-ios command from the react-native cli does only work for simulators.
The pull request adds a new option to the existing command: **"--device 'device-name'" which installs and launches an iOS application on a connected device.**
This makes it easier to build a test environment using react-native for connected devices.

I've tested my code with the following commands:
react-native run-ios --device "Not existing device"
react-native run-ios --device
react-native run-ios --device "name-of-a-simulator"
react-native run-ios --device "name-of-connected-device"

Output of the first three commands:
![example_error_output](https://cloud.githubusercontent.com/assets/9102810/17669443/f53d5948-630d-11e6-9a80-7df2f352c6a3.png)

Additional to the manual command tests i've added a test file 'parseIOSDevicesList-test.js'.

I used **ios-deploy** In order to launch and install the .app-bundle on a connected device.
ios-deploy on github:
Closes https://github.com/facebook/react-native/pull/9414

Differential Revision: D3821638

Pulled By: javache

fbshipit-source-id: c07b7bf25283a966e45613a22ed3184bb1aac714
This commit is contained in:
David Gröger 2016-09-06 08:00:00 -07:00 committed by Facebook Github Bot 7
parent 128b698a03
commit 48ab5eb436
6 changed files with 444 additions and 109 deletions

View File

@ -0,0 +1,196 @@
/**
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.dontMock('../findMatchingSimulator');
const findMatchingSimulator = require('../findMatchingSimulator');
describe('findMatchingSimulator', () => {
it('should find simulator', () => {
expect(findMatchingSimulator({
"devices": {
"iOS 9.2": [
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 4s",
"udid": "B9B5E161-416B-43C4-A78F-729CB96CC8C6"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 5",
"udid": "1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6",
"udid": "BA0D93BD-07E6-4182-9B0A-F60A2474139C"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6 (Plus)",
"udid": "9564ABEE-9EC2-4B4A-B443-D3710929A45A"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6s",
"udid": "D0F29BE7-CC3C-4976-888D-C739B4F50508"
}
]
}
},
'iPhone 6'
)).toEqual({
udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C',
name: 'iPhone 6',
version: 'iOS 9.2'
});
});
it('should return null if no simulators available', () => {
expect(findMatchingSimulator({
"devices": {
"iOS 9.2": [
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 4s",
"udid": "B9B5E161-416B-43C4-A78F-729CB96CC8C6"
},
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 5",
"udid": "1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB"
},
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 6",
"udid": "BA0D93BD-07E6-4182-9B0A-F60A2474139C"
},
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 6 (Plus)",
"udid": "9564ABEE-9EC2-4B4A-B443-D3710929A45A"
},
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 6s",
"udid": "D0F29BE7-CC3C-4976-888D-C739B4F50508"
}
]
}
},
'iPhone 6'
)).toEqual(null);
});
it('should return null if an odd input', () => {
expect(findMatchingSimulator('random string input', 'iPhone 6')).toEqual(null);
});
it('should return the first simulator in list if none is defined', () => {
expect(findMatchingSimulator({
"devices": {
"iOS 9.2": [
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 4s",
"udid": "B9B5E161-416B-43C4-A78F-729CB96CC8C6"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 5",
"udid": "1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6",
"udid": "BA0D93BD-07E6-4182-9B0A-F60A2474139C"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6 (Plus)",
"udid": "9564ABEE-9EC2-4B4A-B443-D3710929A45A"
},
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 6s",
"udid": "D0F29BE7-CC3C-4976-888D-C739B4F50508"
}
]
}
},
null
)).toEqual({
udid: '1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB',
name: 'iPhone 5',
version: 'iOS 9.2'
});
});
it('should return the botted simulator in list if none is defined', () => {
expect(findMatchingSimulator({
"devices": {
"iOS 9.2": [
{
"state": "Shutdown",
"availability": "(unavailable, runtime profile not found)",
"name": "iPhone 4s",
"udid": "B9B5E161-416B-43C4-A78F-729CB96CC8C6"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 5",
"udid": "1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6",
"udid": "BA0D93BD-07E6-4182-9B0A-F60A2474139C"
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6 (Plus)",
"udid": "9564ABEE-9EC2-4B4A-B443-D3710929A45A"
},
{
"state": "Booted",
"availability": "(available)",
"name": "iPhone 6s",
"udid": "D0F29BE7-CC3C-4976-888D-C739B4F50508"
}
]
}
},
null
)).toEqual({
udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508',
name: 'iPhone 6s',
version: 'iOS 9.2'
});
});
});

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.dontMock('../parseIOSDevicesList');
var parseIOSDevicesList = require('../parseIOSDevicesList');
describe('parseIOSDevicesList', () => {
it('parses typical output', () => {
var devices = parseIOSDevicesList([
'Known Devices:',
'Maxs MacBook Pro [11111111-1111-1111-1111-111111111111]',
"Max's iPhone (9.2) [11111111111111111111aaaaaaaaaaaaaaaaaaaa]",
'iPad 2 (9.3) [07538CE4-675B-4EDA-90F2-3DD3CD93309D] (Simulator)',
'iPad Air (9.3) [0745F6D1-6DC5-4427-B9A6-6FBA327ED65A] (Simulator)',
'iPhone 6s (9.3) [3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4] (Simulator)',
'Known Templates:',
'Activity Monitor',
'Blank',
'System Usage',
'Zombies'
].join('\n'));
expect(devices).toEqual([
{name: "Max's iPhone", udid: '11111111111111111111aaaaaaaaaaaaaaaaaaaa', version: '9.2'},
]);
});
it('ignores garbage', () => {
expect(parseIOSDevicesList('Something went terribly wrong (-42)')).toEqual([]);
});
});

View File

@ -1,53 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.dontMock('../parseIOSSimulatorsList');
var parseIOSSimulatorsList = require('../parseIOSSimulatorsList');
describe('parseIOSSimulatorsList', () => {
it('parses typical output', () => {
var simulators = parseIOSSimulatorsList([
'== Devices ==',
'-- iOS 8.1 --',
' iPhone 4s (4FE43B33-EF13-49A5-B6A6-658D32F20988) (Shutdown)',
'-- iOS 8.4 --',
' iPhone 4s (EAB622C7-8ADE-4FAE-A911-94C0CA4709BB) (Shutdown)',
' iPhone 5 (AE1CD3D0-A85B-4A73-B320-9CA7BA4FAEB0) (Shutdown)',
].join('\n'));
expect(simulators).toEqual([
{name: 'iPhone 4s', udid: '4FE43B33-EF13-49A5-B6A6-658D32F20988', version: '8.1'},
{name: 'iPhone 4s', udid: 'EAB622C7-8ADE-4FAE-A911-94C0CA4709BB', version: '8.4'},
{name: 'iPhone 5', udid: 'AE1CD3D0-A85B-4A73-B320-9CA7BA4FAEB0', version: '8.4'},
]);
});
it('ignores unavailable simulators', () => {
var simulators = parseIOSSimulatorsList([
'== Devices ==',
'-- iOS 8.1 --',
' iPhone 4s (4FE43B33-EF13-49A5-B6A6-658D32F20988) (Shutdown)',
'-- Unavailable: com.apple.CoreSimulator.SimRuntime.iOS-8-3 --',
' iPhone 5s (EAB622C7-8ADE-4FAE-A911-94C0CA4709BB) (Shutdown)',
].join('\n'));
expect(simulators).toEqual([{
name: 'iPhone 4s',
udid: '4FE43B33-EF13-49A5-B6A6-658D32F20988',
version: '8.1',
}]);
});
it('ignores garbage', () => {
expect(parseIOSSimulatorsList('Something went terribly wrong (-42)')).toEqual([]);
});
});

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.*
*/
'use strict';
/**
* Takes in a parsed simulator list and a desired name, and returns an object with the matching simulator.
*
* If the simulatorName argument is null, we'll go into default mode and return the currently booted simulator, or if
* none is booted, it will be the first in the list.
*
* @param Object simulators a parsed list from `xcrun simctl list --json devices` command
* @param String|null simulatorName the string with the name of desired simulator. If null, it will use the currently
* booted simulator, or if none are booted, the first in the list.
* @returns {Object} {udid, name, version}
*/
function findMatchingSimulator(simulators, simulatorName) {
if (!simulators.devices) {
return null;
}
const devices = simulators.devices;
var match;
for (let version in devices) {
// Making sure the version of the simulator is an iOS (Removes Apple Watch, etc)
if (version.indexOf('iOS') !== 0) {
continue;
}
for (let i in devices[version]) {
let simulator = devices[version][i];
// Skipping non-available simulator
if (simulator.availability !== '(available)') {
continue;
}
// If there is a booted simulator, we'll use that as instruments will not boot a second simulator
if (simulator.state === 'Booted') {
if (simulatorName !== null) {
console.warn("We couldn't boot your defined simulator due to an already booted simulator. We are limited to one simulator launched at a time.");
}
return {
udid: simulator.udid,
name: simulator.name,
version
};
}
if (simulator.name === simulatorName) {
return {
udid: simulator.udid,
name: simulator.name,
version
};
}
// Keeps track of the first available simulator for use if we can't find one above.
if (simulatorName === null && !match) {
match = {
udid: simulator.udid,
name: simulator.name,
version
};
}
}
}
if (match) {
return match;
}
return null;
}
module.exports = findMatchingSimulator;

View File

@ -10,7 +10,7 @@
*/
'use strict';
type IOSSimulatorInfo = {
type IOSDeviceInfo = {
name: string;
udid: string;
version: string;
@ -19,31 +19,20 @@ type IOSSimulatorInfo = {
/**
* Parses the output of `xcrun simctl list devices` command
*/
function parseIOSSimulatorsList(text: string): Array<IOSSimulatorInfo> {
function parseIOSDevicesList(text: string): Array<IOSDeviceInfo> {
const devices = [];
var currentOS = null;
text.split('\n').forEach((line) => {
var section = line.match(/^-- (.+) --$/);
if (section) {
var header = section[1].match(/^iOS (.+)$/);
if (header) {
currentOS = header[1];
} else {
currentOS = null;
}
return;
}
const device = line.match(/^[ ]*([^()]+) \(([^()]+)\)/);
if (device && currentOS) {
const device = line.match(/(.*?) \((.*?)\) \[(.*?)\]/);
const noSimulator = line.match(/(.*?) \((.*?)\) \[(.*?)\] \((.*?)\)/);
if (device != null && noSimulator == null){
var name = device[1];
var udid = device[2];
devices.push({udid, name, version: currentOS});
var version = device[2];
var udid = device[3];
devices.push({udid, name, version});
}
});
return devices;
}
module.exports = parseIOSSimulatorsList;
module.exports = parseIOSDevicesList;

View File

@ -1,18 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const findXcodeProject = require('./findXcodeProject');
const parseIOSSimulatorsList = require('./parseIOSSimulatorsList');
const parseIOSDevicesList = require('./parseIOSDevicesList');
const findMatchingSimulator = require('./findMatchingSimulator');
function runIOS(argv, config, args) {
process.chdir(args.projectPath);
@ -24,32 +25,68 @@ function runIOS(argv, config, args) {
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 simulators = parseIOSSimulatorsList(
child_process.execFileSync('xcrun', ['simctl', 'list', 'devices'], {encoding: 'utf8'})
const devices = parseIOSDevicesList(
child_process.execFileSync('xcrun', ['instruments', '-s'], {encoding: 'utf8'})
);
const selectedSimulator = matchingSimulator(simulators, args.simulator);
if (args.device) {
const selectedDevice = matchingDevice(devices, args.device);
if (selectedDevice){
runOnDevice(selectedDevice, scheme, xcodeProject);
} else {
if (devices){
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) {
runOnDeviceByUdid(args.udid, scheme, xcodeProject, devices);
} else {
runOnSimulator(xcodeProject, args, inferredSchemeName, scheme);
}
}
function runOnDeviceByUdid(udid, scheme, xcodeProject, devices) {
const selectedDevice = matchingDeviceByUdid(devices, udid);
if (selectedDevice){
runOnDevice(selectedDevice, scheme, xcodeProject);
} else {
if (devices){
console.log('Could not find device with the udid: "' + udid + '".');
console.log('Choose one of the following:');
printFoundDevices(devices);
} else {
console.log('No iOS devices connected.');
}
}
}
function runOnSimulator(xcodeProject, args, inferredSchemeName, scheme){
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(`Cound't find ${args.simulator} simulator`);
}
const simulatorFullName = formattedSimulatorName(selectedSimulator);
const simulatorFullName = formattedDeviceName(selectedSimulator);
console.log(`Launching ${simulatorFullName}...`);
try {
child_process.spawnSync('xcrun', ['instruments', '-w', simulatorFullName]);
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
}
const xcodebuildArgs = [
xcodeProject.isWorkspace ? '-workspace' : '-project', xcodeProject.name,
'-scheme', scheme,
'-destination', `id=${selectedSimulator.udid}`,
'-derivedDataPath', 'build',
];
console.log(`Building using "xcodebuild ${xcodebuildArgs.join(' ')}"`);
child_process.spawnSync('xcodebuild', xcodebuildArgs, {stdio: 'inherit'});
buildProject(xcodeProject, selectedSimulator.udid, scheme);
const appPath = `build/Build/Products/Debug-iphonesimulator/${inferredSchemeName}.app`;
console.log(`Installing ${appPath}`);
@ -65,31 +102,79 @@ function runIOS(argv, config, args) {
child_process.spawnSync('xcrun', ['simctl', 'launch', 'booted', bundleID], {stdio: 'inherit'});
}
function matchingSimulator(simulators, simulatorName) {
for (let i = simulators.length - 1; i >= 0; i--) {
if (simulators[i].name === simulatorName || formattedSimulatorName(simulators[i]) === simulatorName) {
return simulators[i];
function runOnDevice(selectedDevice, scheme, xcodeProject){
buildProject(xcodeProject, selectedDevice.udid, scheme);
const iosDeployInstallArgs = [
'--bundle', 'build/Build/Products/Debug-iphoneos/' + scheme + '.app',
'--id' , selectedDevice.udid,
'--justlaunch'
];
console.log(`installing and launching your app on ${selectedDevice.name}...`);
var 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) {
const xcodebuildArgs = [
xcodeProject.isWorkspace ? '-workspace' : '-project', xcodeProject.name,
'-scheme', scheme,
'-destination', `id=${udid}`,
'-derivedDataPath', 'build',
];
console.log(`Building using "xcodebuild ${xcodebuildArgs.join(' ')}"`);
child_process.spawnSync('xcodebuild', xcodebuildArgs, {stdio: 'inherit'});
}
function matchingDevice(devices, deviceName) {
for (let i = devices.length - 1; i >= 0; i--) {
if (devices[i].name === deviceName || formattedDeviceName(devices[i]) === deviceName) {
return devices[i];
}
}
}
function formattedSimulatorName(simulator) {
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);
}
}
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 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'",
},
],
options: [{
command: '--simulator [string]',
@ -101,7 +186,13 @@ module.exports = {
}, {
command: '--project-path [string]',
description: 'Path relative to project root where the Xcode project '
+ '(.xcodeproj) lives. The default is \'ios\'.',
+ '(.xcodeproj) lives. The default is \'ios\'.',
default: 'ios',
}, {
command: '--device [string]',
description: 'Explicitly set device to use by name',
},{
command: '--udid [string]',
description: 'Explicitly set device to use by udid',
}]
};