Files
crowd-funder-for-time-pwa/scripts/build-ios.sh
T

754 lines
25 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# build-ios.sh
# Author: Matthew Raymer
# Description: iOS build script for TimeSafari application
# Date: 2025-07-11
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Default values
BUILD_MODE="development"
BUILD_TYPE="debug"
OPEN_STUDIO=false
BUILD_IPA=false
BUILD_APP=false
CLEAN_ONLY=false
SYNC_ONLY=false
ASSETS_ONLY=false
DEPLOY_APP=false
AUTO_RUN=false
CUSTOM_API_IP=""
# Function to print iOS-specific usage
print_ios_usage() {
echo "Usage: $0 [options]"
echo ""
echo "iOS Build Options:"
echo " --dev, --development Build for development environment"
echo " --test Build for testing environment"
echo " --prod, --production Build for production environment"
echo " --debug Build debug app (default)"
echo " --release Build release app"
echo " --studio Open Xcode after build"
echo " --ipa Build IPA file"
echo " --app Build app bundle"
echo " --clean Clean build artifacts only"
echo " --sync Sync Capacitor only"
echo " --assets Generate assets only"
echo " --deploy Deploy app to connected device"
echo " --auto-run Auto-run app after build"
echo " --api-ip <ip> Custom IP address for claim API (uses Capacitor default)"
echo ""
echo "Common Options:"
echo " -h, --help Show this help message"
echo " -v, --verbose Enable verbose logging"
echo ""
echo "Examples:"
echo " $0 --dev --studio # Development build + open Xcode"
echo " $0 --prod --ipa # Production IPA build"
echo " $0 --test --app # Testing app build"
echo " $0 --test --auto-run # Test build + auto-run"
echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device"
echo " $0 --dev # Dev build with Capacitor default"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo ""
}
# Function to parse iOS-specific arguments
parse_ios_args() {
local args=("$@")
local i=0
while [ $i -lt ${#args[@]} ]; do
local arg="${args[$i]}"
case $arg in
--dev|--development)
BUILD_MODE="development"
;;
--test)
BUILD_MODE="test"
;;
--prod|--production)
BUILD_MODE="production"
;;
--debug)
BUILD_TYPE="debug"
;;
--release)
BUILD_TYPE="release"
;;
--studio)
OPEN_STUDIO=true
;;
--ipa)
BUILD_IPA=true
;;
--app)
BUILD_APP=true
;;
--clean)
CLEAN_ONLY=true
;;
--sync)
SYNC_ONLY=true
;;
--assets)
ASSETS_ONLY=true
;;
--deploy)
DEPLOY_APP=true
;;
--auto-run)
AUTO_RUN=true
;;
--api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}"
i=$((i + 1)) # Skip the next argument
else
log_error "Error: --api-ip requires an IP address"
exit 1
fi
;;
--api-ip=*)
CUSTOM_API_IP="${arg#*=}"
;;
-h|--help)
print_ios_usage
exit 0
;;
-v|--verbose)
set -x
;;
*)
log_warn "Unknown argument: $arg"
;;
esac
i=$((i + 1))
done
}
# Function to validate iOS environment
validate_ios_environment() {
log_info "Validating iOS build environment..."
# Check for Xcode
if ! command -v xcodebuild &> /dev/null; then
log_error "Xcode not found. Please install Xcode and command line tools."
exit 1
fi
# Check for iOS Simulator
if ! command -v xcrun &> /dev/null; then
log_error "Xcode command line tools not found. Please install with: xcode-select --install"
exit 1
fi
# Check for Capacitor
if ! command -v npx &> /dev/null; then
log_error "npx not found. Please install Node.js and npm."
exit 1
fi
# Check for iOS platform
if [ ! -d "ios" ]; then
log_error "iOS platform not found. Please run: npx cap add ios"
exit 1
fi
log_success "iOS build environment validated"
}
# Function to check iOS resources
check_ios_resources() {
log_info "Checking iOS resources..."
# Check for required assets
if [ ! -f "resources/icon.png" ]; then
log_warn "App icon not found at resources/icon.png"
fi
if [ ! -f "resources/splash.png" ]; then
log_warn "Splash screen not found at resources/splash.png"
fi
# Check for iOS-specific files
if [ ! -f "ios/App/App/Info.plist" ]; then
log_warn "Info.plist not found"
fi
if [ ! -f "ios/App/App/AppDelegate.swift" ]; then
log_warn "AppDelegate.swift not found"
fi
log_success "iOS resource check completed"
}
# iOS app icon appearance variants (Dark, Tinted, …).
# Luminosity appearance values match Apple's asset catalog format (iOS 18+).
readonly _IOS_APP_ICON_APPEARANCE_LUMINOSITY="luminosity"
readonly _IOS_APP_ICON_APPEARANCE_DARK="dark"
readonly _IOS_APP_ICON_APPEARANCE_TINTED="tinted"
# Each row: source_file|luminosity_value|dest_filename
# To add a variant: append one row and place the source PNG under resources/ios/.
readonly _IOS_APP_ICON_APPEARANCE_VARIANTS=(
"resources/ios/icon/icon-dark.png|${_IOS_APP_ICON_APPEARANCE_DARK}|AppIcon-Dark.png"
"resources/ios/icon/icon-tinted.png|${_IOS_APP_ICON_APPEARANCE_TINTED}|AppIcon-Tinted.png"
)
# Update AppIcon.appiconset/Contents.json with one 1024×1024 appearance entry.
# Preserves all other images (including capacitor-assets output); replaces any
# existing entry for the same luminosity appearance.
_update_appicon_contents_for_appearance() {
local contents_json="$1"
local dest_filename="$2"
local luminosity_value="$3"
local tmp_file
tmp_file="$(mktemp)"
if ! jq --arg filename "$dest_filename" \
--arg appearance "$_IOS_APP_ICON_APPEARANCE_LUMINOSITY" \
--arg value "$luminosity_value" \
'
.images |= map(select((.appearances[0].value // "") != $value))
| .images += [{
"appearances": [{
"appearance": $appearance,
"value": $value
}],
"filename": $filename,
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}]
' "$contents_json" > "$tmp_file"; then
rm -f "$tmp_file"
log_error "Failed to update AppIcon Contents.json for appearance: $luminosity_value"
return 1
fi
mv "$tmp_file" "$contents_json"
}
# Install one appearance variant when its source PNG exists.
_apply_ios_app_icon_appearance_variant() {
local appiconset_dir="$1"
local contents_json="$2"
local source_file="$3"
local luminosity_value="$4"
local dest_filename="$5"
if [ ! -f "$source_file" ]; then
log_info "iOS app icon appearance variant not found ($source_file) — skipping"
return 0
fi
if [ ! -d "$appiconset_dir" ]; then
log_warn "AppIcon.appiconset not found — skipping appearance variant ($luminosity_value)"
return 0
fi
if ! cp "$source_file" "$appiconset_dir/$dest_filename"; then
log_error "Failed to copy $source_file to $appiconset_dir/$dest_filename"
return 1
fi
log_info "Installed iOS app icon appearance variant: $luminosity_value ($dest_filename)"
_update_appicon_contents_for_appearance "$contents_json" "$dest_filename" "$luminosity_value"
}
# Post-process capacitor-assets iOS icons: copy optional Dark/Tinted sources and
# register them in AppIcon.appiconset/Contents.json.
apply_ios_app_icon_appearances() {
local appiconset_dir="ios/App/App/Assets.xcassets/AppIcon.appiconset"
local contents_json="$appiconset_dir/Contents.json"
if [ ! -f "$contents_json" ]; then
log_info "AppIcon Contents.json not found — skipping appearance variants"
return 0
fi
if ! command -v jq &> /dev/null; then
log_error "jq is required to install iOS app icon appearance variants (install with: brew install jq)"
return 1
fi
local variant source_file luminosity_value dest_filename
for variant in "${_IOS_APP_ICON_APPEARANCE_VARIANTS[@]}"; do
IFS='|' read -r source_file luminosity_value dest_filename <<< "$variant"
_apply_ios_app_icon_appearance_variant \
"$appiconset_dir" "$contents_json" \
"$source_file" "$luminosity_value" "$dest_filename"
done
}
# Generate iOS assets (capacitor-assets), then apply optional appearance variants.
generate_ios_assets() {
ensure_ios_capacitor_asset_directories
if [ -d "assets" ]; then
log_warn "@capacitor/assets prioritizes the top-level assets/ directory over resources/."
log_warn "This project intentionally uses resources/ as the canonical asset source."
log_warn "Remove the legacy assets/ directory if it is empty or obsolete."
fi
npx capacitor-assets generate --ios
apply_ios_app_icon_appearances
}
# Function to clean iOS build
clean_ios_build() {
log_info "Cleaning iOS build artifacts..."
# Clean Xcode build (temporary output directory)
if [ -d "ios/App/build" ]; then
rm -rf ios/App/build/
log_debug "Cleaned ios/App/build/"
fi
# Clean DerivedData
if [ -d "ios/App/DerivedData" ]; then
rm -rf ios/App/DerivedData/
log_debug "Cleaned ios/App/DerivedData/"
fi
# Clean Capacitor (using npm script instead of invalid cap clean command)
npm run clean:ios || true
log_success "iOS build cleaned"
}
# Function to build iOS app
build_ios_app() {
local build_config=""
local scheme="App"
local destination=""
if [ "$BUILD_TYPE" = "debug" ]; then
build_config="Debug"
# Use device SDK — prebuilt MLKit frameworks (MLImage, MLKitBarcodeScanning) ship
# iOS device slices only and cannot link against the iOS Simulator SDK.
destination="generic/platform=iOS"
else
build_config="Release"
destination="platform=iOS,id=auto"
fi
log_info "Building iOS app (${build_config})..."
cd ios/App
# Prevent pkgx-managed libs (e.g. zlib) from leaking into the iOS SDK linker.
unset LIBRARY_PATH
unset DYLD_LIBRARY_PATH
unset DYLD_FALLBACK_LIBRARY_PATH
# Build the app:
# -quiet: skip the huge export VAR dump (compiler warnings still show unless suppressed below).
# SWIFT_SUPPRESS_WARNINGS / GCC_WARN_INHIBIT_ALL_WARNINGS: quiet CLI output from Pods + plugins;
# build in Xcode for full diagnostics. Real errors still fail the build.
local build_exit=0
xcodebuild -quiet \
-workspace App.xcworkspace \
-scheme "$scheme" \
-configuration "$build_config" \
-destination "$destination" \
build \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
SWIFT_SUPPRESS_WARNINGS=YES \
GCC_WARN_INHIBIT_ALL_WARNINGS=YES || build_exit=$?
cd ../..
if [ $build_exit -ne 0 ]; then
return $build_exit
fi
log_success "iOS app built successfully"
}
# Function to deploy to device
deploy_ios_app() {
log_info "Deploy-app mode: building app and deploying to device"
# Check for connected device
local devices=$(xcrun devicectl list devices --json | grep -c '"state":"booted"' || echo "0")
if [ "$devices" -eq 0 ]; then
log_error "No iOS device connected. Please connect a device and try again."
exit 1
fi
# Build app for device
BUILD_TYPE="debug"
build_ios_app
# Get device ID
local device_id=$(xcrun devicectl list devices --json | grep -o '"identifier":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$device_id" ]; then
log_error "Could not find device ID. Please ensure device is connected and unlocked."
exit 1
fi
# Install app on device
log_info "Installing app on device..."
xcrun devicectl device install app --device "$device_id" ios/App/build/Debug-iphoneos/App.app
log_success "iOS app deployed successfully to device!"
log_info "You can now run the app with: npx cap run ios"
}
# Function to auto-run iOS app
auto_run_ios_app() {
log_step "Auto-running iOS app..."
# Check if we're in debug mode (simulator) or release mode (device)
if [ "$BUILD_TYPE" = "debug" ]; then
log_info "Launching iOS Simulator and installing app"
safe_execute "Launching app" "npx cap run ios" || {
log_error "Failed to launch iOS app on simulator"
return 1
}
else
log_info "Building and installing on real device"
# For release builds, we need to use a different approach
# since npx cap run ios is primarily for simulators
log_warn "Auto-run for release builds requires manual device setup"
log_info "Please use Xcode to run the app on your device"
return 1
fi
log_success "iOS app launched successfully!"
}
# Parse command line arguments
parse_ios_args "$@"
# Print build header
print_header "TimeSafari iOS Build Process"
log_info "Starting iOS build process at $(date)"
log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build
setup_build_env "capacitor" "$BUILD_MODE"
# Setup application directories
setup_app_directories
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Override API servers for iOS development when custom IP is specified
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then
# Use custom IP for physical device development
export VITE_DEFAULT_ENDORSER_API_SERVER="http://${CUSTOM_API_IP}:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://${CUSTOM_API_IP}:3000"
log_info "iOS development mode: Using custom IP ${CUSTOM_API_IP} for physical device"
fi
# Validate iOS environment
validate_ios_environment
# Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then
log_info "Clean-only mode: cleaning build artifacts"
clean_ios_build
safe_execute "Cleaning dist directory" "clean_build_artifacts dist" || exit 1
log_success "Clean completed successfully!"
exit 0
fi
# Xcode 26 / CocoaPods workaround for cap sync (used by sync-only and full build)
# Temporarily downgrade project.pbxproj objectVersion 70 -> 56 so pod install succeeds.
run_cap_sync_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
if [ ! -f "$PROJECT_FILE" ]; then
log_error "Project file not found: $PROJECT_FILE (run full build first?)"
return 1
fi
local current_version
current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version for Capacitor sync"
return 1
fi
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround for Capacitor sync: temporarily downgrading to format 56"
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format for Capacitor sync"
return 1
fi
log_info "Running Capacitor sync..."
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
log_debug "Restoring project format to 70 after Capacitor sync..."
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
log_success "Capacitor sync completed successfully"
else
log_debug "Project format is $current_version, running Capacitor sync normally"
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
return 1
fi
log_success "Capacitor sync completed successfully"
fi
}
# Handle sync-only mode
if [ "$SYNC_ONLY" = true ]; then
log_info "Sync-only mode: syncing with Capacitor (with Xcode 26 workaround if needed)"
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
log_success "Sync completed successfully!"
exit 0
fi
# Handle assets-only mode
if [ "$ASSETS_ONLY" = true ]; then
log_info "Assets-only mode: generating assets"
safe_execute "Generating assets" "generate_ios_assets" || exit 7
log_success "Assets generation completed successfully!"
exit 0
fi
# Handle deploy-app mode
if [ "$DEPLOY_APP" = true ]; then
deploy_ios_app
exit 0
fi
# Step 1: Check iOS resources
check_ios_resources
# Step 2: Clean iOS build
safe_execute "Cleaning iOS build" "clean_ios_build" || exit 1
# Step 3: Clean dist directory
log_info "Cleaning dist directory..."
clean_build_artifacts "dist"
# Step 4: Run TypeScript type checking for test and production builds
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then
safe_execute "Building Capacitor version (test)" "npm run build:capacitor -- --mode test" || exit 3
elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi
# Step 6: Fix Daily Notification Plugin podspec name (must run before pod install)
# ===================================================================
# The Podfile expects TimesafariDailyNotificationPlugin.podspec, but the plugin
# package only includes CapacitorDailyNotification.podspec. This script creates
# the expected podspec file before CocoaPods tries to resolve dependencies.
# ===================================================================
log_info "Fixing Daily Notification Plugin podspec name..."
if [ -f "./scripts/fix-daily-notification-podspec.sh" ]; then
if ./scripts/fix-daily-notification-podspec.sh; then
log_success "Daily Notification Plugin podspec created"
else
log_warn "Failed to create podspec (may already exist)"
fi
else
log_warn "fix-daily-notification-podspec.sh not found, skipping"
fi
# Step 6.5: Install CocoaPods dependencies (with Xcode 26 workaround)
# ===================================================================
# WORKAROUND: Xcode 26 / CocoaPods Compatibility Issue
# ===================================================================
# Xcode 26 uses project format version 70, but CocoaPods' xcodeproj gem
# (1.27.0) only supports up to version 56. This causes pod install to fail.
#
# This workaround temporarily downgrades the project format to 56, runs
# pod install, then restores it to 70. Xcode will automatically upgrade
# it back to 70 when opened, which is fine.
#
# NOTE: Both explicit pod install AND Capacitor sync (which runs pod install
# internally) need this workaround. run_pod_install_with_workaround() is below;
# run_cap_sync_with_workaround() is defined earlier (used by --sync and Step 6.6).
#
# TO REMOVE THIS WORKAROUND IN THE FUTURE:
# 1. Check if xcodeproj gem has been updated: bundle exec gem list xcodeproj
# 2. Test if pod install works without the workaround
# 3. If it works, remove run_pod_install_with_workaround() and run_cap_sync_with_workaround()
# 4. Replace with:
# - safe_execute "Installing CocoaPods dependencies" "cd ios/App && bundle exec pod install && cd ../.." || exit 6
# - safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
# 5. Update this comment to indicate the workaround has been removed
# ===================================================================
run_pod_install_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
log_info "Installing CocoaPods dependencies (with Xcode 26 workaround)..."
# Check if project file exists
if [ ! -f "$PROJECT_FILE" ]; then
log_error "Project file not found: $PROJECT_FILE"
return 1
fi
# Check current format version
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version"
return 1
fi
log_debug "Current project format version: $current_version"
# Only apply workaround if format is 70
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround: temporarily downgrading to format 56"
# Downgrade to format 56 (supported by CocoaPods)
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format"
return 1
fi
# Run pod install
log_info "Running pod install..."
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
# Try to restore format even on failure
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
# Restore to format 70
log_debug "Restoring project format to 70..."
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
fi
log_success "CocoaPods dependencies installed successfully"
else
# Format is not 70, run pod install normally
log_debug "Project format is $current_version, running pod install normally"
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
return 1
fi
log_success "CocoaPods dependencies installed successfully"
fi
}
safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaround" || exit 6
# Step 6.6: Sync with Capacitor (uses run_cap_sync_with_workaround defined above for Xcode 26)
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
# Step 7: Generate assets
safe_execute "Generating assets" "generate_ios_assets" || exit 7
# Step 8: Build iOS app
safe_execute "Building iOS app" "build_ios_app" || exit 5
# Step 9: Build IPA/App if requested
if [ "$BUILD_IPA" = true ]; then
log_info "Building IPA package..."
cd ios/App
xcodebuild -quiet \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath build/App.xcarchive \
archive \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
SWIFT_SUPPRESS_WARNINGS=YES \
GCC_WARN_INHIBIT_ALL_WARNINGS=YES
xcodebuild -quiet -exportArchive \
-archivePath build/App.xcarchive \
-exportPath build/ \
-exportOptionsPlist exportOptions.plist
cd ../..
log_success "IPA package built successfully"
fi
if [ "$BUILD_APP" = true ]; then
log_info "Building app bundle..."
# App bundle is already built in step 7
log_success "App bundle built successfully"
fi
# Step 10: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then
safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9
fi
# Step 11: Open Xcode if requested
if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Xcode" "npx cap open ios" || exit 8
fi
# Print build summary
log_success "iOS build completed successfully!"
log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE"
if [ "$BUILD_IPA" = true ]; then
log_info "IPA build: completed"
fi
if [ "$BUILD_APP" = true ]; then
log_info "App build: completed"
fi
if [ "$AUTO_RUN" = true ]; then
log_info "Auto-run: completed"
fi
if [ "$OPEN_STUDIO" = true ]; then
log_info "Xcode: opened"
fi
print_footer "iOS Build"
# Exit with success
exit 0