Created
August 18, 2026 20:50
-
-
Save leogdion/70857d278be7e2c8e7690aaf977dd3ae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # deploy-xcode.sh — install Xcode + simulator runtimes on a remote Mac, | |
| # pulling the files from another machine. | |
| # | |
| # The xip extracts to Xcode.app (release) or Xcode-beta.app (beta); | |
| # whatever comes out overwrites that same app in /Applications. | |
| # | |
| # Usage: | |
| # ./deploy-xcode.sh <target-machine> <source-machine> <source-path> | |
| # | |
| # target-machine ssh host of the Mac to install Xcode ON | |
| # source-machine ssh host of the Mac that HAS the files | |
| # (pass "local" if the files are on the target itself) | |
| # source-path folder on the source machine containing the files | |
| # | |
| # Examples: | |
| # ./deploy-xcode.sh macmini1 macbookair /Users/leo/Downloads | |
| # ./deploy-xcode.sh macmini2 local ~/Downloads | |
| # | |
| # Files expected in the folder (any xip filename; newest is used): | |
| # Xcode_27_beta_5.xip | |
| # iphonesimulator_*.exportedBundle, watchsimulator_*, appletvsimulator_*, | |
| # xrsimulator_* (and/or legacy *.dmg runtime exports) | |
| # | |
| # Cross-machine copies go THROUGH this machine (scp -3), so only the | |
| # machine you run this from needs ssh access to both ends. | |
| set -euo pipefail | |
| usage="usage: deploy-xcode.sh <target-machine> <source-machine> <source-path>" | |
| TARGET="${1:?$usage}" | |
| SRC_HOST="${2:?$usage}" | |
| SRC_PATH="${3:?$usage}" | |
| [ "$SRC_HOST" = "local" ] || [ "$SRC_HOST" = "$TARGET" ] && SRC_HOST="" | |
| # ---- Locate newest xip at the source --------------------------------------- | |
| list_host="${SRC_HOST:-$TARGET}" | |
| echo "==> Locating newest xip in ${list_host}:${SRC_PATH}" | |
| XIP_AT_SRC=$(ssh "$list_host" "d=\"${SRC_PATH/#\~/\$HOME}\"; ls -t \"\$d\"/*.xip 2>/dev/null | head -n1") || true | |
| [ -n "$XIP_AT_SRC" ] || { echo "No .xip found at ${list_host}:${SRC_PATH}" >&2; exit 1; } | |
| echo "==> Using $(basename "$XIP_AT_SRC")" | |
| # ---- Copy files to the target if they're on another machine ---------------- | |
| if [ -n "$SRC_HOST" ]; then | |
| STAGE="/tmp/xcode-stage" | |
| ssh "$TARGET" "mkdir -p ${STAGE}" | |
| echo "==> Copying $(basename "$XIP_AT_SRC") from ${SRC_HOST} (via this machine)..." | |
| scp -3 "${SRC_HOST}:${XIP_AT_SRC}" "${TARGET}:${STAGE}/" | |
| echo "==> Copying runtime bundles (if any)..." | |
| RUNTIME_LIST=$(ssh "$SRC_HOST" "d=\"${SRC_PATH/#\~/\$HOME}\"; ls -d \"\$d\"/*.exportedBundle \"\$d\"/*imulator*.dmg 2>/dev/null" || true) | |
| IFS=$'\n' | |
| for item in $RUNTIME_LIST; do | |
| scp -3 -r "${SRC_HOST}:${item}" "${TARGET}:${STAGE}/" | |
| done | |
| unset IFS | |
| DIR_ON_TARGET="$STAGE" | |
| CLEAN_STAGE=1 | |
| else | |
| DIR_ON_TARGET="$SRC_PATH" | |
| CLEAN_STAGE=0 | |
| fi | |
| # ---- Install on the target ------------------------------------------------- | |
| ssh "$TARGET" DIR="$DIR_ON_TARGET" CLEAN_STAGE="$CLEAN_STAGE" 'bash -s' <<'REMOTE' | |
| set -euo pipefail | |
| TMP="" | |
| cleanup() { | |
| [ -n "$TMP" ] && rm -rf "$TMP" | |
| [ "$CLEAN_STAGE" = "1" ] && rm -rf "$DIR" | |
| } | |
| trap cleanup EXIT | |
| DIR="${DIR/#\~/$HOME}" | |
| [ -d "$DIR" ] || { echo "Folder $DIR not found on $(hostname)" >&2; exit 1; } | |
| # ---- 1. Extract the xip ----------------------------------------------------- | |
| XIP=$(ls -t "$DIR"/*.xip | head -n1) | |
| TMP=$(mktemp -d /tmp/xcode-deploy.XXXX) | |
| if [[ "$XIP" == /Volumes/* ]]; then | |
| echo "==> Copying xip off the network volume..." | |
| cp "$XIP" "$TMP/"; XIP="$TMP/$(basename "$XIP")" | |
| fi | |
| xattr -c "$XIP" 2>/dev/null || true | |
| echo "==> Extracting $(basename "$XIP") (this takes a while)..." | |
| cd "$TMP" | |
| if command -v unxip >/dev/null 2>&1; then | |
| unxip "$XIP" | |
| else | |
| xip --expand "$XIP" | |
| fi | |
| # ---- 2. Overwrite the same-named app in /Applications ----------------------- | |
| APP=$(ls -d "$TMP"/*.app | head -n1) # Xcode.app or Xcode-beta.app | |
| NAME=$(basename "$APP") | |
| DEST="/Applications/${NAME}" | |
| echo "==> Installing ${NAME}" | |
| sudo rm -rf "$DEST" | |
| sudo mv "$APP" "$DEST" | |
| cd / | |
| # ---- 3. Select, license, first launch -------------------------------------- | |
| sudo xcode-select -s "$DEST" | |
| sudo xcodebuild -license accept | |
| xcodebuild -runFirstLaunch | |
| # ---- 4. Import simulator runtimes ------------------------------------------ | |
| shopt -s nullglob | |
| for bundle in "$DIR"/*.exportedBundle "$DIR"/*imulator*.dmg; do | |
| echo "==> Importing runtime $(basename "$bundle")" | |
| xcodebuild -importPlatform "$bundle" \ | |
| || xcrun simctl runtime add "$bundle" | |
| done | |
| # ---- 5. Verify -------------------------------------------------------------- | |
| echo "==> Installed:" | |
| xcodebuild -version | |
| xcrun simctl runtime list | |
| REMOTE | |
| echo "==> ${TARGET} done." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment