Add support for testing against a specific Xcode version (#609)

* Add support for testing against a specific Xcode version

* Protect against cache poisoning by including the Xcode version and path in the host_platform_remote_properties_override
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 27c0928..20152f7 100644
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -393,6 +393,9 @@
 # The platform used for various steps (e.g. stuff that formerly ran on the "pipeline" workers).
 DEFAULT_PLATFORM = "ubuntu1804"
 
+DEFAULT_XCODE_VERSION = "10.2"
+XCODE_VERSION_REGEX = re.compile(r"^\d+\.\d+(\.\d+)?$")
+
 ENCRYPTED_SAUCELABS_TOKEN = """
 CiQAry63sOlZtTNtuOT5DAOLkum0rGof+DOweppZY1aOWbat8zwSTQAL7Hu+rgHSOr6P4S1cu4YG
 /I1BHsWaOANqUgFt6ip9/CUGGJ1qggsPGXPrmhSbSPqNAIAkpxYzabQ3mfSIObxeBmhKg2dlILA/
@@ -641,6 +644,37 @@
     tmpdir = tempfile.mkdtemp()
     sc_process = None
     try:
+        # Activate the correct Xcode version on macOS machines.
+        if platform == "macos":
+            # Get the Xcode version from the config.
+            xcode_version = task_config.get("xcode_version", DEFAULT_XCODE_VERSION)
+            print_collapsed_group("Activating Xcode {}...".format(xcode_version))
+
+            # Ensure it's a valid version number.
+            if not isinstance(xcode_version, str):
+                raise BuildkiteException(
+                    "Version number '{}' is not a string. Did you forget to put it in quotes?".format(
+                        xcode_version
+                    )
+                )
+            if not XCODE_VERSION_REGEX.match(xcode_version):
+                raise BuildkiteException(
+                    "Invalid Xcode version format '{}', must match the format X.Y[.Z].".format(
+                        xcode_version
+                    )
+                )
+
+            # Check that the selected Xcode version is actually installed on the host.
+            xcode_path = "/Applications/Xcode {}.app".format(xcode_version)
+            if not os.path.exists(xcode_path):
+                raise BuildkiteException("Xcode not found at '{}'.".format(xcode_path))
+
+            # Now activate the specified Xcode version and let it install its required components.
+            # The CI machines have a sudoers config that allows the 'buildkite' user to run exactly
+            # these two commands, so don't change them without also modifying the file there.
+            execute_command(["/usr/bin/sudo", "/usr/bin/xcode-select", "--switch", xcode_path])
+            execute_command(["/usr/bin/sudo", "/usr/bin/xcodebuild", "-runFirstLaunch"])
+
         # If the CI worker runs Bazelisk, we need to forward all required env variables to the test.
         # Otherwise any integration test that invokes Bazel (=Bazelisk in this case) will fail.
         test_env_vars = ["LocalAppData"] if platform == "windows" else ["HOME"]
@@ -1029,6 +1063,20 @@
     ]:
         return []
 
+    platform_cache_key = [platform.encode("utf-8")]
+    if platform == "macos":
+        # macOS version:
+        platform_cache_key.append(subprocess.check_output(["/usr/bin/sw_vers", "-productVersion"]))
+        # Path to Xcode:
+        platform_cache_key.append(subprocess.check_output(["/usr/bin/xcode-select", "-p"]))
+        # Xcode version:
+        platform_cache_key.append(subprocess.check_output(["/usr/bin/xcodebuild", "-version"]))
+
+    platform_cache_digest = hashlib.sha256()
+    for key in platform_cache_key:
+        platform_cache_digest.update(key)
+        platform_cache_digest.update(b":")
+
     flags = [
         "--remote_timeout=60",
         # TODO(ulfjack): figure out how to resolve
@@ -1037,7 +1085,7 @@
         "--disk_cache=",
         "--remote_max_connections=200",
         '--host_platform_remote_properties_override=properties:{name:"platform" value:"%s"}'
-        % platform,
+        % platform_cache_digest.hexdigest(),
     ]
 
     if platform == "macos":