Last active
July 11, 2026 02:28
-
-
Save tiiime/43e41348be734123e0d76e40494734b3 to your computer and use it in GitHub Desktop.
launch.sh — One-command iOS Simulator: build → install → launch
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 | |
| 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" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
Usage: