| // Copyright 2015 Google Inc. All rights reserved. | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //    http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | // Application that finds all Xcodes installed on a given Mac and will return a | 
 | // path for a given version number. | 
 | // | 
 | // If you have 7.0, 6.4.1 and 6.3 installed the inputs will map to: | 
 | // | 
 | // 7,7.0,7.0.0 = 7.0 | 
 | // 6,6.4,6.4.1 = 6.4.1 | 
 | // 6.3,6.3.0 = 6.3 | 
 |  | 
 | #if !defined(__has_feature) || !__has_feature(objc_arc) | 
 | #error "This file requires ARC support." | 
 | #endif | 
 |  | 
 | #import <CoreServices/CoreServices.h> | 
 | #import <Foundation/Foundation.h> | 
 |  | 
 | // Simple data structure for tracking a version of Xcode (i.e. 6.4) with an URL | 
 | // to the application. | 
 | @interface XcodeVersionEntry : NSObject | 
 | @property(readonly) NSString *version; | 
 | @property(readonly) NSString *productVersion; | 
 | @property(readonly) NSURL *url; | 
 | @end | 
 |  | 
 | @implementation XcodeVersionEntry | 
 |  | 
 | - (id)initWithVersion:(NSString *)version | 
 |        productVersion:(NSString *)productVersion | 
 |                   url:(NSURL *)url { | 
 |   if ((self = [super init])) { | 
 |     _version = version; | 
 |     _productVersion = productVersion; | 
 |     _url = url; | 
 |   } | 
 |   return self; | 
 | } | 
 |  | 
 | - (id)description { | 
 |   return [NSString stringWithFormat:@"<%@ %p>: %@ %@", | 
 |                    [self class], self, _version, _url]; | 
 | } | 
 |  | 
 | @end | 
 |  | 
 | // Given an entry, insert it into a dictionary that is keyed by versions. | 
 | // | 
 | // For an entry that is 6.4.1:/Applications/Xcode.app, add it for 6.4.1 and | 
 | // optionally add it for 6.4 and 6 if it is "better" than any entry that may | 
 | // already be there, where "better" is defined as: | 
 | // | 
 | // 1. Under /Applications/. (This avoids mounted Xcode versions taking | 
 | //    precedence over installed versions.) | 
 | // | 
 | // 2. Not older (at least as high version number). | 
 | static void AddEntryToDictionary( | 
 |   XcodeVersionEntry *entry, | 
 |   NSMutableDictionary<NSString *, XcodeVersionEntry *> *dict) { | 
 |   BOOL inApplications = [entry.url.path hasPrefix:@"/Applications/"]; | 
 |   NSString *entryVersion = entry.version; | 
 |   NSString *entryProductVersion = entry.productVersion; | 
 |   NSString *subversion = entryVersion; | 
 |   if (dict[entryVersion] && !inApplications) { | 
 |     return; | 
 |   } | 
 |   dict[entryVersion] = entry; | 
 |   if (entryProductVersion) { | 
 |     dict[entryProductVersion] = entry; | 
 |   } | 
 |   while (YES) { | 
 |     NSRange range = [subversion rangeOfString:@"." options:NSBackwardsSearch]; | 
 |     if (range.length == 0 || range.location == 0) { | 
 |       break; | 
 |     } | 
 |     subversion = [subversion substringToIndex:range.location]; | 
 |     XcodeVersionEntry *subversionEntry = dict[subversion]; | 
 |     if (subversionEntry) { | 
 |       BOOL atLeastAsLarge = | 
 |           ([subversionEntry.version compare:entry.version | 
 |                                     options:NSNumericSearch] != NSOrderedDescending); | 
 |       if (inApplications && atLeastAsLarge) { | 
 |         dict[subversion] = entry; | 
 |       } | 
 |     } else { | 
 |       dict[subversion] = entry; | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | // Given a "version", expand it to at least 3 components by adding .0 as | 
 | // necessary. | 
 | static NSString *ExpandVersion(NSString *version) { | 
 |   NSArray *components = [version componentsSeparatedByString:@"."]; | 
 |   NSString *appendage = nil; | 
 |   if (components.count == 2) { | 
 |     appendage = @".0"; | 
 |   } else if (components.count == 1) { | 
 |     appendage = @".0.0"; | 
 |   } | 
 |   if (appendage) { | 
 |     version = [version stringByAppendingString:appendage]; | 
 |   } | 
 |   return version; | 
 | } | 
 |  | 
 | // Searches for all available Xcodes in the system and returns a dictionary that | 
 | // maps version identifiers of any form (X, X.Y, and X.Y.Z) to the directory | 
 | // where the Xcode bundle lives. | 
 | // | 
 | // If there is a problem locating the Xcodes, prints one or more error messages | 
 | // and returns nil. | 
 | static NSMutableDictionary<NSString *, XcodeVersionEntry *> *FindXcodes() | 
 |   __attribute((ns_returns_retained)) { | 
 |   CFStringRef cfBundleID = CFSTR("com.apple.dt.Xcode"); | 
 |   NSString *bundleID = (__bridge NSString *)cfBundleID; | 
 |  | 
 |   NSMutableDictionary<NSString *, XcodeVersionEntry *> *dict = | 
 |       [[NSMutableDictionary alloc] init]; | 
 |   CFErrorRef cfError; | 
 |   NSArray *array = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier( | 
 |       cfBundleID, &cfError)); | 
 |   if (array == nil) { | 
 |     NSError *nsError = (__bridge NSError *)cfError; | 
 |     fprintf(stderr, "error: %s\n", nsError.description.UTF8String); | 
 |     return nil; | 
 |   } | 
 |  | 
 |   // Scan all bundles but delay returning in case of errors until we are | 
 |   // done. This is to let us log details about all the bundles that were | 
 |   // processed so that a faulty bundle doesn't hide useful information about | 
 |   // other bundles that were found. | 
 |   BOOL errors = NO; | 
 |   for (NSURL *url in array) { | 
 |     NSArray *contents = [ | 
 |       [NSFileManager defaultManager] contentsOfDirectoryAtURL:url | 
 |                                    includingPropertiesForKeys:nil | 
 |                                                       options:0 | 
 |                                                         error:nil]; | 
 |     NSLog(@"Found bundle %@ in %@; contents on disk: %@", | 
 |           bundleID, url, contents); | 
 |  | 
 |     NSBundle *bundle = [NSBundle bundleWithURL:url]; | 
 |     if (bundle == nil) { | 
 |       NSLog(@"ERROR: Unable to open bundle at URL: %@\n", url); | 
 |       errors = YES; | 
 |       continue; | 
 |     } | 
 |  | 
 |     // LSCopyApplicationURLsForBundleIdentifier seems to sometimes return | 
 |     // invalid bundles (e.g. an arbitrary folder), which we should ignore (but | 
 |     // don't treat as an error). | 
 |     // | 
 |     // To work around this issue, we double check to make sure the NSBundle's | 
 |     // bundleIdentifier is that of Xcode's, as invalid bundles won't match. | 
 |     if (![bundle.bundleIdentifier isEqualToString:bundleID]) { | 
 |       NSLog(@"WARNING: Ignoring bundle %@ due to bundleID mismatch " | 
 |             @"(got \"%@\" but expected \"%@\"); info: %@", | 
 |             url, bundle.bundleIdentifier, bundleID, bundle.infoDictionary); | 
 |       continue; | 
 |     } | 
 |  | 
 |     NSString *versionKey = @"CFBundleShortVersionString"; | 
 |     NSString *version = [bundle.infoDictionary objectForKey:versionKey]; | 
 |     if (version == nil) { | 
 |       NSLog(@"ERROR: Cannot find %@ in info for bundle %@; info: %@\n", | 
 |             versionKey, url, bundle.infoDictionary); | 
 |       errors = YES; | 
 |       continue; | 
 |     } | 
 |     NSString *expandedVersion = ExpandVersion(version); | 
 |     NSLog(@"Version strings for %@: short=%@, expanded=%@", | 
 |           url, version, expandedVersion); | 
 |  | 
 |     NSURL *versionPlistUrl = [url URLByAppendingPathComponent:@"Contents/version.plist"]; | 
 |     NSDictionary *versionPlistContents = | 
 |         [[NSDictionary alloc] initWithContentsOfURL:versionPlistUrl]; | 
 |     NSString *productVersion = [versionPlistContents objectForKey:@"ProductBuildVersion"]; | 
 |     if (productVersion) { | 
 |       expandedVersion = [expandedVersion stringByAppendingFormat:@".%@", productVersion]; | 
 |     } | 
 |  | 
 |     NSURL *developerDir = | 
 |         [url URLByAppendingPathComponent:@"Contents/Developer"]; | 
 |     XcodeVersionEntry *entry = [[XcodeVersionEntry alloc] initWithVersion:expandedVersion | 
 |                                                            productVersion:productVersion | 
 |                                                                       url:developerDir]; | 
 |     AddEntryToDictionary(entry, dict); | 
 |   } | 
 |   return errors ? nil : dict; | 
 | } | 
 |  | 
 | // Prints out the located Xcodes as a set of lines where each line contains the | 
 | // list of versions for a given Xcode and its location on disk. | 
 | static void DumpAsVersionsOnly( | 
 |   FILE *output, | 
 |   NSMutableDictionary<NSString *, XcodeVersionEntry *> *dict) { | 
 |   NSMutableDictionary<NSString *, NSMutableSet <NSString *> *> *aliasDict = | 
 |       [[NSMutableDictionary alloc] init]; | 
 |   [dict enumerateKeysAndObjectsUsingBlock:^(NSString *aliasVersion, | 
 |                                             XcodeVersionEntry *entry, | 
 |                                             BOOL *stop) { | 
 |     NSString *versionString = entry.version; | 
 |     if (aliasDict[versionString] == nil) { | 
 |       aliasDict[versionString] = [[NSMutableSet alloc] init]; | 
 |     } | 
 |     [aliasDict[versionString] addObject:aliasVersion]; | 
 |   }]; | 
 |   for (NSString *version in aliasDict) { | 
 |     XcodeVersionEntry *entry = dict[version]; | 
 |     fprintf(output, "%s:%s:%s\n", | 
 |             version.UTF8String, | 
 |             [[aliasDict[version] allObjects] | 
 |                    componentsJoinedByString: @","].UTF8String, | 
 |             entry.url.fileSystemRepresentation); | 
 |   } | 
 | } | 
 |  | 
 | // Prints out the located Xcodes in JSON format. | 
 | static void DumpAsJson( | 
 |   FILE *output, | 
 |   NSMutableDictionary<NSString *, XcodeVersionEntry *> *dict) { | 
 |   fprintf(output, "{\n"); | 
 |   int i = 0; | 
 |   for (NSString *version in dict) { | 
 |     XcodeVersionEntry *entry = dict[version]; | 
 |     fprintf(output, "\t\"%s\": \"%s\"%s\n", | 
 |             version.UTF8String, entry.url.fileSystemRepresentation, | 
 |             ++i == [dict count] ? "" : ","); | 
 |   } | 
 |   fprintf(output, "}\n"); | 
 | } | 
 |  | 
 | // Dumps usage information. | 
 | static void usage(FILE *output) { | 
 |   fprintf( | 
 |       output, | 
 |       "xcode-locator [-v|<version_number>]" | 
 |       "\n\n" | 
 |       "Given a version number or partial version number in x.y.z format, " | 
 |       "will attempt to return the path to the appropriate developer " | 
 |       "directory." | 
 |       "\n\n" | 
 |       "Omitting a version number will list all available versions in JSON " | 
 |       "format, alongside their paths." | 
 |       "\n\n" | 
 |       "Passing -v will list all available fully-specified version numbers " | 
 |       "along with their possible aliases and their developer directory, " | 
 |       "each on a new line. For example:" | 
 |       "\n\n" | 
 |       "7.3.1:7,7.3,7.3.1:/Applications/Xcode.app/Contents/Developer" | 
 |       "\n"); | 
 | } | 
 |  | 
 | int main(int argc, const char * argv[]) { | 
 |   @autoreleasepool { | 
 |     NSString *versionArg = nil; | 
 |     BOOL versionsOnly = NO; | 
 |     if (argc == 1) { | 
 |       versionArg = @""; | 
 |     } else if (argc == 2) { | 
 |       NSString *firstArg = [NSString stringWithUTF8String:argv[1]]; | 
 |       if ([@"-v" isEqualToString:firstArg]) { | 
 |         versionsOnly = YES; | 
 |         versionArg = @""; | 
 |       } else { | 
 |         versionArg = firstArg; | 
 |       } | 
 |     } | 
 |     if (versionArg == nil) { | 
 |       usage(stderr); | 
 |       return 1; | 
 |     } | 
 |  | 
 |     NSMutableDictionary<NSString *, XcodeVersionEntry *> *dict = FindXcodes(); | 
 |     if (dict == nil) { | 
 |       return 1; | 
 |     } | 
 |  | 
 |     XcodeVersionEntry *entry = [dict objectForKey:versionArg]; | 
 |     if (entry) { | 
 |       printf("%s\n", entry.url.fileSystemRepresentation); | 
 |       return 0; | 
 |     } | 
 |  | 
 |     if (versionsOnly) { | 
 |       DumpAsVersionsOnly(stdout, dict); | 
 |     } else { | 
 |       DumpAsJson(stdout, dict); | 
 |     } | 
 |     return ([@"" isEqualToString:versionArg] ? 0 : 1); | 
 |   } | 
 | } |