#!/bin/bash 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 # Configuration paths INFO_PLIST="Gaze/Info.plist" ENTITLEMENTS="Gaze/Gaze.entitlements" PROJECT_FILE="Gaze.xcodeproj/project.pbxproj" PACKAGE_RESOLVED="Gaze.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" BACKUP_DIR=".distribution_configs" # Distribution configurations APPSTORE_CONFIG="${BACKUP_DIR}/appstore" SELF_CONFIG="${BACKUP_DIR}/self" # Sparkle package details SPARKLE_REPO="https://github.com/sparkle-project/Sparkle" SPARKLE_VERSION="2.8.1" # Function to print colored messages print_info() { echo -e "${BLUE}ℹ${NC} $1" } print_success() { echo -e "${GREEN}✓${NC} $1" } print_warning() { echo -e "${YELLOW}⚠${NC} $1" } print_error() { echo -e "${RED}✗${NC} $1" } # Function to check if Sparkle is in Package.resolved has_sparkle_package() { if [ -f "${PACKAGE_RESOLVED}" ]; then grep -q "Sparkle" "${PACKAGE_RESOLVED}" return $? fi return 1 } # Function to remove Sparkle from Xcode project remove_sparkle_package() { print_info "Removing Sparkle package dependency..." # Check if Sparkle exists in the project if ! has_sparkle_package && ! grep -q "Sparkle" "${PROJECT_FILE}"; then print_info "Sparkle already removed" return 0 fi # Backup project file cp "${PROJECT_FILE}" "${PROJECT_FILE}.backup" # Remove Sparkle from Package.resolved if [ -f "${PACKAGE_RESOLVED}" ]; then python3 -c " import json try: with open('${PACKAGE_RESOLVED}', 'r') as f: data = json.load(f) # Filter out Sparkle from pins if 'pins' in data: data['pins'] = [pin for pin in data['pins'] if 'sparkle' not in pin.get('identity', '').lower()] with open('${PACKAGE_RESOLVED}', 'w') as f: json.dump(data, f, indent=2) print('✓ Updated Package.resolved') except Exception as e: print(f'Error: {e}', file=sys.stderr) sys.exit(1) " if [ $? -ne 0 ]; then print_error "Failed to update Package.resolved" mv "${PROJECT_FILE}.backup" "${PROJECT_FILE}" return 1 fi fi # Use Python script to safely remove Sparkle from project.pbxproj if ! python3 .manage_sparkle.py remove "${PROJECT_FILE}"; then print_error "Failed to update project.pbxproj" mv "${PROJECT_FILE}.backup" "${PROJECT_FILE}" return 1 fi rm -f "${PROJECT_FILE}.backup" print_success "Removed Sparkle package dependency" print_info "Xcode will resolve packages on next build" } # Function to add Sparkle to Xcode project add_sparkle_package() { print_info "Adding Sparkle package dependency..." # Check if Sparkle package reference already fully exists (9 references like Lottie) local sparkle_count=$(grep -c "Sparkle" "${PROJECT_FILE}" 2>/dev/null || echo "0") sparkle_count=$(echo "$sparkle_count" | tr -d '[:space:]') if [ "$sparkle_count" -ge 9 ]; then print_info "Sparkle already fully configured" return 0 fi # Backup project file cp "${PROJECT_FILE}" "${PROJECT_FILE}.backup" # Remove any partial Sparkle from Package.resolved if it exists (Xcode will resolve it fresh) if [ -f "${PACKAGE_RESOLVED}" ] && has_sparkle_package; then python3 -c " import json try: with open('${PACKAGE_RESOLVED}', 'r') as f: data = json.load(f) # Filter out Sparkle from pins - Xcode will resolve it fresh if 'pins' in data: data['pins'] = [pin for pin in data['pins'] if 'sparkle' not in pin.get('identity', '').lower()] with open('${PACKAGE_RESOLVED}', 'w') as f: json.dump(data, f, indent=2) print('✓ Cleaned Package.resolved (Xcode will resolve Sparkle)') except Exception as e: print(f'Warning: {e}', file=sys.stderr) " fi # Use Python script to safely add Sparkle to project.pbxproj if ! python3 .manage_sparkle.py add "${PROJECT_FILE}"; then print_error "Failed to update project.pbxproj" mv "${PROJECT_FILE}.backup" "${PROJECT_FILE}" return 1 fi rm -f "${PROJECT_FILE}.backup" print_success "Added Sparkle package dependency" print_info "Run './run build' or open Xcode to resolve packages" return 0 } # Function to create backup directories create_backup_dirs() { mkdir -p "${APPSTORE_CONFIG}" mkdir -p "${SELF_CONFIG}" } # Function to backup current configuration backup_current_config() { local config_name=$1 local config_dir=$2 print_info "Backing up ${config_name} configuration..." # Backup Info.plist if [ -f "${INFO_PLIST}" ]; then cp "${INFO_PLIST}" "${config_dir}/Info.plist" fi # Backup entitlements if [ -f "${ENTITLEMENTS}" ]; then cp "${ENTITLEMENTS}" "${config_dir}/Gaze.entitlements" fi # Backup relevant parts of project.pbxproj (just the Release config) if [ -f "${PROJECT_FILE}" ]; then # Extract the Release configuration section awk '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/ {print}' "${PROJECT_FILE}" > "${config_dir}/project_release.txt" fi print_success "Backed up ${config_name} configuration" } # Function to restore configuration restore_config() { local config_name=$1 local config_dir=$2 print_info "Restoring ${config_name} configuration..." # Restore Info.plist if [ -f "${config_dir}/Info.plist" ]; then cp "${config_dir}/Info.plist" "${INFO_PLIST}" print_success "Restored Info.plist" else print_warning "No Info.plist backup found for ${config_name}" fi # Restore entitlements if [ -f "${config_dir}/Gaze.entitlements" ]; then cp "${config_dir}/Gaze.entitlements" "${ENTITLEMENTS}" print_success "Restored entitlements" else print_warning "No entitlements backup found for ${config_name}" fi # Restore project.pbxproj Release configuration if [ -f "${config_dir}/project_release.txt" ]; then # Check if we're restoring to appstore or self mode based on file content if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${config_dir}/project_release.txt"; then # Add APPSTORE flag to both Debug and Release if ! grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}"; then # Add to Debug configuration sed -i.backup '/27A21B5E2F0F69DD0018C4F3 \/\* Debug \*\/ = {/,/name = Debug;/{ /MARKETING_VERSION = /a\ OTHER_SWIFT_FLAGS = "-D APPSTORE"; }' "${PROJECT_FILE}" # Add to Release configuration sed -i.backup2 '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/{ /MARKETING_VERSION = /a\ OTHER_SWIFT_FLAGS = "-D APPSTORE"; }' "${PROJECT_FILE}" rm -f "${PROJECT_FILE}.backup" "${PROJECT_FILE}.backup2" print_success "Added APPSTORE compiler flag" fi else # Remove APPSTORE flag from both Debug and Release if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}"; then sed -i.backup '/OTHER_SWIFT_FLAGS = "-D APPSTORE";/d' "${PROJECT_FILE}" rm -f "${PROJECT_FILE}.backup" print_success "Removed APPSTORE compiler flag" fi fi else print_warning "No project.pbxproj backup found for ${config_name}" fi } # Function to initialize configurations if they don't exist initialize_configs() { create_backup_dirs # Check if we have existing backups if [ ! -f "${SELF_CONFIG}/Info.plist" ]; then print_info "No self-distribution config found. Creating from current state..." # The current state should be self-distribution (before my changes) # Let's create the self-distribution version with Sparkle keys cat > "${SELF_CONFIG}/Info.plist" <<'EOF' CFBundleExecutable $(EXECUTABLE_NAME) CFBundlePackageType APPL LSUIElement LSApplicationCategoryType public.app-category.productivity CFBundleName Gaze CFBundleDisplayName Gaze CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion $(CURRENT_PROJECT_VERSION) CFBundleShortVersionString $(MARKETING_VERSION) NSHumanReadableCopyright Copyright © 2026 Mike Freno. All rights reserved. SUPublicEDKey Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM= SUFeedURL https://freno.me/api/Gaze/appcast.xml SUEnableAutomaticChecks SUScheduledCheckInterval 86400 SUEnableInstallerLauncherService EOF cat > "${SELF_CONFIG}/Gaze.entitlements" <<'EOF' com.apple.security.app-sandbox com.apple.security.network.client com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks $(PRODUCT_BUNDLE_IDENTIFIER)-spki EOF # Extract current Release config WITHOUT APPSTORE flag awk '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/ { if ($0 !~ /OTHER_SWIFT_FLAGS/) { print } else { # Skip the APPSTORE flag line if ($0 !~ /APPSTORE/) { print } } }' "${PROJECT_FILE}" > "${SELF_CONFIG}/project_release.txt" print_success "Created self-distribution config" fi if [ ! -f "${APPSTORE_CONFIG}/Info.plist" ]; then print_info "Creating App Store config..." # Backup current state as App Store config (after my changes) backup_current_config "App Store" "${APPSTORE_CONFIG}" fi } # Function to show current distribution mode show_current_mode() { print_info "Current configuration:" echo "" # Check for Sparkle keys in Info.plist if grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then echo " Info.plist: ${GREEN}Self-Distribution${NC} (has Sparkle keys)" else echo " Info.plist: ${BLUE}App Store${NC} (no Sparkle keys)" fi # Check for Sparkle entitlements if grep -q "mach-lookup.global-name" "${ENTITLEMENTS}" 2>/dev/null; then echo " Entitlements: ${GREEN}Self-Distribution${NC} (has Sparkle exceptions)" else echo " Entitlements: ${BLUE}App Store${NC} (no Sparkle exceptions)" fi # Check for APPSTORE flag if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}" 2>/dev/null; then echo " Build Settings: ${BLUE}App Store${NC} (has APPSTORE flag)" else echo " Build Settings: ${GREEN}Self-Distribution${NC} (no APPSTORE flag)" fi # Check for Sparkle package dependency if has_sparkle_package || grep -q "Sparkle" "${PROJECT_FILE}" 2>/dev/null; then echo " Package Dependency: ${GREEN}Self-Distribution${NC} (has Sparkle)" else echo " Package Dependency: ${BLUE}App Store${NC} (no Sparkle)" fi echo "" } # Function to switch to App Store configuration switch_to_appstore() { print_info "Switching to App Store distribution configuration..." echo "" # Backup current state if it's self-distribution if grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then backup_current_config "self-distribution" "${SELF_CONFIG}" fi # Remove Sparkle package dependency remove_sparkle_package echo "" # Restore App Store config restore_config "App Store" "${APPSTORE_CONFIG}" echo "" print_success "Switched to App Store distribution mode" print_info "Sparkle framework has been removed from dependencies" print_info "You can now archive and submit to App Store" } # Function to switch to self-distribution configuration switch_to_self() { print_info "Switching to self-distribution configuration..." echo "" # Backup current state if it's App Store if ! grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then backup_current_config "App Store" "${APPSTORE_CONFIG}" fi # Restore self-distribution config restore_config "self-distribution" "${SELF_CONFIG}" echo "" # Add Sparkle package dependency add_sparkle_package echo "" print_success "Switched to self-distribution mode" print_info "Sparkle auto-updates enabled" # Check if Sparkle was added to project.pbxproj (not Package.resolved since Xcode will resolve it) if ! grep -q "Sparkle" "${PROJECT_FILE}"; then echo "" print_warning "⚠️ Sparkle not successfully added to project!" else echo "" print_info "Sparkle package will be resolved on next build" fi } # Function to show usage show_usage() { cat << EOF ${BLUE}Gaze Distribution Configuration Switcher${NC} ${GREEN}Usage:${NC} ./switch_to [command] ${GREEN}Commands:${NC} appstore Switch to App Store distribution configuration - Removes Sparkle package dependency from project - Removes Sparkle keys from Info.plist - Removes Sparkle entitlements - Adds APPSTORE compiler flag self Switch to self-distribution configuration - Prompts to add Sparkle package dependency - Adds Sparkle keys to Info.plist - Adds Sparkle entitlements for XPC services - Removes APPSTORE compiler flag status Show current distribution configuration help Show this help message ${GREEN}Examples:${NC} ./switch_to appstore # Prepare for App Store submission ./switch_to self # Prepare for direct distribution with auto-updates ./switch_to status # Check current configuration ${YELLOW}Note:${NC} Configuration backups are stored in ${BACKUP_DIR}/ EOF } # Main script logic main() { # Check if we're in the right directory if [ ! -f "${PROJECT_FILE}" ]; then print_error "Not in Gaze project directory. Please run from project root." exit 1 fi # Initialize configurations if needed initialize_configs # Parse command case "${1:-}" in appstore) switch_to_appstore ;; self) switch_to_self ;; status) show_current_mode ;; help|--help|-h) show_usage ;; *) print_error "Unknown command: ${1:-}" echo "" show_usage exit 1 ;; esac } # Run main function main "$@"