| // 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 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<NSString *, XcodeVersionEntry *> *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<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 *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<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"); | |
| 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|<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; | |
| NSCharacterSet *versSet = | |
| [NSCharacterSet characterSetWithCharactersInString:@"0123456789."]; | |
| if ([versionArg rangeOfCharacterFromSet:versSet.invertedSet].length | |
| != 0) { | |
| versionArg = nil; | |
| } | |
| } | |
| } | |
| 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); | |
| } | |
| } |