Added `react-native run-ios`

Summary:
Works the same way as `react-native run-android`, but targets iOS simulator instead. Under the hood, it uses `xcodebuild` to compile the app and store it in `ios/build` folder, then triggers `instruments` and `simctl` to install and launch the app on simulator.

Since Facebook relies on BUCK to build and run iOS app, we probably won't use `run-ios` internally. That's why I'm putting this as public PR instead of internal diff.

To test this, I hacked global `react-native` script to install react native from my local checkout instead of from npm, cd into the folder and ran `react-native run-ios`.
Closes https://github.com/facebook/react-native/pull/5119

Reviewed By: svcscm

Differential Revision: D2805199

Pulled By: frantic

fb-gh-sync-id: 423a45ba885cb5e48a16ac22095d757d8cca7e37
This commit is contained in:
Alex Kotliarskyi 2016-01-05 17:11:53 -08:00 committed by facebook-github-bot-6
parent 8772a6a542
commit 9490c2c759
7 changed files with 302 additions and 0 deletions

View File

@ -25,6 +25,7 @@ var link = require('./library/link');
var path = require('path');
var Promise = require('promise');
var runAndroid = require('./runAndroid/runAndroid');
var runIOS = require('./runIOS/runIOS');
var server = require('./server/server');
var TerminalAdapter = require('yeoman-environment/lib/adapter.js');
var yeoman = require('yeoman-environment');
@ -46,6 +47,7 @@ var documentedCommands = {
'link': [link, 'Adds a third-party library to your project. Example: react-native link awesome-camera'],
'android': [generateWrapper, 'generates an Android project for your app'],
'run-android': [runAndroid, 'builds your app and starts it on a connected Android emulator or device'],
'run-ios': [runIOS, 'builds your app and starts it on iOS simulator'],
'upgrade': [upgrade, 'upgrade your app\'s template files to the latest version; run this after ' +
'updating the react-native version in your package.json and running npm install']
};

View File

@ -49,6 +49,9 @@ module.exports = yeoman.generators.NamedBase.extend({
end: function() {
var projectPath = path.resolve(this.destinationRoot(), 'ios', this.name);
this.log(chalk.white.bold('To run your app on iOS:'));
this.log(chalk.white(' cd ' + this.destinationRoot()));
this.log(chalk.white(' react-native run-ios'));
this.log(chalk.white(' - or -'));
this.log(chalk.white(' Open ' + projectPath + '.xcodeproj in Xcode'));
this.log(chalk.white(' Hit the Run button'));
}

View File

@ -0,0 +1,57 @@
/**
* 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('../findXcodeProject');
const findXcodeProject = require('../findXcodeProject');
describe('findXcodeProject', () => {
it('should find *.xcodeproj file', () => {
expect(findXcodeProject([
'.DS_Store',
'AwesomeApp',
'AwesomeApp.xcodeproj',
'AwesomeAppTests',
'PodFile',
'Podfile.lock',
'Pods'
])).toEqual({
name: 'AwesomeApp.xcodeproj',
isWorkspace: false,
});
});
it('should prefer *.xcworkspace', () => {
expect(findXcodeProject([
'.DS_Store',
'AwesomeApp',
'AwesomeApp.xcodeproj',
'AwesomeApp.xcworkspace',
'AwesomeAppTests',
'PodFile',
'Podfile.lock',
'Pods'
])).toEqual({
name: 'AwesomeApp.xcworkspace',
isWorkspace: true,
});
});
it('should return null if nothing found', () => {
expect(findXcodeProject([
'.DS_Store',
'AwesomeApp',
'AwesomeAppTests',
'PodFile',
'Podfile.lock',
'Pods'
])).toEqual(null);
});
});

View File

@ -0,0 +1,53 @@
/**
* 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,43 @@
/**
* 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.
*
* @flow
*/
'use strict';
const path = require('path');
type ProjectInfo = {
name: string;
isWorkspace: boolean;
}
function findXcodeProject(files: Array<string>): ?ProjectInfo {
const sortedFiles = files.sort();
for (let i = sortedFiles.length - 1; i >= 0; i--) {
const fileName = files[i];
const ext = path.extname(fileName);
if (ext === '.xcworkspace') {
return {
name: fileName,
isWorkspace: true,
};
}
if (ext === '.xcodeproj') {
return {
name: fileName,
isWorkspace: false,
};
}
}
return null;
}
module.exports = findXcodeProject;

View File

@ -0,0 +1,49 @@
/**
* 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.
*
* @flow
*/
'use strict';
type IOSSimulatorInfo = {
name: string;
udid: string;
version: string;
}
/**
* Parses the output of `xcrun simctl list devices` command
*/
function parseIOSSimulatorsList(text: string): Array<IOSSimulatorInfo> {
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) {
var name = device[1];
var udid = device[2];
devices.push({udid, name, version: currentOS});
}
});
return devices;
}
module.exports = parseIOSSimulatorsList;

View File

@ -0,0 +1,95 @@
/**
* 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 parseCommandLine = require('../util/parseCommandLine');
const findXcodeProject = require('./findXcodeProject');
const parseIOSSimulatorsList = require('./parseIOSSimulatorsList');
const Promise = require('promise');
/**
* Starts the app on iOS simulator
*/
function runIOS(argv, config) {
return new Promise((resolve, reject) => {
_runIOS(argv, config, resolve, reject);
resolve();
});
}
function _runIOS(argv, config, resolve, reject) {
const args = parseCommandLine([{
command: 'simulator',
description: 'Explicitly set simulator to use',
type: 'string',
required: false,
default: 'iPhone 6',
}], argv);
process.chdir('ios');
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));
console.log(`Found Xcode ${xcodeProject.isWorkspace ? 'workspace' : 'project'} ${xcodeProject.name}`);
const simulators = parseIOSSimulatorsList(
child_process.execFileSync('xcrun', ['simctl', 'list', 'devices'], {encoding: 'utf8'})
);
const selectedSimulator = matchingSimulator(simulators, args.simulator);
if (!selectedSimulator) {
throw new Error(`Cound't find ${args.simulator} simulator`);
}
const simulatorFullName = `${selectedSimulator.name} (${selectedSimulator.version})`;
console.log(`Launching ${simulatorFullName}...`);
try {
child_process.spawnSync('xcrun', ['instruments', '-w', simulatorFullName]);
} 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', inferredSchemeName,
'-destination', `id=${selectedSimulator.udid}`,
'-derivedDataPath', 'build',
];
console.log(`Building using "xcodebuild ${xcodebuildArgs.join(' ')}"`);
child_process.spawnSync('xcodebuild', xcodebuildArgs, {stdio: 'inherit'});
const appPath = `build/Build/Products/Debug-iphonesimulator/${inferredSchemeName}.app`;
console.log(`Installing ${appPath}`);
child_process.spawnSync('xcrun', ['simctl', 'install', 'booted', 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', 'booted', bundleID], {stdio: 'inherit'});
}
function matchingSimulator(simulators, simulatorName) {
for (let i = simulators.length - 1; i >= 0; i--) {
if (simulators[i].name === simulatorName) {
return simulators[i];
}
}
}
module.exports = runIOS;