#!/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 already exists
if has_sparkle_package || grep -q "Sparkle" "${PROJECT_FILE}"; then
print_info "Sparkle already present"
return 0
fi
# Backup project file
cp "${PROJECT_FILE}" "${PROJECT_FILE}.backup"
# Add Sparkle to Package.resolved
if [ ! -f "${PACKAGE_RESOLVED}" ]; then
# Create Package.resolved if it doesn't exist
mkdir -p "$(dirname "${PACKAGE_RESOLVED}")"
cat > "${PACKAGE_RESOLVED}" << 'EOF'
{
"originHash" : "6b3386dc9ff1f3a74f1534de9c41d47137eae0901cfe819ed442f1b241549359",
"pins" : [
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "69faaefa7721fba9e434a52c16adf4329c9084db",
"version" : "4.6.0"
}
}
],
"version" : 3
}
EOF
fi
python3 -c "
import json
try:
with open('${PACKAGE_RESOLVED}', 'r') as f:
data = json.load(f)
# Add Sparkle to pins if not present
# Note: We let Xcode resolve the actual revision
sparkle_pin = {
'identity': 'sparkle',
'kind': 'remoteSourceControl',
'location': '${SPARKLE_REPO}',
'state': {
'version': '${SPARKLE_VERSION}'
}
}
if 'pins' not in data:
data['pins'] = []
# Check if Sparkle already in pins
if not any(pin.get('identity', '').lower() == 'sparkle' for pin in data['pins']):
data['pins'].append(sparkle_pin)
data['pins'].sort(key=lambda x: x.get('identity', ''))
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
# 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"
if ! has_sparkle_package; then
echo ""
print_warning "⚠️Sparkle not successfully added!"
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 "$@"