blob: 20f72df37251327dd6f2990b241ad40a20fbd64b [file] [log] [blame]
// 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 *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;
}
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) {
printf("xcode_locator [-v|<version_number>]\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.\nOmitting a version "
"number will list all available versions in JSON format, alongside their paths.\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.\n"
"For example: '7.3.1:7,7.3,7.3.1:/Applications/Xcode.app/Contents/Developer'.\n");
return 1;
}
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
CFErrorRef cfError;
NSArray *array = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier(
CFSTR("com.apple.dt.Xcode"), &cfError));
if (array == nil) {
NSError *nsError = (__bridge NSError *)cfError;
printf("error: %s\n", nsError.description.UTF8String);
return 1;
}
for (NSURL *url in array) {
NSBundle *bundle = [NSBundle bundleWithURL:url];
if (!bundle) {
printf("error: Unable to open bundle at URL: %s\n", url.description.UTF8String);
return 1;
}
NSString *version = bundle.infoDictionary[@"CFBundleShortVersionString"];
if (!version) {
printf("error: Unable to extract CFBundleShortVersionString from URL: %s\n",
url.description.UTF8String);
return 1;
}
version = ExpandVersion(version);
NSURL *developerDir = [url URLByAppendingPathComponent:@"Contents/Developer"];
XcodeVersionEntry *entry =
[[XcodeVersionEntry alloc] initWithVersion:version url:developerDir];
AddEntryToDictionary(entry, dict);
}
XcodeVersionEntry *entry = [dict objectForKey:versionArg];
if (entry) {
printf("%s\n", entry.url.fileSystemRepresentation);
return 0;
}
if (versionsOnly) {
NSSet *distinctValues = [[NSSet alloc] initWithArray:[dict allValues]];
NSMutableDictionary *aliasDict = [[NSMutableDictionary alloc] init];
for (XcodeVersionEntry *value in distinctValues) {
NSString *versionString = value.version;
if (aliasDict[versionString] == nil) {
aliasDict[versionString] = [[NSMutableSet alloc] init];
}
[aliasDict[versionString] addObjectsFromArray:[dict allKeysForObject:value]];
}
for (NSString *version in aliasDict) {
XcodeVersionEntry *entry = dict[version];
printf("%s:%s:%s\n",
version.UTF8String,
[[aliasDict[version] allObjects] componentsJoinedByString: @","].UTF8String,
entry.url.fileSystemRepresentation);
}
} else {
// Print out list in json format.
printf("{\n");
for (NSString *version in dict) {
XcodeVersionEntry *entry = dict[version];
printf("\t\"%s\": \"%s\",\n", version.UTF8String, entry.url.fileSystemRepresentation);
}
printf("}\n");
}
return ([@"" isEqualToString:versionArg] ? 0 : 1);
}
}