xcode-install 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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