xcode-install 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +20 -0
- data/Gemfile +2 -2
- data/Rakefile +2 -2
- data/bin/xcode-install +5 -5
- data/lib/xcode/install/cleanup.rb +11 -11
- data/lib/xcode/install/command.rb +22 -22
- data/lib/xcode/install/install.rb +29 -29
- data/lib/xcode/install/installed.rb +12 -12
- data/lib/xcode/install/list.rb +22 -22
- data/lib/xcode/install/uninstall.rb +26 -26
- data/lib/xcode/install/update.rb +11 -11
- data/lib/xcode/install/version.rb +1 -1
- data/lib/xcode/install.rb +292 -277
- data/spec/fixtures/devcenter/xcode-20150601.html +212 -0
- data/spec/fixtures/devcenter/xcode-20150608.html +315 -0
- data/spec/fixtures/yolo.json +1 -1
- data/spec/install_spec.rb +19 -19
- data/spec/installed_spec.rb +7 -6
- data/spec/json_spec.rb +18 -18
- data/spec/prerelease_spec.rb +37 -30
- data/spec/spec_helper.rb +5 -5
- data/xcode-install.gemspec +13 -13
- metadata +7 -2
data/lib/xcode/install.rb
CHANGED
@@ -1,283 +1,298 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require 'fastlane_core'
|
2
|
+
require 'fastlane_core/developer_center/developer_center'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'rubygems/version'
|
5
|
+
require 'xcode/install/command'
|
6
|
+
require 'xcode/install/version'
|
7
|
+
|
8
|
+
module CredentialsManager
|
9
|
+
class PasswordManager
|
10
|
+
alias_method :old_ask_for_login, :ask_for_login
|
11
|
+
|
12
|
+
def ask_for_login
|
13
|
+
puts "\nXcodeInstall needs your developer AppleID credentials to access the DevCenter."
|
14
|
+
|
15
|
+
old_ask_for_login
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
7
19
|
|
8
20
|
module FastlaneCore
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
21
|
+
class DeveloperCenter
|
22
|
+
def cookies
|
23
|
+
cookie_string = ''
|
24
|
+
|
25
|
+
page.driver.cookies.each do |_key, cookie|
|
26
|
+
cookie_string << "#{cookie.name}=#{cookie.value};"
|
27
|
+
end
|
28
|
+
|
29
|
+
cookie_string
|
30
|
+
end
|
31
|
+
|
32
|
+
def download_seedlist
|
33
|
+
JSON.parse(page.evaluate_script("$.ajax({data: { start: \"0\", limit: \"1000\", " \
|
34
|
+
"sort: \"dateModified\", dir: \"DESC\", searchTextField: \"\", " \
|
35
|
+
"searchCategories: \"\", search: \"false\" } , type: 'GET', " \
|
36
|
+
"url: '/services-account/QH65B2/downloadws/listDownloads.action', " \
|
37
|
+
'async: false})')['responseText'])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Helper
|
42
|
+
def self.is_test?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
34
46
|
end
|
35
47
|
|
36
48
|
module XcodeInstall
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
49
|
+
class Curl
|
50
|
+
COOKIES_PATH = Pathname.new('/tmp/curl-cookies.txt')
|
51
|
+
|
52
|
+
def fetch(url, directory = nil, cookies = nil, output = nil)
|
53
|
+
options = cookies.nil? ? '' : "-b '#{cookies}' -c #{COOKIES_PATH}"
|
54
|
+
# options += ' -vvv'
|
55
|
+
|
56
|
+
uri = URI.parse(url)
|
57
|
+
output ||= File.basename(uri.path)
|
58
|
+
output = (Pathname.new(directory) + Pathname.new(output)) if directory
|
59
|
+
|
60
|
+
command = "curl #{options} -L -C - -# -o #{output} #{url}"
|
61
|
+
IO.popen(command).each do |fd|
|
62
|
+
puts(fd)
|
63
|
+
end
|
64
|
+
result = $CHILD_STATUS.to_i == 0
|
65
|
+
|
66
|
+
FileUtils.rm_f(COOKIES_PATH)
|
67
|
+
result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Installer
|
72
|
+
attr_reader :xcodes
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
76
|
+
end
|
77
|
+
|
78
|
+
def cache_dir
|
79
|
+
CACHE_DIR
|
80
|
+
end
|
81
|
+
|
82
|
+
def current_symlink
|
83
|
+
File.symlink?(SYMLINK_PATH) ? SYMLINK_PATH : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def download(version)
|
87
|
+
return unless exist?(version)
|
88
|
+
xcode = seedlist.find { |x| x.name == version }
|
89
|
+
dmg_file = Pathname.new(File.basename(xcode.path))
|
90
|
+
|
91
|
+
result = Curl.new.fetch(xcode.url, CACHE_DIR, devcenter.cookies, dmg_file)
|
92
|
+
result ? CACHE_DIR + dmg_file : nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def exist?(version)
|
96
|
+
list_versions.include?(version)
|
97
|
+
end
|
98
|
+
|
99
|
+
def installed?(version)
|
100
|
+
installed_versions.map(&:version).include?(version)
|
101
|
+
end
|
102
|
+
|
103
|
+
def installed_versions
|
104
|
+
@installed ||= installed.map { |x| InstalledXcode.new(x) }.sort do |a, b|
|
105
|
+
Gem::Version.new(a.version) <=> Gem::Version.new(b.version)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def install_dmg(dmgPath, suffix = '', switch = true, clean = true)
|
110
|
+
xcode_path = "/Applications/Xcode#{suffix}.app"
|
111
|
+
|
112
|
+
`hdiutil mount -nobrowse -noverify #{dmgPath}`
|
113
|
+
puts 'Please authenticate for Xcode installation...'
|
114
|
+
source = Dir.glob('/Volumes/Xcode/Xcode*.app').first
|
115
|
+
|
116
|
+
if source.nil?
|
117
|
+
puts 'No `Xcode.app` found in DMG.'
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
`sudo ditto "#{source}" "#{xcode_path}"`
|
122
|
+
`umount "/Volumes/Xcode"`
|
123
|
+
|
124
|
+
enable_developer_mode
|
125
|
+
`sudo xcodebuild -license` unless xcode_license_approved?
|
126
|
+
|
127
|
+
if switch
|
128
|
+
`sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
|
129
|
+
`sudo ln -sf #{xcode_path} #{SYMLINK_PATH}` unless SYMLINK_PATH.exist?
|
130
|
+
|
131
|
+
`sudo xcode-select --switch #{xcode_path}`
|
132
|
+
puts `xcodebuild -version`
|
133
|
+
end
|
134
|
+
|
135
|
+
FileUtils.rm_f(dmgPath) if clean
|
136
|
+
end
|
137
|
+
|
138
|
+
def install_version(version, switch = true, clean = true)
|
139
|
+
return if version.nil?
|
140
|
+
dmg_path = get_dmg(version)
|
141
|
+
fail Informative, "Failed to download Xcode #{version}." if dmg_path.nil?
|
142
|
+
|
143
|
+
install_dmg(dmg_path, "-#{version.split(' ')[0]}", switch, clean)
|
144
|
+
end
|
145
|
+
|
146
|
+
def list_current
|
147
|
+
majors = list_versions.map { |v| v.split('.')[0] }.map { |v| v.split(' ')[0] }
|
148
|
+
majors = majors.select { |v| v.length == 1 }.uniq
|
149
|
+
list_versions.select { |v| v.start_with?(majors.last) }.join("\n")
|
150
|
+
end
|
151
|
+
|
152
|
+
def list
|
153
|
+
list_versions.join("\n")
|
154
|
+
end
|
155
|
+
|
156
|
+
def rm_list_cache
|
157
|
+
FileUtils.rm_f(LIST_FILE)
|
158
|
+
end
|
159
|
+
|
160
|
+
def symlink(version)
|
161
|
+
xcode = installed_versions.find { |x| x.version == version }
|
162
|
+
`sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
|
163
|
+
`sudo ln -sf #{xcode.path} #{SYMLINK_PATH}` unless xcode.nil? || SYMLINK_PATH.exist?
|
164
|
+
end
|
165
|
+
|
166
|
+
def symlinks_to
|
167
|
+
File.absolute_path(File.readlink(current_symlink), SYMLINK_PATH.dirname) if current_symlink
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
CACHE_DIR = Pathname.new("#{ENV['HOME']}/Library/Caches/XcodeInstall")
|
173
|
+
LIST_FILE = CACHE_DIR + Pathname.new('xcodes.bin')
|
174
|
+
MINIMUM_VERSION = Gem::Version.new('4.3')
|
175
|
+
SYMLINK_PATH = Pathname.new('/Applications/Xcode.app')
|
176
|
+
|
177
|
+
def devcenter
|
178
|
+
@devcenter ||= FastlaneCore::DeveloperCenter.new
|
179
|
+
end
|
180
|
+
|
181
|
+
def enable_developer_mode
|
182
|
+
`sudo /usr/sbin/DevToolsSecurity -enable`
|
183
|
+
`sudo /usr/sbin/dseditgroup -o edit -t group -a staff _developer`
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_dmg(version)
|
187
|
+
if ENV.key?('XCODE_INSTALL_CACHE_DIR')
|
188
|
+
cache_path = Pathname.new(ENV['XCODE_INSTALL_CACHE_DIR']) + Pathname.new("xcode-#{version}.dmg")
|
189
|
+
return cache_path if cache_path.exist?
|
190
|
+
end
|
191
|
+
|
192
|
+
download(version)
|
193
|
+
end
|
194
|
+
|
195
|
+
def fetch_seedlist
|
196
|
+
@xcodes = parse_seedlist(devcenter.download_seedlist)
|
197
|
+
|
198
|
+
names = @xcodes.map(&:name)
|
199
|
+
@xcodes += prereleases.reject { |pre| names.include?(pre.name) }
|
200
|
+
|
201
|
+
File.open(LIST_FILE, 'w') do |f|
|
202
|
+
f << Marshal.dump(xcodes)
|
203
|
+
end
|
204
|
+
|
205
|
+
xcodes
|
206
|
+
end
|
207
|
+
|
208
|
+
def installed
|
209
|
+
`mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'" 2>/dev/null`.split("\n")
|
210
|
+
end
|
211
|
+
|
212
|
+
def parse_seedlist(seedlist)
|
213
|
+
seeds = seedlist['downloads'].select do |t|
|
214
|
+
/^Xcode [0-9]/.match(t['name'])
|
215
|
+
end
|
216
|
+
|
217
|
+
xcodes = seeds.map { |x| Xcode.new(x) }.reject { |x| x.version < MINIMUM_VERSION }.sort do |a, b|
|
218
|
+
a.date_modified <=> b.date_modified
|
219
|
+
end
|
220
|
+
|
221
|
+
xcodes.select { |x| x.url.end_with?('.dmg') }
|
222
|
+
end
|
223
|
+
|
224
|
+
def list_versions
|
225
|
+
installed = installed_versions.map(&:version)
|
226
|
+
seedlist.map(&:name).reject { |x| installed.include?(x) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def prereleases
|
230
|
+
page = Nokogiri::HTML.parse(devcenter.download_file('/xcode/downloads/'))
|
231
|
+
links = page.xpath('//a').select { |link| link['href'].end_with?('.dmg') }
|
232
|
+
|
233
|
+
links.map { |pre| Xcode.new_prelease(pre.text.strip.gsub(/.*Xcode /, ''), pre['href']) }
|
234
|
+
end
|
235
|
+
|
236
|
+
def seedlist
|
237
|
+
@xcodes = Marshal.load(File.read(LIST_FILE)) if LIST_FILE.exist? && xcodes.nil?
|
238
|
+
xcodes || fetch_seedlist
|
239
|
+
end
|
240
|
+
|
241
|
+
def xcode_license_approved?
|
242
|
+
!(`/usr/bin/xcrun clang 2>&1` =~ /license/ && !$CHILD_STATUS.success?)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class InstalledXcode
|
247
|
+
attr_reader :path
|
248
|
+
attr_reader :version
|
249
|
+
|
250
|
+
def initialize(path)
|
251
|
+
@path = Pathname.new(path)
|
252
|
+
@version = get_version(path)
|
253
|
+
end
|
254
|
+
|
255
|
+
:private
|
256
|
+
|
257
|
+
def get_version(xcode_path)
|
258
|
+
output = `DEVELOPER_DIR='' "#{xcode_path}/Contents/Developer/usr/bin/xcodebuild" -version`
|
259
|
+
output.split("\n").first.split(' ')[1]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class Xcode
|
264
|
+
attr_reader :date_modified
|
265
|
+
attr_reader :name
|
266
|
+
attr_reader :path
|
267
|
+
attr_reader :url
|
268
|
+
attr_reader :version
|
269
|
+
|
270
|
+
def initialize(json)
|
271
|
+
@date_modified = json['dateModified'].to_i
|
272
|
+
@name = json['name'].gsub(/^Xcode /, '')
|
273
|
+
@path = json['files'].first['remotePath']
|
274
|
+
@url = "https://developer.apple.com/devcenter/download.action?path=#{@path}"
|
275
|
+
|
276
|
+
begin
|
277
|
+
@version = Gem::Version.new(@name.split(' ')[0])
|
278
|
+
rescue
|
279
|
+
@version = Installer::MINIMUM_VERSION
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def to_s
|
284
|
+
"Xcode #{version} -- #{url}"
|
285
|
+
end
|
286
|
+
|
287
|
+
def ==(other)
|
288
|
+
date_modified == other.date_modified && name == other.name && path == other.path && \
|
289
|
+
url == other.url && version == other.version
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.new_prelease(version, url)
|
293
|
+
new('name' => version,
|
294
|
+
'dateModified' => Time.now.to_i,
|
295
|
+
'files' => [{ 'remotePath' => url.split('=').last }])
|
296
|
+
end
|
297
|
+
end
|
283
298
|
end
|