Skip to content

Instantly share code, notes, and snippets.

@xissy
Created November 18, 2025 10:57
Show Gist options
  • Select an option

  • Save xissy/495ec0042015d7e530cfdb357a108f5f to your computer and use it in GitHub Desktop.

Select an option

Save xissy/495ec0042015d7e530cfdb357a108f5f to your computer and use it in GitHub Desktop.
Universal iOS Build and Launch Script
#!/bin/bash
# Universal iOS Build and Launch Script
# Automatically detects project settings or accepts command-line options
#
# Usage: ./ios-build-and-launch.sh [OPTIONS] [device-name]
#
# Options:
# --project PATH Path to .xcodeproj or .xcworkspace (auto-detected if not provided)
# --scheme NAME Scheme name (auto-detected if not provided)
# --bundle-id ID Bundle identifier (auto-detected if not provided)
# --configuration CFG Build configuration (default: Debug)
# --simulator, -s Search only in simulators
# --device, -d Search only in physical devices
# --help, -h Show this help message
#
# Examples:
# ./ios-build-and-launch.sh "iPhone 17 Pro"
# ./ios-build-and-launch.sh --simulator "iPhone 17 Pro"
# ./ios-build-and-launch.sh --device "My iPhone"
# ./ios-build-and-launch.sh --scheme MyScheme --configuration Release "iPhone 17 Pro"
#
# GitHub: https://gist.github.com/[your-gist-id]
# License: MIT
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Default configuration
PROJECT_PATH=""
SCHEME=""
BUNDLE_ID=""
CONFIGURATION="Debug"
FORCE_TYPE="" # "simulator", "device", or "" (auto-detect)
DEVICE_NAME=""
# Get script directory and working directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORKING_DIR="$(pwd)"
# Function to show help
show_help() {
echo -e "${BLUE}Universal iOS Build and Launch Script${NC}"
echo -e "${BLUE}======================================${NC}\n"
echo "Automatically detects project settings or accepts command-line options"
echo ""
echo "Usage: $0 [OPTIONS] [device-name]"
echo ""
echo "Options:"
echo " --project PATH Path to .xcodeproj or .xcworkspace (auto-detected if not provided)"
echo " --scheme NAME Scheme name (auto-detected if not provided)"
echo " --bundle-id ID Bundle identifier (auto-detected if not provided)"
echo " --configuration CFG Build configuration (default: Debug)"
echo " --simulator, -s Search only in simulators"
echo " --device, -d Search only in physical devices"
echo " --help, -h Show this help message"
echo ""
echo "Examples:"
echo " $0 \"iPhone 17 Pro\""
echo " $0 --simulator \"iPhone 17 Pro\""
echo " $0 --device \"My iPhone\""
echo " $0 --scheme MyScheme --configuration Release \"iPhone 17 Pro\""
echo ""
}
# Function to auto-detect project file
detect_project() {
# First try to find .xcworkspace (preferred)
local workspace=$(find "$WORKING_DIR" -maxdepth 1 -name "*.xcworkspace" -type d | head -1)
if [ -n "$workspace" ]; then
echo "$workspace"
return 0
fi
# Fall back to .xcodeproj
local project=$(find "$WORKING_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | head -1)
if [ -n "$project" ]; then
echo "$project"
return 0
fi
return 1
}
# Function to auto-detect scheme using hybrid 3-tier approach
detect_scheme() {
local project_path="$1"
local project_flag=""
local xcodeproj_path=""
# Get project name for matching
local project_name=$(basename "$project_path" | sed 's/\.[^.]*$//')
if [[ "$project_path" == *.xcworkspace ]]; then
project_flag="-workspace"
# For workspace, find the .xcodeproj inside
xcodeproj_path=$(find "$WORKING_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | head -1)
else
project_flag="-project"
xcodeproj_path="$project_path"
fi
# ===================================================================
# TIER 1: Check shared scheme files (most reliable)
# ===================================================================
# Shared schemes exclude auto-generated SPM package schemes
if [ -n "$xcodeproj_path" ]; then
local shared_schemes_dir="$xcodeproj_path/xcshareddata/xcschemes"
if [ -d "$shared_schemes_dir" ]; then
# Get all .xcscheme files
local scheme_files=$(find "$shared_schemes_dir" -name "*.xcscheme" -type f 2>/dev/null)
if [ -n "$scheme_files" ]; then
# Parse each scheme file to find main app schemes
local main_schemes=()
while IFS= read -r scheme_file; do
# Check if scheme is for a main app (.app) and has LaunchAction
local buildable_name=$(grep -A 2 "BuildableName" "$scheme_file" 2>/dev/null | grep "BuildableName = " | head -1)
local has_launch_action=$(grep -c "<LaunchAction" "$scheme_file" 2>/dev/null || echo "0")
# Filter: BuildableName ends with .app and has LaunchAction
if [[ "$buildable_name" =~ \.app\" ]] && [ "$has_launch_action" -gt 0 ]; then
# Exclude extension/widget/watch schemes
local scheme_basename=$(basename "$scheme_file" .xcscheme)
if ! [[ "$scheme_basename" =~ (Extension|Widget|Watch|Tests)$ ]]; then
main_schemes+=("$scheme_basename")
fi
fi
done <<< "$scheme_files"
# If we found main app schemes
if [ ${#main_schemes[@]} -gt 0 ]; then
# Prefer scheme matching project name (case-insensitive)
local project_name_lower=$(echo "$project_name" | tr '[:upper:]' '[:lower:]')
for scheme in "${main_schemes[@]}"; do
local scheme_lower=$(echo "$scheme" | tr '[:upper:]' '[:lower:]')
if [[ "$scheme_lower" == "$project_name_lower" ]]; then
echo "$scheme"
return 0
fi
done
# Otherwise use first main scheme found
echo "${main_schemes[0]}"
return 0
fi
fi
fi
fi
# ===================================================================
# TIER 2: Parse xcodebuild -list and filter patterns
# ===================================================================
local all_schemes=$(xcodebuild "$project_flag" "$project_path" -list 2>/dev/null | \
awk '/Schemes:/,/^$/' | \
grep -v "Schemes:" | \
grep -v "^$" | \
sed 's/^[[:space:]]*//')
if [ -n "$all_schemes" ]; then
# Try exact project name match first (case-insensitive)
local exact_match=$(echo "$all_schemes" | grep -i "^${project_name}$" | head -1)
if [ -n "$exact_match" ]; then
echo "$exact_match"
return 0
fi
# Filter out known package/test/extension patterns
local filtered_schemes=$(echo "$all_schemes" | grep -Ev '(Package|Tests|UITests|Dynamic|Static|Extension|Widget|Watch)' | \
grep -Ev '^(Alamofire|Supabase|Auth|Storage|Realtime|PostgREST|Functions|GoogleSignIn|GoogleUtilities|GTMAppAuth|GTMSessionFetcher|Kingfisher|PostHog|AppAuth|AppCheck|Promises|FBLPromises)')
# Prefer matching project name from filtered schemes
if [ -n "$filtered_schemes" ]; then
local filtered_match=$(echo "$filtered_schemes" | grep -i "^${project_name}$" | head -1)
if [ -n "$filtered_match" ]; then
echo "$filtered_match"
return 0
fi
# Return first filtered scheme
local first_filtered=$(echo "$filtered_schemes" | head -1)
if [ -n "$first_filtered" ]; then
echo "$first_filtered"
return 0
fi
fi
# ===================================================================
# TIER 3: Last resort - return first scheme from all schemes
# ===================================================================
local first_scheme=$(echo "$all_schemes" | head -1)
if [ -n "$first_scheme" ]; then
echo "$first_scheme"
return 0
fi
fi
return 1
}
# Function to auto-detect bundle ID
detect_bundle_id() {
local project_path="$1"
local scheme="$2"
local project_flag=""
if [[ "$project_path" == *.xcworkspace ]]; then
project_flag="-workspace"
else
project_flag="-project"
fi
# Get bundle ID from build settings
local bundle_id=$(xcodebuild "$project_flag" "$project_path" \
-scheme "$scheme" \
-showBuildSettings 2>/dev/null | \
grep "PRODUCT_BUNDLE_IDENTIFIER = " | \
grep -v "DERIVE" | \
head -1 | \
sed 's/.*= *//' | \
xargs)
if [ -n "$bundle_id" ]; then
echo "$bundle_id"
return 0
fi
return 1
}
# Function to list available devices and simulators
list_devices() {
echo -e "${BLUE}πŸ“± Available Physical Devices:${NC}"
PHYSICAL_DEVICES=$(xcrun devicectl list devices 2>/dev/null || echo "")
if [ -n "$PHYSICAL_DEVICES" ]; then
echo "$PHYSICAL_DEVICES"
else
echo " (No physical devices found)"
fi
echo ""
echo -e "${BLUE}πŸ“± Available Simulators:${NC}"
SIMULATORS=$(xcrun simctl list devices available | grep -E "iPhone|iPad" || echo "")
if [ -n "$SIMULATORS" ]; then
echo "$SIMULATORS"
else
echo " (No simulators found)"
fi
echo ""
}
# Function to find physical device UDID by name
find_physical_device() {
local device_name="$1"
local device_list=$(xcrun devicectl list devices 2>/dev/null || echo "")
if [ -z "$device_list" ]; then
return 1
fi
# Remove quotes and special characters for matching
local search_name=$(echo "$device_name" | sed "s/'//g" | sed 's/"//g')
# First try exact match (case insensitive)
local device_line=$(echo "$device_list" | grep -i "^[[:space:]]*$search_name" | head -1)
# If exact match didn't work, try AND logic: all words must be present
if [ -z "$device_line" ]; then
local words_found=true
local candidate_lines="$device_list"
for word in $search_name; do
local clean_word=$(echo "$word" | sed "s/'//g" | sed 's/"//g')
if [ -n "$clean_word" ]; then
candidate_lines=$(echo "$candidate_lines" | grep -i "$clean_word")
if [ -z "$candidate_lines" ]; then
words_found=false
break
fi
fi
done
if [ "$words_found" = true ] && [ -n "$candidate_lines" ]; then
device_line=$(echo "$candidate_lines" | head -1)
fi
fi
if [ -n "$device_line" ]; then
# Try to extract UDID using awk (more reliable for table format)
local udid=$(echo "$device_line" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}$/) print $i}' | head -1)
# Fallback: use grep pattern matching
if [ -z "$udid" ]; then
udid=$(echo "$device_line" | grep -oE "[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}" | head -1)
fi
if [ -n "$udid" ]; then
echo "$udid"
return 0
fi
fi
return 1
}
# Function to find simulator UDID by name
find_simulator() {
local simulator_name="$1"
local simulator_list=$(xcrun simctl list devices available 2>/dev/null | grep -i "$simulator_name" | head -1)
if [ -n "$simulator_list" ]; then
local udid=$(echo "$simulator_list" | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -1)
if [ -n "$udid" ]; then
echo "$udid"
return 0
fi
fi
return 1
}
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
show_help
exit 0
;;
--project)
PROJECT_PATH="$2"
shift 2
;;
--scheme)
SCHEME="$2"
shift 2
;;
--bundle-id)
BUNDLE_ID="$2"
shift 2
;;
--configuration)
CONFIGURATION="$2"
shift 2
;;
--simulator|-s)
FORCE_TYPE="simulator"
shift
;;
--device|-d)
FORCE_TYPE="device"
shift
;;
*)
if [ -z "$DEVICE_NAME" ]; then
DEVICE_NAME="$1"
else
echo -e "${RED}❌ Error: Multiple device names provided${NC}"
echo -e "${YELLOW}Use --help for usage information${NC}"
exit 1
fi
shift
;;
esac
done
# Print header
echo -e "${BLUE}πŸš€ Universal iOS Build and Launch Script${NC}"
echo -e "${BLUE}==========================================${NC}\n"
# Auto-detect project if not provided
if [ -z "$PROJECT_PATH" ]; then
echo -e "${CYAN}πŸ” Auto-detecting project...${NC}"
PROJECT_PATH=$(detect_project)
if [ -z "$PROJECT_PATH" ]; then
echo -e "${RED}❌ Error: Could not find .xcodeproj or .xcworkspace in current directory${NC}"
echo -e "${YELLOW}Please specify project path with --project option${NC}"
exit 1
fi
echo -e "${GREEN}βœ… Found project: $(basename "$PROJECT_PATH")${NC}\n"
else
if [ ! -d "$PROJECT_PATH" ]; then
echo -e "${RED}❌ Error: Project path does not exist: $PROJECT_PATH${NC}"
exit 1
fi
fi
# Auto-detect scheme if not provided
if [ -z "$SCHEME" ]; then
echo -e "${CYAN}πŸ” Auto-detecting scheme...${NC}"
SCHEME=$(detect_scheme "$PROJECT_PATH")
if [ -z "$SCHEME" ]; then
echo -e "${RED}❌ Error: Could not auto-detect scheme${NC}"
echo -e "${YELLOW}Please specify scheme with --scheme option${NC}"
exit 1
fi
echo -e "${GREEN}βœ… Found scheme: $SCHEME${NC}\n"
fi
# Auto-detect bundle ID if not provided
if [ -z "$BUNDLE_ID" ]; then
echo -e "${CYAN}πŸ” Auto-detecting bundle identifier...${NC}"
BUNDLE_ID=$(detect_bundle_id "$PROJECT_PATH" "$SCHEME")
if [ -z "$BUNDLE_ID" ]; then
echo -e "${RED}❌ Error: Could not auto-detect bundle ID${NC}"
echo -e "${YELLOW}Please specify bundle ID with --bundle-id option${NC}"
exit 1
fi
echo -e "${GREEN}βœ… Found bundle ID: $BUNDLE_ID${NC}\n"
fi
# Get product name from project path
PRODUCT_NAME=$(basename "$PROJECT_PATH" | sed 's/\.[^.]*$//')
# If no device name provided, show list and exit
if [ -z "$DEVICE_NAME" ]; then
echo -e "${YELLOW}No device name provided.${NC}\n"
list_devices
echo -e "${YELLOW}Usage: $0 [OPTIONS] [device-name]${NC}"
echo -e "${YELLOW}Use --help for more information${NC}"
exit 0
fi
# Determine project flag
PROJECT_FLAG=""
if [[ "$PROJECT_PATH" == *.xcworkspace ]]; then
PROJECT_FLAG="-workspace"
else
PROJECT_FLAG="-project"
fi
# Step 1: Find device
echo -e "${YELLOW}πŸ“± Step 1: Finding device...${NC}"
DEVICE_UDID=""
DEVICE_TYPE=""
DESTINATION=""
# Search based on force type flag
if [ "$FORCE_TYPE" = "simulator" ]; then
DEVICE_UDID=$(find_simulator "$DEVICE_NAME" 2>/dev/null || echo "")
if [ -n "$DEVICE_UDID" ]; then
DEVICE_TYPE="simulator"
DESTINATION="platform=iOS Simulator,name=$DEVICE_NAME"
echo -e "${GREEN}βœ… Found simulator: $DEVICE_NAME (UDID: $DEVICE_UDID)${NC}\n"
fi
elif [ "$FORCE_TYPE" = "device" ]; then
DEVICE_UDID=$(find_physical_device "$DEVICE_NAME" 2>/dev/null || echo "")
if [ -n "$DEVICE_UDID" ]; then
DEVICE_TYPE="physical"
DESTINATION="generic/platform=iOS"
echo -e "${GREEN}βœ… Found physical device: $DEVICE_NAME (UDID: $DEVICE_UDID)${NC}\n"
fi
else
# Auto-detect: try simulator first, then physical device
DEVICE_UDID=$(find_simulator "$DEVICE_NAME" 2>/dev/null || echo "")
if [ -n "$DEVICE_UDID" ]; then
DEVICE_TYPE="simulator"
DESTINATION="platform=iOS Simulator,name=$DEVICE_NAME"
echo -e "${GREEN}βœ… Found simulator: $DEVICE_NAME (UDID: $DEVICE_UDID)${NC}\n"
else
DEVICE_UDID=$(find_physical_device "$DEVICE_NAME" 2>/dev/null || echo "")
if [ -n "$DEVICE_UDID" ]; then
DEVICE_TYPE="physical"
DESTINATION="generic/platform=iOS"
echo -e "${GREEN}βœ… Found physical device: $DEVICE_NAME (UDID: $DEVICE_UDID)${NC}\n"
fi
fi
fi
if [ -z "$DEVICE_UDID" ]; then
if [ -n "$FORCE_TYPE" ]; then
echo -e "${RED}❌ Error: Could not find $FORCE_TYPE '$DEVICE_NAME'${NC}\n"
else
echo -e "${RED}❌ Error: Could not find device '$DEVICE_NAME'${NC}\n"
fi
list_devices
exit 1
fi
# Step 2: Build for device/simulator
echo -e "${YELLOW}πŸ”¨ Step 2: Building app...${NC}"
if [ "$DEVICE_TYPE" = "simulator" ]; then
SIMULATOR_STATE=$(xcrun simctl list devices | grep "$DEVICE_UDID" | grep -oE '(Booted|Shutdown)' || echo "Shutdown")
if [ "$SIMULATOR_STATE" != "Booted" ]; then
echo -e "${YELLOW}Booting simulator...${NC}"
xcrun simctl boot "$DEVICE_UDID" 2>/dev/null || true
fi
fi
if ! xcodebuild \
"$PROJECT_FLAG" "$PROJECT_PATH" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
-derivedDataPath build \
build 2>&1; then
echo -e "${RED}❌ Build failed!${NC}"
exit 1
fi
echo -e "${GREEN}βœ… Build succeeded${NC}\n"
# Step 3: Get app path
echo -e "${YELLOW}πŸ“¦ Step 3: Locating app bundle...${NC}"
APP_PATH=""
BUILD_DIR=""
if [ "$DEVICE_TYPE" = "physical" ]; then
BUILD_DIR="$CONFIGURATION-iphoneos"
else
BUILD_DIR="$CONFIGURATION-iphonesimulator"
fi
# 1. Check local build directory (from -derivedDataPath build)
LOCAL_BUILD="$WORKING_DIR/build/Build/Products/$BUILD_DIR/$PRODUCT_NAME.app"
if [ -d "$LOCAL_BUILD" ]; then
APP_PATH="$LOCAL_BUILD"
fi
# 2. Check DerivedData (default location)
if [ ! -d "$APP_PATH" ]; then
DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData"
LATEST_BUILD=$(find "$DERIVED_DATA_PATH" -name "$PRODUCT_NAME.app" -type d -path "*/Build/Products/$BUILD_DIR/*" 2>/dev/null | sort -r | head -1)
if [ -n "$LATEST_BUILD" ] && [ -d "$LATEST_BUILD" ]; then
APP_PATH="$LATEST_BUILD"
fi
fi
# 3. Try to get from build settings
if [ ! -d "$APP_PATH" ]; then
BUILD_SETTINGS=$(xcodebuild \
"$PROJECT_FLAG" "$PROJECT_PATH" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
-showBuildSettings 2>/dev/null)
BUILD_PRODUCTS_DIR=$(echo "$BUILD_SETTINGS" | grep "BUILD_PRODUCTS_DIR" | head -1 | sed 's/.*= *//' | xargs)
APP_NAME=$(echo "$BUILD_SETTINGS" | grep "PRODUCT_NAME" | head -1 | sed 's/.*= *//' | xargs)
if [ -z "$APP_NAME" ]; then
APP_NAME="$PRODUCT_NAME"
fi
if [ -n "$BUILD_PRODUCTS_DIR" ] && [ -d "$BUILD_PRODUCTS_DIR/$APP_NAME.app" ]; then
APP_PATH="$BUILD_PRODUCTS_DIR/$APP_NAME.app"
fi
fi
# 4. Last resort: search for app in build directory
if [ ! -d "$APP_PATH" ]; then
FOUND_APP=$(find "$WORKING_DIR/build" -name "$PRODUCT_NAME.app" -type d 2>/dev/null | head -1)
if [ -n "$FOUND_APP" ] && [ -d "$FOUND_APP" ]; then
APP_PATH="$FOUND_APP"
fi
fi
if [ ! -d "$APP_PATH" ]; then
echo -e "${RED}❌ Error: Could not locate app bundle${NC}"
echo "Searched in:"
echo " - $WORKING_DIR/build/Build/Products/$BUILD_DIR/$PRODUCT_NAME.app"
echo " - DerivedData (latest build)"
echo " - Build settings BUILD_PRODUCTS_DIR"
echo " - Any location in build directory"
exit 1
fi
echo -e "${GREEN}βœ… Found app at: $APP_PATH${NC}\n"
# Step 4: Install app
echo -e "${YELLOW}πŸ“² Step 4: Installing app...${NC}"
if [ "$DEVICE_TYPE" = "physical" ]; then
if ! xcrun devicectl device install app \
--device "$DEVICE_UDID" \
"$APP_PATH" 2>&1; then
echo -e "${RED}❌ Installation failed!${NC}"
exit 1
fi
else
if ! xcrun simctl install "$DEVICE_UDID" "$APP_PATH" 2>&1; then
echo -e "${RED}❌ Installation failed!${NC}"
exit 1
fi
fi
echo -e "${GREEN}βœ… App installed successfully${NC}\n"
# Step 5: Launch app
echo -e "${YELLOW}πŸš€ Step 5: Launching app...${NC}"
PROCESS_ID="unknown"
if [ "$DEVICE_TYPE" = "physical" ]; then
LAUNCH_OUTPUT=$(xcrun devicectl device process launch \
--device "$DEVICE_UDID" \
"$BUNDLE_ID" 2>&1) || {
echo -e "${RED}❌ Launch failed!${NC}"
echo "$LAUNCH_OUTPUT"
exit 1
}
PROCESS_ID=$(echo "$LAUNCH_OUTPUT" | grep -i "process" | grep -oE "[0-9]+" | head -1 || echo "unknown")
else
LAUNCH_OUTPUT=$(xcrun simctl launch "$DEVICE_UDID" "$BUNDLE_ID" 2>&1) || {
echo -e "${RED}❌ Launch failed!${NC}"
echo "$LAUNCH_OUTPUT"
exit 1
}
PROCESS_ID=$(echo "$LAUNCH_OUTPUT" | grep -oE '[0-9]+' | head -1 || echo "unknown")
fi
echo -e "${GREEN}βœ… App launched successfully${NC}\n"
# Summary
echo -e "${BLUE}==========================================${NC}"
echo -e "${GREEN}✨ Summary${NC}"
echo -e "${BLUE}==========================================${NC}"
echo "Project: $(basename "$PROJECT_PATH")"
echo "Scheme: $SCHEME"
echo "Configuration: $CONFIGURATION"
echo "Bundle ID: $BUNDLE_ID"
echo "Device Type: $DEVICE_TYPE"
echo "Device: $DEVICE_NAME"
echo "UDID: $DEVICE_UDID"
echo "Process ID: $PROCESS_ID"
echo -e "${BLUE}==========================================${NC}\n"
if [ "$DEVICE_TYPE" = "physical" ]; then
echo -e "${GREEN}πŸŽ‰ Done! The app should now be running on your device.${NC}"
else
echo -e "${GREEN}πŸŽ‰ Done! The app should now be running on the simulator.${NC}"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment