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