Change the Xcode version fallback behavior (#1092)

* Change the Xcode version fallback behavior

If a requested version of Xcode is not available on the CI machine, the
default Xcode version is automatically used instead and a warning
message is printed in the build log.

* Address code review feedback
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 2671b99..76b2ea9 100755
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -18,7 +18,7 @@
 import base64
 import codecs
 import datetime
-import glob
+from glob import glob
 import hashlib
 import json
 import multiprocessing
@@ -1196,25 +1196,43 @@
 
 def activate_xcode(task_config):
     # 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))
+    wanted_xcode_version = task_config.get("xcode_version", DEFAULT_XCODE_VERSION)
+    print_collapsed_group(":xcode: Activating Xcode {}...".format(wanted_xcode_version))
 
     # Ensure it's a valid version number.
-    if not isinstance(xcode_version, str):
+    if not isinstance(wanted_xcode_version, str):
         raise BuildkiteException(
             "Version number '{}' is not a string. Did you forget to put it in quotes?".format(
-                xcode_version
+                wanted_xcode_version
             )
         )
-    if not XCODE_VERSION_REGEX.match(xcode_version):
+    if not XCODE_VERSION_REGEX.match(wanted_xcode_version):
         raise BuildkiteException(
             "Invalid Xcode version format '{}', must match the format X.Y[.Z].".format(
-                xcode_version
+                wanted_xcode_version
             )
         )
 
     # This is used to replace e.g. 11.2 with 11.2.1 without having to update all configs.
-    xcode_version = XCODE_VERSION_OVERRIDES.get(xcode_version, xcode_version)
+    xcode_version = XCODE_VERSION_OVERRIDES.get(wanted_xcode_version, wanted_xcode_version)
+
+    # This falls back to a default version if the selected version is not available.
+    supported_versions = sorted(
+        # Stripping "Xcode" prefix and ".app" suffix from e.g. "Xcode12.0.1.app" leaves just the version number.
+        [os.path.basename(x)[5:-4] for x in glob("/Applications/Xcode*.app")], reverse=True
+    )
+    if xcode_version not in supported_versions:
+        xcode_version = DEFAULT_XCODE_VERSION
+    if xcode_version != wanted_xcode_version:
+        print_collapsed_group(
+            ":xcode: Fixed Xcode version: {} -> {}...".format(wanted_xcode_version, xcode_version)
+        )
+        lines = [
+            "Your selected Xcode version {} was not available on the machine.".format(wanted_xcode_version),
+            "Bazel CI automatically picked a fallback version: {}.".format(xcode_version),
+            "Available versions are: {}.".format(supported_versions),
+        ]
+        execute_command(["buildkite-agent", "annotate", "--style=warning", "\n".join(lines), "--context", "ctx-xcode_version_fixed"])
 
     # Check that the selected Xcode version is actually installed on the host.
     xcode_path = "/Applications/Xcode{}.app".format(xcode_version)
@@ -1315,7 +1333,7 @@
 def merge_and_upload_kythe_kzip(platform, index_upload_gcs):
     print_collapsed_group(":gcloud: Uploading kythe kzip")
 
-    kzips = glob.glob("bazel-out/*/extra_actions/**/*.kzip", recursive=True)
+    kzips = glob("bazel-out/*/extra_actions/**/*.kzip", recursive=True)
 
     build_number = os.getenv("BUILDKITE_BUILD_NUMBER")
     git_commit = os.getenv("BUILDKITE_COMMIT")