diff --git a/docs/commands.md b/docs/commands.md index 9186f2782..665809a98 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -14,6 +14,7 @@ React Native CLI comes with following commands: - [`run-android`](#run-android) - [`build-android`](#build-android) - [`run-ios`](#run-ios) +- [`build-ios`](#build-ios) - [`start`](#start) - [`upgrade`](#upgrade) - [`profile-hermes`](#profile-hermes) @@ -429,6 +430,7 @@ react-native run-ios [options] Builds your app and starts it on iOS simulator. + #### Options #### `--simulator ` @@ -454,7 +456,77 @@ react-native run-ios --simulator "iPhone XS Max" #### `--configuration ` -Explicitly set the scheme configuration to use default: 'Debug'. +[Deprecated] Explicitly set the scheme configuration to use default: 'Debug'. + +#### `--scheme ` + +Explicitly set Xcode scheme to use. + +#### `--device [string]` + +Explicitly set device to use by name. The value is not required if you have a single device connected. + +#### `--udid ` + +Explicitly set device to use by udid. + +#### `--no-packager` + +Do not launch packager while building. + +#### `--verbose` + +Do not use `xcbeautify` or `xcpretty` even if installed. + +#### `--port ` + +Runs packager on specified port. + +Default: `process.env.RCT_METRO_PORT || 8081` + +#### `--xcconfig ` + +Explicitly set `xcconfig` to use in build. + +#### `--buildFolder ` + +Location for iOS build artifacts. Corresponds to Xcode's `-derivedDataPath`. + +### `build-ios` + +Usage: + +```sh +react-native build-ios [options] +``` + +Builds IOS app. + +#### Options +#### `--simulator ` + +> default: iPhone 14 + +Explicitly set the simulator to use. Optionally include iOS version between parenthesis at the end to match an exact version, e.g. `"iPhone 6 (10.0)"`. + +Notes: If selected simulator does not exist, cli will try to run fallback simulators in following order: + +- `iPhone 14` +- `iPhone 13` +- `iPhone 12` +- `iPhone 11` + +Notes: `simulator_name` must be a valid iOS simulator name. If in doubt, open your AwesomeApp/ios/AwesomeApp.xcodeproj folder on XCode and unroll the dropdown menu containing the simulator list. The dropdown menu is situated on the right hand side of the play button (top left corner). + +Example: this will launch your project directly onto the iPhone XS Max simulator: + +```sh +react-native run-ios --simulator "iPhone XS Max" +``` + +#### `--configuration ` + +[Deprecated] Explicitly set the scheme configuration to use default: 'Debug'. #### `--scheme ` diff --git a/packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts b/packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts new file mode 100644 index 000000000..ab70626ce --- /dev/null +++ b/packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts @@ -0,0 +1,164 @@ +import child_process, { + ChildProcess, + SpawnOptionsWithoutStdio, +} from 'child_process'; +import chalk from 'chalk'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import {logger, CLIError} from '@react-native-community/cli-tools'; +import {getLoader} from '@react-native-community/cli-tools'; + +export type BuildFlags = { + mode: string; + packager: boolean; + verbose: boolean; + xcconfig?: string; + buildFolder?: string; + port: number; + terminal: string | undefined; +}; + +export function buildProject( + xcodeProject: IOSProjectInfo, + udid: string | undefined, + scheme: string, + args: BuildFlags, +): Promise { + return new Promise((resolve, reject) => { + const xcodebuildArgs = [ + xcodeProject.isWorkspace ? '-workspace' : '-project', + xcodeProject.name, + ...(args.xcconfig ? ['-xcconfig', args.xcconfig] : []), + ...(args.buildFolder ? ['-derivedDataPath', args.buildFolder] : []), + '-configuration', + args.mode, + '-scheme', + scheme, + '-destination', + udid + ? `id=${udid}` + : args.mode === 'Debug' + ? 'generic/platform=iOS Simulator' + : 'generic/platform=iOS', + ]; + const loader = getLoader(); + logger.info( + `Building ${chalk.dim( + `(using "xcodebuild ${xcodebuildArgs.join(' ')}")`, + )}`, + ); + let xcodebuildOutputFormatter: ChildProcess | any; + if (!args.verbose) { + if (xcbeautifyAvailable()) { + xcodebuildOutputFormatter = child_process.spawn('xcbeautify', [], { + stdio: ['pipe', process.stdout, process.stderr], + }); + } else if (xcprettyAvailable()) { + xcodebuildOutputFormatter = child_process.spawn('xcpretty', [], { + stdio: ['pipe', process.stdout, process.stderr], + }); + } + } + const buildProcess = child_process.spawn( + 'xcodebuild', + xcodebuildArgs, + getProcessOptions(args), + ); + let buildOutput = ''; + let errorOutput = ''; + buildProcess.stdout.on('data', (data: Buffer) => { + const stringData = data.toString(); + buildOutput += stringData; + if (xcodebuildOutputFormatter) { + xcodebuildOutputFormatter.stdin.write(data); + } else { + if (logger.isVerbose()) { + logger.debug(stringData); + } else { + loader.start( + `Building the app${'.'.repeat(buildOutput.length % 10)}`, + ); + } + } + }); + + buildProcess.stderr.on('data', (data: Buffer) => { + errorOutput += data; + }); + buildProcess.on('close', (code: number) => { + if (xcodebuildOutputFormatter) { + xcodebuildOutputFormatter.stdin.end(); + } else { + loader.stop(); + } + if (code !== 0) { + reject( + new CLIError( + ` + Failed to build iOS project. + + We ran "xcodebuild" command but it exited with error code ${code}. To debug build + logs further, consider building your app with Xcode.app, by opening + ${xcodeProject.name}. + `, + xcodebuildOutputFormatter + ? undefined + : buildOutput + '\n' + errorOutput, + ), + ); + return; + } + logger.success('Successfully built the app'); + resolve(buildOutput); + }); + }); +} + +function xcbeautifyAvailable() { + try { + child_process.execSync('xcbeautify --version', { + stdio: [0, 'pipe', 'ignore'], + }); + } catch (error) { + return false; + } + return true; +} + +function xcprettyAvailable() { + try { + child_process.execSync('xcpretty --version', { + stdio: [0, 'pipe', 'ignore'], + }); + } catch (error) { + return false; + } + return true; +} + +function getProcessOptions({ + packager, + terminal, + port, +}: { + packager: boolean; + terminal: string | undefined; + port: number; +}): SpawnOptionsWithoutStdio { + if (packager) { + return { + env: { + ...process.env, + RCT_TERMINAL: terminal, + RCT_METRO_PORT: port.toString(), + }, + }; + } + + return { + env: { + ...process.env, + RCT_TERMINAL: terminal, + RCT_NO_LAUNCH_PACKAGER: 'true', + }, + }; +} diff --git a/packages/cli-platform-ios/src/commands/buildIOS/index.ts b/packages/cli-platform-ios/src/commands/buildIOS/index.ts new file mode 100644 index 000000000..257cf1cf6 --- /dev/null +++ b/packages/cli-platform-ios/src/commands/buildIOS/index.ts @@ -0,0 +1,249 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import path from 'path'; +import chalk from 'chalk'; +import {Config} from '@react-native-community/cli-types'; +import { + logger, + CLIError, + getDefaultUserTerminal, +} from '@react-native-community/cli-tools'; +import {Device} from '../../types'; +import {BuildFlags, buildProject} from './buildProject'; +import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; +import {getDevices} from '../../tools/getDevices'; + +export interface FlagsT extends BuildFlags { + configuration?: string; + simulator?: string; + device?: string | true; + udid?: string; + scheme?: string; +} + +function buildIOS(_: Array, ctx: Config, args: FlagsT) { + if (!ctx.project.ios) { + throw new CLIError( + 'iOS project folder not found. Are you sure this is a React Native project?', + ); + } + + if (args.configuration) { + logger.warn('--configuration has been deprecated. Use --mode instead.'); + logger.warn( + 'Parameters were automatically reassigned to --mode on this run.', + ); + args.mode = args.configuration; + } + + const {xcodeProject, sourceDir} = ctx.project.ios; + + process.chdir(sourceDir); + + if (!xcodeProject) { + throw new CLIError( + `Could not find Xcode project files in "${sourceDir}" folder`, + ); + } + + const inferredSchemeName = path.basename( + xcodeProject.name, + path.extname(xcodeProject.name), + ); + const scheme = args.scheme || inferredSchemeName; + + logger.info( + `Found Xcode ${ + xcodeProject.isWorkspace ? 'workspace' : 'project' + } "${chalk.bold(xcodeProject.name)}"`, + ); + + const extendedArgs = { + ...args, + packager: false, + }; + + // // No need to load all available devices + if (!args.device && !args.udid) { + if (!args.simulator) { + return buildProject(xcodeProject, undefined, scheme, extendedArgs); + } + + /** + * If provided simulator does not exist, try simulators in following order + * - iPhone 14 + * - iPhone 13 + * - iPhone 12 + * - iPhone 11 + */ + const fallbackSimulators = [ + 'iPhone 14', + 'iPhone 13', + 'iPhone 12', + 'iPhone 11', + ]; + + const selectedSimulator = getDestinationSimulator(args, fallbackSimulators); + + return buildProject( + xcodeProject, + selectedSimulator.udid, + scheme, + extendedArgs, + ); + } + + if (args.device && args.udid) { + return logger.error( + 'The `device` and `udid` options are mutually exclusive.', + ); + } + + const devices = getDevices(); + + if (args.udid) { + const device = devices.find((d) => d.udid === args.udid); + if (!device) { + return logger.error( + `Could not find a device with udid: "${chalk.bold( + args.udid, + )}". ${printFoundDevices(devices)}`, + ); + } + + return buildProject(xcodeProject, device.udid, scheme, extendedArgs); + } else { + const physicalDevices = devices.filter((d) => d.type !== 'simulator'); + const device = matchingDevice(physicalDevices, args.device); + if (device) { + return buildProject(xcodeProject, device.udid, scheme, extendedArgs); + } + } +} + +function matchingDevice( + devices: Array, + deviceName: string | true | undefined, +) { + if (deviceName === true) { + const firstIOSDevice = devices.find((d) => d.type === 'device')!; + if (firstIOSDevice) { + logger.info( + `Using first available device named "${chalk.bold( + firstIOSDevice.name, + )}" due to lack of name supplied.`, + ); + return firstIOSDevice; + } else { + logger.error('No iOS devices connected.'); + return undefined; + } + } + const deviceByName = devices.find( + (device) => + device.name === deviceName || formattedDeviceName(device) === deviceName, + ); + if (!deviceByName) { + logger.error( + `Could not find a device named: "${chalk.bold( + String(deviceName), + )}". ${printFoundDevices(devices)}`, + ); + } + return deviceByName; +} + +function formattedDeviceName(simulator: Device) { + return simulator.version + ? `${simulator.name} (${simulator.version})` + : simulator.name; +} + +function printFoundDevices(devices: Array) { + return [ + 'Available devices:', + ...devices.map((device) => ` - ${device.name} (${device.udid})`), + ].join('\n'); +} + +export const iosBuildOptions = [ + { + name: '--simulator ', + description: + 'Explicitly set simulator to use. Optionally include iOS version between ' + + 'parenthesis at the end to match an exact version: "iPhone 6 (10.0)"', + }, + { + name: '--mode ', + description: 'Explicitly set the scheme configuration to use', + default: 'Debug', + }, + { + name: '--configuration ', + description: '[Deprecated] Explicitly set the scheme configuration to use', + }, + { + name: '--scheme ', + description: 'Explicitly set Xcode scheme to use', + }, + { + name: '--device [string]', + description: + 'Explicitly set device to use by name. The value is not required if you have a single device connected.', + }, + { + name: '--udid ', + description: 'Explicitly set device to use by udid', + }, + { + name: '--verbose', + description: 'Do not use xcbeautify or xcpretty even if installed', + }, + { + name: '--port ', + default: process.env.RCT_METRO_PORT || 8081, + parse: Number, + }, + { + name: '--terminal ', + description: + 'Launches the Metro Bundler in a new window using the specified terminal path.', + default: getDefaultUserTerminal(), + }, + { + name: '--xcconfig [string]', + description: 'Explicitly set xcconfig to use', + }, + { + name: '--buildFolder ', + description: + 'Location for iOS build artifacts. Corresponds to Xcode\'s "-derivedDataPath".', + }, +]; + +export default { + name: 'build-ios', + description: 'builds your app on iOS simulator', + func: buildIOS, + examples: [ + { + desc: 'Build the app for the IOS simulator', + cmd: 'react-native build-ios', + }, + { + desc: 'Build the app for all IOS devices', + cmd: 'react-native build-ios --mode "Release"', + }, + { + desc: 'Build the app for a specific IOS device', + cmd: 'react-native build-ios --simulator "IPhone 11"', + }, + ], + options: iosBuildOptions, +}; diff --git a/packages/cli-platform-ios/src/commands/index.ts b/packages/cli-platform-ios/src/commands/index.ts index 2dc3c640e..e091b130a 100644 --- a/packages/cli-platform-ios/src/commands/index.ts +++ b/packages/cli-platform-ios/src/commands/index.ts @@ -1,4 +1,5 @@ import logIOS from './logIOS'; import runIOS from './runIOS'; +import buildIOS from './buildIOS'; -export default [logIOS, runIOS]; +export default [logIOS, runIOS, buildIOS]; diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index 1992570d6..3284da403 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -6,40 +6,29 @@ * */ -import child_process, { - ChildProcess, - SpawnOptionsWithoutStdio, -} from 'child_process'; +import child_process from 'child_process'; import path from 'path'; import fs from 'fs'; import chalk from 'chalk'; import {Config, IOSProjectInfo} from '@react-native-community/cli-types'; -import findMatchingSimulator from './findMatchingSimulator'; -import { - logger, - CLIError, - getDefaultUserTerminal, -} from '@react-native-community/cli-tools'; +import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; +import {logger, CLIError} from '@react-native-community/cli-tools'; +import {BuildFlags, buildProject} from '../buildIOS/buildProject'; +import {iosBuildOptions} from '../buildIOS'; import {Device} from '../../types'; -import ora from 'ora'; + import listIOSDevices, {promptForDeviceSelection} from './listIOSDevices'; -type FlagsT = { +export interface FlagsT extends BuildFlags { simulator?: string; configuration: string; scheme?: string; projectPath: string; device?: string | true; udid?: string; - packager: boolean; - verbose: boolean; - port: number; binaryPath?: string; - terminal: string | undefined; - xcconfig?: string; - buildFolder?: string; listDevices?: boolean; -}; +} async function runIOS(_: Array, ctx: Config, args: FlagsT) { if (!ctx.project.ios) { @@ -60,6 +49,13 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { } } + if (args.configuration) { + logger.warn('--configuration has been deprecated. Use --mode instead.'); + logger.warn( + 'Parameters were automatically reassigned to --mode on this run.', + ); + args.mode = args.configuration; + } const {xcodeProject, sourceDir} = ctx.project.ios; process.chdir(sourceDir); @@ -206,39 +202,26 @@ async function runOnSimulator( args: FlagsT, simulator?: Device, ) { - let selectedSimulator; + // let selectedSimulator; + /** + * If provided simulator does not exist, try simulators in following order + * - iPhone 14 + * - iPhone 13 + * - iPhone 12 + * - iPhone 11 + */ + let selectedSimulator; if (simulator) { selectedSimulator = simulator; } else { - const simulators = getSimulators(); - - /** - * If provided simulator does not exist, try simulators in following order - * - iPhone 14 - * - iPhone 13 - * - iPhone 12 - * - iPhone 11 - */ const fallbackSimulators = [ 'iPhone 14', 'iPhone 13', 'iPhone 12', 'iPhone 11', ]; - - selectedSimulator = fallbackSimulators.reduce( - (matchingSimulator, fallback) => { - return ( - matchingSimulator || - findMatchingSimulator(simulators, {simulator: fallback}) - ); - }, - findMatchingSimulator(simulators, { - udid: args.udid, - simulator: args.simulator ?? fallbackSimulators[0], - }), - ); + selectedSimulator = getDestinationSimulator(args, fallbackSimulators); } if (!selectedSimulator) { @@ -286,7 +269,7 @@ async function runOnSimulator( appPath = getBuildPath( xcodeProject, - args.configuration, + args.mode || args.configuration, buildOutput, scheme, ); @@ -367,7 +350,7 @@ async function runOnDevice( const appPath = getBuildPath( xcodeProject, - args.configuration, + args.mode || args.configuration, buildOutput, scheme, true, @@ -389,7 +372,7 @@ async function runOnDevice( appPath = getBuildPath( xcodeProject, - args.configuration, + args.mode || args.configuration, buildOutput, scheme, ); @@ -423,98 +406,6 @@ async function runOnDevice( return logger.success('Installed the app on the device.'); } -function buildProject( - xcodeProject: IOSProjectInfo, - udid: string | undefined, - scheme: string, - args: FlagsT, -): Promise { - return new Promise((resolve, reject) => { - const xcodebuildArgs = [ - xcodeProject.isWorkspace ? '-workspace' : '-project', - xcodeProject.name, - ...(args.xcconfig ? ['-xcconfig', args.xcconfig] : []), - ...(args.buildFolder ? ['-derivedDataPath', args.buildFolder] : []), - '-configuration', - args.configuration, - '-scheme', - scheme, - '-destination', - `id=${udid}`, - ]; - // @todo use `getLoader` from cli-tools package - const loader = ora(); - logger.info( - `Building ${chalk.dim( - `(using "xcodebuild ${xcodebuildArgs.join(' ')}")`, - )}`, - ); - let xcodebuildOutputFormatter: ChildProcess | any; - if (!args.verbose) { - if (xcbeautifyAvailable()) { - xcodebuildOutputFormatter = child_process.spawn('xcbeautify', [], { - stdio: ['pipe', process.stdout, process.stderr], - }); - } else if (xcprettyAvailable()) { - xcodebuildOutputFormatter = child_process.spawn('xcpretty', [], { - stdio: ['pipe', process.stdout, process.stderr], - }); - } - } - const buildProcess = child_process.spawn( - 'xcodebuild', - xcodebuildArgs, - getProcessOptions(args), - ); - let buildOutput = ''; - let errorOutput = ''; - buildProcess.stdout.on('data', (data: Buffer) => { - const stringData = data.toString(); - buildOutput += stringData; - if (xcodebuildOutputFormatter) { - xcodebuildOutputFormatter.stdin.write(data); - } else { - if (logger.isVerbose()) { - logger.debug(stringData); - } else { - loader.start( - `Building the app${'.'.repeat(buildOutput.length % 10)}`, - ); - } - } - }); - buildProcess.stderr.on('data', (data: Buffer) => { - errorOutput += data; - }); - buildProcess.on('close', (code: number) => { - if (xcodebuildOutputFormatter) { - xcodebuildOutputFormatter.stdin.end(); - } else { - loader.stop(); - } - if (code !== 0) { - reject( - new CLIError( - ` - Failed to build iOS project. - - We ran "xcodebuild" command but it exited with error code ${code}. To debug build - logs further, consider building your app with Xcode.app, by opening - ${xcodeProject.name}. - `, - xcodebuildOutputFormatter - ? undefined - : buildOutput + '\n' + errorOutput, - ), - ); - return; - } - logger.success('Successfully built the app'); - resolve(buildOutput); - }); - }); -} - function bootSimulator(selectedSimulator: Device) { const simulatorFullName = formattedDeviceName(selectedSimulator); logger.info(`Launching ${simulatorFullName}`); @@ -542,7 +433,7 @@ function getTargetPaths(buildSettings: string) { function getBuildPath( xcodeProject: IOSProjectInfo, - configuration: string, + mode: BuildFlags['mode'], buildOutput: string, scheme: string, isCatalyst: boolean = false, @@ -557,7 +448,7 @@ function getBuildPath( '-sdk', getPlatformName(buildOutput), '-configuration', - configuration, + mode, '-showBuildSettings', '-json', ], @@ -591,28 +482,6 @@ function getPlatformName(buildOutput: string) { return platformNameMatch[1]; } -function xcbeautifyAvailable() { - try { - child_process.execSync('xcbeautify --version', { - stdio: [0, 'pipe', 'ignore'], - }); - } catch (error) { - return false; - } - return true; -} - -function xcprettyAvailable() { - try { - child_process.execSync('xcpretty --version', { - stdio: [0, 'pipe', 'ignore'], - }); - } catch (error) { - return false; - } - return true; -} - function matchingDevice( devices: Array, deviceName: string | true | undefined, @@ -658,34 +527,6 @@ function printFoundDevices(devices: Array) { ].join('\n'); } -function getProcessOptions({ - packager, - terminal, - port, -}: { - packager: boolean; - terminal: string | undefined; - port: number; -}): SpawnOptionsWithoutStdio { - if (packager) { - return { - env: { - ...process.env, - RCT_TERMINAL: terminal, - RCT_METRO_PORT: port.toString(), - }, - }; - } - - return { - env: { - ...process.env, - RCT_TERMINAL: terminal, - RCT_NO_LAUNCH_PACKAGER: 'true', - }, - }; -} - export default { name: 'run-ios', description: 'builds your app and starts it on iOS simulator', @@ -706,67 +547,15 @@ export default { }, ], options: [ - { - name: '--simulator ', - description: - 'Explicitly set simulator to use. Optionally include iOS version between ' + - 'parenthesis at the end to match an exact version: "iPhone 6 (10.0)"', - }, - { - name: '--configuration ', - description: 'Explicitly set the scheme configuration to use', - default: 'Debug', - }, - { - name: '--scheme ', - description: 'Explicitly set Xcode scheme to use', - }, - { - name: '--device [string]', - description: - 'Explicitly set device to use by name. The value is not required if you have a single device connected.', - }, - { - name: '--udid ', - description: 'Explicitly set device to use by udid', - }, + ...iosBuildOptions, { name: '--no-packager', description: 'Do not launch packager while building', }, - { - name: '--verbose', - description: 'Do not use xcbeautify or xcpretty even if installed', - }, - { - name: '--port ', - default: process.env.RCT_METRO_PORT || 8081, - parse: Number, - }, { name: '--binary-path ', description: 'Path relative to project root where pre-built .app binary lives.', }, - { - name: '--terminal ', - description: - 'Launches the Metro Bundler in a new window using the specified terminal path.', - default: getDefaultUserTerminal(), - }, - { - name: '--list-devices', - description: - 'List all available iOS devices and simulators and let you choose one to run the app. ', - }, - { - name: '--xcconfig [string]', - description: 'Explicitly set xcconfig to use', - }, - { - name: '--buildFolder ', - description: - 'Location for iOS build artifacts. Corresponds to Xcode\'s "-derivedDataPath".', - }, ], }; diff --git a/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts b/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts index a91bbf4d9..df19cd07c 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts @@ -1,6 +1,6 @@ import {Device} from '../../types'; -import parseIOSDevicesList from './parseIOSDevicesList'; -import parseXctraceIOSDevicesList from './parseXctraceIOSDevicesList'; +import parseIOSDevicesList from '../../tools/parseIOSDevicesList'; +import parseXctraceIOSDevicesList from '../../tools/parseXctraceIOSDevicesList'; import execa from 'execa'; import {logger} from '@react-native-community/cli-tools'; import prompts from 'prompts'; diff --git a/packages/cli-platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts b/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts rename to packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts diff --git a/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts b/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts new file mode 100644 index 000000000..5faadab0b --- /dev/null +++ b/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts @@ -0,0 +1,221 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import execa from 'execa'; +import {getDevices} from '../getDevices'; + +jest.dontMock('../getDevices'); + +jest.mock('execa', () => { + return {sync: jest.fn()}; +}); + +const expectedOutput = { + xctraceListLatest: { + stdout: [ + '== Devices ==', + 'Maxs MacBook Pro (11111111-1111-1111-1111-111111111111)', + "Max's iPhone (9.2) (00008030-000D19512210802E)", + 'other-iphone (9.2) (72a186ccfd93472a186ccfd934)', + '', + '== Simulators ==', + 'iPad 2 (9.3) (07538CE4-675B-4EDA-90F2-3DD3CD93309D)', + 'iPad Air (9.3) (0745F6D1-6DC5-4427-B9A6-6FBA327ED65A)', + 'iPhone 6s (9.3) (3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4)', + 'Known Templates:', + 'Activity Monitor', + 'Blank', + 'System Usage', + 'Zombies', + ].join('\n'), + stderr: '', + }, + xctraceListOld: { + stderr: [ + '== Devices ==', + 'Maxs MacBook Pro (11111111-1111-1111-1111-111111111111)', + "Max's iPhone (9.2) (00008030-000D19512210802E)", + 'other-iphone (9.2) (72a186ccfd93472a186ccfd934)', + '', + '== Simulators ==', + 'iPad 2 (9.3) (07538CE4-675B-4EDA-90F2-3DD3CD93309D)', + 'iPad Air (9.3) (0745F6D1-6DC5-4427-B9A6-6FBA327ED65A)', + 'iPhone 6s (9.3) (3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4)', + 'Known Templates:', + 'Activity Monitor', + 'Blank', + 'System Usage', + 'Zombies', + ].join('\n'), + }, + depracatedList: { + stdout: [ + 'Known Devices:', + 'Maxs MacBook Pro [11111111-1111-1111-1111-111111111111]', + "Max's iPhone (9.2) [00008030-000D19512210802E]", + 'other-iphone (9.2) [72a186ccfd93472a186ccfd934]', + 'iPad 2 (9.3) [07538CE4-675B-4EDA-90F2-3DD3CD93309D] (Simulator)', + 'iPad Air (9.3) [0745F6D1-6DC5-4427-B9A6-6FBA327ED65A] (Simulator)', + 'iPhone 6s (9.3) [3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4] (Simulator)', + 'Known Templates:', + 'Activity Monitor', + 'Blank', + 'System Usage', + 'Zombies', + ].join('\n'), + stderr: '', + }, +}; + +describe('getDevices', () => { + it('parses typical output for xctrace list for xcode 12.5+', () => { + (execa.sync as jest.Mock).mockReturnValueOnce( + expectedOutput.xctraceListLatest, + ); + const devices = getDevices(); + + expect(devices).toEqual([ + { + name: 'Maxs MacBook Pro', + udid: '11111111-1111-1111-1111-111111111111', + type: 'catalyst', + }, + { + name: "Max's iPhone", + udid: '00008030-000D19512210802E', + version: '9.2', + type: 'device', + }, + { + name: 'other-iphone', + type: 'device', + udid: '72a186ccfd93472a186ccfd934', + version: '9.2', + }, + { + name: 'iPad 2', + udid: '07538CE4-675B-4EDA-90F2-3DD3CD93309D', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPad Air', + udid: '0745F6D1-6DC5-4427-B9A6-6FBA327ED65A', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPhone 6s', + udid: '3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4', + version: '9.3', + type: 'simulator', + }, + ]); + }); + + it('parses typical output for xctrace list for xcode upto 12.5', () => { + (execa.sync as jest.Mock).mockReturnValueOnce( + expectedOutput.xctraceListOld, + ); + const devices = getDevices(); + + expect(devices).toEqual([ + { + name: 'Maxs MacBook Pro', + udid: '11111111-1111-1111-1111-111111111111', + type: 'catalyst', + }, + { + name: "Max's iPhone", + udid: '00008030-000D19512210802E', + version: '9.2', + type: 'device', + }, + { + name: 'other-iphone', + type: 'device', + udid: '72a186ccfd93472a186ccfd934', + version: '9.2', + }, + { + name: 'iPad 2', + udid: '07538CE4-675B-4EDA-90F2-3DD3CD93309D', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPad Air', + udid: '0745F6D1-6DC5-4427-B9A6-6FBA327ED65A', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPhone 6s', + udid: '3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4', + version: '9.3', + type: 'simulator', + }, + ]); + }); + + it('parses typical output for deprecated list', () => { + (execa.sync as jest.Mock).mockImplementation((_, [command]) => { + if (command === 'xctrace') { + throw new Error('some error'); + } + return expectedOutput.depracatedList; + }).mock; + const devices = getDevices(); + + expect(devices).toEqual([ + { + name: 'Maxs MacBook Pro', + udid: '11111111-1111-1111-1111-111111111111', + type: 'catalyst', + }, + { + name: "Max's iPhone", + udid: '00008030-000D19512210802E', + version: '9.2', + type: 'device', + }, + { + name: 'other-iphone', + type: 'device', + udid: '72a186ccfd93472a186ccfd934', + version: '9.2', + }, + { + name: 'iPad 2', + udid: '07538CE4-675B-4EDA-90F2-3DD3CD93309D', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPad Air', + udid: '0745F6D1-6DC5-4427-B9A6-6FBA327ED65A', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPhone 6s', + udid: '3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4', + version: '9.3', + type: 'simulator', + }, + ]); + }); + + it('ignores garbage', () => { + (execa.sync as jest.Mock).mockReturnValueOnce({ + stdout: 'Something went terribly wrong (-42)', + stderr: '', + }); + expect(getDevices()).toEqual([]); + }); +}); diff --git a/packages/cli-platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts b/packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts rename to packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts diff --git a/packages/cli-platform-ios/src/commands/runIOS/__tests__/parseXctraceIOSDevicesList.test.ts b/packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts similarity index 100% rename from packages/cli-platform-ios/src/commands/runIOS/__tests__/parseXctraceIOSDevicesList.test.ts rename to packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts diff --git a/packages/cli-platform-ios/src/commands/runIOS/findMatchingSimulator.ts b/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts similarity index 99% rename from packages/cli-platform-ios/src/commands/runIOS/findMatchingSimulator.ts rename to packages/cli-platform-ios/src/tools/findMatchingSimulator.ts index 6041bf8ff..e0a51a662 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/findMatchingSimulator.ts +++ b/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts @@ -6,7 +6,7 @@ * */ -import {Device} from '../../types'; +import {Device} from '../types'; /** * Takes in a parsed simulator list and a desired name, and returns an object with the matching simulator. The desired diff --git a/packages/cli-platform-ios/src/tools/getDestinationSimulator.ts b/packages/cli-platform-ios/src/tools/getDestinationSimulator.ts new file mode 100644 index 000000000..214b90634 --- /dev/null +++ b/packages/cli-platform-ios/src/tools/getDestinationSimulator.ts @@ -0,0 +1,45 @@ +import child_process from 'child_process'; +import {CLIError} from '@react-native-community/cli-tools'; +import {Device} from '../types'; +import findMatchingSimulator from './findMatchingSimulator'; + +type FlagsT = { + simulator?: string; + udid?: string; +}; + +export function getDestinationSimulator( + args: FlagsT, + fallbackSimulators: string[] = [], +) { + let simulators: {devices: {[index: string]: Array}}; + try { + simulators = JSON.parse( + child_process.execFileSync( + 'xcrun', + ['simctl', 'list', '--json', 'devices'], + {encoding: 'utf8'}, + ), + ); + } catch (error) { + throw new CLIError( + 'Could not get the simulator list from Xcode. Please open Xcode and try running project directly from there to resolve the remaining issues.', + error, + ); + } + + const selectedSimulator = fallbackSimulators.reduce((simulator, fallback) => { + return ( + simulator || findMatchingSimulator(simulators, {simulator: fallback}) + ); + }, findMatchingSimulator(simulators, args)); + + if (!selectedSimulator) { + throw new CLIError( + `No simulator available with ${ + args.simulator ? `name "${args.simulator}"` : `udid "${args.udid}"` + }`, + ); + } + return selectedSimulator; +} diff --git a/packages/cli-platform-ios/src/tools/getDevices.ts b/packages/cli-platform-ios/src/tools/getDevices.ts new file mode 100644 index 000000000..e7ad4c968 --- /dev/null +++ b/packages/cli-platform-ios/src/tools/getDevices.ts @@ -0,0 +1,24 @@ +import execa from 'execa'; +import {logger} from '@react-native-community/cli-tools'; +import parseIOSDevicesList from './parseIOSDevicesList'; +import parseXctraceIOSDevicesList from './parseXctraceIOSDevicesList'; +import {Device} from '../types'; + +export function getDevices(): Device[] { + let devices; + try { + const out = execa.sync('xcrun', ['xctrace', 'list', 'devices']); + devices = parseXctraceIOSDevicesList( + // Xcode 12.5 introduced a change to output the list to stdout instead of stderr + out.stderr === '' ? out.stdout : out.stderr, + ); + } catch (e) { + logger.warn( + 'Support for Xcode 11 and older is deprecated. Please upgrade to Xcode 12.', + ); + devices = parseIOSDevicesList( + execa.sync('xcrun', ['instruments', '-s']).stdout, + ); + } + return devices; +} diff --git a/packages/cli-platform-ios/src/commands/runIOS/parseIOSDevicesList.ts b/packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts similarity index 97% rename from packages/cli-platform-ios/src/commands/runIOS/parseIOSDevicesList.ts rename to packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts index b7255afdc..09c3509e1 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/parseIOSDevicesList.ts +++ b/packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ -import {Device} from '../../types'; +import {Device} from '../types'; /** * Parses the output of the `xcrun instruments -s` command and returns metadata diff --git a/packages/cli-platform-ios/src/commands/runIOS/parseXctraceIOSDevicesList.ts b/packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts similarity index 97% rename from packages/cli-platform-ios/src/commands/runIOS/parseXctraceIOSDevicesList.ts rename to packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts index 22fa4dfeb..cf7bec464 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/parseXctraceIOSDevicesList.ts +++ b/packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ -import {Device} from '../../types'; +import {Device} from '../types'; /** * Parses the output of the `xcrun instruments -s` command and returns metadata