// 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 #import // Simple data structure for tracking a version of Xcode (i.e. 6.4) with an URL // to the appplication. @interface XcodeVersionEntry : NSObject @property(readonly) NSString *version; @property(readonly) NSURL *url; @end @implementation XcodeVersionEntry - (id)initWithVersion:(NSString *)version url:(NSURL *)url { if ((self = [super init])) { _version = version; _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 *dict) { BOOL inApplications = [entry.url.path rangeOfString:@"/Applications/"].location != NSNotFound; NSString *entryVersion = entry.version; NSString *subversion = entryVersion; if (dict[entryVersion] && !inApplications) { return; } dict[entryVersion] = 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] == 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 *FindXcodes() __attribute((ns_returns_retained)) { CFStringRef bundleID = CFSTR("com.apple.dt.Xcode"); NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; CFErrorRef cfError; NSArray *array = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier( bundleID, &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; } 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 *developerDir = [url URLByAppendingPathComponent:@"Contents/Developer"]; XcodeVersionEntry *entry = [[XcodeVersionEntry alloc] initWithVersion:expandedVersion 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 *dict) { NSSet *distinctValues = [[NSSet alloc] initWithArray:dict.allValues]; NSMutableDictionary *> *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 *dict) { fprintf(output, "{\n"); for (NSString *version in dict) { XcodeVersionEntry *entry = dict[version]; fprintf(output, "\t\"%s\": \"%s\",\n", version.UTF8String, entry.url.fileSystemRepresentation); } fprintf(output, "}\n"); } // Dumps usage information. static void usage(FILE *output) { fprintf( output, "xcode-locator [-v|]" "\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; NSCharacterSet *versSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789."]; if ([versionArg rangeOfCharacterFromSet:versSet.invertedSet].length != 0) { versionArg = nil; } } } if (versionArg == nil) { usage(stderr); return 1; } NSMutableDictionary *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); } }