general: appstore build stuff

This commit is contained in:
Michael Freno
2026-01-13 11:31:34 -05:00
parent b7393568af
commit e379c4e3e4
18 changed files with 784 additions and 19 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSUIElement</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>CFBundleName</key>
<string>Gaze</string>
<key>CFBundleDisplayName</key>
<string>Gaze</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
</dict>
</plist>

View File

@@ -0,0 +1,36 @@
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Gaze/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.4.0;
OTHER_SWIFT_FLAGS = "-D APPSTORE";
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSUIElement</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>CFBundleName</key>
<string>Gaze</string>
<key>CFBundleDisplayName</key>
<string>Gaze</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
<key>SUPublicEDKey</key>
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
<key>SUFeedURL</key>
<string>https://freno.me/api/Gaze/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUScheduledCheckInterval</key>
<integer>86400</integer>
<key>SUEnableInstallerLauncherService</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,35 @@
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Gaze/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;

214
DISTRIBUTION.md Normal file
View File

@@ -0,0 +1,214 @@
# Gaze Distribution Guide
This guide explains how to build and distribute Gaze for both the Mac App Store and direct distribution with auto-updates.
## Distribution Methods
Gaze supports two distribution methods:
1. **Self-Distribution** (Direct Download) - Includes Sparkle for automatic updates
2. **Mac App Store** - Uses Apple's update mechanism, no Sparkle
## Quick Start
### Switching Between Distributions
Use the `switch_to` script to configure the project for each distribution method:
```bash
# For self-distribution with Sparkle auto-updates
./switch_to self
# For Mac App Store submission
./switch_to appstore
# Check current configuration
./switch_to status
```
### What Gets Changed
The `switch_to` script automatically manages:
**Self-Distribution Mode:**
- ✅ Adds Sparkle keys to `Info.plist` (SUPublicEDKey, SUFeedURL, etc.)
- ✅ Adds Sparkle entitlements for XPC services
- ✅ Removes `APPSTORE` compiler flag
- ✅ Enables UpdateManager with Sparkle framework
**App Store Mode:**
- ✅ Removes all Sparkle keys from `Info.plist`
- ✅ Removes Sparkle entitlements
- ✅ Adds `-D APPSTORE` compiler flag
- ✅ Disables Sparkle code at compile time
## Building for Self-Distribution
```bash
# 1. Switch to self-distribution mode
./switch_to self
```
The script will:
- Prompt for version bump (major/minor/patch)
- Build and code sign with Developer ID
- Notarize the app with Apple
- Create a signed DMG
- Generate Sparkle appcast with EdDSA signature
- (Optional) Upload to S3 if credentials are configured
## Building for Mac App Store
```bash
# 1. Switch to App Store mode
./switch_to appstore
# 2. Add Run Script Phase in Xcode (one-time setup)
# See section below
# 3. Archive and distribute via Xcode
# Product → Archive
# Window → Organizer → Distribute App → App Store Connect
```
### Required: Run Script Phase
For App Store builds, you **must** add this Run Script phase in Xcode:
1. Open Gaze.xcodeproj in Xcode
2. Select the Gaze target → Build Phases
3. Click + → New Run Script Phase
4. Name it: "Remove Sparkle for App Store"
5. Place it **after** "Embed Frameworks"
6. Add this script:
```bash
#!/bin/bash
if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then
echo "Removing Sparkle framework for App Store build..."
rm -rf "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework"
echo "Sparkle framework removed successfully"
fi
```
This ensures Sparkle.framework is removed from the app bundle before submission.
## Configuration Files
Configuration backups are stored in `.distribution_configs/`:
- `.distribution_configs/appstore/` - App Store configuration
- `.distribution_configs/self/` - Self-distribution configuration
These backups are created automatically and used when switching between modes.
## Validation
Before submitting to App Store Connect, verify your configuration:
```bash
./switch_to status
```
Expected output for App Store:
```
Info.plist: App Store (no Sparkle keys)
Entitlements: App Store (no Sparkle exceptions)
Build Settings: App Store (has APPSTORE flag)
```
Expected output for Self-Distribution:
```
Info.plist: Self-Distribution (has Sparkle keys)
Entitlements: Self-Distribution (has Sparkle exceptions)
Build Settings: Self-Distribution (no APPSTORE flag)
```
## Troubleshooting
### App Store Validation Fails
**Error: "App sandbox not enabled" with Sparkle executables**
- Solution: Make sure you ran `./switch_to appstore` and added the Run Script phase
**Error: "Bad Bundle Executable" or "CFBundlePackageType"**
- Solution: These are now fixed in the Info.plist
**Error: Still seeing Sparkle in the build**
- Solution: Clean build folder (⌘⇧K) and rebuild
### Self-Distribution Issues
**Sparkle updates not working**
- Verify: `./switch_to status` shows "Self-Distribution" mode
- Check: Info.plist contains SUPublicEDKey and SUFeedURL
- Verify: Appcast is accessible at the SUFeedURL
**Code signing issues**
- Check `.env` file has correct credentials
- Verify Developer ID certificate: `security find-identity -v -p codesigning`
## Environment Variables
For self-distribution, create a `.env` file with:
```bash
# Required for code signing
DEVELOPER_ID_APPLICATION="Developer ID Application: Your Name (TEAM_ID)"
APPLE_TEAM_ID="XXXXXXXXXX"
# Required for notarization
NOTARY_KEYCHAIN_PROFILE="notary-profile"
# Optional for S3 upload
AWS_ACCESS_KEY_ID="your-key"
AWS_SECRET_ACCESS_KEY="your-secret"
AWS_BUCKET_NAME="your-bucket"
AWS_REGION="us-east-1"
```
Setup notarization profile (one-time):
```bash
xcrun notarytool store-credentials "notary-profile" \
--apple-id "your@email.com" \
--team-id "TEAM_ID"
```
## Version Management
The `self_distribute` script handles version bumping:
- **Major** (X.0.0) - Breaking changes
- **Minor** (x.X.0) - New features
- **Patch** (x.x.X) - Bug fixes
- **Custom** - Any version string
- **Keep** - Increment build number only
Git tags are created automatically for new versions.
## Testing
### Test Self-Distribution Build
```bash
./switch_to self
# Test the DMG on a clean macOS system
```
### Test App Store Build
```bash
./switch_to appstore
# Archive in Xcode
# Use TestFlight for testing before release
```
## Best Practices
1. **Always use `switch_to`** - Don't manually edit configuration files
2. **Check status before building** - Use `./switch_to status`
3. **Clean builds** - Run Clean Build Folder when switching modes
4. **Test thoroughly** - Test both distribution methods separately
5. **Commit before switching** - Use git to track configuration changes
## Support
For issues or questions:
- GitHub Issues: https://github.com/mikefreno/Gaze/issues
- Check AGENTS.md for development guidelines

View File

@@ -121,6 +121,7 @@
buildPhases = (
27A21B382F0F69DC0018C4F3 /* Sources */,
27A21B392F0F69DC0018C4F3 /* Frameworks */,
27D081082F16AA7100FF3A31 /* Run Script */,
27A21B3A2F0F69DC0018C4F3 /* Resources */,
);
buildRules = (
@@ -257,6 +258,27 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
27D081082F16AA7100FF3A31 /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = " #!/bin/bash\n if [[ \"${OTHER_SWIFT_FLAGS}\" == *\"APPSTORE\"* ]]; then\n echo \"Removing Sparkle framework for App Store build...\"\n rm -rf \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework\"\n echo \"Sparkle framework removed successfully\"\n fi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
27A21B382F0F69DC0018C4F3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -475,6 +497,7 @@
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.4.0;
OTHER_SWIFT_FLAGS = "-D APPSTORE";
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;

View File

View File

@@ -193,8 +193,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
window.backgroundColor = .clear
window.contentView = NSHostingView(rootView: content)
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.acceptsMouseMovedEvents = !requiresFocus
window.ignoresMouseEvents = !requiresFocus
// Allow mouse events for all reminders (needed for dismiss button)
window.acceptsMouseMovedEvents = true
window.ignoresMouseEvents = false
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)

View File

@@ -6,10 +6,5 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
</dict>
</plist>

View File

@@ -12,6 +12,15 @@ struct GazeApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var settingsManager = SettingsManager.shared
init() {
// Handle test launch arguments
if TestingEnvironment.shouldSkipOnboarding {
SettingsManager.shared.settings.hasCompletedOnboarding = true
} else if TestingEnvironment.shouldResetOnboarding {
SettingsManager.shared.settings.hasCompletedOnboarding = false
}
}
var body: some Scene {
// Onboarding window (only shown when not completed)
WindowGroup {

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSUIElement</key>
<true/>
<key>LSApplicationCategoryType</key>
@@ -18,15 +22,5 @@
<string>$(MARKETING_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
<key>SUPublicEDKey</key>
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
<key>SUFeedURL</key>
<string>https://freno.me/api/Gaze/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUScheduledCheckInterval</key>
<integer>86400</integer>
<key>SUEnableInstallerLauncherService</key>
<true/>
</dict>
</plist>

View File

@@ -7,24 +7,31 @@
import Combine
import Foundation
#if !APPSTORE
import Sparkle
#endif
@MainActor
class UpdateManager: NSObject, ObservableObject {
static let shared = UpdateManager()
#if !APPSTORE
private var updaterController: SPUStandardUpdaterController?
private var automaticallyChecksObservation: NSKeyValueObservation?
private var lastCheckDateObservation: NSKeyValueObservation?
#endif
@Published var automaticallyChecksForUpdates = false
@Published var lastUpdateCheckDate: Date?
private override init() {
super.init()
#if !APPSTORE
setupUpdater()
#endif
}
#if !APPSTORE
private func setupUpdater() {
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
@@ -57,17 +64,24 @@ class UpdateManager: NSObject, ObservableObject {
}
}
}
#endif
func checkForUpdates() {
#if !APPSTORE
guard let updater = updaterController?.updater else {
print("Updater not initialized")
return
}
updater.checkForUpdates()
#else
print("Updates are managed by the App Store")
#endif
}
deinit {
#if !APPSTORE
automaticallyChecksObservation?.invalidate()
lastCheckDateObservation?.invalidate()
#endif
}
}

View File

@@ -101,13 +101,15 @@ struct LookAwayReminderView: View {
}
private func startCountdown() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
let timer = Timer(timeInterval: 1.0, repeats: true) { [self] _ in
if remainingSeconds > 0 {
remainingSeconds -= 1
} else {
dismiss()
}
}
RunLoop.current.add(timer, forMode: .common)
self.timer = timer
}
private func dismiss() {

View File

@@ -101,13 +101,15 @@ struct UserTimerOverlayReminderView: View {
}
private func startCountdown() {
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
let timer = Timer(timeInterval: 1.0, repeats: true) { [self] _ in
if remainingSeconds > 0 {
remainingSeconds -= 1
} else {
dismiss()
}
}
RunLoop.current.add(timer, forMode: .common)
countdownTimer = timer
}
private func dismiss() {

View File

@@ -1,6 +1,10 @@
#!/bin/bash
set -e
# Ensure we're using self-distribution configuration
echo "🔄 Switching to self-distribution configuration..."
./switch_to self
# Load environment variables from .env file
if [ -f .env ]; then
set -a

348
switch_to Executable file
View File

@@ -0,0 +1,348 @@
#!/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"
BACKUP_DIR=".distribution_configs"
# Distribution configurations
APPSTORE_CONFIG="${BACKUP_DIR}/appstore"
SELF_CONFIG="${BACKUP_DIR}/self"
# 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 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
if ! grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}"; then
# Find the Release config section and add the flag
sed -i.backup '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/{
/MARKETING_VERSION = /a\
OTHER_SWIFT_FLAGS = "-D APPSTORE";
}' "${PROJECT_FILE}"
rm -f "${PROJECT_FILE}.backup"
print_success "Added APPSTORE compiler flag"
fi
else
# Remove APPSTORE flag
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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSUIElement</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>CFBundleName</key>
<string>Gaze</string>
<key>CFBundleDisplayName</key>
<string>Gaze</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
<key>SUPublicEDKey</key>
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
<key>SUFeedURL</key>
<string>https://freno.me/api/Gaze/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUScheduledCheckInterval</key>
<integer>86400</integer>
<key>SUEnableInstallerLauncherService</key>
<true/>
</dict>
</plist>
EOF
cat > "${SELF_CONFIG}/Gaze.entitlements" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
</dict>
</plist>
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
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
# Restore App Store config
restore_config "App Store" "${APPSTORE_CONFIG}"
echo ""
print_success "Switched to App Store distribution mode"
print_warning "Remember to add the Run Script phase to remove Sparkle framework!"
echo ""
echo "Run Script to add in Xcode Build Phases:"
echo '#!/bin/bash'
echo 'if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then'
echo ' rm -rf "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework"'
echo 'fi'
}
# 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 ""
print_success "Switched to self-distribution mode"
print_info "Sparkle auto-updates enabled"
}
# 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 keys from Info.plist
- Removes Sparkle entitlements
- Adds APPSTORE compiler flag
self Switch to self-distribution configuration
- 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 "$@"