Skip to content

Commit dec46c3

Browse files
authoredJan 15, 2024
fix(cli-platform-apple): properly receive simulators (#2251)
* fix: properly receive simulators * fix: physical devices are always booted * chore: add missing description to function * chore: move from `child_process` to `execa` * test: add test for merged output * chore: apply code review suggestions
1 parent 42ce0ed commit dec46c3

File tree

5 files changed

+136
-107
lines changed

5 files changed

+136
-107
lines changed
 

‎packages/cli-platform-apple/src/commands/logCommand/createLog.ts

+10-28
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import {Config, IOSProjectConfig} from '@react-native-community/cli-types';
33
import {spawnSync} from 'child_process';
44
import os from 'os';
55
import path from 'path';
6-
import getSimulators from '../../tools/getSimulators';
7-
import listDevices, {stripPlatform} from '../../tools/listDevices';
6+
import listDevices from '../../tools/listDevices';
87
import {getPlatformInfo} from '../runCommand/getPlatformInfo';
98
import {BuilderCommand, Device} from '../../types';
109
import {supportedPlatforms} from '../../config/supportedPlatforms';
@@ -31,48 +30,31 @@ const createLog =
3130
throw new CLIError(`Unable to find ${platformName} platform config`);
3231
}
3332

34-
// Here we're using two command because first command `xcrun simctl list --json devices` outputs `state` but doesn't return `available`. But second command `xcrun xcdevice list` outputs `available` but doesn't output `state`. So we need to connect outputs of both commands.
35-
const simulators = getSimulators();
36-
const bootedSimulators = Object.keys(simulators.devices)
37-
.map((key) => simulators.devices[key])
38-
.reduce((acc, val) => acc.concat(val), [])
39-
.filter(({state}) => state === 'Booted');
40-
4133
const {sdkNames} = getPlatformInfo(platformName);
42-
const devices = await listDevices(sdkNames);
43-
44-
const availableSimulators = devices.filter(
45-
({type, isAvailable}) => type === 'simulator' && isAvailable,
46-
);
34+
const allDevices = await listDevices(sdkNames);
35+
const simulators = allDevices.filter(({type}) => type === 'simulator');
4736

48-
if (availableSimulators.length === 0) {
37+
if (simulators.length === 0) {
4938
logger.error('No simulators detected. Install simulators via Xcode.');
5039
return;
5140
}
5241

53-
const bootedAndAvailableSimulators = bootedSimulators
54-
.map((booted) => {
55-
const available = availableSimulators.find(
56-
({udid}) => udid === booted.udid,
57-
);
58-
return {...available, ...booted};
59-
})
60-
.filter(({sdk}) => sdk && sdkNames.includes(stripPlatform(sdk)));
42+
const booted = simulators.filter(({state}) => state === 'Booted');
6143

62-
if (bootedAndAvailableSimulators.length === 0) {
44+
if (booted.length === 0) {
6345
logger.error(
6446
`No booted and available ${platformReadableName} simulators found.`,
6547
);
6648
return;
6749
}
6850

69-
if (args.interactive && bootedAndAvailableSimulators.length > 1) {
51+
if (args.interactive && booted.length > 1) {
7052
const udid = await promptForDeviceToTailLogs(
7153
platformReadableName,
72-
bootedAndAvailableSimulators,
54+
booted,
7355
);
7456

75-
const simulator = bootedAndAvailableSimulators.find(
57+
const simulator = booted.find(
7658
({udid: deviceUDID}) => deviceUDID === udid,
7759
);
7860

@@ -84,7 +66,7 @@ const createLog =
8466

8567
tailDeviceLogs(simulator);
8668
} else {
87-
tailDeviceLogs(bootedAndAvailableSimulators[0]);
69+
tailDeviceLogs(booted[0]);
8870
}
8971
};
9072

‎packages/cli-platform-apple/src/commands/runCommand/createRun.ts

+10-16
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,14 @@ const createRun =
151151

152152
const devices = await listDevices(sdkNames);
153153

154-
const availableDevices = devices.filter(
155-
({isAvailable}) => isAvailable === true,
156-
);
157-
158-
if (availableDevices.length === 0) {
154+
if (devices.length === 0) {
159155
return logger.error(
160156
`${platformReadableName} devices or simulators not detected. Install simulators via Xcode or connect a physical ${platformReadableName} device`,
161157
);
162158
}
163159

164160
const fallbackSimulator =
165-
platformName === 'ios' ? getFallbackSimulator(args) : availableDevices[0];
161+
platformName === 'ios' ? getFallbackSimulator(args) : devices[0];
166162

167163
if (args.listDevices || args.interactive) {
168164
if (args.device || args.udid) {
@@ -180,7 +176,7 @@ const createRun =
180176
);
181177

182178
const selectedDevice = await promptForDeviceSelection(
183-
availableDevices,
179+
devices,
184180
preferredDevice,
185181
);
186182

@@ -222,12 +218,12 @@ const createRun =
222218
}
223219

224220
if (!args.device && !args.udid && !args.simulator) {
225-
const bootedDevices = availableDevices.filter(
226-
({type}) => type === 'device',
221+
const bootedSimulators = devices.filter(
222+
({state, type}) => state === 'Booted' && type === 'simulator',
227223
);
228-
const bootedSimulators = devices.filter(({type}) => type === 'simulator');
224+
const bootedDevices = devices.filter(({type}) => type === 'device'); // Physical devices here are always booted
225+
const booted = [...bootedSimulators, ...bootedDevices];
229226

230-
const booted = [...bootedDevices, ...bootedSimulators];
231227
if (booted.length === 0) {
232228
logger.info(
233229
'No booted devices or simulators found. Launching first available simulator...',
@@ -276,12 +272,12 @@ const createRun =
276272
}
277273

278274
if (args.udid) {
279-
const device = availableDevices.find((d) => d.udid === args.udid);
275+
const device = devices.find((d) => d.udid === args.udid);
280276
if (!device) {
281277
return logger.error(
282278
`Could not find a device with udid: "${chalk.bold(
283279
args.udid,
284-
)}". ${printFoundDevices(availableDevices)}`,
280+
)}". ${printFoundDevices(devices)}`,
285281
);
286282
}
287283
if (device.type === 'simulator') {
@@ -304,9 +300,7 @@ const createRun =
304300
);
305301
}
306302
} else if (args.device) {
307-
const physicalDevices = availableDevices.filter(
308-
({type}) => type !== 'simulator',
309-
);
303+
const physicalDevices = devices.filter(({type}) => type !== 'simulator');
310304
const device = matchingDevice(physicalDevices, args.device);
311305
if (device) {
312306
return runOnDevice(

‎packages/cli-platform-apple/src/tools/__tests__/listDevices.test.ts

+78-37
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ jest.mock('execa', () => {
1313
return {sync: jest.fn()};
1414
});
1515

16-
const xcrunOut = `
16+
beforeEach(() => {
17+
(execa.sync as jest.Mock)
18+
.mockReturnValueOnce({stdout: xcrunXcdeviceOut})
19+
.mockReturnValueOnce({stdout: xcrunSimctlOut});
20+
});
21+
22+
const xcrunXcdeviceOut = `
1723
[
1824
{
1925
"simulator" : true,
@@ -63,6 +69,19 @@ const xcrunOut = `
6369
"modelName" : "iPhone SE (3rd generation)",
6470
"name" : "iPhone SE (3rd generation)"
6571
},
72+
{
73+
"simulator" : true,
74+
"operatingSystemVersion" : "17.0 (21A328)",
75+
"available" : true,
76+
"platform" : "com.apple.platform.iphonesimulator",
77+
"modelCode" : "iPhone16,2",
78+
"identifier" : "B3D623E3-9907-4E0A-B76B-13B13A47FE92",
79+
"architecture" : "arm64",
80+
"modelUTI" : "com.apple.iphone-15-pro-max-1",
81+
"modelName" : "iPhone 15 Pro Max",
82+
"name" : "iPhone 15 Pro Max",
83+
"ignored" : false
84+
},
6685
{
6786
"simulator" : false,
6887
"operatingSystemVersion" : "13.0.1 (22A400)",
@@ -161,9 +180,40 @@ const xcrunOut = `
161180
]
162181
`;
163182

183+
const xcrunSimctlOut = `
184+
{
185+
"devices" : {
186+
"com.apple.CoreSimulator.SimRuntime.iOS-16-2" : [
187+
{
188+
"lastBootedAt" : "2023-05-09T11:08:32Z",
189+
"dataPath" : "<REPLACED_ROOT>/Library/Developer/CoreSimulator/Devices/54B1D3DE-A943-4867-BA6A-B82BFE3A7904/data",
190+
"dataPathSize" : 4630163456,
191+
"logPath" : "<REPLACED_ROOT>/Library/Logs/CoreSimulator/54B1D3DE-A943-4867-BA6A-B82BFE3A7904",
192+
"udid" : "54B1D3DE-A943-4867-BA6A-B82BFE3A7904",
193+
"isAvailable" : false,
194+
"availabilityError" : "runtime profile not found using System match policy",
195+
"deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-14",
196+
"state" : "Shutdown",
197+
"name" : "iPhone 14"
198+
},
199+
{
200+
"lastBootedAt" : "2024-01-07T15:33:06Z",
201+
"dataPath" : "<REPLACED_ROOT>/Library/Developer/CoreSimulator/Devices/B3D623E3-9907-4E0A-B76B-13B13A47FE92/data",
202+
"dataPathSize" : 4181225472,
203+
"logPath" : "<REPLACED_ROOT>/Library/Logs/CoreSimulator/B3D623E3-9907-4E0A-B76B-13B13A47FE92",
204+
"udid" : "B3D623E3-9907-4E0A-B76B-13B13A47FE92",
205+
"isAvailable" : true,
206+
"logPathSize" : 745472,
207+
"deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max",
208+
"state" : "Shutdown",
209+
"name" : "iPhone 15 Pro Max"
210+
}
211+
]
212+
}
213+
}`;
214+
164215
describe('listDevices', () => {
165-
it('parses output from xcdevice list for iOS', async () => {
166-
(execa.sync as jest.Mock).mockReturnValueOnce({stdout: xcrunOut});
216+
it('parses output list for iOS', async () => {
167217
const devices = await listDevices(['iphoneos', 'iphonesimulator']);
168218

169219
// Find all available simulators
@@ -186,17 +236,6 @@ describe('listDevices', () => {
186236
type: 'simulator',
187237
});
188238

189-
// Find all available iPhone's event when not available
190-
expect(devices).toContainEqual({
191-
name: 'Adam’s iPhone',
192-
isAvailable: false,
193-
udid: '1234567890-0987654321',
194-
version: '16.2 (20C65)',
195-
sdk: 'com.apple.platform.iphoneos',
196-
availabilityError:
197-
'To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.',
198-
type: 'device',
199-
});
200239
// Filter out AppleTV
201240
expect(devices).not.toContainEqual({
202241
isAvailable: false,
@@ -227,8 +266,7 @@ describe('listDevices', () => {
227266
});
228267
});
229268

230-
it('parses output from xcdevice list for tvOS', async () => {
231-
(execa.sync as jest.Mock).mockReturnValueOnce({stdout: xcrunOut});
269+
it('parses output for tvOS', async () => {
232270
const devices = await listDevices(['appletvos', 'appletvsimulator']);
233271

234272
// Filter out all available simulators
@@ -242,28 +280,7 @@ describe('listDevices', () => {
242280
type: 'simulator',
243281
});
244282

245-
// Filter out all available iPhone's event when not available
246-
expect(devices).not.toContainEqual({
247-
name: 'Adam’s iPhone',
248-
isAvailable: false,
249-
udid: '1234567890-0987654321',
250-
version: '16.2 (20C65)',
251-
sdk: 'com.apple.platform.iphoneos',
252-
availabilityError:
253-
'To use Adam’s iPhone for development, enable Developer Mode in Settings → Privacy & Security.',
254-
type: 'device',
255-
});
256-
257283
// Find AppleTV
258-
expect(devices).toContainEqual({
259-
isAvailable: false,
260-
name: 'Living Room',
261-
udid: '7656fbf922891c8a2c7682c9d845eaa6954c24d8',
262-
sdk: 'com.apple.platform.appletvos',
263-
version: '16.1 (20K71)',
264-
availabilityError: 'Living Room is not connected',
265-
type: 'device',
266-
});
267284
expect(devices).toContainEqual({
268285
isAvailable: true,
269286
name: 'Apple TV 4K (2nd generation)',
@@ -284,4 +301,28 @@ describe('listDevices', () => {
284301
type: 'device',
285302
});
286303
});
304+
305+
it('parses and merges output from two commands', async () => {
306+
const devices = await listDevices(['iphoneos', 'iphonesimulator']);
307+
308+
expect(devices).toContainEqual({
309+
availabilityError: undefined,
310+
dataPath:
311+
'<REPLACED_ROOT>/Library/Developer/CoreSimulator/Devices/B3D623E3-9907-4E0A-B76B-13B13A47FE92/data',
312+
dataPathSize: 4181225472,
313+
deviceTypeIdentifier:
314+
'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max',
315+
isAvailable: true,
316+
lastBootedAt: '2024-01-07T15:33:06Z',
317+
logPath:
318+
'<REPLACED_ROOT>/Library/Logs/CoreSimulator/B3D623E3-9907-4E0A-B76B-13B13A47FE92',
319+
logPathSize: 745472,
320+
name: 'iPhone 15 Pro Max',
321+
sdk: 'com.apple.platform.iphonesimulator',
322+
state: 'Shutdown',
323+
type: 'simulator',
324+
udid: 'B3D623E3-9907-4E0A-B76B-13B13A47FE92',
325+
version: '17.0 (21A328)',
326+
});
327+
});
287328
});

‎packages/cli-platform-apple/src/tools/getSimulators.ts

-24
This file was deleted.

‎packages/cli-platform-apple/src/tools/listDevices.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,45 @@ const parseXcdeviceList = (text: string, sdkNames: string[] = []): Device[] => {
4949
return devices;
5050
};
5151

52+
/**
53+
* Executes `xcrun xcdevice list` and `xcrun simctl list --json devices`, and connects parsed output of these two commands. We are running these two commands as they are necessary to display both physical devices and simulators. However, it's important to note that neither command provides a combined output of both.
54+
* @param sdkNames
55+
* @returns List of available devices and simulators.
56+
*/
5257
async function listDevices(sdkNames: string[]): Promise<Device[]> {
53-
const out = execa.sync('xcrun', ['xcdevice', 'list']).stdout;
54-
return parseXcdeviceList(out, sdkNames);
58+
const xcdeviceOutput = execa.sync('xcrun', ['xcdevice', 'list']).stdout;
59+
const parsedXcdeviceOutput = parseXcdeviceList(xcdeviceOutput, sdkNames);
60+
61+
const simctlOutput = JSON.parse(
62+
execa.sync('xcrun', ['simctl', 'list', '--json', 'devices']).stdout,
63+
);
64+
65+
const parsedSimctlOutput: Device[] = Object.keys(simctlOutput.devices)
66+
.map((key) => simctlOutput.devices[key])
67+
.reduce((acc, val) => acc.concat(val), []);
68+
69+
const merged: Device[] = [];
70+
const matchedUdids = new Set();
71+
72+
parsedXcdeviceOutput.forEach((first) => {
73+
const match = parsedSimctlOutput.find(
74+
(second) => first.udid === second.udid,
75+
);
76+
if (match) {
77+
matchedUdids.add(first.udid);
78+
merged.push({...first, ...match});
79+
} else {
80+
merged.push({...first});
81+
}
82+
});
83+
84+
parsedSimctlOutput.forEach((item) => {
85+
if (!matchedUdids.has(item.udid)) {
86+
merged.push({...item});
87+
}
88+
});
89+
90+
return merged.filter(({isAvailable}) => isAvailable === true);
5591
}
5692

5793
export function stripPlatform(platform: string): string {

0 commit comments

Comments
 (0)
Please sign in to comment.