Skip to content

Instantly share code, notes, and snippets.

@leogdion
Created August 18, 2026 20:50
Show Gist options
  • Select an option

  • Save leogdion/70857d278be7e2c8e7690aaf977dd3ae to your computer and use it in GitHub Desktop.

Select an option

Save leogdion/70857d278be7e2c8e7690aaf977dd3ae to your computer and use it in GitHub Desktop.
#!/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