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:
parent
8772a6a542
commit
9490c2c759
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue