55#import " XCWPrivateSimulatorBooter.h"
66#import " XCWProcessRunner.h"
77
8+ #import < errno.h>
9+ #import < stdlib.h>
10+ #import < string.h>
11+
812static NSString * const XCWSimctlErrorDomain = @" SimDeck.Simctl" ;
913
1014@interface XCWSimctl ()
@@ -19,6 +23,9 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
1923 timeoutSec : (NSTimeInterval )timeoutSec
2024 error : (NSError * _Nullable __autoreleasing *)error ;
2125+ (nullable NSDictionary *)listJSONPayloadWithError : (NSError * _Nullable __autoreleasing *)error ;
26+ + (NSError *)errorWithDescription : (NSString *)description code : (NSInteger )code ;
27+ - (BOOL )installAppBundleAtPath : (NSString *)appPath simulatorUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error ;
28+ - (BOOL )installIPAAtPath : (NSString *)ipaPath simulatorUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error ;
2229
2330@end
2431
@@ -43,6 +50,70 @@ + (nullable NSDictionary *)listJSONPayloadWithError:(NSError * _Nullable __autor
4350 return [value isKindOfClass: [NSNumber class ]] ? value : nil ;
4451}
4552
53+ static NSString * _Nullable XCWCreateTemporaryDirectory (NSString *prefix, NSError * _Nullable __autoreleasing *error) {
54+ NSString *templatePath = [NSTemporaryDirectory () stringByAppendingPathComponent: [NSString stringWithFormat: @" %@ -XXXXXX" , prefix]];
55+ char *directoryTemplate = strdup (templatePath.fileSystemRepresentation );
56+ if (directoryTemplate == NULL ) {
57+ if (error != NULL ) {
58+ *error = [XCWSimctl errorWithDescription: @" Failed to allocate temporary IPA extraction path." code: 15 ];
59+ }
60+ return nil ;
61+ }
62+
63+ char *createdPath = mkdtemp (directoryTemplate);
64+ if (createdPath == NULL ) {
65+ if (error != NULL ) {
66+ *error = [XCWSimctl errorWithDescription: [NSString stringWithFormat: @" Failed to create temporary IPA extraction directory: %s " , strerror (errno)] code: 15 ];
67+ }
68+ free (directoryTemplate);
69+ return nil ;
70+ }
71+
72+ NSString *path = [[NSFileManager defaultManager ] stringWithFileSystemRepresentation: createdPath
73+ length: strlen (createdPath)];
74+ free (directoryTemplate);
75+ return path;
76+ }
77+
78+ static NSString * _Nullable XCWAppBundlePathInExtractedIPA (NSString *extractedPath, NSError * _Nullable __autoreleasing *error) {
79+ NSFileManager *fileManager = [NSFileManager defaultManager ];
80+ NSString *payloadPath = [extractedPath stringByAppendingPathComponent: @" Payload" ];
81+ BOOL isDirectory = NO ;
82+ if (![fileManager fileExistsAtPath: payloadPath isDirectory: &isDirectory] || !isDirectory) {
83+ if (error != NULL ) {
84+ *error = [XCWSimctl errorWithDescription: @" IPA archive did not contain a Payload directory." code: 15 ];
85+ }
86+ return nil ;
87+ }
88+
89+ NSArray <NSString *> *entries = [fileManager contentsOfDirectoryAtPath: payloadPath error: error];
90+ if (entries == nil ) {
91+ return nil ;
92+ }
93+
94+ NSMutableArray <NSString *> *appPaths = [NSMutableArray array ];
95+ for (NSString *entry in entries) {
96+ if ([entry.pathExtension caseInsensitiveCompare: @" app" ] != NSOrderedSame) {
97+ continue ;
98+ }
99+ NSString *candidatePath = [payloadPath stringByAppendingPathComponent: entry];
100+ BOOL candidateIsDirectory = NO ;
101+ if ([fileManager fileExistsAtPath: candidatePath isDirectory: &candidateIsDirectory] && candidateIsDirectory) {
102+ [appPaths addObject: candidatePath];
103+ }
104+ }
105+
106+ if (appPaths.count == 0 ) {
107+ if (error != NULL ) {
108+ *error = [XCWSimctl errorWithDescription: @" IPA archive did not contain a Payload/*.app bundle." code: 15 ];
109+ }
110+ return nil ;
111+ }
112+
113+ [appPaths sortUsingSelector: @selector (localizedStandardCompare: )];
114+ return appPaths.firstObject ;
115+ }
116+
46117static NSString *XCWRuntimeDisplayName (NSDictionary *runtime, NSString *runtimeIdentifier) {
47118 NSString *name = XCWStringValue (runtime[@" name" ]);
48119 if (name.length > 0 && ![name isEqualToString: runtimeIdentifier]) {
@@ -591,6 +662,54 @@ - (BOOL)eraseSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable __aut
591662}
592663
593664- (BOOL )installAppAtPath : (NSString *)appPath simulatorUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error {
665+ NSString *extension = appPath.pathExtension .lowercaseString ;
666+ if ([extension isEqualToString: @" ipa" ]) {
667+ return [self installIPAAtPath: appPath simulatorUDID: udid error: error];
668+ }
669+ if (![extension isEqualToString: @" app" ]) {
670+ if (error != NULL ) {
671+ *error = [self .class errorWithDescription: @" iOS simulator install expects an `.app` bundle or `.ipa` archive." code: 15 ];
672+ }
673+ return NO ;
674+ }
675+ return [self installAppBundleAtPath: appPath simulatorUDID: udid error: error];
676+ }
677+
678+ - (BOOL )installIPAAtPath : (NSString *)ipaPath simulatorUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error {
679+ NSString *extractedPath = XCWCreateTemporaryDirectory (@" simdeck-ipa" , error);
680+ if (extractedPath == nil ) {
681+ return NO ;
682+ }
683+
684+ XCWProcessResult *extractResult = [XCWProcessRunner runLaunchPath: @" /usr/bin/ditto"
685+ arguments: @[@" -x" , @" -k" , ipaPath, extractedPath]
686+ inputData: nil
687+ timeoutSec: 180
688+ error: error];
689+ if (extractResult == nil ) {
690+ [[NSFileManager defaultManager ] removeItemAtPath: extractedPath error: nil ];
691+ return NO ;
692+ }
693+ if (extractResult.terminationStatus != 0 ) {
694+ if (error != NULL ) {
695+ *error = [self .class errorWithDescription: extractResult.stderrString.length > 0 ? extractResult.stderrString : @" Unable to extract IPA archive." code: 15 ];
696+ }
697+ [[NSFileManager defaultManager ] removeItemAtPath: extractedPath error: nil ];
698+ return NO ;
699+ }
700+
701+ NSString *appBundlePath = XCWAppBundlePathInExtractedIPA (extractedPath, error);
702+ if (appBundlePath == nil ) {
703+ [[NSFileManager defaultManager ] removeItemAtPath: extractedPath error: nil ];
704+ return NO ;
705+ }
706+
707+ BOOL ok = [self installAppBundleAtPath: appBundlePath simulatorUDID: udid error: error];
708+ [[NSFileManager defaultManager ] removeItemAtPath: extractedPath error: nil ];
709+ return ok;
710+ }
711+
712+ - (BOOL )installAppBundleAtPath : (NSString *)appPath simulatorUDID : (NSString *)udid error : (NSError * _Nullable __autoreleasing *)error {
594713 XCWProcessResult *result = [self .class runSimctl: @[@" install" , udid, appPath] error: error];
595714 if (result == nil ) {
596715 return NO ;
0 commit comments