Managing multiple Xcode versions on CI using Fastlane
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.
[fastlane] 2.211.0 📱 Support for more phone sizes in deliver ✅ Now using Xcodes app for checking and installing versions of Xcode 💪 Match can now use Apple Silicon Macs in iOS/iPadOS profiles 🚀 New spaceship APIs for interacting with resolution center … and more!
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.
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.
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.
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
xcodesaction expects a valid RubyGems style requirements for the version parameter. For example, you would select a stable version of Xcode by passing
14.1, and a pre-release version by passing
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
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_onlyrequires authentication with Apple's Developer Portal as well as
sudopermissions 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
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
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
iOS CI Newsletter