99
1010@interface XCWSimctl ()
1111
12+ - (nullable NSString *)createSingleSimulatorWithName : (NSString *)name
13+ deviceTypeIdentifier : (NSString *)deviceTypeIdentifier
14+ runtimeIdentifier : (nullable NSString *)runtimeIdentifier
15+ error : (NSError * _Nullable __autoreleasing *)error ;
1216+ (nullable XCWProcessResult *)runSimctl : (NSArray <NSString *> *)arguments
1317 error : (NSError * _Nullable __autoreleasing *)error ;
1418+ (nullable XCWProcessResult *)runSimctl : (NSArray <NSString *> *)arguments
1519 timeoutSec : (NSTimeInterval )timeoutSec
1620 error : (NSError * _Nullable __autoreleasing *)error ;
21+ + (nullable NSDictionary *)listJSONPayloadWithError : (NSError * _Nullable __autoreleasing *)error ;
1722
1823@end
1924
@@ -34,6 +39,10 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
3439 return [value isKindOfClass: [NSString class ]] ? value : @" " ;
3540}
3641
42+ static NSNumber *XCWNumberValue (id value) {
43+ return [value isKindOfClass: [NSNumber class ]] ? value : nil ;
44+ }
45+
3746static NSString *XCWRuntimeDisplayName (NSDictionary *runtime, NSString *runtimeIdentifier) {
3847 NSString *name = XCWStringValue (runtime[@" name" ]);
3948 if (name.length > 0 && ![name isEqualToString: runtimeIdentifier]) {
@@ -64,22 +73,8 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
6473@implementation XCWSimctl
6574
6675- (nullable NSArray <NSDictionary *> *)listSimulatorsWithError : (NSError * _Nullable __autoreleasing *)error {
67- XCWProcessResult *result = [self .class runSimctl: @[@" list" , @" --json" ] error: error];
68- if (result == nil ) {
69- return nil ;
70- }
71- if (result.terminationStatus != 0 ) {
72- if (error != NULL ) {
73- *error = [self .class errorWithDescription: result.stderrString.length > 0 ? result.stderrString : @" simctl list failed" code: 1 ];
74- }
75- return nil ;
76- }
77-
78- NSDictionary *payload = [NSJSONSerialization JSONObjectWithData: result.stdoutData options: 0 error: error];
79- if (![payload isKindOfClass: [NSDictionary class ]]) {
80- if (error != NULL && *error == nil ) {
81- *error = [self .class errorWithDescription: @" Unable to parse simctl JSON output." code: 2 ];
82- }
76+ NSDictionary *payload = [self .class listJSONPayloadWithError: error];
77+ if (payload == nil ) {
8378 return nil ;
8479 }
8580
@@ -168,6 +163,230 @@ @implementation XCWSimctl
168163 return flattened;
169164}
170165
166+ - (nullable NSDictionary *)simulatorCreationOptionsWithError : (NSError * _Nullable __autoreleasing *)error {
167+ NSDictionary *payload = [self .class listJSONPayloadWithError: error];
168+ if (payload == nil ) {
169+ return nil ;
170+ }
171+
172+ NSArray *runtimeArray = XCWArrayPayload (payload[@" runtimes" ], @" runtimes" );
173+ NSMutableArray <NSDictionary *> *runtimes = [NSMutableArray array ];
174+ NSMutableDictionary <NSString *, NSMutableArray <NSString *> *> *runtimeIdentifiersByDeviceType = [NSMutableDictionary dictionary ];
175+
176+ for (NSDictionary *runtime in runtimeArray) {
177+ if (![runtime isKindOfClass: [NSDictionary class ]]) {
178+ continue ;
179+ }
180+
181+ NSString *identifier = XCWStringValue (runtime[@" identifier" ]);
182+ if (identifier.length == 0 ) {
183+ continue ;
184+ }
185+
186+ BOOL isAvailable = [runtime[@" isAvailable" ] respondsToSelector: @selector (boolValue )] ? [runtime[@" isAvailable" ] boolValue ] : YES ;
187+ if (!isAvailable) {
188+ continue ;
189+ }
190+
191+ NSMutableArray <NSString *> *supportedDeviceTypeIdentifiers = [NSMutableArray array ];
192+ NSArray *supportedDeviceTypes = XCWArrayPayload (runtime[@" supportedDeviceTypes" ], @" supportedDeviceTypes" );
193+ for (NSDictionary *deviceType in supportedDeviceTypes) {
194+ if (![deviceType isKindOfClass: [NSDictionary class ]]) {
195+ continue ;
196+ }
197+ NSString *deviceTypeIdentifier = XCWStringValue (deviceType[@" identifier" ]);
198+ if (deviceTypeIdentifier.length == 0 ) {
199+ continue ;
200+ }
201+ [supportedDeviceTypeIdentifiers addObject: deviceTypeIdentifier];
202+ NSMutableArray <NSString *> *runtimeIdentifiers = runtimeIdentifiersByDeviceType[deviceTypeIdentifier];
203+ if (runtimeIdentifiers == nil ) {
204+ runtimeIdentifiers = [NSMutableArray array ];
205+ runtimeIdentifiersByDeviceType[deviceTypeIdentifier] = runtimeIdentifiers;
206+ }
207+ [runtimeIdentifiers addObject: identifier];
208+ }
209+
210+ NSMutableDictionary *entry = [NSMutableDictionary dictionary ];
211+ entry[@" identifier" ] = identifier;
212+ entry[@" name" ] = XCWRuntimeDisplayName (runtime, identifier);
213+ entry[@" isAvailable" ] = @YES ;
214+ entry[@" supportedDeviceTypeIdentifiers" ] = supportedDeviceTypeIdentifiers;
215+
216+ NSString *platform = XCWStringValue (runtime[@" platform" ]);
217+ if (platform.length > 0 ) {
218+ entry[@" platform" ] = platform;
219+ }
220+ NSString *version = XCWStringValue (runtime[@" version" ]);
221+ if (version.length > 0 ) {
222+ entry[@" version" ] = version;
223+ }
224+ NSString *buildVersion = XCWStringValue (runtime[@" buildversion" ]);
225+ if (buildVersion.length > 0 ) {
226+ entry[@" buildVersion" ] = buildVersion;
227+ }
228+
229+ [runtimes addObject: entry];
230+ }
231+
232+ NSArray *deviceTypesArray = XCWArrayPayload (payload[@" devicetypes" ], @" devicetypes" );
233+ NSMutableArray <NSDictionary *> *deviceTypes = [NSMutableArray array ];
234+ for (NSDictionary *deviceType in deviceTypesArray) {
235+ if (![deviceType isKindOfClass: [NSDictionary class ]]) {
236+ continue ;
237+ }
238+
239+ NSString *identifier = XCWStringValue (deviceType[@" identifier" ]);
240+ if (identifier.length == 0 ) {
241+ continue ;
242+ }
243+ NSArray <NSString *> *supportedRuntimeIdentifiers = runtimeIdentifiersByDeviceType[identifier];
244+ if (supportedRuntimeIdentifiers.count == 0 ) {
245+ continue ;
246+ }
247+
248+ NSMutableDictionary *entry = [NSMutableDictionary dictionary ];
249+ entry[@" identifier" ] = identifier;
250+ NSString *name = XCWStringValue (deviceType[@" name" ]);
251+ entry[@" name" ] = name.length > 0 ? name : identifier;
252+ entry[@" supportedRuntimeIdentifiers" ] = supportedRuntimeIdentifiers;
253+
254+ NSString *productFamily = XCWStringValue (deviceType[@" productFamily" ]);
255+ if (productFamily.length > 0 ) {
256+ entry[@" productFamily" ] = productFamily;
257+ }
258+ NSString *modelIdentifier = XCWStringValue (deviceType[@" modelIdentifier" ]);
259+ if (modelIdentifier.length > 0 ) {
260+ entry[@" modelIdentifier" ] = modelIdentifier;
261+ }
262+ NSString *minRuntimeVersionString = XCWStringValue (deviceType[@" minRuntimeVersionString" ]);
263+ if (minRuntimeVersionString.length > 0 ) {
264+ entry[@" minRuntimeVersionString" ] = minRuntimeVersionString;
265+ }
266+ NSString *maxRuntimeVersionString = XCWStringValue (deviceType[@" maxRuntimeVersionString" ]);
267+ if (maxRuntimeVersionString.length > 0 ) {
268+ entry[@" maxRuntimeVersionString" ] = maxRuntimeVersionString;
269+ }
270+ NSNumber *minRuntimeVersion = XCWNumberValue (deviceType[@" minRuntimeVersion" ]);
271+ if (minRuntimeVersion != nil ) {
272+ entry[@" minRuntimeVersion" ] = minRuntimeVersion;
273+ }
274+ NSNumber *maxRuntimeVersion = XCWNumberValue (deviceType[@" maxRuntimeVersion" ]);
275+ if (maxRuntimeVersion != nil ) {
276+ entry[@" maxRuntimeVersion" ] = maxRuntimeVersion;
277+ }
278+
279+ [deviceTypes addObject: entry];
280+ }
281+
282+ return @{
283+ @" deviceTypes" : deviceTypes,
284+ @" runtimes" : runtimes,
285+ };
286+ }
287+
288+ - (nullable NSDictionary *)createSimulatorWithName : (NSString *)name
289+ deviceTypeIdentifier : (NSString *)deviceTypeIdentifier
290+ runtimeIdentifier : (nullable NSString *)runtimeIdentifier
291+ pairedWatchName : (nullable NSString *)pairedWatchName
292+ pairedWatchDeviceTypeIdentifier : (nullable NSString *)pairedWatchDeviceTypeIdentifier
293+ pairedWatchRuntimeIdentifier : (nullable NSString *)pairedWatchRuntimeIdentifier
294+ error : (NSError * _Nullable __autoreleasing *)error {
295+ NSString *primaryUDID = [self createSingleSimulatorWithName: name
296+ deviceTypeIdentifier: deviceTypeIdentifier
297+ runtimeIdentifier: runtimeIdentifier
298+ error: error];
299+ if (primaryUDID.length == 0 ) {
300+ return nil ;
301+ }
302+
303+ NSMutableDictionary *result = [@{
304+ @" udid" : primaryUDID,
305+ } mutableCopy];
306+
307+ NSString *watchName = [pairedWatchName stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
308+ NSString *watchDeviceTypeIdentifier = [pairedWatchDeviceTypeIdentifier stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
309+ if (watchName.length == 0 && watchDeviceTypeIdentifier.length == 0 ) {
310+ return result;
311+ }
312+ if (watchName.length == 0 || watchDeviceTypeIdentifier.length == 0 ) {
313+ if (error != NULL ) {
314+ *error = [self .class errorWithDescription: @" Paired watch creation requires both a watch name and device type." code: 23 ];
315+ }
316+ return nil ;
317+ }
318+
319+ NSString *watchUDID = [self createSingleSimulatorWithName: watchName
320+ deviceTypeIdentifier: watchDeviceTypeIdentifier
321+ runtimeIdentifier: pairedWatchRuntimeIdentifier
322+ error: error];
323+ if (watchUDID.length == 0 ) {
324+ return nil ;
325+ }
326+
327+ XCWProcessResult *pairResult = [self .class runSimctl: @[@" pair" , watchUDID, primaryUDID]
328+ timeoutSec: 120
329+ error: error];
330+ if (pairResult == nil ) {
331+ return nil ;
332+ }
333+ if (pairResult.terminationStatus != 0 ) {
334+ if (error != NULL ) {
335+ *error = [self .class errorWithDescription: pairResult.stderrString.length > 0 ? pairResult.stderrString : @" Unable to pair watch simulator." code: 24 ];
336+ }
337+ return nil ;
338+ }
339+
340+ result[@" pairedWatchUDID" ] = watchUDID;
341+ return result;
342+ }
343+
344+ - (nullable NSString *)createSingleSimulatorWithName : (NSString *)name
345+ deviceTypeIdentifier : (NSString *)deviceTypeIdentifier
346+ runtimeIdentifier : (nullable NSString *)runtimeIdentifier
347+ error : (NSError * _Nullable __autoreleasing *)error {
348+ NSString *trimmedName = [name stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
349+ NSString *trimmedDeviceTypeIdentifier = [deviceTypeIdentifier stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
350+ NSString *trimmedRuntimeIdentifier = [runtimeIdentifier stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
351+ if (trimmedName.length == 0 ) {
352+ if (error != NULL ) {
353+ *error = [self .class errorWithDescription: @" Simulator name is required." code: 19 ];
354+ }
355+ return nil ;
356+ }
357+ if (trimmedDeviceTypeIdentifier.length == 0 ) {
358+ if (error != NULL ) {
359+ *error = [self .class errorWithDescription: @" Device type identifier is required." code: 20 ];
360+ }
361+ return nil ;
362+ }
363+
364+ NSMutableArray <NSString *> *arguments = [@[@" create" , trimmedName, trimmedDeviceTypeIdentifier] mutableCopy ];
365+ if (trimmedRuntimeIdentifier.length > 0 ) {
366+ [arguments addObject: trimmedRuntimeIdentifier];
367+ }
368+
369+ XCWProcessResult *result = [self .class runSimctl: arguments timeoutSec: 120 error: error];
370+ if (result == nil ) {
371+ return nil ;
372+ }
373+ if (result.terminationStatus != 0 ) {
374+ if (error != NULL ) {
375+ *error = [self .class errorWithDescription: result.stderrString.length > 0 ? result.stderrString : @" Unable to create simulator." code: 21 ];
376+ }
377+ return nil ;
378+ }
379+
380+ NSString *udid = [result.stdoutString stringByTrimmingCharactersInSet: NSCharacterSet .whitespaceAndNewlineCharacterSet];
381+ if (udid.length == 0 ) {
382+ if (error != NULL ) {
383+ *error = [self .class errorWithDescription: @" simctl create did not return a simulator UDID." code: 22 ];
384+ }
385+ return nil ;
386+ }
387+ return udid;
388+ }
389+
171390- (nullable NSDictionary *)simulatorWithUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error {
172391 for (NSDictionary *simulator in [self listSimulatorsWithError: error] ?: @[]) {
173392 if ([simulator[@" udid" ] isEqualToString: udid]) {
@@ -447,6 +666,28 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
447666 return [self runSimctl: arguments timeoutSec: 0 error: error];
448667}
449668
669+ + (nullable NSDictionary *)listJSONPayloadWithError : (NSError * _Nullable __autoreleasing *)error {
670+ XCWProcessResult *result = [self runSimctl: @[@" list" , @" --json" ] error: error];
671+ if (result == nil ) {
672+ return nil ;
673+ }
674+ if (result.terminationStatus != 0 ) {
675+ if (error != NULL ) {
676+ *error = [self errorWithDescription: result.stderrString.length > 0 ? result.stderrString : @" simctl list failed" code: 1 ];
677+ }
678+ return nil ;
679+ }
680+
681+ NSDictionary *payload = [NSJSONSerialization JSONObjectWithData: result.stdoutData options: 0 error: error];
682+ if (![payload isKindOfClass: [NSDictionary class ]]) {
683+ if (error != NULL && *error == nil ) {
684+ *error = [self errorWithDescription: @" Unable to parse simctl JSON output." code: 2 ];
685+ }
686+ return nil ;
687+ }
688+ return payload;
689+ }
690+
450691+ (nullable XCWProcessResult *)runSimctl : (NSArray <NSString *> *)arguments
451692 timeoutSec : (NSTimeInterval )timeoutSec
452693 error : (NSError * _Nullable __autoreleasing *)error {
0 commit comments