Managing multiple Xcode versions on CI using Fastlane

Sponsored
RevenueCat logo
RevenueCat Paywalls

Add paywalls to your iOS app's in one line of code! With RevenueCat Paywalls you can remotely configure and edit your entire paywall view without waiting on App Review.

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.

fastlane/Fastfile
# 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:

Terminal
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:

fastlane/Fastfile
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 passing 14.1, and a pre-release version by passing 14.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:

.xcode-version
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:

fastlane/Fastfile
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 as sudo 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.

fastlane/Fastfile
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:

fastlane/Fastfile
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.

fastlane/Fastfile
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.

fastlane/Fastfile
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.

fastlane/Fastfile
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