#!/bin/bash # Pin bazel to the Xcode that `xcode-select` points at. # # Bazel finds Xcodes through LaunchServices, and on macOS 27 LaunchServices reports only the # newest installed Xcode. With a beta next to the release Xcode bazel silently takes the beta: # wrong Info.plist stamps, a different cache key for every action, and a stub Metal toolchain. # (Same problem and fix as exteraGram's XTGXcodeConfig.sh on this Mac.) # # Writes two gitignored files: # build-system/GGXcode/BUILD explicit xcode_version + xcode_config for the selected Xcode # build-system/GGXcode.bazelrc --xcode_version_config and DEVELOPER_DIR for every action, # XCODE_VERSION for repository rules and module extensions # `.bazelrc` try-imports the rc file, so a checkout without it falls back to bazel's detection. # The strip action does not take the shell env; build-system/GGPatches/ fixes that in rules_apple. set -euo pipefail cd "$(dirname "$0")/.." OUT_DIR="build-system/GGXcode" OUT_RC="build-system/GGXcode.bazelrc" developer_dir="$(xcode-select -p)" run() { env -i DEVELOPER_DIR="$developer_dir" PATH=/usr/bin:/bin "$@"; } version_output="$(run xcodebuild -version)" short="$(printf '%s\n' "$version_output" | sed -n 's/^Xcode //p' | head -1)" build="$(printf '%s\n' "$version_output" | sed -n 's/^Build version //p' | head -1)" if [ -z "$short" ] || [ -z "$build" ]; then echo "GGXcodeConfig: cannot read 'xcodebuild -version' for $developer_dir" >&2 exit 1 fi # bazel names a version major.minor.patch.build and accepts every shorter spelling as an alias. IFS=. read -r major minor patch <<< "$short" expanded="${major}.${minor:-0}.${patch:-0}" full="${expanded}.${build}" name="version$(printf '%s' "$full" | tr . _)" aliases="" for alias in "$full" "$build" "$short" "$expanded" "$major"; do case ",$aliases," in *",'$alias',"*) continue ;; esac aliases="${aliases:+$aliases, }'$alias'" done sdk_version() { run xcrun --sdk "$1" --show-sdk-version 2>/dev/null || true; } mkdir -p "$OUT_DIR" { echo "# Generated by build-system/GGXcodeConfig.sh for $developer_dir — do not edit." echo "load('@build_bazel_apple_support//xcode:xcode_config.bzl', 'xcode_config')" echo "load('@build_bazel_apple_support//xcode:xcode_version.bzl', 'xcode_version')" echo echo "package(default_visibility = ['//visibility:public'])" echo echo "xcode_version(" echo " name = '$name'," echo " version = '$full'," echo " aliases = [$aliases]," for pair in ios:iphoneos tvos:appletvos macos:macosx visionos:xros watchos:watchos; do sdk="$(sdk_version "${pair##*:}")" [ -n "$sdk" ] && echo " default_${pair%%:*}_sdk_version = '$sdk'," done echo ")" echo echo "xcode_config(" echo " name = 'host_xcodes'," echo " versions = [':$name']," echo " default = ':$name'," echo ")" } > "$OUT_DIR/BUILD" { echo "# Generated by build-system/GGXcodeConfig.sh — do not edit." echo "build --xcode_version_config=//build-system/GGXcode:host_xcodes" echo "build --action_env=DEVELOPER_DIR=$developer_dir" # rules_swift's system_sdk extension lists the installed Xcodes once and keeps the list until # DEVELOPER_DIR or XCODE_VERSION changes. An update in place (App Store: Xcode.app 27.1 -> 27.2, # 2026-10-31) keeps the path, so the stale list matched no Xcode and analysis failed. The version # here makes every Xcode update re-run it. echo "build --repo_env=XCODE_VERSION=$full" } > "$OUT_RC" echo "xcode : $full ($developer_dir) -> $OUT_DIR/BUILD, $OUT_RC"