Skip to content

Instantly share code, notes, and snippets.

@tiiime
Last active July 11, 2026 02:28
Show Gist options
  • Select an option

  • Save tiiime/43e41348be734123e0d76e40494734b3 to your computer and use it in GitHub Desktop.

Select an option

Save tiiime/43e41348be734123e0d76e40494734b3 to your computer and use it in GitHub Desktop.
launch.sh — One-command iOS Simulator: build → install → launch
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
# ============================================================
# launch.sh — Build, install, and run an iOS app on Simulator
# ============================================================
usage() {
cat <<'USAGE'
Usage: launch.sh --scheme NAME [options]
Core options:
--scheme NAME Xcode scheme name (required)
--project PATH .xcodeproj path (default: first found or <scheme>.xcodeproj)
--workspace PATH .xcworkspace path (optional, takes precedence)
--configuration MODE Build configuration (default: Debug)
Simulator options:
--sim-name NAME Simulator device name (default: auto — picks best available iPhone)
--sim-udid UDID Explicit simulator UDID (overrides --sim-name)
Build options:
--derived-data PATH Derived data path (default: build/DerivedData)
--skip-build Skip build, install/launch an already-built app
--app-path PATH Path to .app bundle (required with --skip-build, or auto-guessed)
Output:
--verbose, -v Show full build/simctl output
-h, --help Show this message
Examples:
launch.sh --scheme MyApp
launch.sh --scheme MyApp --sim-name "iPhone 16 Pro"
launch.sh --scheme MyApp --skip-build --app-path build/DerivedData/Build/Products/Debug-iphonesimulator/MyApp.app
USAGE
}
# ---------- argument parsing ----------
SCHEME=""
PROJECT=""
WORKSPACE=""
CONFIGURATION="Debug"
SIM_NAME=""
SIM_UDID=""
DERIVED_DATA="build/DerivedData"
SKIP_BUILD=0
APP_PATH_ARG=""
VERBOSE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--scheme) SCHEME="$2"; shift 2 ;;
--project) PROJECT="$2"; shift 2 ;;
--workspace) WORKSPACE="$2"; shift 2 ;;
--configuration) CONFIGURATION="$2"; shift 2 ;;
--sim-name) SIM_NAME="$2"; shift 2 ;;
--sim-udid) SIM_UDID="$2"; shift 2 ;;
--derived-data) DERIVED_DATA="$2"; shift 2 ;;
--app-path) APP_PATH_ARG="$2"; shift 2 ;;
--skip-build) SKIP_BUILD=1; shift ;;
--verbose|-v) VERBOSE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$SCHEME" ]]; then
echo "ERROR: --scheme is required." >&2
usage
exit 1
fi
# ---------- resolve simulator ----------
resolve_sim_destination() {
local name="${1:-}"
local udid="${2:-}"
local name_lower
name_lower="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
if [[ -n "$udid" ]]; then
echo "platform=iOS Simulator,id=$udid"
return 0
fi
python3 - "$name" "$name_lower" <<'PY'
import json, os, re, subprocess, sys
name = sys.argv[1]
name_lower = sys.argv[2]
def runtime_version(runtime_key: str):
m = re.search(r"iOS[- ](\d+)(?:[\.-](\d+))?", runtime_key)
if not m:
return (0, 0)
return (int(m.group(1)), int(m.group(2) or 0))
variant_rank = {"pro max": 6, "pro": 5, "plus": 4, "air": 3, "": 2, "mini": 1, "e": 0}
def model_rank(device_name: str):
n = 0
m = re.search(r"iPhone\s+(\d+)", device_name)
if m:
n = int(m.group(1))
lower = device_name.lower()
if "pro max" in lower: suffix = "pro max"
elif "pro" in lower: suffix = "pro"
elif "plus" in lower: suffix = "plus"
elif "air" in lower: suffix = "air"
elif re.search(r"iphone\s+\d+e\b", lower): suffix = "e"
elif "mini" in lower: suffix = "mini"
else: suffix = ""
return (n, variant_rank.get(suffix, 0))
raw = subprocess.check_output(["xcrun", "simctl", "list", "devices", "-j"], text=True)
data = json.loads(raw)
candidates = []
for runtime_key, devices in data.get("devices", {}).items():
for d in devices:
if not d.get("isAvailable"):
continue
if "iPhone" not in d.get("name", ""):
continue
candidates.append({
"name": d.get("name", ""),
"udid": d.get("udid", ""),
"state": d.get("state", ""),
"runtime": runtime_key,
"runtime_version": runtime_version(runtime_key),
})
if not candidates:
print("")
sys.exit(1)
# Honour explicit sim name
if name and name_lower != "auto":
matches = [c for c in candidates if c["name"] == name]
if matches:
booted = [c for c in matches if c["state"] == "Booted"]
chosen = max(booted or matches, key=lambda c: c["runtime_version"])
print(f"platform=iOS Simulator,id={chosen['udid']}")
sys.exit(0)
# Prefer a booted iPhone
booted = [c for c in candidates if c["state"] == "Booted"]
if booted:
chosen = max(booted, key=lambda c: (c["runtime_version"], model_rank(c["name"])))
print(f"platform=iOS Simulator,id={chosen['udid']}")
sys.exit(0)
# Fallback: latest runtime + best model
chosen = max(candidates, key=lambda c: (c["runtime_version"], model_rank(c["name"])))
print(f"platform=iOS Simulator,id={chosen['udid']}")
PY
}
DESTINATION="$(resolve_sim_destination "$SIM_NAME" "$SIM_UDID")"
if [[ -z "$DESTINATION" ]]; then
echo "ERROR: No available iOS Simulator found." >&2
exit 1
fi
SIM_UDID="${DESTINATION##*id=}"
# ---------- helpers ----------
step() { printf " \033[1;34m●\033[0m %s\n" "$1"; }
ok() { printf "\033[1;32m✔\033[0m %s\n" "$1"; }
die() { printf "\033[1;31m✘\033[0m %s\n" "$1" >&2; exit 1; }
# ---------- resolve app path ----------
if [[ -n "$APP_PATH_ARG" ]]; then
APP_PATH="$APP_PATH_ARG"
elif [[ $SKIP_BUILD -eq 0 ]]; then
# ---------- resolve project / workspace (build only) ----------
if [[ -z "$WORKSPACE" ]]; then
local_wss=(*.xcworkspace)
if [[ ${#local_wss[@]} -gt 0 && -d "${local_wss[0]}" ]]; then
WORKSPACE="${local_wss[0]}"
fi
fi
if [[ -z "$PROJECT" ]]; then
local_prjs=(*.xcodeproj)
if [[ ${#local_prjs[@]} -gt 0 && -d "${local_prjs[0]}" ]]; then
PROJECT="${local_prjs[0]}"
fi
fi
if [[ -z "$PROJECT" && -z "$WORKSPACE" ]]; then
PROJECT="${SCHEME}.xcodeproj"
fi
if [[ -n "$WORKSPACE" ]]; then
BUILD_FILE_FLAG=(-workspace "$WORKSPACE")
elif [[ -d "$PROJECT" ]]; then
BUILD_FILE_FLAG=(-project "$PROJECT")
else
die "No .xcodeproj or .xcworkspace found; use --app-path for a pre-built .app"
fi
# ---------- build ----------
DERIVED="$(cd "$(dirname "$DERIVED_DATA")" 2>/dev/null && pwd)/$(basename "$DERIVED_DATA")" || DERIVED="$DERIVED_DATA"
PLATFORM_SUFFIX="-iphonesimulator"
step "build"
set +e
if [[ $VERBOSE -eq 1 ]]; then
xcodebuild \
"${BUILD_FILE_FLAG[@]}" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
-derivedDataPath "$DERIVED" \
build
BUILD_STATUS=$?
else
BUILD_LOG="$(mktemp)"
xcodebuild \
"${BUILD_FILE_FLAG[@]}" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
-derivedDataPath "$DERIVED" \
build >"$BUILD_LOG" 2>&1
BUILD_STATUS=$?
fi
set -e
if [[ $BUILD_STATUS -ne 0 ]]; then
if [[ $VERBOSE -eq 0 ]]; then
echo "--- build log ---" >&2
tail -30 "$BUILD_LOG" >&2
fi
die "build failed (status $BUILD_STATUS)"
fi
[[ $VERBOSE -eq 0 && -n "${BUILD_LOG:-}" ]] && rm -f "$BUILD_LOG"
APP_PATH="$DERIVED/Build/Products/${CONFIGURATION}${PLATFORM_SUFFIX}/${SCHEME}.app"
else
APP_PATH="$DERIVED_DATA/Build/Products/${CONFIGURATION}-iphonesimulator/${SCHEME}.app"
fi
[[ -d "$APP_PATH" ]] || die "App not found at $APP_PATH"
# ---------- boot simulator ----------
step "boot"
open -a Simulator >/dev/null 2>&1 || true
xcrun simctl boot "$SIM_UDID" >/dev/null 2>&1 || true
xcrun simctl bootstatus "$SIM_UDID" -b >/dev/null 2>&1
# ---------- install ----------
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print:CFBundleIdentifier" "$APP_PATH/Info.plist")
step "install"
INSTALL_OUT="$(xcrun simctl install "$SIM_UDID" "$APP_PATH" 2>&1)" || {
echo "$INSTALL_OUT" >&2
die "install failed"
}
# ---------- launch ----------
step "launch"
LAUNCH_OUT="$(xcrun simctl launch "$SIM_UDID" "$BUNDLE_ID" 2>&1)" || {
echo "$LAUNCH_OUT" >&2
die "launch failed"
}
ok "$SCHEME launched"
@tiiime

tiiime commented Jul 11, 2026

Copy link
Copy Markdown
Author

From zero to running app in a single command. Auto-detects .xcodeproj / .xcworkspace,
auto-picks the best available iPhone Simulator, builds, boots, installs, and launches.

Features:

  • Zero config: auto-discovers project/workspace and optimal iPhone Simulator
  • Clean output: just ● build → ● boot → ● install → ● launch (failed builds show tail errors)
  • --verbose / -v switches to full xcodebuild output
  • --skip-build + --app-path skips compilation, deploys an existing .app
  • --sim-name "iPhone 16 Pro" or --sim-udid for explicit device targeting

Usage:

  ./launch.sh --scheme diandian                # build + launch
  ./launch.sh --scheme diandian -v             # verbose
  ./launch.sh --scheme diandian --skip-build    # skip build, launch only

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment