blob: 20f72df37251327dd6f2990b241ad40a20fbd64b [file] [log] [blame]
Chris Parsonsf4888182016-01-08 00:42:14 +00001// Copyright 2015 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Application that finds all Xcodes installed on a given Mac and will return a path
16// for a given version number.
17// If you have 7.0, 6.4.1 and 6.3 installed the inputs will map to:
18// 7,7.0,7.0.0 = 7.0
19// 6,6.4,6.4.1 = 6.4.1
20// 6.3,6.3.0 = 6.3
21
22#if !defined(__has_feature) || !__has_feature(objc_arc)
23#error "This file requires ARC support."
24#endif
25
26#import <CoreServices/CoreServices.h>
27#import <Foundation/Foundation.h>
28
29// Simple data structure for tracking a version of Xcode (i.e. 6.4) with an URL to the
30// appplication.
31@interface XcodeVersionEntry : NSObject
32@property(readonly) NSString *version;
33@property(readonly) NSURL *url;
34@end
35
36@implementation XcodeVersionEntry
37
38- (id)initWithVersion:(NSString *)version url:(NSURL *)url {
39 if ((self = [super init])) {
40 _version = version;
41 _url = url;
42 }
43 return self;
44}
45
46- (id)description {
47 return [NSString stringWithFormat:@"<%@ %p>: %@ %@", [self class], self, _version, _url];
48}
49
50@end
51
52// Given an entry, insert it into a dictionary that is keyed by versions.
53// For an entry that is 6.4.1:/Applications/Xcode.app
Chris Parsons6e0715152017-01-03 19:08:09 +000054// Add it for 6.4.1, and optionally add it for 6.4 and 6 if it is "better" than any entry that may
55// already be there, where "better" is defined as:
56// 1. Under /Applications/. (This avoids mounted xcode versions taking precedence over installed
57// versions.)
58// 2. Not older (at least as high version number).
Chris Parsonsf4888182016-01-08 00:42:14 +000059static void AddEntryToDictionary(XcodeVersionEntry *entry, NSMutableDictionary *dict) {
Chris Parsons6e0715152017-01-03 19:08:09 +000060 BOOL inApplications = [entry.url.path rangeOfString:@"/Applications/"].location != NSNotFound;
Chris Parsonsf4888182016-01-08 00:42:14 +000061 NSString *entryVersion = entry.version;
62 NSString *subversion = entryVersion;
Chris Parsons6e0715152017-01-03 19:08:09 +000063 if (dict[entryVersion] && !inApplications) {
64 return;
65 }
Chris Parsonsf4888182016-01-08 00:42:14 +000066 dict[entryVersion] = entry;
67 while (YES) {
68 NSRange range = [subversion rangeOfString:@"." options:NSBackwardsSearch];
69 if (range.length == 0 || range.location == 0) {
70 break;
71 }
72 subversion = [subversion substringToIndex:range.location];
73 XcodeVersionEntry *subversionEntry = dict[subversion];
74 if (subversionEntry) {
Chris Parsons6e0715152017-01-03 19:08:09 +000075 BOOL atLeastAsLarge =
76 ([subversionEntry.version compare:entry.version] == NSOrderedDescending);
77 if (inApplications && atLeastAsLarge) {
Chris Parsonsf4888182016-01-08 00:42:14 +000078 dict[subversion] = entry;
79 }
80 } else {
81 dict[subversion] = entry;
82 }
83 }
84}
85
86// Given a "version", expand it to at least 3 components by adding .0 as necessary.
87static NSString *ExpandVersion(NSString *version) {
88 NSArray *components = [version componentsSeparatedByString:@"."];
89 NSString *appendage = nil;
90 if (components.count == 2) {
91 appendage = @".0";
92 } else if (components.count == 1) {
93 appendage = @".0.0";
94 }
95 if (appendage) {
96 version = [version stringByAppendingString:appendage];
97 }
98 return version;
99}
100
101int main(int argc, const char * argv[]) {
102 @autoreleasepool {
Chris Parsons197543a2016-05-27 18:17:00 +0000103 NSString *versionArg = nil;
Chris Parsons8bddd692016-06-08 18:17:51 +0000104 BOOL versionsOnly = NO;
Chris Parsonsf4888182016-01-08 00:42:14 +0000105 if (argc == 1) {
Chris Parsons197543a2016-05-27 18:17:00 +0000106 versionArg = @"";
Chris Parsonsf4888182016-01-08 00:42:14 +0000107 } else if (argc == 2) {
Chris Parsons8bddd692016-06-08 18:17:51 +0000108 NSString *firstArg = [NSString stringWithUTF8String:argv[1]];
109 if ([@"-v" isEqualToString:firstArg]) {
110 versionsOnly = YES;
111 versionArg = @"";
112 } else {
113 versionArg = firstArg;
114 NSCharacterSet *versSet =
115 [NSCharacterSet characterSetWithCharactersInString:@"0123456789."];
116 if ([versionArg rangeOfCharacterFromSet:versSet.invertedSet].length != 0) {
117 versionArg = nil;
118 }
Chris Parsonsf4888182016-01-08 00:42:14 +0000119 }
120 }
Chris Parsons197543a2016-05-27 18:17:00 +0000121 if (versionArg == nil) {
Chris Parsons8bddd692016-06-08 18:17:51 +0000122 printf("xcode_locator [-v|<version_number>]\n"
Chris Parsonsf4888182016-01-08 00:42:14 +0000123 "Given a version number, or partial version number in x.y.z format, will attempt "
Chris Parsonsc8424a62016-02-18 00:09:21 +0000124 "to return the path to the appropriate developer directory.\nOmitting a version "
Chris Parsons8bddd692016-06-08 18:17:51 +0000125 "number will list all available versions in JSON format, alongside their paths.\n"
126 "Passing -v will list all available fully-specified version numbers along with "
Chris Parsons32b3ef52016-06-23 22:26:02 +0000127 "their possible aliases and their developer directory, each on a new line.\n"
128 "For example: '7.3.1:7,7.3,7.3.1:/Applications/Xcode.app/Contents/Developer'.\n");
Chris Parsonsf4888182016-01-08 00:42:14 +0000129 return 1;
130 }
131
132 NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
133 CFErrorRef cfError;
134 NSArray *array = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier(
135 CFSTR("com.apple.dt.Xcode"), &cfError));
136 if (array == nil) {
137 NSError *nsError = (__bridge NSError *)cfError;
138 printf("error: %s\n", nsError.description.UTF8String);
139 return 1;
140 }
141 for (NSURL *url in array) {
142 NSBundle *bundle = [NSBundle bundleWithURL:url];
143 if (!bundle) {
144 printf("error: Unable to open bundle at URL: %s\n", url.description.UTF8String);
145 return 1;
146 }
147 NSString *version = bundle.infoDictionary[@"CFBundleShortVersionString"];
148 if (!version) {
149 printf("error: Unable to extract CFBundleShortVersionString from URL: %s\n",
150 url.description.UTF8String);
151 return 1;
152 }
153 version = ExpandVersion(version);
Chris Parsonsc8424a62016-02-18 00:09:21 +0000154 NSURL *developerDir = [url URLByAppendingPathComponent:@"Contents/Developer"];
155 XcodeVersionEntry *entry =
156 [[XcodeVersionEntry alloc] initWithVersion:version url:developerDir];
Chris Parsonsf4888182016-01-08 00:42:14 +0000157 AddEntryToDictionary(entry, dict);
158 }
159
Chris Parsons197543a2016-05-27 18:17:00 +0000160 XcodeVersionEntry *entry = [dict objectForKey:versionArg];
Chris Parsonsf4888182016-01-08 00:42:14 +0000161 if (entry) {
162 printf("%s\n", entry.url.fileSystemRepresentation);
163 return 0;
164 }
165
Chris Parsons8bddd692016-06-08 18:17:51 +0000166 if (versionsOnly) {
167 NSSet *distinctValues = [[NSSet alloc] initWithArray:[dict allValues]];
Chris Parsons32b3ef52016-06-23 22:26:02 +0000168 NSMutableDictionary *aliasDict = [[NSMutableDictionary alloc] init];
Chris Parsons8bddd692016-06-08 18:17:51 +0000169 for (XcodeVersionEntry *value in distinctValues) {
Chris Parsons32b3ef52016-06-23 22:26:02 +0000170 NSString *versionString = value.version;
171 if (aliasDict[versionString] == nil) {
172 aliasDict[versionString] = [[NSMutableSet alloc] init];
173 }
174 [aliasDict[versionString] addObjectsFromArray:[dict allKeysForObject:value]];
175 }
176 for (NSString *version in aliasDict) {
177 XcodeVersionEntry *entry = dict[version];
178 printf("%s:%s:%s\n",
179 version.UTF8String,
180 [[aliasDict[version] allObjects] componentsJoinedByString: @","].UTF8String,
181 entry.url.fileSystemRepresentation);
Chris Parsons8bddd692016-06-08 18:17:51 +0000182 }
183 } else {
184 // Print out list in json format.
185 printf("{\n");
186 for (NSString *version in dict) {
187 XcodeVersionEntry *entry = dict[version];
188 printf("\t\"%s\": \"%s\",\n", version.UTF8String, entry.url.fileSystemRepresentation);
189 }
190 printf("}\n");
Chris Parsonsf4888182016-01-08 00:42:14 +0000191 }
Chris Parsons197543a2016-05-27 18:17:00 +0000192 return ([@"" isEqualToString:versionArg] ? 0 : 1);
Chris Parsonsf4888182016-01-08 00:42:14 +0000193 }
194}