Keith Smiley | About | RSS | Mastodon

Locking Xcode versions in bazel

When using bazel on a team, one of the things you quickly want to do is stand up a remote cache. This allows bazel to download build artifacts instead of spending CPU cycles reproducing things that have already been built by someone else.

In order for bazel to guarantee that downloading the artifacts instead of building them will produce the same results, it must ensure that all the inputs of your build are the same as a previous build.1 For macOS and iOS builds bazel's inputs include the version of Xcode you're using. This means if developers on your team use different versions of Xcode, they cannot share the same build cache.

Bazel discovers your currently installed Xcode versions by running xcode_locator, and then generating a BUILD file that contains an entry for every version you currently have installed. The result looks something like this:2

xcode_version(
    name = "version12_4_0_12D4e",
    version = "12.4.0.12D4e",
    aliases = ["12.4.0", "12.4", "12.4.0.12D4e"],
    default_ios_sdk_version = "14.4",
    default_tvos_sdk_version = "14.3",
    default_macos_sdk_version = "11.1",
    default_watchos_sdk_version = "7.2",
)

xcode_version(
    name = "version12_2_0_12B45b",
    version = "12.2.0.12B45b",
    aliases = ["12.2.0", "12", "12.2", "12.2.0.12B45b"],
    default_ios_sdk_version = "14.2",
    default_tvos_sdk_version = "14.2",
    default_macos_sdk_version = "11.0",
    default_watchos_sdk_version = "7.1",
)

xcode_config(
    name = "host_xcodes",
    versions = [":version12_4_0_12D4e", ":version12_2_0_12B45b"],
    default = ":version12_4_0_12D4e",
)

To fetch the contents of this file on your machine you can run:

cat bazel-$(basename $PWD)/external/local_config_xcode/BUILD

In order to enforce developers use the same version, you can short circuit bazel's Xcode discovery and instead reference a local target that you provide.3

To do this, you can setup your target in the BUILD file at the root of your project (or somewhere else if you'd prefer). Using the contents from the example above, but only including the Xcode versions you want to support, it will contain:

xcode_version(
    name = "version12_4_0_12D4e",
    version = "12.4.0.12D4e",
    aliases = ["12.4.0", "12.4", "12.4.0.12D4e"],
    default_ios_sdk_version = "14.4",
    default_tvos_sdk_version = "14.3",
    default_macos_sdk_version = "11.1",
    default_watchos_sdk_version = "7.2",
)

xcode_config(
    name = "host_xcodes",
    versions = [":version12_4_0_12D4e"],
    default = ":version12_4_0_12D4e",
)

Then you can add this to your .bazelrc:

build --xcode_version_config=//:host_xcodes

This way if a developer tries to build with a version of Xcode that is not explicitly supported, they see this error:

ERROR: /.../BUILD.bazel:31:11: Compiling something failed: I/O exception during sandboxed execution: Running '/.../xcode-locator 12.4.0.12D4e' failed with code 1.
This most likely indicates that xcode version 12.4.0.12D4e is not available on the host machine.

This is a simple solution if you only want to support a single version of Xcode at once. Often it's useful to support multiple Xcode versions for testing, even if not all versions will get cache hits. In this case, one solution is to include multiple versions in your BUILD file:

xcode_version(
    name = "version12_4_0_12D4e",
    version = "12.4.0.12D4e",
    aliases = ["12D4e"],
    default_ios_sdk_version = "14.4",
    default_tvos_sdk_version = "14.3",
    default_macos_sdk_version = "11.1",
    default_watchos_sdk_version = "7.2",
)

xcode_version(
    name = "version12_2_0_12B45b",
    version = "12.2.0.12B45b",
    aliases = ["12B45b"],
    default_ios_sdk_version = "14.2",
    default_tvos_sdk_version = "14.2",
    default_macos_sdk_version = "11.0",
    default_watchos_sdk_version = "7.1",
)

xcode_config(
    name = "host_xcodes",
    versions = [":version12_4_0_12D4e", ":version12_2_0_12B45b"],
    default = ":version12_4_0_12D4e",
)

Which is very similar to our first example, except we replaced the aliases with the build numbers from each version. This is a perfect unique value to differentiate between multiple versions during the Xcode beta cycle. Now we can explicitly pass the xcode_version argument with the build number you want to use. You can retrieve the build number from your current Xcode version like this:

% xcodebuild -version | tail -1 | cut -d " " -f3
12D4e

Using this method you can support multiple versions of Xcode, while still respecting the user's xcode-select value (or DEVELOPER_DIR environment variable).

  1. Debugging remote cache misses is something you'll need to get used to doing. This documentation is very helpful. 

  2. This example doesn't include the remote execution configuration, but it works similarity 

  3. This also improves repository setup time. Otherwise it increases with the number of Xcode versions you have installed.