xcode-install-citrus 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +33 -0
  3. data/.gitattributes +1 -0
  4. data/.gitignore +16 -0
  5. data/.rubocop.yml +21 -0
  6. data/.rubocop_todo.yml +78 -0
  7. data/Gemfile +12 -0
  8. data/LICENSE +22 -0
  9. data/README.md +198 -0
  10. data/Rakefile +16 -0
  11. data/bin/xcversion +12 -0
  12. data/bin//360/237/216/211 +3 -0
  13. data/lib/xcode/install.rb +761 -0
  14. data/lib/xcode/install/cleanup.rb +14 -0
  15. data/lib/xcode/install/cli.rb +30 -0
  16. data/lib/xcode/install/command.rb +32 -0
  17. data/lib/xcode/install/install.rb +55 -0
  18. data/lib/xcode/install/installed.rb +24 -0
  19. data/lib/xcode/install/list.rb +17 -0
  20. data/lib/xcode/install/select.rb +36 -0
  21. data/lib/xcode/install/selected.rb +12 -0
  22. data/lib/xcode/install/simulators.rb +65 -0
  23. data/lib/xcode/install/uninstall.rb +36 -0
  24. data/lib/xcode/install/update.rb +14 -0
  25. data/lib/xcode/install/version.rb +3 -0
  26. data/spec/cli_spec.rb +16 -0
  27. data/spec/curl_spec.rb +26 -0
  28. data/spec/fixtures/devcenter/xcode-20150414.html +263 -0
  29. data/spec/fixtures/devcenter/xcode-20150427.html +263 -0
  30. data/spec/fixtures/devcenter/xcode-20150508.html +279 -0
  31. data/spec/fixtures/devcenter/xcode-20150601.html +212 -0
  32. data/spec/fixtures/devcenter/xcode-20150608.html +315 -0
  33. data/spec/fixtures/devcenter/xcode-20150624.html +318 -0
  34. data/spec/fixtures/devcenter/xcode-20150909.html +309 -0
  35. data/spec/fixtures/devcenter/xcode-20160601.html +1872 -0
  36. data/spec/fixtures/devcenter/xcode-20160705-alt.html +317 -0
  37. data/spec/fixtures/devcenter/xcode-20160705.html +1909 -0
  38. data/spec/fixtures/devcenter/xcode-20160922.html +878 -0
  39. data/spec/fixtures/devcenter/xcode-20161024.html +663 -0
  40. data/spec/fixtures/hdiutil.plist +31 -0
  41. data/spec/fixtures/mail-verify.html +222 -0
  42. data/spec/fixtures/not_registered_as_developer.json +11 -0
  43. data/spec/fixtures/xcode.json +1 -0
  44. data/spec/fixtures/xcode_63.json +46 -0
  45. data/spec/fixtures/yolo.json +1 -0
  46. data/spec/install_spec.rb +62 -0
  47. data/spec/installed_spec.rb +19 -0
  48. data/spec/installer_spec.rb +101 -0
  49. data/spec/json_spec.rb +32 -0
  50. data/spec/list_spec.rb +57 -0
  51. data/spec/prerelease_spec.rb +108 -0
  52. data/spec/spec_helper.rb +16 -0
  53. data/spec/uninstall_spec.rb +12 -0
  54. data/xcode-install.gemspec +32 -0
  55. metadata +195 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: '08bb5c58f02c6b8197158cc57fd841962a590bf2'
4
+ data.tar.gz: e262e9cad8c871954b51c50be708cdf13e646469
5
+ SHA512:
6
+ metadata.gz: 8c55e695859a7ad22b18facead4a81301d11f319b11a8e039f597d6643a971ed8f88b91e81fdf2a3141b5d953b47d9c164e87911c05261521573f56450571b7d
7
+ data.tar.gz: 93a3db0026b0fde53a6b97b48a4f4c8f4effc5f94d3b80bb0172674108a29f7ebd88be638b0bcb5f70822846c3dce851eda1ea2fc800a322d3554c23397ec405
@@ -0,0 +1,33 @@
1
+ version: 2
2
+
3
+ jobs:
4
+ build:
5
+ macos:
6
+ xcode: "9.0"
7
+ working_directory: ~/xcode-install
8
+ shell: /bin/bash --login -eo pipefail
9
+ steps:
10
+ - checkout
11
+
12
+ # See Also: https://discuss.circleci.com/t/circleci-2-0-ios-error-installing-gems/23291/4
13
+ - run:
14
+ name: Set Ruby Version
15
+ command: echo "ruby-2.4" > ~/.ruby-version
16
+
17
+ - run:
18
+ name: Install ruby dependencies
19
+ command: bundle install
20
+
21
+ - run:
22
+ name: Run test
23
+ command: bundle exec rake spec
24
+
25
+ - run:
26
+ name: Run lint
27
+ command: bundle exec rake rubocop
28
+
29
+ workflows:
30
+ version: 2
31
+ build_and_test:
32
+ jobs:
33
+ - build
data/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ spec/fixtures/* linguist-documentation
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .DS_Store
16
+ test
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ Metrics/LineLength:
4
+ Max: 215
5
+
6
+ Style/AsciiComments:
7
+ Enabled: false
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+
12
+ Style/FileName:
13
+ Exclude:
14
+ - bin/🎉
15
+ - bin/xcode-install
16
+
17
+ Style/SpecialGlobalVars:
18
+ Enabled: false
19
+
20
+ Lint/Void:
21
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,78 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-08-24 11:09:20 +0200 using RuboCop version 0.49.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 3
10
+ # Cop supports --auto-correct.
11
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
12
+ # SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
13
+ Layout/IndentHeredoc:
14
+ Exclude:
15
+ - 'lib/xcode/install.rb'
16
+
17
+ # Offense count: 12
18
+ Metrics/AbcSize:
19
+ Max: 44
20
+
21
+ # Offense count: 4
22
+ # Configuration parameters: CountComments, ExcludedMethods.
23
+ Metrics/BlockLength:
24
+ Max: 76
25
+
26
+ # Offense count: 1
27
+ # Configuration parameters: CountComments.
28
+ Metrics/ClassLength:
29
+ Max: 246
30
+
31
+ # Offense count: 3
32
+ Metrics/CyclomaticComplexity:
33
+ Max: 10
34
+
35
+ # Offense count: 11
36
+ # Configuration parameters: CountComments.
37
+ Metrics/MethodLength:
38
+ Max: 51
39
+
40
+ # Offense count: 1
41
+ # Configuration parameters: CountKeywordArgs.
42
+ Metrics/ParameterLists:
43
+ Max: 7
44
+
45
+ # Offense count: 3
46
+ Metrics/PerceivedComplexity:
47
+ Max: 12
48
+
49
+ # Offense count: 1
50
+ # Cop supports --auto-correct.
51
+ Performance/CompareWithBlock:
52
+ Exclude:
53
+ - 'lib/xcode/install.rb'
54
+
55
+ # Offense count: 1
56
+ # Cop supports --auto-correct.
57
+ # Configuration parameters: IncludeActiveSupportAliases.
58
+ Performance/DoubleStartEndWith:
59
+ Exclude:
60
+ - 'lib/xcode/install.rb'
61
+
62
+ # Offense count: 1
63
+ Security/MarshalLoad:
64
+ Exclude:
65
+ - 'lib/xcode/install.rb'
66
+
67
+ # Offense count: 15
68
+ # Cop supports --auto-correct.
69
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
70
+ # SupportedStyles: only_raise, only_fail, semantic
71
+ Style/SignalException:
72
+ Exclude:
73
+ - 'lib/xcode/install.rb'
74
+ - 'lib/xcode/install/cli.rb'
75
+ - 'lib/xcode/install/install.rb'
76
+ - 'lib/xcode/install/select.rb'
77
+ - 'lib/xcode/install/simulators.rb'
78
+ - 'lib/xcode/install/uninstall.rb'
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'bacon'
7
+ gem 'coveralls', require: false
8
+ gem 'mocha', '~> 0.11.4'
9
+ gem 'mocha-on-bacon'
10
+ gem 'prettybacon'
11
+ gem 'rubocop', '~> 0.49.1', require: false
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Boris Bügling
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Xcode::Install
2
+
3
+ [![Gem Version](http://img.shields.io/gem/v/xcode-install.svg?style=flat)](http://badge.fury.io/rb/xcode-install) [![CircleCI](https://circleci.com/gh/xcpretty/xcode-install.svg?style=svg)](https://circleci.com/gh/xcpretty/xcode-install)
4
+
5
+ Install and update your Xcodes automatically.
6
+
7
+ ```
8
+ $ gem install xcode-install
9
+ $ xcversion install 6.3
10
+ ```
11
+
12
+ This tool uses the [Downloads for Apple Developer](https://developer.apple.com/download/more/) page.
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ $ gem install xcode-install
18
+ ```
19
+
20
+ Note: unfortunately, XcodeInstall has a transitive dependency on a gem with native extensions and this is not really fixable at this point in time. If you are installing this on a machine without a working compiler, please use these alternative instructions instead:
21
+
22
+ ```
23
+ $ curl -sL -O https://github.com/neonichu/ruby-domain_name/releases/download/v0.5.99999999/domain_name-0.5.99999999.gem
24
+ $ gem install domain_name-0.5.99999999.gem
25
+ $ gem install --conservative xcode-install
26
+ $ rm -f domain_name-0.5.99999999.gem
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ XcodeInstall needs environment variables with your credentials to access the Apple Developer
32
+ Center, they are stored using the [credentials_manager][1] of [fastlane][2]:
33
+
34
+ ```
35
+ XCODE_INSTALL_USER
36
+ XCODE_INSTALL_PASSWORD
37
+ ```
38
+
39
+ ### List
40
+
41
+ To list available versions:
42
+
43
+ ```
44
+ $ xcversion list
45
+ 6.0.1
46
+ 6.1
47
+ 6.1.1
48
+ 6.2 (installed)
49
+ 6.3
50
+ ```
51
+
52
+ Already installed versions are marked with `(installed)`.
53
+ (Use `$ xcversion installed` to only list installed Xcodes with their path).
54
+
55
+ To update the list of available versions, run:
56
+
57
+ ```
58
+ $ xcversion update
59
+ ```
60
+
61
+ ### Install
62
+
63
+ To install a certain version, simply:
64
+
65
+ ```
66
+ $ xcversion install 8
67
+ ########################################################### 82.1%
68
+ ######################################################################## 100.0%
69
+ Please authenticate for Xcode installation...
70
+
71
+ Xcode 8
72
+ Build version 6D570
73
+ ```
74
+
75
+ This will download and install that version of Xcode. Then you can start it from `/Applications` as usual.
76
+ The new version will also be automatically selected for CLI commands (see below).
77
+
78
+ #### GMs and beta versions
79
+
80
+ Note: GMs and beta versions usually have special names, e.g.
81
+
82
+ ```
83
+ $ xcversion list
84
+ 7 GM seed
85
+ 7.1 beta
86
+ ```
87
+
88
+ They have to be installed using the full name, e.g. `xcversion install '7 GM seed'`.
89
+
90
+ ### Select
91
+
92
+ To see the currently selected version, run
93
+ ```
94
+ $ xcversion selected
95
+ ```
96
+
97
+ To select a version as active, run
98
+ ```
99
+ $ xcversion select 8
100
+ ```
101
+
102
+ To select a version as active and change the symlink at `/Applications/Xcode`, run
103
+ ```
104
+ $ xcversion select 8 --symlink
105
+ ```
106
+
107
+ ### Command Line Tools
108
+
109
+ XcodeInstall can also install Xcode's Command Line Tools by calling `xcversion install-cli-tools`.
110
+
111
+ ### Simulators
112
+
113
+ XcodeInstall can also manage your local simulators using the `simulators` command.
114
+
115
+ ```
116
+ $ xcversion simulators
117
+ Xcode 6.4 (/Applications/Xcode-6.4.app)
118
+ iOS 7.1 Simulator (installed)
119
+ iOS 8.1 Simulator (not installed)
120
+ iOS 8.2 Simulator (not installed)
121
+ iOS 8.3 Simulator (installed)
122
+ Xcode 7.2.1 (/Applications/Xcode-7.2.1.app)
123
+ iOS 8.1 Simulator (not installed)
124
+ iOS 8.2 Simulator (not installed)
125
+ iOS 8.3 Simulator (installed)
126
+ iOS 8.4 Simulator (not installed)
127
+ iOS 9.0 Simulator (not installed)
128
+ iOS 9.1 Simulator (not installed)
129
+ tvOS 9.0 Simulator (not installed)
130
+ watchOS 2.0 Simulator (installed)
131
+ ```
132
+
133
+ To install a simulator, use `--install` and the beginning of a simulator name:
134
+
135
+ ```
136
+ $ xcversion simulators --install='iOS 8.4'
137
+ ########################################################### 82.1%
138
+ ######################################################################## 100.0%
139
+ Please authenticate to install iOS 8.4 Simulator...
140
+
141
+ Successfully installed iOS 8.4 Simulator
142
+ ```
143
+
144
+ ## Limitations
145
+
146
+ Unfortunately, the installation size of Xcodes downloaded will be bigger than when downloading via the Mac App Store, see [#10](/../../issues/10) and feel free to dupe the radar. 📡
147
+
148
+ XcodeInstall automatically installs additional components so that it is immediately usable from the
149
+ commandline. Unfortunately, Xcode will load third-party plugins even in that situation, which leads
150
+ to a dialog popping up. Feel free to dupe [the radar][5]. 📡
151
+
152
+ XcodeInstall normally relies on the Spotlight index to locate installed versions of Xcode. If you use it while
153
+ indexing is happening, it might show inaccurate results and it will not be able to see installed
154
+ versions on unindexed volumes.
155
+
156
+ To workaround the Spotlight limitation, XcodeInstall searches `/Applications` folder to locate Xcodes when Spotlight is disabled on the machine, or when Spotlight query for Xcode does not return any results. But it still won't work if your Xcodes are not located under `/Applications` folder.
157
+
158
+ ## Thanks
159
+
160
+ Thanks to [@neonichu](https://github.com/neonichu), the original (and best) author.
161
+
162
+ [This][3] downloading script which has been used for some inspiration, also [this][4]
163
+ for doing the installation. Additionally, many thanks to everyone who has contributed to this
164
+ project, especially [@henrikhodne][6] and [@lacostej][7] for making XcodeInstall C extension free.
165
+
166
+ ## Contributing
167
+
168
+ 1. Fork it ( https://github.com/KrauseFx/xcode-install/fork )
169
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
170
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
171
+ 4. Push to the branch (`git push origin my-new-feature`)
172
+ 5. Create a new Pull Request
173
+
174
+ ### Running tests
175
+
176
+ ```
177
+ bundle exec rake spec
178
+ ```
179
+
180
+ ### Running code style linter
181
+
182
+ ```
183
+ bundle exec rubocop -a
184
+ ```
185
+
186
+ ## License
187
+
188
+ This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file.
189
+
190
+ > This project and all fastlane tools are in no way affiliated with Apple Inc or Google. This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. All fastlane tools run on your own computer or server, so your credentials or other sensitive information will never leave your own computer. You are responsible for how you use fastlane tools.
191
+
192
+ [1]: https://github.com/fastlane/fastlane/tree/master/credentials_manager#using-environment-variables
193
+ [2]: http://fastlane.tools
194
+ [3]: http://atastypixel.com/blog/resuming-adc-downloads-cos-safari-sucks/
195
+ [4]: https://github.com/magneticbear/Jenkins_Bootstrap
196
+ [5]: http://www.openradar.me/22001810
197
+ [6]: https://github.com/henrikhodne
198
+ [7]: https://github.com/lacostej
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+
4
+ def specs(dir)
5
+ FileList["spec/#{dir}/*_spec.rb"].shuffle.join(' ')
6
+ end
7
+
8
+ desc 'Runs all the specs'
9
+ task :spec do
10
+ sh "bundle exec bacon #{specs('**')}"
11
+ end
12
+
13
+ desc 'Lints all the files'
14
+ RuboCop::RakeTask.new(:rubocop)
15
+
16
+ task default: %i[spec rubocop]
data/bin/xcversion ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if $PROGRAM_NAME == __FILE__
4
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
5
+ require 'rubygems'
6
+ require 'bundler/setup'
7
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
8
+ end
9
+
10
+ require 'xcode/install'
11
+
12
+ XcodeInstall::Command.run(ARGV)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ load File.expand_path('../xcversion', __FILE__)
@@ -0,0 +1,761 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'rexml/document'
4
+ require 'spaceship'
5
+ require 'json'
6
+ require 'rubygems/version'
7
+ require 'xcode/install/command'
8
+ require 'xcode/install/version'
9
+ require 'shellwords'
10
+ require 'open3'
11
+ require 'fastlane/action'
12
+ require 'fastlane/actions/verify_build'
13
+
14
+ module XcodeInstall
15
+ CACHE_DIR = Pathname.new("#{ENV['HOME']}/Library/Caches/XcodeInstall")
16
+ class Curl
17
+ COOKIES_PATH = Pathname.new('/tmp/curl-cookies.txt')
18
+
19
+ # @param url: The URL to download
20
+ # @param directory: The directory to download this file into
21
+ # @param cookies: Any cookies we should use for the download (used for auth with Apple)
22
+ # @param output: A PathName for where we want to store the file
23
+ # @param progress: parse and show the progress?
24
+ # @param progress_block: A block that's called whenever we have an updated progress %
25
+ # the parameter is a single number that's literally percent (e.g. 1, 50, 80 or 100)
26
+ # rubocop:disable Metrics/AbcSize
27
+ def fetch(url: nil,
28
+ directory: nil,
29
+ cookies: nil,
30
+ output: nil,
31
+ progress: nil,
32
+ progress_block: nil)
33
+ options = cookies.nil? ? [] : ['--cookie', cookies, '--cookie-jar', COOKIES_PATH]
34
+
35
+ uri = URI.parse(url)
36
+ output ||= File.basename(uri.path)
37
+ output = (Pathname.new(directory) + Pathname.new(output)) if directory
38
+
39
+ # Piping over all of stderr over to a temporary file
40
+ # the file content looks like this:
41
+ # 0 4766M 0 6835k 0 0 573k 0 2:21:58 0:00:11 2:21:47 902k
42
+ # This way we can parse the current %
43
+ # The header is
44
+ # % Total % Received % Xferd Average Speed Time Time Time Current
45
+ #
46
+ # Discussion for this on GH: https://github.com/KrauseFx/xcode-install/issues/276
47
+ # It was not easily possible to reimplement the same system using built-in methods
48
+ # especially when it comes to resuming downloads
49
+ # Piping over stderror to Ruby directly didn't work, due to the lack of flushing
50
+ # from curl. The only reasonable way to trigger this, is to pipe things directly into a
51
+ # local file, and parse that, and just poll that. We could get real time updates using
52
+ # the `tail` command or similar, however the download task is not time sensitive enough
53
+ # to make this worth the extra complexity, that's why we just poll and
54
+ # wait for the process to be finished
55
+ progress_log_file = File.join(CACHE_DIR, "progress.#{Time.now.to_i}.progress")
56
+ FileUtils.rm_f(progress_log_file)
57
+
58
+ retry_options = ['--retry', '3']
59
+ command = [
60
+ 'curl',
61
+ '--disable',
62
+ *options,
63
+ *retry_options,
64
+ '--location',
65
+ '--continue-at',
66
+ '-',
67
+ '--output',
68
+ output,
69
+ url
70
+ ].map(&:to_s)
71
+
72
+ command_string = command.collect(&:shellescape).join(' ')
73
+ command_string += " 2> #{progress_log_file}" # to not run shellescape on the `2>`
74
+
75
+ # Run the curl command in a loop, retry when curl exit status is 18
76
+ # "Partial file. Only a part of the file was transferred."
77
+ # https://curl.haxx.se/mail/archive-2008-07/0098.html
78
+ # https://github.com/KrauseFx/xcode-install/issues/210
79
+ 3.times do
80
+ # Non-blocking call of Open3
81
+ # We're not using the block based syntax, as the bacon testing
82
+ # library doesn't seem to support writing tests for it
83
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command_string)
84
+
85
+ # Poll the file and see if we're done yet
86
+ while wait_thr.alive?
87
+ sleep(0.5) # it's not critical for this to be real-time
88
+ next unless File.exist?(progress_log_file) # it might take longer for it to be created
89
+
90
+ progress_content = File.read(progress_log_file).split("\r").last
91
+
92
+ # Print out the progress for the CLI
93
+ if progress
94
+ print "\r#{progress_content}%"
95
+ $stdout.flush
96
+ end
97
+
98
+ # Call back the block for other processes that might be interested
99
+ matched = progress_content.match(/^\s*(\d+)/)
100
+ next unless matched && matched.length == 2
101
+ percent = matched[1].to_i
102
+ progress_block.call(percent) if progress_block
103
+ end
104
+
105
+ # as we're not making use of the block-based syntax
106
+ # we need to manually close those
107
+ stdin.close
108
+ stdout.close
109
+ stderr.close
110
+
111
+ return wait_thr.value.success? if wait_thr.value.success?
112
+ end
113
+ false
114
+ ensure
115
+ FileUtils.rm_f(COOKIES_PATH)
116
+ FileUtils.rm_f(progress_log_file)
117
+ end
118
+ end
119
+
120
+ # rubocop:disable Metrics/ClassLength
121
+ class Installer
122
+ attr_reader :xcodes
123
+
124
+ def initialize
125
+ FileUtils.mkdir_p(CACHE_DIR)
126
+ end
127
+
128
+ def cache_dir
129
+ CACHE_DIR
130
+ end
131
+
132
+ def current_symlink
133
+ File.symlink?(SYMLINK_PATH) ? SYMLINK_PATH : nil
134
+ end
135
+
136
+ def download(version, progress, url = nil, progress_block = nil)
137
+ xcode = find_xcode_version(version) if url.nil?
138
+ return if url.nil? && xcode.nil?
139
+
140
+ dmg_file = Pathname.new(File.basename(url || xcode.path))
141
+
142
+ result = Curl.new.fetch(
143
+ url: url || xcode.url,
144
+ directory: CACHE_DIR,
145
+ cookies: url ? nil : spaceship.cookie,
146
+ output: dmg_file,
147
+ progress: progress,
148
+ progress_block: progress_block
149
+ )
150
+ result ? CACHE_DIR + dmg_file : nil
151
+ end
152
+
153
+ def find_xcode_version(version)
154
+ # By checking for the name and the version we have the best success rate
155
+ # Sometimes the user might pass
156
+ # "4.3 for Lion"
157
+ # or they might pass an actual Gem::Version
158
+ # Gem::Version.new("8.0.0")
159
+ # which should automatically match with "Xcode 8"
160
+
161
+ begin
162
+ parsed_version = Gem::Version.new(version)
163
+ rescue ArgumentError
164
+ nil
165
+ end
166
+
167
+ seedlist.each do |current_seed|
168
+ return current_seed if current_seed.name == version
169
+ return current_seed if parsed_version && current_seed.version == parsed_version
170
+ end
171
+ nil
172
+ end
173
+
174
+ def exist?(version)
175
+ return true if find_xcode_version(version)
176
+ false
177
+ end
178
+
179
+ def installed?(version)
180
+ installed_versions.map(&:version).include?(version)
181
+ end
182
+
183
+ def installed_versions
184
+ installed.map { |x| InstalledXcode.new(x) }.sort do |a, b|
185
+ Gem::Version.new(a.version) <=> Gem::Version.new(b.version)
186
+ end
187
+ end
188
+
189
+ # Returns an array of `XcodeInstall::Xcode`
190
+ # <XcodeInstall::Xcode:0x007fa1d451c390
191
+ # @date_modified=2015,
192
+ # @name="6.4",
193
+ # @path="/Developer_Tools/Xcode_6.4/Xcode_6.4.dmg",
194
+ # @url=
195
+ # "https://developer.apple.com/devcenter/download.action?path=/Developer_Tools/Xcode_6.4/Xcode_6.4.dmg",
196
+ # @version=Gem::Version.new("6.4")>,
197
+ #
198
+ # the resulting list is sorted with the most recent release as first element
199
+ def seedlist
200
+ @xcodes = Marshal.load(File.read(LIST_FILE)) if LIST_FILE.exist? && xcodes.nil?
201
+ all_xcodes = (xcodes || fetch_seedlist)
202
+
203
+ # We have to set the `installed` value here, as we might still use
204
+ # the cached list of available Xcode versions, but have a new Xcode
205
+ # installed in the mean-time
206
+ cached_installed_versions = installed_versions.map(&:bundle_version)
207
+ all_xcodes.each do |current_xcode|
208
+ current_xcode.installed = cached_installed_versions.include?(current_xcode.version)
209
+ end
210
+
211
+ all_xcodes.sort_by(&:version)
212
+ end
213
+
214
+ def install_dmg(dmg_path, suffix = '', switch = true, clean = true)
215
+ prompt = "Please authenticate for Xcode installation.\nPassword: "
216
+ xcode_path = "/Applications/Xcode#{suffix}.app"
217
+
218
+ if dmg_path.extname == '.xip'
219
+ `xip -x #{dmg_path}`
220
+ xcode_orig_path = File.join(Dir.pwd, 'Xcode.app')
221
+ xcode_beta_path = File.join(Dir.pwd, 'Xcode-beta.app')
222
+ if Pathname.new(xcode_orig_path).exist?
223
+ `sudo -p "#{prompt}" mv "#{xcode_orig_path}" "#{xcode_path}"`
224
+ elsif Pathname.new(xcode_beta_path).exist?
225
+ `sudo -p "#{prompt}" mv "#{xcode_beta_path}" "#{xcode_path}"`
226
+ else
227
+ out = <<-HELP
228
+ No `Xcode.app(or Xcode-beta.app)` found in XIP. Please remove #{dmg_path} if you
229
+ suspect a corrupted download or run `xcversion update` to see if the version
230
+ you tried to install has been pulled by Apple. If none of this is true,
231
+ please open a new GH issue.
232
+ HELP
233
+ $stderr.puts out.tr("\n", ' ')
234
+ return
235
+ end
236
+ else
237
+ mount_dir = mount(dmg_path)
238
+ source = Dir.glob(File.join(mount_dir, 'Xcode*.app')).first
239
+
240
+ if source.nil?
241
+ out = <<-HELP
242
+ No `Xcode.app` found in DMG. Please remove #{dmg_path} if you suspect a corrupted
243
+ download or run `xcversion update` to see if the version you tried to install
244
+ has been pulled by Apple. If none of this is true, please open a new GH issue.
245
+ HELP
246
+ $stderr.puts out.tr("\n", ' ')
247
+ return
248
+ end
249
+
250
+ `sudo -p "#{prompt}" ditto "#{source}" "#{xcode_path}"`
251
+ `umount "/Volumes/Xcode"`
252
+ end
253
+
254
+ xcode = InstalledXcode.new(xcode_path)
255
+
256
+ unless xcode.verify_integrity
257
+ `sudo rm -rf #{xcode_path}`
258
+ return
259
+ end
260
+
261
+ enable_developer_mode
262
+ xcode.approve_license
263
+ xcode.install_components
264
+
265
+ if switch
266
+ `sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
267
+ `sudo ln -sf #{xcode_path} #{SYMLINK_PATH}` unless SYMLINK_PATH.exist?
268
+
269
+ `sudo xcode-select --switch #{xcode_path}`
270
+ puts `xcodebuild -version`
271
+ end
272
+
273
+ FileUtils.rm_f(dmg_path) if clean
274
+ end
275
+
276
+ # rubocop:disable Metrics/ParameterLists
277
+ def install_version(version, switch = true, clean = true, install = true, progress = true, url = nil, show_release_notes = true, progress_block = nil)
278
+ dmg_path = get_dmg(version, progress, url, progress_block)
279
+ fail Informative, "Failed to download Xcode #{version}." if dmg_path.nil?
280
+
281
+ if install
282
+ install_dmg(dmg_path, "-#{version.to_s.split(' ').join('.')}", switch, clean)
283
+ else
284
+ puts "Downloaded Xcode #{version} to '#{dmg_path}'"
285
+ end
286
+
287
+ open_release_notes_url(version) if show_release_notes && !url
288
+ end
289
+
290
+ def open_release_notes_url(version)
291
+ return if version.nil?
292
+ xcode = seedlist.find { |x| x.name == version }
293
+ `open #{xcode.release_notes_url}` unless xcode.nil? || xcode.release_notes_url.nil?
294
+ end
295
+
296
+ def list_annotated(xcodes_list)
297
+ installed = installed_versions.map(&:version)
298
+ xcodes_list.map do |x|
299
+ xcode_version = x.split(' ').first # exclude "beta N", "for Lion".
300
+ xcode_version << '.0' unless xcode_version.include?('.')
301
+
302
+ installed.include?(xcode_version) ? "#{x} (installed)" : x
303
+ end.join("\n")
304
+ end
305
+
306
+ def list
307
+ list_annotated(list_versions.sort_by(&:to_f))
308
+ end
309
+
310
+ def rm_list_cache
311
+ FileUtils.rm_f(LIST_FILE)
312
+ end
313
+
314
+ def symlink(version)
315
+ xcode = installed_versions.find { |x| x.version == version }
316
+ `sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
317
+ `sudo ln -sf #{xcode.path} #{SYMLINK_PATH}` unless xcode.nil? || SYMLINK_PATH.exist?
318
+ end
319
+
320
+ def symlinks_to
321
+ File.absolute_path(File.readlink(current_symlink), SYMLINK_PATH.dirname) if current_symlink
322
+ end
323
+
324
+ def mount(dmg_path)
325
+ plist = hdiutil('mount', '-plist', '-nobrowse', '-noverify', dmg_path.to_s)
326
+ document = REXML::Document.new(plist)
327
+ node = REXML::XPath.first(document, "//key[.='mount-point']/following-sibling::*[1]")
328
+ fail Informative, 'Failed to mount image.' unless node
329
+ node.text
330
+ end
331
+
332
+ private
333
+
334
+ def spaceship
335
+ @spaceship ||= begin
336
+ begin
337
+ Spaceship.login(ENV['XCODE_INSTALL_USER'], ENV['XCODE_INSTALL_PASSWORD'])
338
+ rescue Spaceship::Client::InvalidUserCredentialsError
339
+ raise 'The specified Apple developer account credentials are incorrect.'
340
+ rescue Spaceship::Client::NoUserCredentialsError
341
+ raise <<-HELP
342
+ Please provide your Apple developer account credentials via the
343
+ XCODE_INSTALL_USER and XCODE_INSTALL_PASSWORD environment variables.
344
+ HELP
345
+ end
346
+
347
+ if ENV.key?('XCODE_INSTALL_TEAM_ID')
348
+ Spaceship.client.team_id = ENV['XCODE_INSTALL_TEAM_ID']
349
+ end
350
+ Spaceship.client
351
+ end
352
+ end
353
+
354
+ LIST_FILE = CACHE_DIR + Pathname.new('xcodes.bin')
355
+ MINIMUM_VERSION = Gem::Version.new('4.3')
356
+ SYMLINK_PATH = Pathname.new('/Applications/Xcode.app')
357
+
358
+ def enable_developer_mode
359
+ `sudo /usr/sbin/DevToolsSecurity -enable`
360
+ `sudo /usr/sbin/dseditgroup -o edit -t group -a staff _developer`
361
+ end
362
+
363
+ def get_dmg(version, progress = true, url = nil, progress_block = nil)
364
+ if url
365
+ path = Pathname.new(url)
366
+ return path if path.exist?
367
+ end
368
+ if ENV.key?('XCODE_INSTALL_CACHE_DIR')
369
+ Pathname.glob(ENV['XCODE_INSTALL_CACHE_DIR'] + '/*').each do |fpath|
370
+ return fpath if /^xcode_#{version}\.dmg|xip$/ =~ fpath.basename.to_s
371
+ end
372
+ end
373
+
374
+ download(version, progress, url, progress_block)
375
+ end
376
+
377
+ def fetch_seedlist
378
+ @xcodes = parse_seedlist(spaceship.send(:request, :post,
379
+ '/services-account/QH65B2/downloadws/listDownloads.action').body)
380
+
381
+ names = @xcodes.map(&:name)
382
+ @xcodes += prereleases.reject { |pre| names.include?(pre.name) }
383
+
384
+ File.open(LIST_FILE, 'wb') do |f|
385
+ f << Marshal.dump(xcodes)
386
+ end
387
+
388
+ xcodes
389
+ end
390
+
391
+ def installed
392
+ result = `mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'" 2>/dev/null`.split("\n")
393
+ if result.empty?
394
+ result = `find /Applications -maxdepth 1 -name '*.app' -type d -exec sh -c \
395
+ 'if [ "$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
396
+ "{}/Contents/Info.plist" 2>/dev/null)" == "com.apple.dt.Xcode" ]; then echo "{}"; fi' ';'`.split("\n")
397
+ end
398
+ result
399
+ end
400
+
401
+ def parse_seedlist(seedlist)
402
+ fail Informative, seedlist['resultString'] unless seedlist['resultCode'].eql? 0
403
+
404
+ seeds = Array(seedlist['downloads']).select do |t|
405
+ /^Xcode [0-9]/.match(t['name'])
406
+ end
407
+
408
+ xcodes = seeds.map { |x| Xcode.new(x) }.reject { |x| x.version < MINIMUM_VERSION }.sort do |a, b|
409
+ a.date_modified <=> b.date_modified
410
+ end
411
+
412
+ xcodes.select { |x| x.url.end_with?('.dmg') || x.url.end_with?('.xip') }
413
+ end
414
+
415
+ def list_versions
416
+ seedlist.map(&:name)
417
+ end
418
+
419
+ def prereleases
420
+ body = spaceship.send(:request, :get, '/download/').body
421
+
422
+ links = body.scan(%r{<a.+?href="(.+?/Xcode.+?/Xcode_(.+?)\.(dmg|xip))".*>(.*)</a>})
423
+ links = links.map do |link|
424
+ parent = link[0].scan(%r{path=(/.*/.*/)}).first.first
425
+ match = body.scan(/#{Regexp.quote(parent)}(.+?.pdf)/).first
426
+ if match
427
+ link + [parent + match.first]
428
+ else
429
+ link + [nil]
430
+ end
431
+ end
432
+ links = links.map { |pre| Xcode.new_prerelease(pre[1].strip.tr('_', ' '), pre[0], pre[4]) }
433
+
434
+ if links.count.zero?
435
+ rg = %r{platform-title.*Xcode.* beta.*<\/p>}
436
+ scan = body.scan(rg)
437
+
438
+ if scan.count.zero?
439
+ rg = %r{Xcode.* GM.*<\/p>}
440
+ scan = body.scan(rg)
441
+ end
442
+
443
+ return [] if scan.empty?
444
+
445
+ version = scan.first.gsub(/<.*?>/, '').gsub(/.*Xcode /, '')
446
+ link = body.scan(%r{<button .*"(.+?.(dmg|xip))".*</button>}).first.first
447
+ notes = body.scan(%r{<a.+?href="(/go/\?id=xcode-.+?)".*>(.*)</a>}).first.first
448
+ links << Xcode.new(version, link, notes)
449
+ end
450
+
451
+ links
452
+ end
453
+
454
+ def hdiutil(*args)
455
+ io = IO.popen(['hdiutil', *args])
456
+ result = io.read
457
+ io.close
458
+ unless $?.exitstatus.zero?
459
+ file_path = args[-1]
460
+ if `file -b #{file_path}`.start_with?('HTML')
461
+ fail Informative, "Failed to mount #{file_path}, logging into your account from a browser should tell you what is going wrong."
462
+ end
463
+ fail Informative, 'Failed to invoke hdiutil.'
464
+ end
465
+ result
466
+ end
467
+ end
468
+
469
+ class Simulator
470
+ attr_reader :version
471
+ attr_reader :name
472
+ attr_reader :identifier
473
+ attr_reader :source
474
+ attr_reader :xcode
475
+
476
+ def initialize(downloadable)
477
+ @version = Gem::Version.new(downloadable['version'])
478
+ @install_prefix = apply_variables(downloadable['userInfo']['InstallPrefix'])
479
+ @name = apply_variables(downloadable['name'])
480
+ @identifier = apply_variables(downloadable['identifier'])
481
+ @source = apply_variables(downloadable['source'])
482
+ end
483
+
484
+ def installed?
485
+ # FIXME: use downloadables' `InstalledIfAllReceiptsArePresentOrNewer` key
486
+ File.directory?(@install_prefix)
487
+ end
488
+
489
+ def installed_string
490
+ installed? ? 'installed' : 'not installed'
491
+ end
492
+
493
+ def to_s
494
+ "#{name} (#{installed_string})"
495
+ end
496
+
497
+ def xcode
498
+ Installer.new.installed_versions.find do |x|
499
+ x.available_simulators.find do |s|
500
+ s.version == version
501
+ end
502
+ end
503
+ end
504
+
505
+ def download(progress, progress_block = nil)
506
+ result = Curl.new.fetch(
507
+ url: source,
508
+ directory: CACHE_DIR,
509
+ progress: progress,
510
+ progress_block: progress_block
511
+ )
512
+ result ? dmg_path : nil
513
+ end
514
+
515
+ def install(progress, should_install)
516
+ dmg_path = download(progress)
517
+ fail Informative, "Failed to download #{@name}." if dmg_path.nil?
518
+
519
+ return unless should_install
520
+ prepare_package unless pkg_path.exist?
521
+ puts "Please authenticate to install #{name}..."
522
+ `sudo installer -pkg #{pkg_path} -target /`
523
+ fail Informative, "Could not install #{name}, please try again" unless installed?
524
+ source_receipts_dir = '/private/var/db/receipts'
525
+ target_receipts_dir = "#{@install_prefix}/System/Library/Receipts"
526
+ FileUtils.mkdir_p(target_receipts_dir)
527
+ FileUtils.cp("#{source_receipts_dir}/#{@identifier}.bom", target_receipts_dir)
528
+ FileUtils.cp("#{source_receipts_dir}/#{@identifier}.plist", target_receipts_dir)
529
+ puts "Successfully installed #{name}"
530
+ end
531
+
532
+ :private
533
+
534
+ def prepare_package
535
+ puts 'Mounting DMG'
536
+ mount_location = Installer.new.mount(dmg_path)
537
+ puts 'Expanding pkg'
538
+ expanded_pkg_path = CACHE_DIR + identifier
539
+ FileUtils.rm_rf(expanded_pkg_path)
540
+ `pkgutil --expand #{mount_location}/*.pkg #{expanded_pkg_path}`
541
+ puts "Expanded pkg into #{expanded_pkg_path}"
542
+ puts 'Unmounting DMG'
543
+ `umount #{mount_location}`
544
+ puts 'Setting package installation location'
545
+ package_info_path = expanded_pkg_path + 'PackageInfo'
546
+ package_info_contents = File.read(package_info_path)
547
+ File.open(package_info_path, 'w') do |f|
548
+ f << package_info_contents.sub('pkg-info', %(pkg-info install-location="#{@install_prefix}"))
549
+ end
550
+ puts 'Rebuilding package'
551
+ `pkgutil --flatten #{expanded_pkg_path} #{pkg_path}`
552
+ FileUtils.rm_rf(expanded_pkg_path)
553
+ end
554
+
555
+ def dmg_path
556
+ CACHE_DIR + Pathname.new(source).basename
557
+ end
558
+
559
+ def pkg_path
560
+ CACHE_DIR + "#{identifier}.pkg"
561
+ end
562
+
563
+ def apply_variables(template)
564
+ variable_map = {
565
+ '$(DOWNLOADABLE_VERSION_MAJOR)' => version.to_s.split('.')[0],
566
+ '$(DOWNLOADABLE_VERSION_MINOR)' => version.to_s.split('.')[1],
567
+ '$(DOWNLOADABLE_IDENTIFIER)' => identifier,
568
+ '$(DOWNLOADABLE_VERSION)' => version.to_s
569
+ }.freeze
570
+ variable_map.each do |key, value|
571
+ next unless template.include?(key)
572
+ template.sub!(key, value)
573
+ end
574
+ template
575
+ end
576
+ end
577
+
578
+ class InstalledXcode
579
+ TEAM_IDENTIFIER = '59GAB85EFG'.freeze
580
+ AUTHORITY = 'Software Signing'.freeze
581
+
582
+ attr_reader :path
583
+ attr_reader :version
584
+ attr_reader :bundle_version
585
+ attr_reader :uuid
586
+ attr_reader :downloadable_index_url
587
+ attr_reader :available_simulators
588
+
589
+ def initialize(path)
590
+ @path = Pathname.new(path)
591
+ end
592
+
593
+ def version
594
+ @version ||= fetch_version
595
+ end
596
+
597
+ def bundle_version
598
+ @bundle_version ||= Gem::Version.new(bundle_version_string)
599
+ end
600
+
601
+ def uuid
602
+ @uuid ||= plist_entry(':DVTPlugInCompatibilityUUID')
603
+ end
604
+
605
+ def downloadable_index_url
606
+ @downloadable_index_url ||= begin
607
+ if Gem::Version.new(version) >= Gem::Version.new('8.1')
608
+ "https://devimages-cdn.apple.com/downloads/xcode/simulators/index-#{bundle_version}-#{uuid}.dvtdownloadableindex"
609
+ else
610
+ "https://devimages.apple.com.edgekey.net/downloads/xcode/simulators/index-#{bundle_version}-#{uuid}.dvtdownloadableindex"
611
+ end
612
+ end
613
+ end
614
+
615
+ def approve_license
616
+ if Gem::Version.new(version) < Gem::Version.new('7.3')
617
+ license_info_path = File.join(@path, 'Contents/Resources/LicenseInfo.plist')
618
+ license_id = `/usr/libexec/PlistBuddy -c 'Print :licenseID' #{license_info_path}`
619
+ license_type = `/usr/libexec/PlistBuddy -c 'Print :licenseType' #{license_info_path}`
620
+ license_plist_path = '/Library/Preferences/com.apple.dt.Xcode.plist'
621
+ `sudo rm -rf #{license_plist_path}`
622
+ if license_type == 'GM'
623
+ `sudo /usr/libexec/PlistBuddy -c "add :IDELastGMLicenseAgreedTo string #{license_id}" #{license_plist_path}`
624
+ `sudo /usr/libexec/PlistBuddy -c "add :IDEXcodeVersionForAgreedToGMLicense string #{version}" #{license_plist_path}`
625
+ else
626
+ `sudo /usr/libexec/PlistBuddy -c "add :IDELastBetaLicenseAgreedTo string #{license_id}" #{license_plist_path}`
627
+ `sudo /usr/libexec/PlistBuddy -c "add :IDEXcodeVersionForAgreedToBetaLicense string #{version}" #{license_plist_path}`
628
+ end
629
+ else
630
+ `sudo #{@path}/Contents/Developer/usr/bin/xcodebuild -license accept`
631
+ end
632
+ end
633
+
634
+ def available_simulators
635
+ @available_simulators ||= JSON.parse(`curl -Ls #{downloadable_index_url} | plutil -convert json -o - -`)['downloadables'].map do |downloadable|
636
+ Simulator.new(downloadable)
637
+ end
638
+ rescue JSON::ParserError
639
+ return []
640
+ end
641
+
642
+ def install_components
643
+ # starting with Xcode 9, we have `xcodebuild -runFirstLaunch` available to do package
644
+ # postinstalls using a documented option
645
+ if Gem::Version.new(version) >= Gem::Version.new('9')
646
+ `sudo #{@path}/Contents/Developer/usr/bin/xcodebuild -runFirstLaunch`
647
+ else
648
+ Dir.glob("#{@path}/Contents/Resources/Packages/*.pkg").each do |pkg|
649
+ `sudo installer -pkg #{pkg} -target /`
650
+ end
651
+ end
652
+ osx_build_version = `sw_vers -buildVersion`.chomp
653
+ tools_version = `/usr/libexec/PlistBuddy -c "Print :ProductBuildVersion" "#{@path}/Contents/version.plist"`.chomp
654
+ cache_dir = `getconf DARWIN_USER_CACHE_DIR`.chomp
655
+ `touch #{cache_dir}com.apple.dt.Xcode.InstallCheckCache_#{osx_build_version}_#{tools_version}`
656
+ end
657
+
658
+ # This method might take a few ms, this could be improved by implementing https://github.com/KrauseFx/xcode-install/issues/273
659
+ def fetch_version
660
+ output = `DEVELOPER_DIR='' "#{@path}/Contents/Developer/usr/bin/xcodebuild" -version`
661
+ return '0.0' if output.nil? || output.empty? # ¯\_(ツ)_/¯
662
+ output.split("\n").first.split(' ')[1]
663
+ end
664
+
665
+ def verify_integrity
666
+ verify_app_security_assessment && verify_app_cert
667
+ end
668
+
669
+ :private
670
+
671
+ def bundle_version_string
672
+ digits = plist_entry(':DTXcode').to_i.to_s
673
+ if digits.length < 3
674
+ digits.split(//).join('.')
675
+ else
676
+ "#{digits[0..-3]}.#{digits[-2]}.#{digits[-1]}"
677
+ end
678
+ end
679
+
680
+ def plist_entry(keypath)
681
+ `/usr/libexec/PlistBuddy -c "Print :#{keypath}" "#{path}/Contents/Info.plist"`.chomp
682
+ end
683
+
684
+ def verify_app_security_assessment
685
+ puts `/usr/sbin/spctl --assess --verbose=4 --type execute #{@path}`
686
+ $?.exitstatus.zero?
687
+ end
688
+
689
+ def verify_app_cert
690
+ cert_info = Fastlane::Actions::VerifyBuildAction.gather_cert_info(@path.to_s)
691
+ apple_team_identifier_result = cert_info['team_identifier'] == TEAM_IDENTIFIER
692
+ apple_authority_result = cert_info['authority'].include?(AUTHORITY)
693
+ apple_team_identifier_result && apple_authority_result
694
+ end
695
+ end
696
+
697
+ # A version of Xcode we fetched from the Apple Developer Portal
698
+ # we can download & install.
699
+ #
700
+ # Sample object:
701
+ # <XcodeInstall::Xcode:0x007fa1d451c390
702
+ # @date_modified=2015,
703
+ # @name="6.4",
704
+ # @path="/Developer_Tools/Xcode_6.4/Xcode_6.4.dmg",
705
+ # @url=
706
+ # "https://developer.apple.com/devcenter/download.action?path=/Developer_Tools/Xcode_6.4/Xcode_6.4.dmg",
707
+ # @version=Gem::Version.new("6.4")>,
708
+ class Xcode
709
+ attr_reader :date_modified
710
+
711
+ # The name might include extra information like "for Lion" or "beta 2"
712
+ attr_reader :name
713
+ attr_reader :path
714
+ attr_reader :url
715
+ attr_reader :version
716
+ attr_reader :release_notes_url
717
+
718
+ # Accessor since it's set by the `Installer`
719
+ attr_accessor :installed
720
+
721
+ alias installed? installed
722
+
723
+ def initialize(json, url = nil, release_notes_url = nil)
724
+ if url.nil?
725
+ @date_modified = json['dateModified'].to_i
726
+ @name = json['name'].gsub(/^Xcode /, '')
727
+ @path = json['files'].first['remotePath']
728
+ url_prefix = 'https://developer.apple.com/devcenter/download.action?path='
729
+ @url = "#{url_prefix}#{@path}"
730
+ @release_notes_url = "#{url_prefix}#{json['release_notes_path']}" if json['release_notes_path']
731
+ else
732
+ @name = json
733
+ @path = url.split('/').last
734
+ url_prefix = 'https://developer.apple.com/'
735
+ @url = "#{url_prefix}#{url}"
736
+ @release_notes_url = "#{url_prefix}#{release_notes_url}"
737
+ end
738
+
739
+ begin
740
+ @version = Gem::Version.new(@name.split(' ')[0])
741
+ rescue
742
+ @version = Installer::MINIMUM_VERSION
743
+ end
744
+ end
745
+
746
+ def to_s
747
+ "Xcode #{version} -- #{url}"
748
+ end
749
+
750
+ def ==(other)
751
+ date_modified == other.date_modified && name == other.name && path == other.path && \
752
+ url == other.url && version == other.version
753
+ end
754
+
755
+ def self.new_prerelease(version, url, release_notes_path)
756
+ new('name' => version,
757
+ 'files' => [{ 'remotePath' => url.split('=').last }],
758
+ 'release_notes_path' => release_notes_path)
759
+ end
760
+ end
761
+ end