diff --git a/local-cli/cli.js b/local-cli/cli.js index 9fd1fda7f1a508..767c642476d323 100644 --- a/local-cli/cli.js +++ b/local-cli/cli.js @@ -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'] }; diff --git a/local-cli/generator-ios/index.js b/local-cli/generator-ios/index.js index 1e1afb67b78e02..28e3c442d7872e 100644 --- a/local-cli/generator-ios/index.js +++ b/local-cli/generator-ios/index.js @@ -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')); } diff --git a/local-cli/runIOS/__tests__/findXcodeProject-test.js b/local-cli/runIOS/__tests__/findXcodeProject-test.js new file mode 100644 index 00000000000000..d0e3832be54e8a --- /dev/null +++ b/local-cli/runIOS/__tests__/findXcodeProject-test.js @@ -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); + }); +}); diff --git a/local-cli/runIOS/__tests__/parseIOSSimulatorsList-test.js b/local-cli/runIOS/__tests__/parseIOSSimulatorsList-test.js new file mode 100644 index 00000000000000..92396fe4a82a16 --- /dev/null +++ b/local-cli/runIOS/__tests__/parseIOSSimulatorsList-test.js @@ -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([]); + }); +}); diff --git a/local-cli/runIOS/findXcodeProject.js b/local-cli/runIOS/findXcodeProject.js new file mode 100644 index 00000000000000..0d6cd302c6d0a6 --- /dev/null +++ b/local-cli/runIOS/findXcodeProject.js @@ -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): ?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; diff --git a/local-cli/runIOS/parseIOSSimulatorsList.js b/local-cli/runIOS/parseIOSSimulatorsList.js new file mode 100644 index 00000000000000..66f0d0ffaf62d7 --- /dev/null +++ b/local-cli/runIOS/parseIOSSimulatorsList.js @@ -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 { + 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; diff --git a/local-cli/runIOS/runIOS.js b/local-cli/runIOS/runIOS.js new file mode 100644 index 00000000000000..00249820e0d1e7 --- /dev/null +++ b/local-cli/runIOS/runIOS.js @@ -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;