├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .rubocop.yml
├── .rubocop_todo.yml
├── Gemfile
├── LICENSE
├── MIGRATION.md
├── README.md
├── Rakefile
├── XCODE_VERSION.md
├── bin
├── xcversion
└── 🎉
├── lib
└── xcode
│ ├── install.rb
│ └── install
│ ├── cleanup.rb
│ ├── cli.rb
│ ├── command.rb
│ ├── install.rb
│ ├── installed.rb
│ ├── list.rb
│ ├── select.rb
│ ├── selected.rb
│ ├── simulators.rb
│ ├── uninstall.rb
│ ├── update.rb
│ └── version.rb
├── spec
├── cli_spec.rb
├── curl_spec.rb
├── fixtures
│ ├── devcenter
│ │ ├── xcode-20150414.html
│ │ ├── xcode-20150427.html
│ │ ├── xcode-20150508.html
│ │ ├── xcode-20150601.html
│ │ ├── xcode-20150608.html
│ │ ├── xcode-20150624.html
│ │ ├── xcode-20150909.html
│ │ ├── xcode-20160601.html
│ │ ├── xcode-20160705-alt.html
│ │ ├── xcode-20160705.html
│ │ ├── xcode-20160922.html
│ │ └── xcode-20161024.html
│ ├── hdiutil.plist
│ ├── mail-verify.html
│ ├── not_registered_as_developer.json
│ ├── xcode.json
│ ├── xcode_63.json
│ └── yolo.json
├── install_spec.rb
├── installed_spec.rb
├── installer_spec.rb
├── json_spec.rb
├── list_spec.rb
├── prerelease_spec.rb
├── spec_helper.rb
└── uninstall_spec.rb
└── xcode-install.gemspec
/.gitattributes:
--------------------------------------------------------------------------------
1 | spec/fixtures/* linguist-documentation
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | strategy:
7 | fail-fast: false
8 | matrix:
9 | ruby: ["2.5", "2.6", "2.7", "3.0"]
10 |
11 | runs-on: macos-latest
12 | steps:
13 | # Setup env
14 | - uses: actions/checkout@v2
15 | - uses: ruby/setup-ruby@v1
16 | with:
17 | ruby-version: "${{ matrix.ruby }}"
18 |
19 | # Show env
20 | - name: Show macOS version
21 | run: sw_vers
22 | - name: Show env versions
23 | run: |
24 | ruby --version
25 | bundler --version
26 | echo $HOME
27 |
28 | # Prepare
29 | - name: Install bundler 2.2.20
30 | run: gem install bundler -v "~> 2.2.20"
31 | - name: Install ruby dependencies
32 | run: |
33 | bundle config --local clean 'true'
34 | bundle config --local path '.vendor'
35 | bundle config --local jobs 8
36 | bundle config --local without 'system_tests'
37 | bundle install
38 |
39 | - name: Run test
40 | run: bundle exec rake spec
41 | - name: Run lint
42 | run: bundle exec rake rubocop
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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
17 | .vendor
18 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
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
22 |
--------------------------------------------------------------------------------
/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2021-07-06 16:06:45 UTC using RuboCop version 1.12.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: 1
10 | # Configuration parameters: Include.
11 | # Include: **/*.gemspec
12 | Gemspec/RequiredRubyVersion:
13 | Exclude:
14 | - 'xcode-install.gemspec'
15 |
16 | # Offense count: 3
17 | # Cop supports --auto-correct.
18 | Layout/ClosingHeredocIndentation:
19 | Exclude:
20 | - 'lib/xcode/install.rb'
21 |
22 | # Offense count: 12
23 | # Cop supports --auto-correct.
24 | Layout/EmptyLineAfterGuardClause:
25 | Exclude:
26 | - 'lib/xcode/install.rb'
27 | - 'lib/xcode/install/cleanup.rb'
28 | - 'lib/xcode/install/simulators.rb'
29 | - 'lib/xcode/install/uninstall.rb'
30 |
31 | # Offense count: 3
32 | # Cop supports --auto-correct.
33 | Layout/HeredocIndentation:
34 | Exclude:
35 | - 'lib/xcode/install.rb'
36 |
37 | # Offense count: 3
38 | # Cop supports --auto-correct.
39 | Lint/BooleanSymbol:
40 | Exclude:
41 | - 'lib/xcode/install/install.rb'
42 | - 'lib/xcode/install/select.rb'
43 | - 'lib/xcode/install/uninstall.rb'
44 |
45 | # Offense count: 6
46 | Lint/DuplicateMethods:
47 | Exclude:
48 | - 'lib/xcode/install.rb'
49 |
50 | # Offense count: 2
51 | # Configuration parameters: MaximumRangeSize.
52 | Lint/MissingCopEnableDirective:
53 | Exclude:
54 | - 'lib/xcode/install.rb'
55 |
56 | # Offense count: 1
57 | # Cop supports --auto-correct.
58 | Lint/UriRegexp:
59 | Exclude:
60 | - 'lib/xcode/install/install.rb'
61 |
62 | # Offense count: 12
63 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
64 | Metrics/AbcSize:
65 | Max: 45
66 |
67 | # Offense count: 5
68 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
69 | # IgnoredMethods: refine
70 | Metrics/BlockLength:
71 | Max: 76
72 |
73 | # Offense count: 6
74 | # Configuration parameters: IgnoredMethods.
75 | Metrics/CyclomaticComplexity:
76 | Max: 10
77 |
78 | # Offense count: 17
79 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
80 | Metrics/MethodLength:
81 | Max: 50
82 |
83 | # Offense count: 1
84 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
85 | Metrics/ParameterLists:
86 | Max: 7
87 |
88 | # Offense count: 2
89 | # Configuration parameters: IgnoredMethods.
90 | Metrics/PerceivedComplexity:
91 | Max: 12
92 |
93 | # Offense count: 1
94 | Security/MarshalLoad:
95 | Exclude:
96 | - 'lib/xcode/install.rb'
97 |
98 | # Offense count: 16
99 | # Cop supports --auto-correct.
100 | # Configuration parameters: EnforcedStyle.
101 | # SupportedStyles: separated, grouped
102 | Style/AccessorGrouping:
103 | Exclude:
104 | - 'lib/xcode/install.rb'
105 |
106 | # Offense count: 1
107 | Style/CombinableLoops:
108 | Exclude:
109 | - 'lib/xcode/install.rb'
110 |
111 | # Offense count: 1
112 | # Cop supports --auto-correct.
113 | Style/Encoding:
114 | Exclude:
115 | - 'xcode-install.gemspec'
116 |
117 | # Offense count: 14
118 | # Cop supports --auto-correct.
119 | Style/ExpandPathArguments:
120 | Exclude:
121 | - 'bin/xcversion'
122 | - 'bin/🎉'
123 | - 'spec/cli_spec.rb'
124 | - 'spec/curl_spec.rb'
125 | - 'spec/install_spec.rb'
126 | - 'spec/installed_spec.rb'
127 | - 'spec/installer_spec.rb'
128 | - 'spec/json_spec.rb'
129 | - 'spec/list_spec.rb'
130 | - 'spec/prerelease_spec.rb'
131 | - 'spec/spec_helper.rb'
132 | - 'spec/uninstall_spec.rb'
133 | - 'xcode-install.gemspec'
134 |
135 | # Offense count: 28
136 | # Cop supports --auto-correct.
137 | # Configuration parameters: EnforcedStyle.
138 | # SupportedStyles: always, always_true, never
139 | Style/FrozenStringLiteralComment:
140 | Enabled: false
141 |
142 | # Offense count: 2
143 | # Cop supports --auto-correct.
144 | Style/IfUnlessModifier:
145 | Exclude:
146 | - 'lib/xcode/install.rb'
147 |
148 | # Offense count: 8
149 | # Configuration parameters: AllowedMethods.
150 | # AllowedMethods: respond_to_missing?
151 | Style/OptionalBooleanParameter:
152 | Exclude:
153 | - 'lib/xcode/install.rb'
154 |
155 | # Offense count: 1
156 | # Cop supports --auto-correct.
157 | Style/RedundantBegin:
158 | Exclude:
159 | - 'lib/xcode/install.rb'
160 |
161 | # Offense count: 2
162 | # Cop supports --auto-correct.
163 | Style/RedundantRegexpEscape:
164 | Exclude:
165 | - 'lib/xcode/install.rb'
166 |
167 | # Offense count: 1
168 | # Cop supports --auto-correct.
169 | # Configuration parameters: AllowMultipleReturnValues.
170 | Style/RedundantReturn:
171 | Exclude:
172 | - 'lib/xcode/install.rb'
173 |
174 | # Offense count: 2
175 | # Cop supports --auto-correct.
176 | # Configuration parameters: EnforcedStyle.
177 | # SupportedStyles: implicit, explicit
178 | Style/RescueStandardError:
179 | Exclude:
180 | - 'lib/xcode/install.rb'
181 |
182 | # Offense count: 1
183 | # Cop supports --auto-correct.
184 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods.
185 | # AllowedMethods: present?, blank?, presence, try, try!
186 | Style/SafeNavigation:
187 | Exclude:
188 | - 'lib/xcode/install.rb'
189 |
190 | # Offense count: 14
191 | # Cop supports --auto-correct.
192 | # Configuration parameters: EnforcedStyle.
193 | # SupportedStyles: only_raise, only_fail, semantic
194 | Style/SignalException:
195 | Exclude:
196 | - 'lib/xcode/install.rb'
197 | - 'lib/xcode/install/install.rb'
198 | - 'lib/xcode/install/select.rb'
199 | - 'lib/xcode/install/simulators.rb'
200 | - 'lib/xcode/install/uninstall.rb'
201 |
202 | # Offense count: 2
203 | # Cop supports --auto-correct.
204 | Style/StderrPuts:
205 | Exclude:
206 | - 'lib/xcode/install.rb'
207 |
208 | # Offense count: 4
209 | # Cop supports --auto-correct.
210 | Style/StringConcatenation:
211 | Exclude:
212 | - 'lib/xcode/install.rb'
213 | - 'spec/spec_helper.rb'
214 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
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', '~> 1.18', require: false
12 | end
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
23 |
--------------------------------------------------------------------------------
/MIGRATION.md:
--------------------------------------------------------------------------------
1 | # This project is being sunset
2 |
3 | A brief history of `xcode-install` aka `xcversion`, as well as a guide on how to migrate from it, to a more modern tool.
4 |
5 | For brevity sake I'm going to refer to this project always as `xcode-install` in this document.
6 |
7 | ## Overview
8 |
9 | The more time goes by, the more we realized this project had already provided the community the value it needed, served its purpose, and become obsolete. We believe it was the time to officially sunset this project, and this document will guide you to use a more modern and well maintained tool.
10 |
11 | ## Some Context
12 |
13 | `xcode-install` (originally a @neonichu's project, which got transferred to @KrauseFx, which got transferred to the @xcpretty GitHub organization), had been around since April 2015, back when there were no other good options to manage multiple versions of Xcode.
14 |
15 | Fast forward to Feb 2019, [`xcodes`](https://github.com/RobotsAndPencils/xcodes) was born to provide a more user friendly experience. It based itself off of this project to figure out the complex Xcode downloading logic, but has since then been actively maintained and new features are incorporated into it on a regular basis. To name a few, that are not present in `xcode-install`:
16 |
17 | - GUI (via [`Xcodes.app`](https://github.com/RobotsAndPencils/XcodesApp))
18 | - Support to `aria2` (which has been requested in https://github.com/xcpretty/xcode-install/issues/425 but we never got to implement it), which uses up to 16 connections to download Xcode 3-5x faster
19 | - Support to [`unxip`](https://github.com/saagarjha/unxip), providing unxipping up to 70% faster
20 | - New in Xcode 14: Sessionless downloads, just announced by [@xcodesapp](https://twitter.com/xcodesapp): https://twitter.com/xcodesapp/status/1570991082359627779?s=46&t=qVETxqxGI7ZZsFLLrledIg, available in https://github.com/RobotsAndPencils/XcodesApp/releases/tag/v1.8.0b16
21 |
22 | These features, plus the fact that this project wasn't getting the attention it needed to keep supporting newer versions of Xcode and bug fixes, made us believe it was time to sunset this project.
23 |
24 | # Migrating _fastlane_ actions that depend on the `xcode-install` gem, to use `xcodes`
25 |
26 | As of https://github.com/fastlane/fastlane/pull/20672, a new action was introduced to _fastlane_ called `xcodes`. You can find its full documentation here: https://docs.fastlane.tools/actions/xcodes
27 |
28 | Keep in mind that the `xcodes` action depends on [`xcodes`](https://github.com/RobotsAndPencils/xcodes) CLI, so make sure you have it installed in your environment. For the installation guide, see: https://github.com/RobotsAndPencils/xcodes#installation
29 |
30 | There are 3 actions that depend on `xcode-install` gem. Below you can find how to migrate each one of them:
31 |
32 | ## 1. `xcode_install`
33 |
34 | `xcode_install` used to receive an Xcode version and "install if needed", which is the new `xcodes` action's main purpose.
35 |
36 | Before:
37 |
38 | ```ruby
39 | xcode_install(
40 | version: '14',
41 | username: 'example@example.com',
42 | team_id: 'ABCD1234',
43 | download_retry_attempts: 5,
44 | )
45 | ```
46 |
47 | Now:
48 |
49 | The `team_id` and `download_retry_attempts` options are no longer needed (nor supported).
50 |
51 | ```ruby
52 | xcodes(
53 | version: '14',
54 | username: 'example@example.com',
55 | )
56 | ```
57 |
58 | ## 2. `xcversion`
59 |
60 | `xcversion` used to receive an Xcode version and select it for the current build steps, which in `xcodes` action that's the `select_for_current_build_only` option.
61 |
62 | Before:
63 |
64 | ```ruby
65 | xcversion(version: '14')
66 | ```
67 |
68 | Now:
69 |
70 | ```ruby
71 | xcodes(
72 | version: '14',
73 | select_for_current_build_only: true,
74 | )
75 | ```
76 |
77 | ## 3. `ensure_xcode_version`
78 |
79 | This action wasn't migrated to use `xcodes` within _fastlane_ yet, mainly because of the somewhat complex logic around the non-strict version checking. This document as well as fastlane's `ensure_xcode_version` action documentation will be updated when the new `xcodes` action officially deprecates the `ensure_xcode_version` action. For now, if you don't use the `strict: false` option of `ensure_xcode_version`, you can migrate to `xcodes` action by passing `select_for_current_build_only: true`, which will raise an error if the given version can't be selected:
80 |
81 | Before:
82 |
83 | ```ruby
84 | ensure_xcode_version(
85 | version: '14',
86 | strict: false,
87 | )
88 | ```
89 |
90 | ```ruby
91 | ensure_xcode_version(version: '14')
92 | ```
93 |
94 | Now:
95 |
96 | ```ruby
97 | xcodes(
98 | version: '14',
99 | select_for_current_build_only: true,
100 | )
101 | ```
102 |
103 | ## Managing Simulator Runtimes
104 |
105 | You can install them via, e.g.:
106 |
107 | ```sh
108 | xcodes runtimes install 'iOS 16.0'
109 | ```
110 |
111 | More documentation about this will be created soon, directly in the `xcodes` repository.
112 |
113 | ## Advanced Usage
114 |
115 | If there are other use cases that you don't see covered so far, check out the full documentation here: https://docs.fastlane.tools/actions/xcodes
116 |
117 | All the lanes that supported `.xcode-version` still support it :tada:
118 |
119 | # Migrating `xcode-install` CLI to `xcodes`
120 |
121 | If you're using `xcode-install` as a CLI, the process to migrate to `xcodes` is more straightforward: simply visit https://github.com/RobotsAndPencils/xcodes and check their installation and usage guide.
122 |
123 | `xcode-install` CLI supported `.xcode-version` and so does `xcodes` :tada:
124 |
125 | ## Shout Outs & Mentions
126 |
127 | Huge shout out to @neonichu, @KrauseFx, @mrcljx, @jpsim, @timsutton, and many other contributors (which you can check here: https://github.com/xcpretty/xcode-install/graphs/contributors) for the work they put into this project! It advanced the state of the art in its field, and the community benefitted a lot from it! `xcodes` wouldn't be where it is today without your effort into this project 💟
128 |
129 | Thank you all, and see you on the other side!
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Xcode::Install
2 |
3 | [](http://badge.fury.io/rb/xcode-install) [](https://github.com/xcpretty/xcode-install/actions)
4 |
5 | # This project is being sunset. See the migration guide here: [MIGRATION.md](/MIGRATION.md)
6 |
7 | Install and update your Xcodes automatically.
8 |
9 | ```
10 | $ gem install xcode-install
11 | $ xcversion install 6.3
12 | ```
13 |
14 | This tool uses the [Downloads for Apple Developer](https://developer.apple.com/download/more/) page.
15 |
16 | ## Installation
17 |
18 | ```
19 | $ gem install xcode-install
20 | ```
21 |
22 | 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:
23 |
24 | ```
25 | $ curl -sL -O https://github.com/neonichu/ruby-domain_name/releases/download/v0.5.99999999/domain_name-0.5.99999999.gem
26 | $ gem install domain_name-0.5.99999999.gem
27 | $ gem install --conservative xcode-install
28 | $ rm -f domain_name-0.5.99999999.gem
29 | ```
30 |
31 | ## Usage
32 |
33 | XcodeInstall needs environment variables with your credentials to access the Apple Developer
34 | Center, they are stored using the [credentials_manager][1] of [fastlane][2]:
35 |
36 | ```
37 | XCODE_INSTALL_USER
38 | XCODE_INSTALL_PASSWORD
39 | ```
40 |
41 | ### List
42 |
43 | To list available versions:
44 |
45 | ```
46 | $ xcversion list
47 | 6.0.1
48 | 6.1
49 | 6.1.1
50 | 6.2 (installed)
51 | 6.3
52 | ```
53 |
54 | Already installed versions are marked with `(installed)`.
55 | (Use `$ xcversion installed` to only list installed Xcodes with their path).
56 |
57 | To update the list of available versions, run:
58 |
59 | ```
60 | $ xcversion update
61 | ```
62 |
63 | ### Install
64 |
65 | To install a certain version, simply:
66 |
67 | ```
68 | $ xcversion install 8
69 | ########################################################### 82.1%
70 | ######################################################################## 100.0%
71 | Please authenticate for Xcode installation...
72 |
73 | Xcode 8
74 | Build version 6D570
75 | ```
76 |
77 | This will download and install that version of Xcode. Then you can start it from `/Applications` as usual.
78 | The new version will also be automatically selected for CLI commands (see below).
79 |
80 | #### GMs and beta versions
81 |
82 | Note: GMs and beta versions usually have special names, e.g.
83 |
84 | ```
85 | $ xcversion list
86 | 7 GM seed
87 | 7.1 beta
88 | ```
89 |
90 | They have to be installed using the full name, e.g. `xcversion install '7 GM seed'`.
91 |
92 | #### `.xcode-version`
93 |
94 | We recommend the creation of a `.xcode-version` file to explicitly declare and store the Xcode version to be used by your CI environment as well as your team.
95 |
96 | ```
97 | 12.5
98 | ```
99 |
100 | Read [the proposal](/XCODE_VERSION.md) of `.xcode-version`.
101 |
102 | ### Select
103 |
104 | To see the currently selected version, run
105 | ```
106 | $ xcversion selected
107 | ```
108 |
109 | To select a version as active, run
110 | ```
111 | $ xcversion select 8
112 | ```
113 |
114 | To select a version as active and change the symlink at `/Applications/Xcode`, run
115 | ```
116 | $ xcversion select 8 --symlink
117 | ```
118 |
119 | ### Command Line Tools
120 |
121 | XcodeInstall can also install Xcode's Command Line Tools by calling `xcversion install-cli-tools`.
122 |
123 | ### Simulators
124 |
125 | XcodeInstall can also manage your local simulators using the `simulators` command.
126 |
127 | ```
128 | $ xcversion simulators
129 | Xcode 6.4 (/Applications/Xcode-6.4.app)
130 | iOS 7.1 Simulator (installed)
131 | iOS 8.1 Simulator (not installed)
132 | iOS 8.2 Simulator (not installed)
133 | iOS 8.3 Simulator (installed)
134 | Xcode 7.2.1 (/Applications/Xcode-7.2.1.app)
135 | iOS 8.1 Simulator (not installed)
136 | iOS 8.2 Simulator (not installed)
137 | iOS 8.3 Simulator (installed)
138 | iOS 8.4 Simulator (not installed)
139 | iOS 9.0 Simulator (not installed)
140 | iOS 9.1 Simulator (not installed)
141 | tvOS 9.0 Simulator (not installed)
142 | watchOS 2.0 Simulator (installed)
143 | ```
144 |
145 | To install a simulator, use `--install` and the beginning of a simulator name:
146 |
147 | ```
148 | $ xcversion simulators --install='iOS 8.4'
149 | ########################################################### 82.1%
150 | ######################################################################## 100.0%
151 | Please authenticate to install iOS 8.4 Simulator...
152 |
153 | Successfully installed iOS 8.4 Simulator
154 | ```
155 |
156 | ## Limitations
157 |
158 | 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. 📡
159 |
160 | XcodeInstall automatically installs additional components so that it is immediately usable from the
161 | commandline. Unfortunately, Xcode will load third-party plugins even in that situation, which leads
162 | to a dialog popping up. Feel free to dupe [the radar][5]. 📡
163 |
164 | XcodeInstall normally relies on the Spotlight index to locate installed versions of Xcode. If you use it while
165 | indexing is happening, it might show inaccurate results and it will not be able to see installed
166 | versions on unindexed volumes.
167 |
168 | 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.
169 |
170 | ## Thanks
171 |
172 | Thanks to [@neonichu](https://github.com/neonichu), the original (and best) author.
173 |
174 | [This][3] downloading script which has been used for some inspiration, also [this][4]
175 | for doing the installation. Additionally, many thanks to everyone who has contributed to this
176 | project, especially [@henrikhodne][6] and [@lacostej][7] for making XcodeInstall C extension free.
177 |
178 | ## Contributing
179 |
180 | 1. Fork it ( https://github.com/xcpretty/xcode-install/fork )
181 | 2. Create your feature branch (`git checkout -b my-new-feature`)
182 | 3. Commit your changes (`git commit -am 'Add some feature'`)
183 | 4. Push to the branch (`git push origin my-new-feature`)
184 | 5. Create a new Pull Request
185 |
186 | ### Running tests
187 |
188 | ```
189 | bundle exec rake spec
190 | ```
191 |
192 | ### Running code style linter
193 |
194 | ```
195 | bundle exec rubocop -a
196 | ```
197 |
198 | ## License
199 |
200 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file.
201 |
202 | > 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.
203 |
204 | [1]: https://github.com/fastlane/fastlane/tree/master/credentials_manager#using-environment-variables
205 | [2]: http://fastlane.tools
206 | [3]: http://atastypixel.com/blog/resuming-adc-downloads-cos-safari-sucks/
207 | [4]: https://github.com/magneticbear/Jenkins_Bootstrap
208 | [5]: http://www.openradar.me/22001810
209 | [6]: https://github.com/henrikhodne
210 | [7]: https://github.com/lacostej
211 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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]
17 |
--------------------------------------------------------------------------------
/XCODE_VERSION.md:
--------------------------------------------------------------------------------
1 | # `.xcode-version`
2 |
3 | ## Introduction
4 |
5 | This is a proposal for a new standard for the iOS community: a text-based file that defines the Xcode version to use to compile and package a given iOS project.
6 |
7 | This will be used by this gem, however it's designed in a way that any tool in the future can pick it up, no matter if it's Ruby based, Swift, JavaScript, etc.
8 |
9 | Similar to the [.ruby-version file](https://en.wikipedia.org/wiki/Ruby_Version_Manager), the `.xcode-version` file allows any CI system or IDE to automatically install and switch to the Xcode version needed for a given project to successfully compile your project.
10 |
11 | ## Filename
12 |
13 | The filename must always be `.xcode-version`.
14 |
15 | ## File location
16 |
17 | The file must be located in the same directory as your Xcode project/workspace, and you should add it to your versioning system (e.g. git).
18 |
19 | ## File content
20 |
21 | The file content must be a simple string in a text file. The file may or may not end with an empty new line, this gem is responsible for stripping out the trailing `\n` (if used).
22 |
23 | ### Sample files
24 |
25 | To define an official Xcode release
26 |
27 | ```
28 | 9.3
29 | ```
30 |
31 | ```
32 | 7.2.1
33 | ```
34 |
35 | You can also use pre-releases
36 |
37 | ```
38 | 11.5 GM Seed
39 | ```
40 |
41 | ```
42 | 12 beta 6
43 | ```
44 |
45 | Always following the same version naming listed by `xcversion list`.
46 |
47 | **Note**: Be aware that pre-releases might be eventually taken down from Apple's servers, meaning that it won't allow you to have fully reproducible builds as you won't be able to download the Xcode release once it's gone.
48 |
49 | It is recommended to only use non-beta releases in an `.xcode-version` file to have fully reproducible builds that you'll be able to run in a few years also.
50 |
--------------------------------------------------------------------------------
/bin/xcversion:
--------------------------------------------------------------------------------
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)
13 |
--------------------------------------------------------------------------------
/bin/🎉:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | load File.expand_path('../xcversion', __FILE__)
4 |
--------------------------------------------------------------------------------
/lib/xcode/install.rb:
--------------------------------------------------------------------------------
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'
12 | require 'fastlane/helper/sh_helper'
13 | require 'fastlane/action'
14 | require 'fastlane/actions/verify_xcode'
15 |
16 | module XcodeInstall
17 | CACHE_DIR = Pathname.new("#{ENV['HOME']}/Library/Caches/XcodeInstall")
18 | class Curl
19 | COOKIES_PATH = Pathname.new('/tmp/curl-cookies.txt')
20 |
21 | # @param url: The URL to download
22 | # @param directory: The directory to download this file into
23 | # @param cookies: Any cookies we should use for the download (used for auth with Apple)
24 | # @param output: A PathName for where we want to store the file
25 | # @param progress: parse and show the progress?
26 | # @param progress_block: A block that's called whenever we have an updated progress %
27 | # the parameter is a single number that's literally percent (e.g. 1, 50, 80 or 100)
28 | # @param retry_download_count: A count to retry the downloading Xcode dmg/xip
29 | def fetch(url: nil,
30 | directory: nil,
31 | cookies: nil,
32 | output: nil,
33 | progress: nil,
34 | progress_block: nil,
35 | retry_download_count: 3)
36 | options = cookies.nil? ? [] : ['--cookie', cookies, '--cookie-jar', COOKIES_PATH]
37 |
38 | uri = URI.parse(url)
39 | output ||= File.basename(uri.path)
40 | output = (Pathname.new(directory) + Pathname.new(output)) if directory
41 |
42 | # Piping over all of stderr over to a temporary file
43 | # the file content looks like this:
44 | # 0 4766M 0 6835k 0 0 573k 0 2:21:58 0:00:11 2:21:47 902k
45 | # This way we can parse the current %
46 | # The header is
47 | # % Total % Received % Xferd Average Speed Time Time Time Current
48 | #
49 | # Discussion for this on GH: https://github.com/KrauseFx/xcode-install/issues/276
50 | # It was not easily possible to reimplement the same system using built-in methods
51 | # especially when it comes to resuming downloads
52 | # Piping over stderror to Ruby directly didn't work, due to the lack of flushing
53 | # from curl. The only reasonable way to trigger this, is to pipe things directly into a
54 | # local file, and parse that, and just poll that. We could get real time updates using
55 | # the `tail` command or similar, however the download task is not time sensitive enough
56 | # to make this worth the extra complexity, that's why we just poll and
57 | # wait for the process to be finished
58 | progress_log_file = File.join(CACHE_DIR, "progress.#{Time.now.to_i}.progress")
59 | FileUtils.rm_f(progress_log_file)
60 |
61 | retry_options = ['--retry', '3']
62 | command = [
63 | 'curl',
64 | '--disable',
65 | *options,
66 | *retry_options,
67 | '--location',
68 | '--continue-at',
69 | '-',
70 | '--output',
71 | output,
72 | url
73 | ].map(&:to_s)
74 |
75 | command_string = command.collect(&:shellescape).join(' ')
76 | command_string += " 2> #{progress_log_file}" # to not run shellescape on the `2>`
77 |
78 | # Run the curl command in a loop, retry when curl exit status is 18
79 | # "Partial file. Only a part of the file was transferred."
80 | # https://curl.haxx.se/mail/archive-2008-07/0098.html
81 | # https://github.com/KrauseFx/xcode-install/issues/210
82 | retry_download_count.times do
83 | wait_thr = poll_file(command_string: command_string, progress_log_file: progress_log_file, progress: progress, progress_block: progress_block)
84 | return wait_thr.value.success? if wait_thr.value.success?
85 | end
86 | false
87 | ensure
88 | FileUtils.rm_f(COOKIES_PATH)
89 | FileUtils.rm_f(progress_log_file)
90 | end
91 |
92 | def poll_file(command_string:, progress_log_file:, progress: nil, progress_block: nil)
93 | # Non-blocking call of Open3
94 | # We're not using the block based syntax, as the bacon testing
95 | # library doesn't seem to support writing tests for it
96 | stdin, stdout, stderr, wait_thr = Open3.popen3(command_string)
97 |
98 | # Poll the file and see if we're done yet
99 | while wait_thr.alive?
100 | sleep(0.5) # it's not critical for this to be real-time
101 | next unless File.exist?(progress_log_file) # it might take longer for it to be created
102 |
103 | progress_content = File.read(progress_log_file).split("\r").last || ''
104 |
105 | # Print out the progress for the CLI
106 | if progress
107 | print "\r#{progress_content}%"
108 | $stdout.flush
109 | end
110 |
111 | # Call back the block for other processes that might be interested
112 | matched = progress_content.match(/^\s*(\d+)/)
113 | next unless matched && matched.length == 2
114 | percent = matched[1].to_i
115 | progress_block.call(percent) if progress_block
116 | end
117 |
118 | # as we're not making use of the block-based syntax
119 | # we need to manually close those
120 | stdin.close
121 | stdout.close
122 | stderr.close
123 |
124 | wait_thr
125 | end
126 | end
127 |
128 | # rubocop:disable Metrics/ClassLength
129 | class Installer
130 | attr_reader :xcodes
131 |
132 | def initialize
133 | FileUtils.mkdir_p(CACHE_DIR)
134 | end
135 |
136 | def cache_dir
137 | CACHE_DIR
138 | end
139 |
140 | def current_symlink
141 | File.symlink?(SYMLINK_PATH) ? SYMLINK_PATH : nil
142 | end
143 |
144 | def download(version, progress, url = nil, progress_block = nil, retry_download_count = 3)
145 | xcode = find_xcode_version(version) if url.nil?
146 | return if url.nil? && xcode.nil?
147 |
148 | dmg_file = Pathname.new(File.basename(url || xcode.path))
149 |
150 | result = Curl.new.fetch(
151 | url: url || xcode.url,
152 | directory: CACHE_DIR,
153 | cookies: url ? nil : spaceship.cookie,
154 | output: dmg_file,
155 | progress: progress,
156 | progress_block: progress_block,
157 | retry_download_count: retry_download_count
158 | )
159 | result ? CACHE_DIR + dmg_file : nil
160 | end
161 |
162 | def find_xcode_version(version)
163 | # By checking for the name and the version we have the best success rate
164 | # Sometimes the user might pass
165 | # "4.3 for Lion"
166 | # or they might pass an actual Gem::Version
167 | # Gem::Version.new("8.0.0")
168 | # which should automatically match with "Xcode 8"
169 |
170 | begin
171 | parsed_version = Gem::Version.new(version)
172 | rescue ArgumentError
173 | nil
174 | end
175 |
176 | seedlist.each do |current_seed|
177 | return current_seed if current_seed.name == version
178 | end
179 |
180 | seedlist.each do |current_seed|
181 | return current_seed if parsed_version && current_seed.version == parsed_version
182 | end
183 |
184 | nil
185 | end
186 |
187 | def exist?(version)
188 | return true if find_xcode_version(version)
189 | false
190 | end
191 |
192 | def installed?(version)
193 | installed_versions.map(&:version).include?(version)
194 | end
195 |
196 | def installed_versions
197 | installed.map { |x| InstalledXcode.new(x) }.sort do |a, b|
198 | Gem::Version.new(a.version) <=> Gem::Version.new(b.version)
199 | end
200 | end
201 |
202 | # Returns an array of `XcodeInstall::Xcode`
203 | # ,
210 | #
211 | # the resulting list is sorted with the most recent release as first element
212 | def seedlist
213 | @xcodes = Marshal.load(File.read(LIST_FILE)) if LIST_FILE.exist? && xcodes.nil?
214 | all_xcodes = (xcodes || fetch_seedlist)
215 |
216 | # We have to set the `installed` value here, as we might still use
217 | # the cached list of available Xcode versions, but have a new Xcode
218 | # installed in the mean-time
219 | cached_installed_versions = installed_versions.map(&:bundle_version)
220 | all_xcodes.each do |current_xcode|
221 | current_xcode.installed = cached_installed_versions.include?(current_xcode.version)
222 | end
223 |
224 | all_xcodes.sort_by { |seed| [seed.version, -seed.date_modified] }.reverse
225 | end
226 |
227 | def install_dmg(dmg_path, suffix = '', switch = true, clean = true)
228 | prompt = "Please authenticate for Xcode installation.\nPassword: "
229 | xcode_path = "/Applications/Xcode#{suffix}.app"
230 |
231 | if dmg_path.extname == '.xip'
232 | `xip -x #{dmg_path}`
233 | xcode_orig_path = File.join(Dir.pwd, 'Xcode.app')
234 | xcode_beta_path = File.join(Dir.pwd, 'Xcode-beta.app')
235 | if Pathname.new(xcode_orig_path).exist?
236 | `sudo -p "#{prompt}" mv "#{xcode_orig_path}" "#{xcode_path}"`
237 | elsif Pathname.new(xcode_beta_path).exist?
238 | `sudo -p "#{prompt}" mv "#{xcode_beta_path}" "#{xcode_path}"`
239 | else
240 | out = <<-HELP
241 | No `Xcode.app(or Xcode-beta.app)` found in XIP. Please remove #{dmg_path} if you
242 | suspect a corrupted download or run `xcversion update` to see if the version
243 | you tried to install has been pulled by Apple. If none of this is true,
244 | please open a new GH issue.
245 | HELP
246 | $stderr.puts out.tr("\n", ' ')
247 | return
248 | end
249 | else
250 | mount_dir = mount(dmg_path)
251 | source = Dir.glob(File.join(mount_dir, 'Xcode*.app')).first
252 |
253 | if source.nil?
254 | out = <<-HELP
255 | No `Xcode.app` found in DMG. Please remove #{dmg_path} if you suspect a corrupted
256 | download or run `xcversion update` to see if the version you tried to install
257 | has been pulled by Apple. If none of this is true, please open a new GH issue.
258 | HELP
259 | $stderr.puts out.tr("\n", ' ')
260 | return
261 | end
262 |
263 | `sudo -p "#{prompt}" ditto "#{source}" "#{xcode_path}"`
264 | `umount "/Volumes/Xcode"`
265 | end
266 |
267 | xcode = InstalledXcode.new(xcode_path)
268 |
269 | unless xcode.verify_integrity
270 | `sudo rm -rf #{xcode_path}`
271 | return
272 | end
273 |
274 | enable_developer_mode
275 | xcode.approve_license
276 | xcode.install_components
277 |
278 | if switch
279 | `sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
280 | `sudo ln -sf #{xcode_path} #{SYMLINK_PATH}` unless SYMLINK_PATH.exist?
281 |
282 | `sudo xcode-select --switch #{xcode_path}`
283 | puts `xcodebuild -version`
284 | end
285 |
286 | FileUtils.rm_f(dmg_path) if clean
287 | end
288 |
289 | # rubocop:disable Metrics/ParameterLists
290 | def install_version(version, switch = true, clean = true, install = true, progress = true, url = nil, show_release_notes = true, progress_block = nil, retry_download_count = 3)
291 | dmg_path = get_dmg(version, progress, url, progress_block, retry_download_count)
292 | fail Informative, "Failed to download Xcode #{version}." if dmg_path.nil?
293 |
294 | if install
295 | install_dmg(dmg_path, "-#{version.to_s.split(' ').join('.')}", switch, clean)
296 | else
297 | puts "Downloaded Xcode #{version} to '#{dmg_path}'"
298 | end
299 |
300 | open_release_notes_url(version) if show_release_notes && !url
301 | end
302 |
303 | def open_release_notes_url(version)
304 | return if version.nil?
305 | xcode = seedlist.find { |x| x.name == version }
306 | `open #{xcode.release_notes_url}` unless xcode.nil? || xcode.release_notes_url.nil?
307 | end
308 |
309 | def list_annotated(xcodes_list)
310 | installed = installed_versions.map(&:appname_version)
311 |
312 | xcodes_list.map do |x|
313 | xcode_version = x.split(' ') # split version and "beta N", "for Lion"
314 | xcode_version[0] << '.0' unless xcode_version[0].include?('.')
315 |
316 | # to match InstalledXcode.appname_version format
317 | version = Gem::Version.new(xcode_version.join('.'))
318 |
319 | installed.include?(version) ? "#{x} (installed)" : x
320 | end.join("\n")
321 | end
322 |
323 | def list
324 | list_annotated(list_versions.sort { |first, second| compare_versions(first, second) })
325 | end
326 |
327 | def rm_list_cache
328 | FileUtils.rm_f(LIST_FILE)
329 | end
330 |
331 | def symlink(version)
332 | xcode = installed_versions.find { |x| x.version == version }
333 | `sudo rm -f #{SYMLINK_PATH}` unless current_symlink.nil?
334 | `sudo ln -sf #{xcode.path} #{SYMLINK_PATH}` unless xcode.nil? || SYMLINK_PATH.exist?
335 | end
336 |
337 | def symlinks_to
338 | File.absolute_path(File.readlink(current_symlink), SYMLINK_PATH.dirname) if current_symlink
339 | end
340 |
341 | def mount(dmg_path)
342 | plist = hdiutil('mount', '-plist', '-nobrowse', '-noverify', dmg_path.to_s)
343 | document = REXML::Document.new(plist)
344 | node = REXML::XPath.first(document, "//key[.='mount-point']/following-sibling::*[1]")
345 | fail Informative, 'Failed to mount image.' unless node
346 | node.text
347 | end
348 |
349 | private
350 |
351 | def spaceship
352 | @spaceship ||= begin
353 | begin
354 | Spaceship.login(ENV['XCODE_INSTALL_USER'], ENV['XCODE_INSTALL_PASSWORD'])
355 | rescue Spaceship::Client::InvalidUserCredentialsError
356 | raise 'The specified Apple developer account credentials are incorrect.'
357 | rescue Spaceship::Client::NoUserCredentialsError
358 | raise <<-HELP
359 | Please provide your Apple developer account credentials via the
360 | XCODE_INSTALL_USER and XCODE_INSTALL_PASSWORD environment variables.
361 | HELP
362 | end
363 |
364 | if ENV.key?('XCODE_INSTALL_TEAM_ID')
365 | Spaceship.client.team_id = ENV['XCODE_INSTALL_TEAM_ID']
366 | end
367 | Spaceship.client
368 | end
369 | end
370 |
371 | LIST_FILE = CACHE_DIR + Pathname.new('xcodes.bin')
372 | MINIMUM_VERSION = Gem::Version.new('4.3')
373 | SYMLINK_PATH = Pathname.new('/Applications/Xcode.app')
374 |
375 | def enable_developer_mode
376 | `sudo /usr/sbin/DevToolsSecurity -enable`
377 | `sudo /usr/sbin/dseditgroup -o edit -t group -a staff _developer`
378 | end
379 |
380 | def get_dmg(version, progress = true, url = nil, progress_block = nil, retry_download_count = 3)
381 | if url
382 | path = Pathname.new(url)
383 | return path if path.exist?
384 | end
385 | if ENV.key?('XCODE_INSTALL_CACHE_DIR')
386 | Pathname.glob(ENV['XCODE_INSTALL_CACHE_DIR'] + '/*').each do |fpath|
387 | return fpath if /^xcode_#{version}\.dmg|xip$/ =~ fpath.basename.to_s
388 | end
389 | end
390 |
391 | download(version, progress, url, progress_block, retry_download_count)
392 | end
393 |
394 | def fetch_seedlist
395 | @xcodes = parse_seedlist(spaceship.send(:request, :post,
396 | '/services-account/QH65B2/downloadws/listDownloads.action').body)
397 |
398 | names = @xcodes.map(&:name)
399 | @xcodes += prereleases.reject { |pre| names.include?(pre.name) }
400 |
401 | File.open(LIST_FILE, 'wb') do |f|
402 | f << Marshal.dump(xcodes)
403 | end
404 |
405 | xcodes
406 | end
407 |
408 | def installed
409 | result = `mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'" 2>/dev/null`.split("\n")
410 | if result.empty?
411 | result = `find /Applications -maxdepth 1 -name '*.app' -type d -exec sh -c \
412 | 'if [ "$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
413 | "{}/Contents/Info.plist" 2>/dev/null)" == "com.apple.dt.Xcode" ]; then echo "{}"; fi' ';'`.split("\n")
414 | end
415 | result
416 | end
417 |
418 | def parse_seedlist(seedlist)
419 | fail Informative, seedlist['resultString'] unless seedlist['resultCode'].eql? 0
420 |
421 | seeds = Array(seedlist['downloads']).select do |t|
422 | /^Xcode [0-9]/.match(t['name'])
423 | end
424 |
425 | xcodes = seeds.map { |x| Xcode.new(x) }.reject { |x| x.version < MINIMUM_VERSION }.sort do |a, b|
426 | a.date_modified <=> b.date_modified
427 | end
428 |
429 | xcodes.select { |x| x.url.end_with?('.dmg') || x.url.end_with?('.xip') }
430 | end
431 |
432 | def list_versions
433 | seedlist.map(&:name)
434 | end
435 |
436 | def prereleases
437 | body = spaceship.send(:request, :get, '/download/').body
438 |
439 | links = body.scan(%r{(.*)})
440 | links = links.map do |link|
441 | parent = link[0].scan(%r{path=(/.*/.*/)}).first.first
442 | match = body.scan(/#{Regexp.quote(parent)}(.+?.pdf)/).first
443 | if match
444 | link + [parent + match.first]
445 | else
446 | link + [nil]
447 | end
448 | end
449 | links = links.map { |pre| Xcode.new_prerelease(pre[1].strip.tr('_', ' '), pre[0], pre[4]) }
450 |
451 | if links.count.zero?
452 | rg = %r{platform-title.*Xcode.* beta.*<\/p>}
453 | scan = body.scan(rg)
454 |
455 | if scan.count.zero?
456 | rg = %r{Xcode.* GM.*<\/p>}
457 | scan = body.scan(rg)
458 | end
459 |
460 | return [] if scan.empty?
461 |
462 | version = scan.first.gsub(/<.*?>/, '').gsub(/.*Xcode /, '')
463 | link = body.scan(%r{