Permalink
Browse files

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 #5119

Reviewed By: svcscm

Differential Revision: D2805199

Pulled By: frantic

fb-gh-sync-id: 423a45ba885cb5e48a16ac22095d757d8cca7e37
  • Loading branch information...
frantic authored and facebook-github-bot-6 committed Jan 6, 2016
1 parent 8772a6a commit 9490c2c759fc2d88907a7a450d307be7f717853a
View
@@ -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']
};
@@ -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'));
}
@@ -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);
+ });
+});
@@ -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([]);
+ });
+});
@@ -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;
@@ -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
@@ -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));

This comment has been minimized.

Show comment
Hide comment
@mosesoak

mosesoak Mar 2, 2016

Contributor

Thanks for doing this. We can't use this immediately though since we use different schemes in our project that load their own plist files. It would be great to be able to pass in a --scheme argument to run-ios.

@mosesoak

mosesoak Mar 2, 2016

Contributor

Thanks for doing this. We can't use this immediately though since we use different schemes in our project that load their own plist files. It would be great to be able to pass in a --scheme argument to run-ios.

This comment has been minimized.

Show comment
Hide comment
@frantic

frantic Mar 5, 2016

Contributor

Please send a PR!

@frantic

frantic Mar 5, 2016

Contributor

Please send a PR!

+ 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')],

This comment has been minimized.

Show comment
Hide comment
@gre

gre Feb 2, 2016

Contributor

this doesn't work if user rename its $(PRODUCT_NAME) to a different name of the appPath , maybe it should be inferredSchemeName ?

@gre

gre Feb 2, 2016

Contributor

this doesn't work if user rename its $(PRODUCT_NAME) to a different name of the appPath , maybe it should be inferredSchemeName ?

This comment has been minimized.

Show comment
Hide comment
@frantic

frantic Feb 3, 2016

Contributor

Good catch, happy to accept a PR

@frantic

frantic Feb 3, 2016

Contributor

Good catch, happy to accept a PR

+ {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;

1 comment on commit 9490c2c

@qingfeng

This comment has been minimized.

Show comment
Hide comment
@qingfeng

qingfeng Feb 1, 2016

Contributor

cool

Contributor

qingfeng commented on 9490c2c Feb 1, 2016

cool

Please sign in to comment.