From 8dcde340392902ddbf9417db1f06defbbc3f316f Mon Sep 17 00:00:00 2001 From: Rory Fitzpatrick <96067+roryf@users.noreply.github.com> Date: Sun, 12 Mar 2023 13:29:12 +0000 Subject: [PATCH] fix: correctly list ios devices and simulators (#1863) * fix: correctly list ios devices and simulators * chore: fix docs and incorrect import * chore: fix snapshot --- __e2e__/__snapshots__/config.test.ts.snap | 10 + docs/commands.md | 12 +- .../src/commands/buildIOS/index.ts | 4 +- .../src/commands/runIOS/index.ts | 25 +- .../src/commands/runIOS/listIOSDevices.ts | 46 ---- .../__tests__/findMatchingSimulator.test.ts | 37 --- .../src/tools/__tests__/getDevices.test.ts | 221 ----------------- .../tools/__tests__/listIOSDevices.test.ts | 224 ++++++++++++++++++ .../__tests__/parseIOSDevicesList.test.ts | 76 ------ .../parseXctraceIOSDevicesList.test.ts | 78 ------ .../src/tools/findMatchingSimulator.ts | 2 +- .../cli-platform-ios/src/tools/getDevices.ts | 24 -- .../src/tools/listIOSDevices.ts | 80 +++++++ .../src/tools/parseIOSDevicesList.ts | 47 ---- .../src/tools/parseXctraceIOSDevicesList.ts | 53 ----- .../cli-platform-ios/src/tools/prompts.ts | 26 ++ 16 files changed, 366 insertions(+), 599 deletions(-) delete mode 100644 packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts delete mode 100644 packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts create mode 100644 packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts delete mode 100644 packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts delete mode 100644 packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts delete mode 100644 packages/cli-platform-ios/src/tools/getDevices.ts create mode 100644 packages/cli-platform-ios/src/tools/listIOSDevices.ts delete mode 100644 packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts delete mode 100644 packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts create mode 100644 packages/cli-platform-ios/src/tools/prompts.ts diff --git a/__e2e__/__snapshots__/config.test.ts.snap b/__e2e__/__snapshots__/config.test.ts.snap index b5ac0aaf8..ab8784d6d 100644 --- a/__e2e__/__snapshots__/config.test.ts.snap +++ b/__e2e__/__snapshots__/config.test.ts.snap @@ -20,6 +20,16 @@ exports[`shows up current config without unnecessary output 1`] = ` "<>" ] }, + { + "name": "build-ios", + "description": "builds your app on iOS simulator", + "examples": [ + "<>" + ], + "options": [ + "<>" + ] + }, { "name": "log-android", "description": "starts logkitty" diff --git a/docs/commands.md b/docs/commands.md index f674bb987..04dbe7e95 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -497,6 +497,12 @@ Explicitly set `xcconfig` to use in build. Location for iOS build artifacts. Corresponds to Xcode's `-derivedDataPath`. +#### `--list-devices` + +> default: false + +List all available iOS devices and simulators and let you choose one to run the app. + ### `build-ios` Usage: @@ -571,12 +577,6 @@ Location for iOS build artifacts. Corresponds to Xcode's `-derivedDataPath`. Installs passed binary instead of building a fresh one. -#### `--list-devices` - -> default: false - -List all available iOS devices and simulators and let you choose one to run the app. - ### `start` Usage: diff --git a/packages/cli-platform-ios/src/commands/buildIOS/index.ts b/packages/cli-platform-ios/src/commands/buildIOS/index.ts index 64746f3c3..a42812d64 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/buildIOS/index.ts @@ -17,10 +17,10 @@ import { import {Device} from '../../types'; import {BuildFlags, buildProject} from './buildProject'; import {getDestinationSimulator} from '../../tools/getDestinationSimulator'; -import {getDevices} from '../../tools/getDevices'; import {getProjectInfo} from '../../tools/getProjectInfo'; import {checkIfConfigurationExists} from '../../tools/checkIfConfigurationExists'; import {getConfigurationScheme} from '../../tools/getConfigurationScheme'; +import listIOSDevices from '../../tools/listIOSDevices'; export interface FlagsT extends BuildFlags { configuration?: string; @@ -116,7 +116,7 @@ function buildIOS(_: Array, ctx: Config, args: FlagsT) { ); } - const devices = getDevices(); + const devices = listIOSDevices(); if (args.udid) { const device = devices.find((d) => d.udid === args.udid); diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index ebba9a46e..5659671d2 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -16,10 +16,11 @@ import {logger, CLIError} from '@react-native-community/cli-tools'; import {BuildFlags, buildProject} from '../buildIOS/buildProject'; import {iosBuildOptions} from '../buildIOS'; import {Device} from '../../types'; -import listIOSDevices, {promptForDeviceSelection} from './listIOSDevices'; +import listIOSDevices from '../../tools/listIOSDevices'; import {checkIfConfigurationExists} from '../../tools/checkIfConfigurationExists'; import {getProjectInfo} from '../../tools/getProjectInfo'; import {getConfigurationScheme} from '../../tools/getConfigurationScheme'; +import {promptForDeviceSelection} from '../../tools/prompts'; export interface FlagsT extends BuildFlags { simulator?: string; @@ -92,6 +93,8 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { } "${chalk.bold(xcodeProject.name)}"`, ); + const availableDevices = listIOSDevices(); + if (args.listDevices) { if (args.device || args.udid) { logger.warn( @@ -100,7 +103,6 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { } and "list-devices" parameters were passed to "run" command. We will list available devices and let you choose from one.`, ); } - const availableDevices = await listIOSDevices(); const selectedDevice = await promptForDeviceSelection(availableDevices); if (!selectedDevice) { throw new CLIError( @@ -114,10 +116,10 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { } } - const devices = await listIOSDevices(); - if (!args.device && !args.udid && !args.simulator) { - const bootedDevices = devices.filter(({type}) => type === 'device'); + const bootedDevices = availableDevices.filter( + ({type, isAvailable}) => type === 'device' && isAvailable, + ); const simulators = getSimulators(); const bootedSimulators = Object.keys(simulators.devices) @@ -151,12 +153,12 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { } if (args.udid) { - const device = devices.find((d) => d.udid === args.udid); + const device = availableDevices.find((d) => d.udid === args.udid); if (!device) { return logger.error( `Could not find a device with udid: "${chalk.bold( args.udid, - )}". ${printFoundDevices(devices)}`, + )}". ${printFoundDevices(availableDevices)}`, ); } if (device.type === 'simulator') { @@ -165,7 +167,9 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { return runOnDevice(device, scheme, xcodeProject, args); } } else if (args.device) { - const physicalDevices = devices.filter((d) => d.type !== 'simulator'); + const physicalDevices = availableDevices.filter( + (d) => d.type !== 'simulator', + ); const device = matchingDevice(physicalDevices, args.device); if (device) { return runOnDevice(device, scheme, xcodeProject, args); @@ -571,5 +575,10 @@ export default { description: 'Path relative to project root where pre-built .app binary lives.', }, + { + name: '--list-devices', + description: + 'List all available iOS devices and simulators and let you choose one to run the app. ', + }, ], }; diff --git a/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts b/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts deleted file mode 100644 index df19cd07c..000000000 --- a/packages/cli-platform-ios/src/commands/runIOS/listIOSDevices.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {Device} from '../../types'; -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'; -import chalk from 'chalk'; - -export async function promptForDeviceSelection( - availableDevices: Device[], -): Promise { - const {device} = await prompts({ - type: 'select', - name: 'device', - message: 'Select the device you want to use', - choices: availableDevices - .filter((d) => d.type === 'device' || d.type === 'simulator') - .map((d) => ({ - title: `${chalk.bold(d.name)}`, - value: d, - })), - min: 1, - }); - return device; -} - -async function listIOSDevices(): Promise { - 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; -} - -export default listIOSDevices; diff --git a/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts b/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts index 89a7d38d9..e70919b1f 100644 --- a/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts +++ b/packages/cli-platform-ios/src/tools/__tests__/findMatchingSimulator.test.ts @@ -888,43 +888,6 @@ describe('findMatchingSimulator', () => { ).toEqual(null); }); - it('should return AppleTV devices if in the list', () => { - expect( - findMatchingSimulator( - { - devices: { - 'com.apple.CoreSimulator.SimRuntime.tvOS-11-2': [ - { - state: 'Booted', - availability: '(available)', - name: 'Apple TV', - udid: '816C30EA-38EA-41AC-BFDA-96FB632D522E', - }, - { - state: 'Shutdown', - availability: '(available)', - name: 'Apple TV 4K', - udid: 'BCBB7E4B-D872-4D61-BC61-7C9805551075', - }, - { - state: 'Shutdown', - availability: '(available)', - name: 'Apple TV 4K (at 1080p)', - udid: '1DE12308-1C14-4F0F-991E-A3ADC41BDFFC', - }, - ], - }, - }, - {simulator: 'Apple TV'}, - ), - ).toEqual({ - udid: '816C30EA-38EA-41AC-BFDA-96FB632D522E', - name: 'Apple TV', - booted: true, - version: 'tvOS 11.2', - }); - }); - it('should return a simulator by UDID', () => { expect( findMatchingSimulator( diff --git a/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts b/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts deleted file mode 100644 index 5faadab0b..000000000 --- a/packages/cli-platform-ios/src/tools/__tests__/getDevices.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * 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/tools/__tests__/listIOSDevices.test.ts b/packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts new file mode 100644 index 000000000..7d0470f4f --- /dev/null +++ b/packages/cli-platform-ios/src/tools/__tests__/listIOSDevices.test.ts @@ -0,0 +1,224 @@ +/** + * Copyright (c) Meta, 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 listIOSDevices from '../listIOSDevices'; + +jest.mock('execa', () => { + return {sync: jest.fn()}; +}); + +const xcrunOut = ` +[ + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20J373)", + "available" : true, + "platform" : "com.apple.platform.appletvsimulator", + "modelCode" : "AppleTV11,1", + "identifier" : "F022AD06-DFD3-4B9F-B4DD-3C30E17E7CE6", + "architecture" : "arm64", + "modelUTI" : "com.apple.apple-tv-4k-2nd", + "modelName" : "Apple TV 4K (2nd generation)", + "name" : "Apple TV 4K (2nd generation)" + }, + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20J373)", + "available" : true, + "platform" : "com.apple.platform.appletvsimulator", + "modelCode" : "AppleTV5,3", + "identifier" : "76DB4F99-2EEC-47F4-9DFB-239F2091DFCD", + "architecture" : "arm64", + "modelUTI" : "com.apple.apple-tv-4", + "modelName" : "Apple TV", + "name" : "Apple TV" + }, + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20A360)", + "available" : true, + "platform" : "com.apple.platform.iphonesimulator", + "modelCode" : "iPhone14,8", + "identifier" : "A88CFA2A-05C8-44EE-9B67-7AEFE1624E2F", + "architecture" : "arm64", + "modelUTI" : "com.apple.iphone-14-plus-1", + "modelName" : "iPhone 14 Plus", + "name" : "iPhone 14 Plus" + }, + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20A360)", + "available" : true, + "platform" : "com.apple.platform.iphonesimulator", + "modelCode" : "iPhone14,6", + "identifier" : "1202C373-7381-433C-84FA-EF6741078CC1", + "architecture" : "arm64", + "modelUTI" : "com.apple.iphone-se3-1", + "modelName" : "iPhone SE (3rd generation)", + "name" : "iPhone SE (3rd generation)" + }, + { + "simulator" : false, + "operatingSystemVersion" : "13.0.1 (22A400)", + "interface" : "usb", + "available" : true, + "platform" : "com.apple.platform.macosx", + "modelCode" : "MacBookPro18,1", + "identifier" : "11111111-131230917230918374", + "architecture" : "arm64e", + "modelUTI" : "com.apple.macbookpro-16-2021", + "modelName" : "MacBook Pro", + "name" : "My Mac" + }, + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20A360)", + "available" : true, + "platform" : "com.apple.platform.iphonesimulator", + "modelCode" : "iPhone14,7", + "identifier" : "D83F179F-6C0B-45BA-9104-45397BA3FFB9", + "architecture" : "arm64", + "modelUTI" : "com.apple.iphone-14-1", + "modelName" : "iPhone 14", + "name" : "iPhone 14" + }, + { + "simulator" : true, + "operatingSystemVersion" : "16.0 (20J373)", + "available" : true, + "platform" : "com.apple.platform.appletvsimulator", + "modelCode" : "AppleTV11,1", + "identifier" : "D1DD7196-8ADE-445B-9BD8-B1FE8CE2FAFB", + "architecture" : "arm64", + "modelUTI" : "com.apple.apple-tv-4k-2nd", + "modelName" : "Apple TV 4K (at 1080p) (2nd generation)", + "name" : "Apple TV 4K (at 1080p) (2nd generation)" + }, + { + "modelCode" : "iPhone12,1", + "simulator" : false, + "modelName" : "iPhone 11", + "error" : { + "code" : 6, + "failureReason" : "", + "underlyingErrors" : [ + { + "code" : 4, + "failureReason" : "", + "description" : "Adam’s iPhone is locked.", + "recoverySuggestion" : "To use Adam’s iPhone with Xcode, unlock it.", + "domain" : "DVTDeviceIneligibilityErrorDomain" + } + ], + "description" : "To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.", + "recoverySuggestion" : "", + "domain" : "DVTDeviceIneligibilityErrorDomain" + }, + "operatingSystemVersion" : "16.2 (20C65)", + "identifier" : "1234567890-0987654321", + "platform" : "com.apple.platform.iphoneos", + "architecture" : "arm64e", + "interface" : "usb", + "available" : false, + "name" : "Adam’s iPhone", + "modelUTI" : "com.apple.iphone-11-2" + }, + { + "modelCode" : "AppleTV11,1", + "simulator" : false, + "modelName" : "Apple TV 4K (2nd generation)", + "error" : { + "code" : -13, + "failureReason" : "", + "underlyingErrors" : [ + { + "code" : 4, + "failureReason" : "", + "description" : "Living Room is locked.", + "recoverySuggestion" : "To use Living Room with Xcode, unlock it.", + "domain" : "DVTDeviceIneligibilityErrorDomain" + } + ], + "description" : "Living Room is not connected", + "recoverySuggestion" : "Xcode will continue when Living Room is connected and unlocked.", + "domain" : "com.apple.platform.iphoneos" + }, + "operatingSystemVersion" : "16.1 (20K71)", + "identifier" : "7656fbf922891c8a2c7682c9d845eaa6954c24d8", + "platform" : "com.apple.platform.appletvos", + "architecture" : "arm64e", + "interface" : "usb", + "available" : false, + "name" : "Living Room", + "modelUTI" : "com.apple.apple-tv-4k-2nd" + } +] +`; + +describe('listIOSDevices', () => { + it('parses output from xcdevice list', () => { + (execa.sync as jest.Mock).mockReturnValueOnce({stdout: xcrunOut}); + const devices = listIOSDevices(); + + // Find all available simulators + expect(devices).toContainEqual({ + name: 'iPhone 14 Plus', + isAvailable: true, + udid: 'A88CFA2A-05C8-44EE-9B67-7AEFE1624E2F', + version: '16.0 (20A360)', + availabilityError: undefined, + type: 'simulator', + }); + expect(devices).toContainEqual({ + name: 'iPhone SE (3rd generation)', + isAvailable: true, + udid: '1202C373-7381-433C-84FA-EF6741078CC1', + version: '16.0 (20A360)', + availabilityError: undefined, + type: 'simulator', + }); + + // Find all available iPhone's event when not available + expect(devices).toContainEqual({ + name: 'Adam’s iPhone', + isAvailable: false, + udid: '1234567890-0987654321', + version: '16.2 (20C65)', + availabilityError: + 'To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.', + type: 'device', + }); + // Filter out AppleTV + expect(devices).not.toContainEqual({ + isAvailable: false, + name: 'Living Room', + udid: '7656fbf922891c8a2c7682c9d845eaa6954c24d8', + version: '16.1 (20K71)', + availabilityError: 'Living Room is not connected', + type: 'device', + }); + expect(devices).not.toContainEqual({ + isAvailable: true, + name: 'Apple TV 4K (2nd generation)', + udid: 'F022AD06-DFD3-4B9F-B4DD-3C30E17E7CE6', + version: '16.0 (20J373)', + availabilityError: undefined, + type: 'simulator', + }); + // Filter out macOS + expect(devices).not.toContainEqual({ + isAvailable: true, + name: 'My Mac', + udid: '11111111-131230917230918374', + version: '13.0.1 (22A400)', + availabilityError: undefined, + type: 'device', + }); + }); +}); diff --git a/packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts b/packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts deleted file mode 100644 index 7da7e06c4..000000000 --- a/packages/cli-platform-ios/src/tools/__tests__/parseIOSDevicesList.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 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 parseIOSDevicesList from '../parseIOSDevicesList'; - -jest.dontMock('../parseIOSDevicesList'); - -describe('parseIOSDevicesList', () => { - it('parses typical output', () => { - const devices = parseIOSDevicesList( - [ - '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'), - ); - - 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', () => { - expect(parseIOSDevicesList('Something went terribly wrong (-42)')).toEqual( - [], - ); - }); -}); diff --git a/packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts b/packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts deleted file mode 100644 index 937787bfc..000000000 --- a/packages/cli-platform-ios/src/tools/__tests__/parseXctraceIOSDevicesList.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 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 parseXctraceIOSDevicesList from '../parseXctraceIOSDevicesList'; - -jest.dontMock('../parseXctraceIOSDevicesList'); - -describe('parseXctraceIOSDevicesList', () => { - it('parses typical output', () => { - const devices = parseXctraceIOSDevicesList( - [ - '== 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'), - ); - - 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', () => { - expect( - parseXctraceIOSDevicesList('Something went terribly wrong (-42)'), - ).toEqual([]); - }); -}); diff --git a/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts b/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts index e0a51a662..9f2d1e795 100644 --- a/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts +++ b/packages/cli-platform-ios/src/tools/findMatchingSimulator.ts @@ -66,7 +66,7 @@ function findMatchingSimulator( } // Making sure the version of the simulator is an iOS or tvOS (Removes Apple Watch, etc) - if (!version.includes('iOS') && !version.includes('tvOS')) { + if (!version.includes('iOS')) { continue; } if (simulatorVersion && !version.endsWith(simulatorVersion)) { diff --git a/packages/cli-platform-ios/src/tools/getDevices.ts b/packages/cli-platform-ios/src/tools/getDevices.ts deleted file mode 100644 index e7ad4c968..000000000 --- a/packages/cli-platform-ios/src/tools/getDevices.ts +++ /dev/null @@ -1,24 +0,0 @@ -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/tools/listIOSDevices.ts b/packages/cli-platform-ios/src/tools/listIOSDevices.ts new file mode 100644 index 000000000..4df45c565 --- /dev/null +++ b/packages/cli-platform-ios/src/tools/listIOSDevices.ts @@ -0,0 +1,80 @@ +import {Device} from '../types'; +import execa from 'execa'; +import prompts from 'prompts'; +import chalk from 'chalk'; + +type DeviceOutput = { + modelCode: string; + simulator: boolean; + modelName: string; + error: { + code: number; + failureReason: string; + underlyingErrors: [ + { + code: number; + failureReason: string; + description: string; + recoverySuggestion: string; + domain: string; + }, + ]; + description: string; + recoverySuggestion: string; + domain: string; + }; + operatingSystemVersion: string; + identifier: string; + platform: string; + architecture: string; + interface: string; + available: boolean; + name: string; + modelUTI: string; +}; + +export async function promptForDeviceSelection( + availableDevices: Device[], +): Promise { + const {device} = await prompts({ + type: 'select', + name: 'device', + message: 'Select the device you want to use', + choices: availableDevices + .filter((d) => d.type === 'device' || d.type === 'simulator') + .map((d) => ({ + title: `${chalk.bold(d.name)}`, + value: d, + })), + min: 1, + }); + return device; +} + +const parseXcdeviceList = (text: string): Device[] => { + const rawOutput = JSON.parse(text) as DeviceOutput[]; + + const devices: Device[] = rawOutput + .filter( + (device) => + !device.platform.includes('appletv') && + !device.platform.includes('macos'), + ) + .sort((device) => (device.simulator ? 1 : -1)) + .map((device) => ({ + isAvailable: device.available, + name: device.name, + udid: device.identifier, + version: device.operatingSystemVersion, + availabilityError: device.error?.description, + type: device.simulator ? 'simulator' : 'device', + })); + return devices; +}; + +function listIOSDevices(): Device[] { + const out = execa.sync('xcrun', ['xcdevice', 'list']).stdout; + return parseXcdeviceList(out); +} + +export default listIOSDevices; diff --git a/packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts b/packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts deleted file mode 100644 index 09c3509e1..000000000 --- a/packages/cli-platform-ios/src/tools/parseIOSDevicesList.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * 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 {Device} from '../types'; - -/** - * Parses the output of the `xcrun instruments -s` command and returns metadata - * about available iOS simulators and physical devices, as well as host Mac for - * Catalyst purposes. - * - * Expected text looks roughly like this: - * - * ``` - * Known Devices: - * this-mac-device [UDID] - * A Physical Device (OS Version) [UDID] - * A Simulator Device (OS Version) [UDID] (Simulator) - * ``` - */ -function parseIOSDevicesList(text: string): Array { - const devices: Array = []; - - text.split('\n').forEach((line) => { - const device = line.match( - /(.*?) (\(([0-9.]+)\) )?\[([0-9A-F-]+)\]( \(Simulator\))?/i, - ); - if (device) { - const [, name, , version, udid, isSimulator] = device; - const metadata: Device = {name, udid}; - if (version) { - metadata.version = version; - metadata.type = isSimulator ? 'simulator' : 'device'; - } else { - metadata.type = 'catalyst'; - } - devices.push(metadata); - } - }); - - return devices; -} - -export default parseIOSDevicesList; diff --git a/packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts b/packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts deleted file mode 100644 index cf7bec464..000000000 --- a/packages/cli-platform-ios/src/tools/parseXctraceIOSDevicesList.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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 {Device} from '../types'; - -/** - * Parses the output of the `xcrun instruments -s` command and returns metadata - * about available iOS simulators and physical devices, as well as host Mac for - * Catalyst purposes. - * - * Expected text looks roughly like this: - * - * ``` - * == Devices == - * this-mac-device [UDID] - * A Physical Device (OS Version) (UDID) - * - * == Simulators == - * A Simulator Device (OS Version) (UDID) - * ``` - */ -function parseIOSDevicesList(text: string): Array { - const devices: Array = []; - let isSimulator = false; - if (text.indexOf('== Simulators ==') === -1) { - return []; - } - text.split('\n').forEach((line) => { - if (line === '== Simulators ==') { - isSimulator = true; - } - const device = line.match(/(.*?) (\(([0-9.]+)\) )?\(([0-9A-F-]+)\)/i); - if (device) { - const [, name, , version, udid] = device; - const metadata: Device = {name, udid}; - if (version) { - metadata.version = version; - metadata.type = isSimulator ? 'simulator' : 'device'; - } else { - metadata.type = 'catalyst'; - } - devices.push(metadata); - } - }); - - return devices; -} - -export default parseIOSDevicesList; diff --git a/packages/cli-platform-ios/src/tools/prompts.ts b/packages/cli-platform-ios/src/tools/prompts.ts new file mode 100644 index 000000000..256a6f4c7 --- /dev/null +++ b/packages/cli-platform-ios/src/tools/prompts.ts @@ -0,0 +1,26 @@ +import chalk from 'chalk'; +import prompts from 'prompts'; +import {Device} from '../types'; + +export async function promptForDeviceSelection( + availableDevices: Device[], +): Promise { + const {device} = await prompts({ + type: 'select', + name: 'device', + message: 'Select the device you want to use', + choices: availableDevices + .filter((d) => d.type === 'device' || d.type === 'simulator') + .map((d) => ({ + title: `${chalk.bold(d.name)} ${ + !d.isAvailable && !!d.availabilityError + ? chalk.red(`(unavailable - ${d.availabilityError})`) + : '' + }`, + value: d, + disabled: !d.isAvailable, + })), + min: 1, + }); + return device; +}