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.
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? ? '' : "-b '#{cookies}' -c #{COOKIES_PATH}"
16
- # options += ' -vvv'
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 ? '-#' : '-s'
23
- command = "curl #{options} -L -C - #{progress} -o #{output} #{url}"
24
- IO.popen(command).each do |fd|
25
- puts(fd)
26
- end
27
- result = $CHILD_STATUS.to_i == 0
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) || url
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(dmgPath, suffix = '', switch = true, clean = true)
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
- `hdiutil mount -nobrowse -noverify #{dmgPath}`
76
- puts 'Please authenticate for Xcode installation...'
77
- source = Dir.glob('/Volumes/Xcode/Xcode*.app').first
78
-
79
- if source.nil?
80
- out <<-HELP
81
- No `Xcode.app` found in DMG. Please remove #{dmgPath} if you suspect a corrupted
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
- $stderr.puts out.gsub("\n", ' ')
86
- return
87
- end
93
+ $stderr.puts out.tr("\n", ' ')
94
+ return
95
+ end
88
96
 
89
- `sudo ditto "#{source}" "#{xcode_path}"`
90
- `umount "/Volumes/Xcode"`
97
+ `sudo -p "#{prompt}" ditto "#{source}" "#{xcode_path}"`
98
+ `umount "/Volumes/Xcode"`
99
+ end
91
100
 
92
- if not verify_integrity(xcode_path)
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(dmgPath) if clean
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.join("\n")
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.join("\n")
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["XCODE_INSTALL_USER"], ENV["XCODE_INSTALL_PASSWORD"])
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?("XCODE_INSTALL_TEAM_ID")
170
- Spaceship.client.team_id = ENV["XCODE_INSTALL_TEAM_ID"]
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, '/services-account/QH65B2/downloadws/listDownloads.action', {
200
- start: "0",
201
- limit: "1000",
202
- sort: "dateModified",
203
- dir: "DESC",
204
- searchTextField: "",
205
- searchCategories: "",
206
- search: "false",
207
- }).body)
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
- installed = installed_versions.map(&:version)
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, '/xcode/download/').body
247
- links=body.scan(/<a.+?href="(.+?.dmg)".*>(.*)<\/a>/)
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(/path=(\/.*\/.*\/)/).first.first
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 += [parent + match.first]
275
+ link + [parent + match.first]
253
276
  else
254
- link += [nil]
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 = `hdiutil mount -nobrowse -noverify #{dmg_path}`.scan(/\/Volumes.*\n/).first.chomp
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 ||= get_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
- `sudo installer -pkg #{@path}/Contents/Resources/Packages/MobileDevice.pkg -target /`
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 get_version
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
- @date_modified = json['dateModified'].to_i
446
- @name = json['name'].gsub(/^Xcode /, '')
447
- @path = json['files'].first['remotePath']
448
- url_prefix = 'https://developer.apple.com/devcenter/download.action?path='
449
- @url = "#{url_prefix}#{@path}"
450
- @release_notes_url = "#{url_prefix}#{json['release_notes_path']}" if json['release_notes_path']
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
- lambda { Command::InstallCLITools.run }.should.raise(SystemExit)
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