xcode-install-citrus 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +33 -0
- data/.gitattributes +1 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +78 -0
- data/Gemfile +12 -0
- data/LICENSE +22 -0
- data/README.md +198 -0
- data/Rakefile +16 -0
- data/bin/xcversion +12 -0
- data/bin//360/237/216/211 +3 -0
- data/lib/xcode/install.rb +761 -0
- data/lib/xcode/install/cleanup.rb +14 -0
- data/lib/xcode/install/cli.rb +30 -0
- data/lib/xcode/install/command.rb +32 -0
- data/lib/xcode/install/install.rb +55 -0
- data/lib/xcode/install/installed.rb +24 -0
- data/lib/xcode/install/list.rb +17 -0
- data/lib/xcode/install/select.rb +36 -0
- data/lib/xcode/install/selected.rb +12 -0
- data/lib/xcode/install/simulators.rb +65 -0
- data/lib/xcode/install/uninstall.rb +36 -0
- data/lib/xcode/install/update.rb +14 -0
- data/lib/xcode/install/version.rb +3 -0
- data/spec/cli_spec.rb +16 -0
- data/spec/curl_spec.rb +26 -0
- data/spec/fixtures/devcenter/xcode-20150414.html +263 -0
- data/spec/fixtures/devcenter/xcode-20150427.html +263 -0
- data/spec/fixtures/devcenter/xcode-20150508.html +279 -0
- data/spec/fixtures/devcenter/xcode-20150601.html +212 -0
- data/spec/fixtures/devcenter/xcode-20150608.html +315 -0
- data/spec/fixtures/devcenter/xcode-20150624.html +318 -0
- data/spec/fixtures/devcenter/xcode-20150909.html +309 -0
- data/spec/fixtures/devcenter/xcode-20160601.html +1872 -0
- data/spec/fixtures/devcenter/xcode-20160705-alt.html +317 -0
- data/spec/fixtures/devcenter/xcode-20160705.html +1909 -0
- data/spec/fixtures/devcenter/xcode-20160922.html +878 -0
- data/spec/fixtures/devcenter/xcode-20161024.html +663 -0
- data/spec/fixtures/hdiutil.plist +31 -0
- data/spec/fixtures/mail-verify.html +222 -0
- data/spec/fixtures/not_registered_as_developer.json +11 -0
- data/spec/fixtures/xcode.json +1 -0
- data/spec/fixtures/xcode_63.json +46 -0
- data/spec/fixtures/yolo.json +1 -0
- data/spec/install_spec.rb +62 -0
- data/spec/installed_spec.rb +19 -0
- data/spec/installer_spec.rb +101 -0
- data/spec/json_spec.rb +32 -0
- data/spec/list_spec.rb +57 -0
- data/spec/prerelease_spec.rb +108 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/uninstall_spec.rb +12 -0
- data/xcode-install.gemspec +32 -0
- 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
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
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,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
|