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 path = require('path');
|
||||||
var Promise = require('promise');
|
var Promise = require('promise');
|
||||||
var runAndroid = require('./runAndroid/runAndroid');
|
var runAndroid = require('./runAndroid/runAndroid');
|
||||||
|
var runIOS = require('./runIOS/runIOS');
|
||||||
var server = require('./server/server');
|
var server = require('./server/server');
|
||||||
var TerminalAdapter = require('yeoman-environment/lib/adapter.js');
|
var TerminalAdapter = require('yeoman-environment/lib/adapter.js');
|
||||||
var yeoman = require('yeoman-environment');
|
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'],
|
'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'],
|
'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-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 ' +
|
'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']
|
'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() {
|
end: function() {
|
||||||
var projectPath = path.resolve(this.destinationRoot(), 'ios', this.name);
|
var projectPath = path.resolve(this.destinationRoot(), 'ios', this.name);
|
||||||
this.log(chalk.white.bold('To run your app on iOS:'));
|
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(' Open ' + projectPath + '.xcodeproj in Xcode'));
|
||||||
this.log(chalk.white(' Hit the Run button'));
|
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