#!/bin/bash # Exit on error set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${BLUE}[STEP]${NC} $1" } # Validation functions check_command() { if ! command -v $1 &> /dev/null; then # Try rbenv shims for pod command if [ "$1" = "pod" ] && [ -f "$HOME/.rbenv/shims/pod" ]; then log_info "Found pod in rbenv shims" return 0 fi log_error "$1 is not installed. Please install it first." exit 1 fi } # Get pod command (handles rbenv) get_pod_command() { if command -v pod &> /dev/null; then echo "pod" elif [ -f "$HOME/.rbenv/shims/pod" ]; then echo "$HOME/.rbenv/shims/pod" else log_error "CocoaPods (pod) not found. Please install CocoaPods first." exit 1 fi } check_environment() { log_step "Checking environment..." # Check for required tools check_command "xcodebuild" check_command "pod" check_command "node" check_command "npm" # Check for Xcode if ! xcodebuild -version &> /dev/null; then log_error "Xcode is not installed or not properly configured" exit 1 fi # Check Node.js version NODE_VERSION=$(node -v | cut -d. -f1 | tr -d 'v') if [ "$NODE_VERSION" -lt 14 ]; then log_error "Node.js version 14 or higher is required" exit 1 fi log_info "Environment check passed" } # Parse arguments TARGET="simulator" BUILD_CONFIG="Debug" while [[ $# -gt 0 ]]; do case $1 in --simulator) TARGET="simulator" shift ;; --device) TARGET="device" shift ;; --release) BUILD_CONFIG="Release" shift ;; --help) echo "Usage: $0 [--simulator|--device] [--release]" echo "" echo "Options:" echo " --simulator Build for iOS Simulator (default)" echo " --device Build for physical device" echo " --release Build Release configuration (default: Debug)" echo " --help Show this help message" exit 0 ;; *) log_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; esac done # Check if iOS test app exists TEST_APP_DIR="test-apps/ios-test-app" if [ ! -d "$TEST_APP_DIR" ]; then log_error "iOS test app not found at $TEST_APP_DIR" log_info "The iOS test app needs to be created first." log_info "See doc/directives/0003-iOS-Android-Parity-Directive.md for requirements." exit 1 fi # Main build function build_ios_test_app() { log_step "Building iOS test app..." # Get repo root before changing directories (we're currently in repo root from main()) REPO_ROOT="$(pwd)" # Navigate to iOS App directory (where workspace is located) IOS_APP_DIR="$TEST_APP_DIR/ios/App" if [ ! -d "$IOS_APP_DIR" ]; then log_error "iOS App directory not found: $IOS_APP_DIR" exit 1 fi cd "$IOS_APP_DIR" || exit 1 # Check for workspace or project (these are directories, not files) if [ -d "App.xcworkspace" ]; then WORKSPACE="App.xcworkspace" SCHEME="App" elif [ -d "App.xcodeproj" ]; then PROJECT="App.xcodeproj" SCHEME="App" else log_error "No Xcode workspace or project found in $IOS_APP_DIR" log_info "Expected: App.xcworkspace or App.xcodeproj" log_info "Found files: $(ls -la | head -10)" exit 1 fi # Install CocoaPods dependencies log_step "Installing CocoaPods dependencies..." POD_CMD=$(get_pod_command) if [ -f "Podfile" ]; then if ! $POD_CMD install; then log_error "CocoaPods installation failed" exit 1 fi log_info "CocoaPods dependencies installed" else log_warn "No Podfile found, skipping pod install" fi # Copy canonical UI from www/index.html to test app log_step "Copying canonical UI from www/index.html..." # Use REPO_ROOT calculated before changing directories CANONICAL_UI="$REPO_ROOT/www/index.html" IOS_UI_SOURCE="$REPO_ROOT/$TEST_APP_DIR/App/App/Public/index.html" IOS_UI_RUNTIME="$REPO_ROOT/$TEST_APP_DIR/ios/App/App/public/index.html" if [ -f "$CANONICAL_UI" ]; then # Copy to source location (for Capacitor sync) if cp "$CANONICAL_UI" "$IOS_UI_SOURCE"; then log_info "Copied canonical UI to iOS test app source" else log_error "Failed to copy canonical UI to source" exit 1 fi # Also copy directly to runtime location (in case sync doesn't run or is cached) if [ -d "$(dirname "$IOS_UI_RUNTIME")" ]; then if cp "$CANONICAL_UI" "$IOS_UI_RUNTIME"; then log_info "Copied canonical UI to iOS test app runtime" else log_warn "Failed to copy canonical UI to runtime (may be synced later)" fi else log_warn "Runtime directory not found, will be created by Capacitor sync" fi else log_warn "Canonical UI not found at $CANONICAL_UI, skipping copy" fi # Sync Capacitor from test app root (where capacitor.config.json is located) # This must run from test-apps/ios-test-app, not from ios/App/App log_step "Syncing Capacitor..." TEST_APP_ROOT="$REPO_ROOT/$TEST_APP_DIR" if [ -f "$TEST_APP_ROOT/capacitor.config.json" ] || [ -f "$TEST_APP_ROOT/capacitor.config.ts" ]; then if command -v npx &> /dev/null; then # Save current directory CURRENT_DIR="$(pwd)" # Change to test app root for sync cd "$TEST_APP_ROOT" || exit 1 if ! npx cap sync ios; then log_error "Capacitor sync failed" cd "$CURRENT_DIR" || exit 1 exit 1 fi # Return to ios/App directory cd "$CURRENT_DIR" || exit 1 log_info "Capacitor synced" else log_warn "npx not found, skipping Capacitor sync" fi else log_warn "Capacitor config not found at $TEST_APP_ROOT, skipping sync" fi # Build TypeScript/JavaScript if package.json exists if [ -f "package.json" ]; then log_step "Building web assets..." if [ -f "package.json" ] && grep -q "\"build\"" package.json; then if ! npm run build; then log_error "Web assets build failed" exit 1 fi log_info "Web assets built" fi fi # Determine SDK and destination if [ "$TARGET" = "simulator" ]; then SDK="iphonesimulator" # Initialize simulator variables SIMULATOR_ID="" SIMULATOR_NAME="" # Auto-detect available iPhone simulator using device ID (more reliable) log_step "Detecting available iPhone simulator..." SIMULATOR_LINE=$(xcrun simctl list devices available 2>&1 | grep -i "iPhone" | head -1) if [ -n "$SIMULATOR_LINE" ]; then # Extract device ID (UUID in parentheses) SIMULATOR_ID=$(echo "$SIMULATOR_LINE" | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/') # Extract device name (everything before the first parenthesis) SIMULATOR_NAME=$(echo "$SIMULATOR_LINE" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -n "$SIMULATOR_ID" ] && [ "$SIMULATOR_ID" != "Shutdown" ] && [ "$SIMULATOR_ID" != "Booted" ]; then # Use device ID (most reliable) DESTINATION="platform=iOS Simulator,id=$SIMULATOR_ID" log_info "Building for iOS Simulator ($SIMULATOR_NAME, ID: $SIMULATOR_ID)..." elif [ -n "$SIMULATOR_NAME" ]; then # Fallback to device name DESTINATION="platform=iOS Simulator,name=$SIMULATOR_NAME" log_info "Building for iOS Simulator ($SIMULATOR_NAME)..." else # Last resort: generic destination DESTINATION="platform=iOS Simulator,name=Any iOS Simulator Device" log_warn "Using generic simulator destination" fi else # No iPhone simulators found, use generic DESTINATION="platform=iOS Simulator,name=Any iOS Simulator Device" log_warn "No iPhone simulators found, using generic destination" fi ARCHIVE_PATH="build/ios-test-app-simulator.xcarchive" else SDK="iphoneos" DESTINATION="generic/platform=iOS" ARCHIVE_PATH="build/ios-test-app-device.xcarchive" fi # Ensure UI is copied to runtime location one more time before build # (in case sync didn't run or was cached) log_step "Ensuring canonical UI is in runtime location before build..." if [ -f "$CANONICAL_UI" ] && [ -d "$(dirname "$IOS_UI_RUNTIME")" ]; then if cp "$CANONICAL_UI" "$IOS_UI_RUNTIME"; then log_info "Canonical UI copied to runtime location (pre-build)" fi fi # Clean build folder (removes old DerivedData) log_step "Cleaning build folder..." if [ -n "$WORKSPACE" ]; then xcodebuild clean -workspace "$WORKSPACE" -scheme "$SCHEME" -configuration "$BUILD_CONFIG" -sdk "$SDK" || true else xcodebuild clean -project "$PROJECT" -scheme "$SCHEME" -configuration "$BUILD_CONFIG" -sdk "$SDK" || true fi # Also clean DerivedData for this specific project to remove cached HTML log_step "Cleaning DerivedData for fresh build..." DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData" if [ -d "$DERIVED_DATA_PATH" ]; then # Find and remove DerivedData folders for this project find "$DERIVED_DATA_PATH" -maxdepth 1 -type d -name "App-*" -exec rm -rf {} \; 2>/dev/null || true log_info "DerivedData cleaned" fi # Build log_step "Building for $TARGET ($BUILD_CONFIG)..." if [ -n "$WORKSPACE" ]; then if ! xcodebuild build \ -workspace "$WORKSPACE" \ -scheme "$SCHEME" \ -configuration "$BUILD_CONFIG" \ -sdk "$SDK" \ -destination "$DESTINATION" \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO; then log_error "Build failed" exit 1 fi else if ! xcodebuild build \ -project "$PROJECT" \ -scheme "$SCHEME" \ -configuration "$BUILD_CONFIG" \ -sdk "$SDK" \ -destination "$DESTINATION" \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO; then log_error "Build failed" exit 1 fi fi log_info "Build successful!" # Find the built app in DerivedData if [ "$TARGET" = "simulator" ]; then # Xcode builds to DerivedData, find the app there DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData" APP_PATH=$(find "$DERIVED_DATA_PATH" -name "App.app" -path "*/Build/Products/Debug-iphonesimulator/*" -type d 2>/dev/null | head -1) if [ -n "$APP_PATH" ]; then log_info "App built at: $APP_PATH" # Force copy canonical UI directly into built app bundle (ensures latest version) log_step "Copying canonical UI into built app bundle..." BUILT_APP_HTML="$APP_PATH/public/index.html" if [ -f "$CANONICAL_UI" ] && [ -d "$(dirname "$BUILT_APP_HTML")" ]; then if cp "$CANONICAL_UI" "$BUILT_APP_HTML"; then log_info "✅ Canonical UI copied directly into app bundle" else log_warn "Failed to copy UI into app bundle (may use cached version)" fi fi log_info "" # Boot simulator if not already booted log_step "Checking simulator status..." if [ -n "$SIMULATOR_ID" ]; then SIMULATOR_STATE=$(xcrun simctl list devices | grep "$SIMULATOR_ID" | grep -o "(Booted\|Shutdown)" | head -1) if [ "$SIMULATOR_STATE" != "Booted" ]; then log_step "Booting simulator ($SIMULATOR_NAME)..." xcrun simctl boot "$SIMULATOR_ID" 2>/dev/null || log_warn "Simulator may already be booting" # Open Simulator app if not already open if ! pgrep -x "Simulator" > /dev/null; then log_step "Opening Simulator app..." open -a Simulator fi # Wait for simulator to fully boot (up to 60 seconds) log_step "Waiting for simulator to boot (this may take up to 60 seconds)..." BOOT_TIMEOUT=60 ELAPSED=0 CURRENT_STATE="Shutdown" while [ $ELAPSED -lt $BOOT_TIMEOUT ]; do CURRENT_STATE=$(xcrun simctl list devices | grep "$SIMULATOR_ID" | grep -o "(Booted\|Shutdown)" | head -1) if [ "$CURRENT_STATE" = "Booted" ]; then log_info "Simulator booted successfully (took ${ELAPSED}s)" # Give it a few more seconds to fully initialize sleep 3 break fi if [ $((ELAPSED % 5)) -eq 0 ] && [ $ELAPSED -gt 0 ]; then log_info "Still waiting... (${ELAPSED}s elapsed)" fi sleep 1 ELAPSED=$((ELAPSED + 1)) done if [ "$CURRENT_STATE" != "Booted" ]; then log_warn "Simulator may not have finished booting (waited ${ELAPSED}s)" log_warn "You may need to manually boot the simulator and try again" else # Verify simulator is actually ready (not just booted) log_info "Verifying simulator is ready..." READY_ATTEMPTS=0 MAX_READY_ATTEMPTS=10 while [ $READY_ATTEMPTS -lt $MAX_READY_ATTEMPTS ]; do # Try a simple command to verify simulator is responsive if xcrun simctl list devices | grep "$SIMULATOR_ID" | grep -q "Booted"; then # Try to get device info to verify it's responsive if xcrun simctl get_app_container "$SIMULATOR_ID" com.apple.Preferences >/dev/null 2>&1; then log_info "Simulator is ready" break fi fi sleep 1 READY_ATTEMPTS=$((READY_ATTEMPTS + 1)) done if [ $READY_ATTEMPTS -eq $MAX_READY_ATTEMPTS ]; then log_warn "Simulator may not be fully ready yet" fi fi else log_info "Simulator already booted" # Verify it's actually ready if ! xcrun simctl get_app_container "$SIMULATOR_ID" com.apple.Preferences >/dev/null 2>&1; then log_warn "Simulator is booted but may not be fully ready" log_info "Waiting a few seconds for simulator to be ready..." sleep 5 fi fi # Uninstall existing app (if present) to ensure clean install log_step "Uninstalling existing app (if present)..." APP_BUNDLE_ID="com.timesafari.dailynotification.test" if xcrun simctl uninstall "$SIMULATOR_ID" "$APP_BUNDLE_ID" 2>&1; then log_info "Existing app uninstalled" else # App may not be installed, which is fine log_info "No existing app to uninstall (or uninstall failed - continuing anyway)" fi # Wait a moment after uninstall sleep 1 # Install the app log_step "Installing app on simulator..." if xcrun simctl install "$SIMULATOR_ID" "$APP_PATH" 2>&1; then log_info "App installed successfully" else log_warn "Install may have failed (app may already be installed)" fi # Wait a moment for install to complete sleep 1 # Launch the app (try multiple methods) log_step "Launching app..." LAUNCH_SUCCESS=false LAUNCH_ERROR="" # Wait a moment for simulator to be fully ready sleep 2 # Method 1: Direct launch (capture output to check for errors) # Note: Bundle ID is com.timesafari.dailynotification.test log_info "Attempting to launch app..." LAUNCH_OUTPUT=$(xcrun simctl launch "$SIMULATOR_ID" "$APP_BUNDLE_ID" 2>&1) LAUNCH_EXIT_CODE=$? if [ $LAUNCH_EXIT_CODE -eq 0 ]; then # Check if output contains process ID (successful launch) # Format can be either "PID" or "bundle: PID" if echo "$LAUNCH_OUTPUT" | grep -qE "^[0-9]+$|^[^:]+: [0-9]+$"; then LAUNCH_SUCCESS=true # Extract PID (either standalone number or after colon) APP_PID=$(echo "$LAUNCH_OUTPUT" | sed -E 's/^[^:]*:? *([0-9]+).*/\1/' | head -1) log_info "✅ App launched successfully! (PID: $APP_PID)" else # Launch command succeeded but may not have actually launched log_warn "Launch command returned success but output unexpected: $LAUNCH_OUTPUT" fi else # Capture error message LAUNCH_ERROR="$LAUNCH_OUTPUT" log_warn "Launch failed: $LAUNCH_ERROR" fi # Method 2: Verify app is actually running if [ "$LAUNCH_SUCCESS" = false ]; then log_info "Checking if app is already running..." sleep 2 RUNNING_APPS=$(xcrun simctl listapps "$SIMULATOR_ID" 2>/dev/null | grep -A 5 "$APP_BUNDLE_ID" || echo "") if [ -n "$RUNNING_APPS" ]; then log_info "App appears to be installed. Trying to verify it's running..." # Try to get app state APP_STATE=$(xcrun simctl listapps "$SIMULATOR_ID" 2>/dev/null | grep -A 10 "$APP_BUNDLE_ID" | grep "ApplicationType" || echo "") if [ -n "$APP_STATE" ]; then log_info "App found in simulator. Attempting manual launch..." # Try opening via Simulator app open -a Simulator sleep 1 # Try launch one more time if xcrun simctl launch "$SIMULATOR_ID" "$APP_BUNDLE_ID" >/dev/null 2>&1; then LAUNCH_SUCCESS=true log_info "✅ App launched successfully on retry!" fi fi fi fi # Final verification: check if app process is running if [ "$LAUNCH_SUCCESS" = true ]; then sleep 2 # Try to verify app is running by checking if we can get its container if xcrun simctl get_app_container "$SIMULATOR_ID" "$APP_BUNDLE_ID" >/dev/null 2>&1; then log_info "✅ Verified: App is installed and accessible" else log_warn "⚠️ Launch reported success but app verification failed" log_warn " The app may still be starting. Check the Simulator." fi else log_warn "❌ Automatic launch failed" log_info "" log_info "The app is installed. To launch manually:" log_info " 1. Open Simulator app (if not already open)" log_info " 2. Find the app icon on the home screen and tap it" log_info " 3. Or run: xcrun simctl launch $SIMULATOR_ID $APP_BUNDLE_ID" if [ -n "$LAUNCH_ERROR" ]; then log_info "" log_info "Launch error details:" log_info " $LAUNCH_ERROR" fi fi log_info "" log_info "✅ Build and deployment complete!" else log_info "" log_info "To run on simulator manually:" log_info " xcrun simctl install booted \"$APP_PATH\"" log_info " xcrun simctl launch booted com.timesafari.dailynotification.test" fi else log_warn "Could not find built app in DerivedData" log_info "App was built successfully, but path detection failed." log_info "You can find it in Xcode's DerivedData folder or run from Xcode directly." fi else log_info "" log_info "To install on device:" log_info " Open App.xcworkspace in Xcode" log_info " Select your device" log_info " Press Cmd+R to build and run" fi cd - > /dev/null } # Main execution main() { log_info "iOS Test App Build Script" log_info "Target: $TARGET | Configuration: $BUILD_CONFIG" log_info "" check_environment # Get absolute path to repo root SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" cd "$REPO_ROOT" build_ios_test_app log_info "" log_info "✅ Build complete!" } main "$@"