xcode-install 1.1.0 → 2.0.0
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/.gitignore +1 -0
- data/.rubocop.yml +11 -10
- data/.rubocop_todo.yml +34 -0
- data/.travis.yml +5 -1
- data/Gemfile +1 -0
- data/README.md +28 -15
- data/Rakefile +5 -1
- data/lib/xcode/install/cli.rb +1 -2
- data/lib/xcode/install/install.rb +7 -8
- data/lib/xcode/install/installed.rb +10 -1
- data/lib/xcode/install/select.rb +1 -1
- data/lib/xcode/install/simulators.rb +9 -8
- data/lib/xcode/install/uninstall.rb +5 -4
- data/lib/xcode/install/version.rb +1 -1
- data/lib/xcode/install.rb +116 -61
- data/spec/cli_spec.rb +1 -1
- data/spec/curl_spec.rb +26 -0
- data/spec/fixtures/devcenter/xcode-20160601.html +1872 -0
- data/spec/fixtures/hdiutil.plist +31 -0
- data/spec/fixtures/mail-verify.html +222 -0
- data/spec/install_spec.rb +41 -25
- data/spec/prerelease_spec.rb +7 -0
- data/spec/uninstall_spec.rb +12 -0
- data/xcode-install.gemspec +4 -2
- metadata +39 -24
- data/bin/xcode-install +0 -5
data/lib/xcode/install.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
require 'pathname'
|
|
3
|
+
require 'rexml/document'
|
|
3
4
|
require 'spaceship'
|
|
4
5
|
require 'json'
|
|
5
6
|
require 'rubygems/version'
|
|
@@ -12,19 +13,20 @@ module XcodeInstall
|
|
|
12
13
|
COOKIES_PATH = Pathname.new('/tmp/curl-cookies.txt')
|
|
13
14
|
|
|
14
15
|
def fetch(url, directory = nil, cookies = nil, output = nil, progress = true)
|
|
15
|
-
options = cookies.nil? ?
|
|
16
|
-
# options
|
|
16
|
+
options = cookies.nil? ? [] : ['--cookie', cookies, '--cookie-jar', COOKIES_PATH]
|
|
17
|
+
# options << ' -vvv'
|
|
17
18
|
|
|
18
19
|
uri = URI.parse(url)
|
|
19
20
|
output ||= File.basename(uri.path)
|
|
20
21
|
output = (Pathname.new(directory) + Pathname.new(output)) if directory
|
|
21
22
|
|
|
22
|
-
progress = progress ? '
|
|
23
|
-
command =
|
|
24
|
-
IO.popen(command)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
progress = progress ? '--progress-bar' : '--silent'
|
|
24
|
+
command = ['curl', *options, '--location', '--continue-at', '-', progress, '--output', output, url].map(&:to_s)
|
|
25
|
+
io = IO.popen(command)
|
|
26
|
+
io.each { |line| puts line }
|
|
27
|
+
io.close
|
|
28
|
+
|
|
29
|
+
result = $?.exitstatus == 0
|
|
28
30
|
|
|
29
31
|
FileUtils.rm_f(COOKIES_PATH)
|
|
30
32
|
result
|
|
@@ -47,11 +49,11 @@ module XcodeInstall
|
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def download(version, progress, url = nil)
|
|
50
|
-
return unless exist?(version)
|
|
51
|
-
xcode = seedlist.find { |x| x.name == version }
|
|
52
|
+
return unless url || exist?(version)
|
|
53
|
+
xcode = seedlist.find { |x| x.name == version } unless url
|
|
52
54
|
dmg_file = Pathname.new(File.basename(url || xcode.path))
|
|
53
55
|
|
|
54
|
-
result = Curl.new.fetch(url || xcode.url, CACHE_DIR, spaceship.cookie, dmg_file, progress)
|
|
56
|
+
result = Curl.new.fetch(url || xcode.url, CACHE_DIR, url ? nil : spaceship.cookie, dmg_file, progress)
|
|
55
57
|
result ? CACHE_DIR + dmg_file : nil
|
|
56
58
|
end
|
|
57
59
|
|
|
@@ -69,27 +71,34 @@ module XcodeInstall
|
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
def install_dmg(
|
|
74
|
+
def install_dmg(dmg_path, suffix = '', switch = true, clean = true)
|
|
75
|
+
prompt = "Please authenticate for Xcode installation.\nPassword: "
|
|
73
76
|
xcode_path = "/Applications/Xcode#{suffix}.app"
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
if dmg_path.extname == '.xip'
|
|
79
|
+
`xar -x -f #{dmg_path} --exclude 'Metadata'`
|
|
80
|
+
`sudo -p "#{prompt}" ditto -x Content /Applications`
|
|
81
|
+
`sudo -p "#{prompt}" mv /Applications/Xcode-beta.app "#{xcode_path}"`
|
|
82
|
+
FileUtils.rm_f('Content')
|
|
83
|
+
else
|
|
84
|
+
mount_dir = mount(dmg_path)
|
|
85
|
+
source = Dir.glob(File.join(mount_dir, 'Xcode*.app')).first
|
|
86
|
+
|
|
87
|
+
if source.nil?
|
|
88
|
+
out = <<-HELP
|
|
89
|
+
No `Xcode.app` found in DMG. Please remove #{dmg_path} if you suspect a corrupted
|
|
82
90
|
download or run `xcversion update` to see if the version you tried to install
|
|
83
91
|
has been pulled by Apple. If none of this is true, please open a new GH issue.
|
|
84
92
|
HELP
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
$stderr.puts out.tr("\n", ' ')
|
|
94
|
+
return
|
|
95
|
+
end
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
`sudo -p "#{prompt}" ditto "#{source}" "#{xcode_path}"`
|
|
98
|
+
`umount "/Volumes/Xcode"`
|
|
99
|
+
end
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
unless verify_integrity(xcode_path)
|
|
93
102
|
`sudo rm -f #{xcode_path}`
|
|
94
103
|
return
|
|
95
104
|
end
|
|
@@ -107,7 +116,7 @@ HELP
|
|
|
107
116
|
puts `xcodebuild -version`
|
|
108
117
|
end
|
|
109
118
|
|
|
110
|
-
FileUtils.rm_f(
|
|
119
|
+
FileUtils.rm_f(dmg_path) if clean
|
|
111
120
|
end
|
|
112
121
|
|
|
113
122
|
def install_version(version, switch = true, clean = true, install = true, progress = true, url = nil)
|
|
@@ -116,7 +125,7 @@ HELP
|
|
|
116
125
|
|
|
117
126
|
install_dmg(dmg_path, "-#{version.split(' ')[0]}", switch, clean) if install
|
|
118
127
|
|
|
119
|
-
open_release_notes_url(version)
|
|
128
|
+
open_release_notes_url(version) unless url
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
def open_release_notes_url(version)
|
|
@@ -125,14 +134,20 @@ HELP
|
|
|
125
134
|
`open #{xcode.release_notes_url}` unless xcode.nil? || xcode.release_notes_url.nil?
|
|
126
135
|
end
|
|
127
136
|
|
|
137
|
+
def list_annotated(xcodes_list)
|
|
138
|
+
installed = installed_versions.map(&:version)
|
|
139
|
+
xcodes_list.map { |x| installed.include?(x) ? "#{x} (installed)" : x }.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
128
142
|
def list_current
|
|
129
143
|
stable_majors = list_versions.reject { |v| /beta/i =~ v }.map { |v| v.split('.')[0] }.map { |v| v.split(' ')[0] }
|
|
130
144
|
latest_stable_major = stable_majors.select { |v| v.length == 1 }.uniq.sort.last.to_i
|
|
131
|
-
list_versions.select { |v| v.split('.')[0].to_i >= latest_stable_major }.sort
|
|
145
|
+
current_versions = list_versions.select { |v| v.split('.')[0].to_i >= latest_stable_major }.sort
|
|
146
|
+
list_annotated(current_versions)
|
|
132
147
|
end
|
|
133
148
|
|
|
134
149
|
def list
|
|
135
|
-
list_versions.
|
|
150
|
+
list_annotated(list_versions.sort)
|
|
136
151
|
end
|
|
137
152
|
|
|
138
153
|
def rm_list_cache
|
|
@@ -149,12 +164,20 @@ HELP
|
|
|
149
164
|
File.absolute_path(File.readlink(current_symlink), SYMLINK_PATH.dirname) if current_symlink
|
|
150
165
|
end
|
|
151
166
|
|
|
167
|
+
def mount(dmg_path)
|
|
168
|
+
plist = hdiutil('mount', '-plist', '-nobrowse', '-noverify', dmg_path.to_s)
|
|
169
|
+
document = REXML::Document.new(plist)
|
|
170
|
+
node = REXML::XPath.first(document, "//key[.='mount-point']/following-sibling::*[1]")
|
|
171
|
+
fail Informative, 'Failed to mount image.' unless node
|
|
172
|
+
node.text
|
|
173
|
+
end
|
|
174
|
+
|
|
152
175
|
private
|
|
153
176
|
|
|
154
177
|
def spaceship
|
|
155
178
|
@spaceship ||= begin
|
|
156
179
|
begin
|
|
157
|
-
Spaceship.login(ENV[
|
|
180
|
+
Spaceship.login(ENV['XCODE_INSTALL_USER'], ENV['XCODE_INSTALL_PASSWORD'])
|
|
158
181
|
rescue Spaceship::Client::InvalidUserCredentialsError
|
|
159
182
|
$stderr.puts 'The specified Apple developer account credentials are incorrect.'
|
|
160
183
|
exit(1)
|
|
@@ -166,8 +189,8 @@ HELP
|
|
|
166
189
|
exit(1)
|
|
167
190
|
end
|
|
168
191
|
|
|
169
|
-
if ENV.key?(
|
|
170
|
-
Spaceship.client.team_id = ENV[
|
|
192
|
+
if ENV.key?('XCODE_INSTALL_TEAM_ID')
|
|
193
|
+
Spaceship.client.team_id = ENV['XCODE_INSTALL_TEAM_ID']
|
|
171
194
|
end
|
|
172
195
|
Spaceship.client
|
|
173
196
|
end
|
|
@@ -196,15 +219,15 @@ HELP
|
|
|
196
219
|
end
|
|
197
220
|
|
|
198
221
|
def fetch_seedlist
|
|
199
|
-
@xcodes = parse_seedlist(spaceship.send(:request, :get,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
@xcodes = parse_seedlist(spaceship.send(:request, :get,
|
|
223
|
+
'/services-account/QH65B2/downloadws/listDownloads.action',
|
|
224
|
+
start: '0',
|
|
225
|
+
limit: '1000',
|
|
226
|
+
sort: 'dateModified',
|
|
227
|
+
dir: 'DESC',
|
|
228
|
+
searchTextField: '',
|
|
229
|
+
searchCategories: '',
|
|
230
|
+
search: 'false').body)
|
|
208
231
|
|
|
209
232
|
names = @xcodes.map(&:name)
|
|
210
233
|
@xcodes += prereleases.reject { |pre| names.include?(pre.name) }
|
|
@@ -238,23 +261,32 @@ HELP
|
|
|
238
261
|
end
|
|
239
262
|
|
|
240
263
|
def list_versions
|
|
241
|
-
|
|
242
|
-
seedlist.map(&:name).reject { |x| installed.include?(x) }
|
|
264
|
+
seedlist.map(&:name)
|
|
243
265
|
end
|
|
244
266
|
|
|
245
267
|
def prereleases
|
|
246
|
-
body=spaceship.send(:request, :get, '/
|
|
247
|
-
|
|
268
|
+
body = spaceship.send(:request, :get, '/download/').body
|
|
269
|
+
|
|
270
|
+
links = body.scan(%r{<a.+?href="(.+?.dmg)".*>(.*)</a>})
|
|
248
271
|
links = links.map do |link|
|
|
249
|
-
parent = link[0].scan(
|
|
272
|
+
parent = link[0].scan(%r{path=(/.*/.*/)}).first.first
|
|
250
273
|
match = body.scan(/#{Regexp.quote(parent)}(.+?.pdf)/).first
|
|
251
274
|
if match
|
|
252
|
-
link
|
|
275
|
+
link + [parent + match.first]
|
|
253
276
|
else
|
|
254
|
-
link
|
|
277
|
+
link + [nil]
|
|
255
278
|
end
|
|
256
279
|
end
|
|
257
|
-
links.map { |pre| Xcode.new_prerelease(pre[1].strip.gsub(/.*Xcode /, ''), pre[0], pre[2]) }
|
|
280
|
+
links = links.map { |pre| Xcode.new_prerelease(pre[1].strip.gsub(/.*Xcode /, ''), pre[0], pre[2]) }
|
|
281
|
+
|
|
282
|
+
if links.count == 0
|
|
283
|
+
version = body.scan(/Xcode.* beta/).last.sub(/<.*?>/, '').gsub(/.*Xcode /, '')
|
|
284
|
+
link = body.scan(%r{<button .*"(.+?.xip)".*</button>}).first.first
|
|
285
|
+
notes = body.scan(%r{<a.+?href="(/go/\?id=xcode-.+?)".*>(.*)</a>}).first.first
|
|
286
|
+
links << Xcode.new(version, link, notes)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
links
|
|
258
290
|
end
|
|
259
291
|
|
|
260
292
|
def seedlist
|
|
@@ -266,6 +298,20 @@ HELP
|
|
|
266
298
|
puts `/usr/sbin/spctl --assess --verbose=4 --type execute #{path}`
|
|
267
299
|
$?.exitstatus == 0
|
|
268
300
|
end
|
|
301
|
+
|
|
302
|
+
def hdiutil(*args)
|
|
303
|
+
io = IO.popen(['hdiutil', *args])
|
|
304
|
+
result = io.read
|
|
305
|
+
io.close
|
|
306
|
+
unless $?.exitstatus == 0
|
|
307
|
+
file_path = args[-1]
|
|
308
|
+
if `file -b #{file_path}`.start_with?('HTML')
|
|
309
|
+
fail Informative, "Failed to mount #{file_path}, logging into your account from a browser should tell you what is going wrong."
|
|
310
|
+
end
|
|
311
|
+
fail Informative, 'Failed to invoke hdiutil.'
|
|
312
|
+
end
|
|
313
|
+
result
|
|
314
|
+
end
|
|
269
315
|
end
|
|
270
316
|
|
|
271
317
|
class Simulator
|
|
@@ -327,7 +373,7 @@ HELP
|
|
|
327
373
|
|
|
328
374
|
def prepare_package
|
|
329
375
|
puts 'Mounting DMG'
|
|
330
|
-
mount_location =
|
|
376
|
+
mount_location = Installer.new.mount(dmg_path)
|
|
331
377
|
puts 'Expanding pkg'
|
|
332
378
|
expanded_pkg_path = CACHE_DIR + identifier
|
|
333
379
|
FileUtils.rm_rf(expanded_pkg_path)
|
|
@@ -382,7 +428,7 @@ HELP
|
|
|
382
428
|
end
|
|
383
429
|
|
|
384
430
|
def version
|
|
385
|
-
@version ||=
|
|
431
|
+
@version ||= fetch_version
|
|
386
432
|
end
|
|
387
433
|
|
|
388
434
|
def bundle_version
|
|
@@ -413,7 +459,9 @@ HELP
|
|
|
413
459
|
end
|
|
414
460
|
|
|
415
461
|
def install_components
|
|
416
|
-
|
|
462
|
+
Dir.glob("#{@path}/Contents/Resources/Packages/*.pkg").each do |pkg|
|
|
463
|
+
`sudo installer -pkg #{pkg} -target /`
|
|
464
|
+
end
|
|
417
465
|
osx_build_version = `sw_vers -buildVersion`.chomp
|
|
418
466
|
tools_version = `/usr/libexec/PlistBuddy -c "Print :ProductBuildVersion" "#{@path}/Contents/version.plist"`.chomp
|
|
419
467
|
cache_dir = `getconf DARWIN_USER_CACHE_DIR`.chomp
|
|
@@ -426,7 +474,7 @@ HELP
|
|
|
426
474
|
`/usr/libexec/PlistBuddy -c "Print :#{keypath}" "#{path}/Contents/Info.plist"`.chomp
|
|
427
475
|
end
|
|
428
476
|
|
|
429
|
-
def
|
|
477
|
+
def fetch_version
|
|
430
478
|
output = `DEVELOPER_DIR='' "#{@path}/Contents/Developer/usr/bin/xcodebuild" -version`
|
|
431
479
|
return '0.0' if output.nil? # ¯\_(ツ)_/¯
|
|
432
480
|
output.split("\n").first.split(' ')[1]
|
|
@@ -441,13 +489,21 @@ HELP
|
|
|
441
489
|
attr_reader :version
|
|
442
490
|
attr_reader :release_notes_url
|
|
443
491
|
|
|
444
|
-
def initialize(json)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
492
|
+
def initialize(json, url = nil, release_notes_url = nil)
|
|
493
|
+
if url.nil?
|
|
494
|
+
@date_modified = json['dateModified'].to_i
|
|
495
|
+
@name = json['name'].gsub(/^Xcode /, '')
|
|
496
|
+
@path = json['files'].first['remotePath']
|
|
497
|
+
url_prefix = 'https://developer.apple.com/devcenter/download.action?path='
|
|
498
|
+
@url = "#{url_prefix}#{@path}"
|
|
499
|
+
@release_notes_url = "#{url_prefix}#{json['release_notes_path']}" if json['release_notes_path']
|
|
500
|
+
else
|
|
501
|
+
@name = json
|
|
502
|
+
@path = url.split('/').last
|
|
503
|
+
url_prefix = 'https://developer.apple.com/'
|
|
504
|
+
@url = "#{url_prefix}#{url}"
|
|
505
|
+
@release_notes_url = "#{url_prefix}#{release_notes_url}"
|
|
506
|
+
end
|
|
451
507
|
|
|
452
508
|
begin
|
|
453
509
|
@version = Gem::Version.new(@name.split(' ')[0])
|
|
@@ -467,7 +523,6 @@ HELP
|
|
|
467
523
|
|
|
468
524
|
def self.new_prerelease(version, url, release_notes_path)
|
|
469
525
|
new('name' => version,
|
|
470
|
-
'dateModified' => Time.now.to_i,
|
|
471
526
|
'files' => [{ 'remotePath' => url.split('=').last }],
|
|
472
527
|
'release_notes_path' => release_notes_path)
|
|
473
528
|
end
|
data/spec/cli_spec.rb
CHANGED
|
@@ -4,7 +4,7 @@ module XcodeInstall
|
|
|
4
4
|
describe Command::InstallCLITools do
|
|
5
5
|
it 'fails if tools are already installed' do
|
|
6
6
|
Command::InstallCLITools.any_instance.expects(:installed?).returns(true)
|
|
7
|
-
|
|
7
|
+
-> { Command::InstallCLITools.run }.should.raise(SystemExit)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
it 'runs if tools are not installed' do
|
data/spec/curl_spec.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
module XcodeInstall
|
|
4
|
+
def self.silence_stderr
|
|
5
|
+
begin
|
|
6
|
+
orig_stderr = $stderr.clone
|
|
7
|
+
$stderr.reopen File.new('/dev/null', 'w')
|
|
8
|
+
retval = yield
|
|
9
|
+
ensure
|
|
10
|
+
$stderr.reopen orig_stderr
|
|
11
|
+
end
|
|
12
|
+
retval
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe Curl do
|
|
16
|
+
it 'reports failure' do
|
|
17
|
+
`true`
|
|
18
|
+
curl = XcodeInstall::Curl.new
|
|
19
|
+
result = nil
|
|
20
|
+
XcodeInstall.silence_stderr do
|
|
21
|
+
result = curl.fetch('http://0.0.0.0/test')
|
|
22
|
+
end
|
|
23
|
+
result.should == false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|