Skip to content

Commit

Permalink
Added react-native run-ios
Browse files Browse the repository at this point in the history
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 9490c2c
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 0 deletions.
2 changes: 2 additions & 0 deletions local-cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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']
};
Expand Down
3 changes: 3 additions & 0 deletions local-cli/generator-ios/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down
57 changes: 57 additions & 0 deletions local-cli/runIOS/__tests__/findXcodeProject-test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
53 changes: 53 additions & 0 deletions local-cli/runIOS/__tests__/parseIOSSimulatorsList-test.js
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
43 changes: 43 additions & 0 deletions local-cli/runIOS/findXcodeProject.js
Original file line number Diff line number Diff line change
@@ -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;
49 changes: 49 additions & 0 deletions local-cli/runIOS/parseIOSSimulatorsList.js
Original file line number Diff line number Diff line change
@@ -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;
95 changes: 95 additions & 0 deletions local-cli/runIOS/runIOS.js
Original file line number Diff line number Diff line change
@@ -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.

Copy link
@mosesoak

mosesoak Mar 2, 2016

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.

Copy link
@frantic

frantic Mar 5, 2016

Author 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.

Copy link
@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.

Copy link
@frantic

frantic Feb 3, 2016

Author 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool

Please sign in to comment.