Managing multiple Xcode versions on CI using Fastlane
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
The 2.211.0 release of Fastlane comes with a brand new action: xcodes. This new action, implemented by @rogerluan_, allows you to install, select and manage multiple versions of Xcode directly from Fastlane.
As well as introducing a new action, this release deprecates the existing xcode-install and xcversion actions too.
This post will explore the current state of Xcode version management on CI using Fastlane, and will explain how the new xcodes action works.
Selecting a version of Xcode
Selecting a version of Xcode at the beginning of Fastlane’s execution can be a very useful way of making your workflows as portable and reusable as possible for you and your team.
Fastlane usually runs in a CI environment, which means that you want to make sure that selecting an Xcode version does not meddle with the system’s settings and does not require any manual intervention, such as asking for sudo permissions.
Both the existing xcode_select and the new xcodes actions are able to achieve this by setting the DEVELOPER_DIR
environment variable to the path of an Xcode application.
Contrary to what running xcode-select -s
does, setting the DEVELOPER_DIR
environment variable does not require root permissions and only affects the current shell session.
Using xcode_select
The xcode_select action is the simplest and less intrusive way of selecting an Xcode version. The action requires the full path to an Xcode application (e.g. /Applications/Xcode_14.1.app
) and it uses this path to set the DEVELOPER_DIR
environment variable.
# Set the version before every lane...
before_all do
xcode_select("/Applications/Xcode-14.1.app")
end
A big downside to this approach is that, since xcode_select requires a full path to the Xcode application, you need to have a strict naming convention for all Xcode installations across all your CI runners.
Even if you don’t use self-hosted runners, if you want your lanes to work locally, you need to ensure that the path to your local Xcode installation matches that of the CI runner.
Using xcodes
The alternative to using xcode_select now that both xcode-install and xcversion are depecrated, is the xcodes action. This action is a wrapper around the xcodes CLI, an application I have used for a very long time to manage Xcode versions on my machine.
The downside to this new lane is that it does not work out-of-the-box as it relies on the system having the xcodes cli installed.
Installing xcodes
The xcodes command line tool is available on Homebrew, and can be installed by running:
brew install robotsandpencils/made/xcodes
Selecting an installed version using xcodes
Similarily to the way xcode_select works, xcodes can be used to select a version of Xcode which is already installed in the system.
The xcodes action uses a version parameter instead of a full path to a .app
file and finds the correct Xcode application in the system from the given version.
By default, the action will install the queried version of Xcode if it can’t be found in the system and select it system-wide. This behaviour can be disabled by setting the select_for_current_build_only
parameter to true
, which I would thoroughly recommend:
before_all do
xcodes(version: "14.1", select_for_current_build_only: true)
end
Note that the
xcodes
action expects a valid RubyGems style requirements for the version parameter. For example, you would select a stable version of Xcode by passing14.1
, and a pre-release version by passing14.1.beta.3
.
Creating an .xcode-version
file
The xcodes
action can also be used to select a version of Xcode based on the contents of a .xcode-version
file at the root of your project. This is a very common pattern in other version managers such as rbenv, nvm and swiftenv.
Let’s consider the following .xcode-version
file:
14.1
You can then modify the code in the previous section to use the version specified in the .xcode-version
file by not passing the version
parameter to the xcodes action:
before_all do
xcodes(select_for_current_build_only: true)
end
Install if needed
Now that we’ve seen how you can select a version of Xcode that is already installed in the system, let’s see how you can install one that’s not available locally.
Note that running the action without
select_for_current_build_only
requires authentication with Apple’s Developer Portal as well assudo
permissions to change the selected Xcode version system-wide.
To make the xcodes action install a version of Xcode if it can’t be found in the system, simply set the select_for_current_build_only
parameter to false
(or don’t pass it at all as it defaults to false
).
You must be aware that this has a potentially undesirable side-effect: the action will select the newly installed version of Xcode system-wide, requiring sudo permissions.
lane :install_xcode do |values|
# Specifying the version directly
xcodes(version: values[:version])
# Or using a .xcode-version file
xcodes
end
The list of available Xcode versions to download is always updated before a new version is installed. If you don’t want the list to be updated, you can set the update_list
parameter to false
:
lane :install_xcode do |values|
# Specifying the version directly
xcodes(version: values[:version], update_list: false)
# Or using a .xcode-version file
xcodes(update_list: false)
end
Going the extra mile 🏃♂️
If you’d like to get some extra validation in place and make sure that the selected version of Xcode is the one you expect, there are some extra actions you can use.
Ensuring the selected version of Xcode is correct
You can verify that the correct version of Xcode has indeed been selected by using the ensure_xcode_version action.
This action takes in a version string as a parameter and fails the build if the selected version of Xcode does not match the input string.
before_all do
# Select the version of Xcode for this build only
xcodes(version: "14.1", select_for_current_build_only: true)
# Make sure that the correct version of Xcode is selected
ensure_xcode_version(version: "14.1")
end
It is also worth mentioning that, similarly to the way the xcodes action works, ensure_xcode_version looks for a version string in a .xcode-version
file at the root of the repository only if the version
parameter isn’t present in the action call.
before_all do
# Select the version of Xcode for this build only
# Uses `.xcode-version` file
xcodes(select_for_current_build_only: true)
# Make sure that the correct version of Xcode is selected
# Uses `.xcode-version` file
ensure_xcode_version
end
Validate the newly installed version of Xcode
If you are installing a new version of Xcode using the xcodes action, you can use verify_xcode to ensure that the newly installed version of Xcode is properly signed by Apple.
The action can be passed a path to the Xcode application to verify, or alternatively it can use the currently selected version of Xcode if the xcode_path
parameter is omitted.
lane :install_xcode do |values|
# Install and select an Xcode version
xcodes(version: values[:version])
# 1️⃣ Verify the newly installed version of Xcode
# Gets the path from the environment
verify_xcode
# 2️⃣ Verify the newly installed version of Xcode
# Uses the path parameter
verify_xcode(xcode_path: "/Applications/Xcode.14.1.app")
end