general: debugging appstore release

This commit is contained in:
Michael Freno
2026-01-13 12:59:36 -05:00
parent e065d72d9a
commit 8291d55b13
7 changed files with 402 additions and 11 deletions

View File

@@ -0,0 +1,201 @@
# Archive Post-Action Setup (REQUIRED for App Store)
## The Problem
App Store validation fails with:
```
App sandbox not enabled. The following executables must include the
"com.apple.security.app-sandbox" entitlement...
Sparkle.framework/Versions/B/Autoupdate
Sparkle.framework/Versions/B/Updater.app
...
```
Sparkle.framework **must be physically removed** from the app bundle for App Store distribution.
## Why Build Phase Scripts Don't Work
Build Phase scripts run during compilation when files are code-signed and locked by macOS. Even with `chmod` and `chflags`, you get "Operation not permitted" due to System Integrity Protection.
## The Solution: Archive Post-Action
Archive Post-Actions run **after** the archive completes, when files are no longer locked. This is the correct place to remove Sparkle.
---
## Setup Instructions (2 minutes)
### 1. Open Scheme Editor
In Xcode: **Product → Scheme → Edit Scheme...** (or press **⌘<**)
### 2. Select Archive
Click **Archive** in the left sidebar
### 3. Add Post-Action Script
- At the bottom, under "Post-actions", click the **+** button
- Select **New Run Script Action**
### 4. Configure the Action
**Provide build settings from:** Select **Gaze** from the dropdown (IMPORTANT!)
**Shell:** Leave as `/bin/bash`
**Script:** Copy and paste this entire script:
```bash
if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🗑️ Removing Sparkle from archived app..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
SPARKLE_PATH="${ARCHIVE_PATH}/Products/Applications/Gaze.app/Contents/Frameworks/Sparkle.framework"
if [ -d "$SPARKLE_PATH" ]; then
echo "📂 Found Sparkle at: $SPARKLE_PATH"
# Make writable and remove
chmod -R u+w "$SPARKLE_PATH" 2>/dev/null || true
chflags -R nouchg "$SPARKLE_PATH" 2>/dev/null || true
rm -rf "$SPARKLE_PATH"
if [ ! -d "$SPARKLE_PATH" ]; then
echo "✅ Sparkle framework removed successfully!"
else
echo "❌ ERROR: Could not remove Sparkle framework"
echo " This will cause App Store validation to fail"
exit 1
fi
else
echo " Sparkle framework not found (already removed)"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Archive ready for App Store distribution"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
echo "✓ Self-distribution archive - Sparkle retained"
fi
```
### 5. Save
Click **Close** to save the scheme changes
---
## Verification Steps
### Test the Archive
1. **Switch to App Store mode:**
```bash
./switch_to appstore
```
2. **Archive in Xcode:**
- **Product → Archive** (or **⌘⇧B** then Archive)
- Watch the build log - you should see the post-action output
3. **Check the archive contents:**
- **Window → Organizer**
- Select your latest Gaze archive
- Right-click → **Show in Finder**
- Right-click the `.xcarchive` file → **Show Package Contents**
- Navigate to: `Products/Applications/Gaze.app/Contents/Frameworks/`
- **Verify:** Only `Lottie.framework` should be present ✅
4. **Distribute to App Store:**
- In Organizer, click **Distribute App**
- Choose **App Store Connect**
- Complete the distribution wizard
- **Validation should now pass!** ✅
---
## What You Should See
### In the Build Log (after archiving):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🗑️ Removing Sparkle from archived app...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📂 Found Sparkle at: [path]
✅ Sparkle framework removed successfully!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Archive ready for App Store distribution
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### In the Frameworks folder:
```
Gaze.app/Contents/Frameworks/
└── Lottie.framework/
```
No Sparkle.framework! ✅
---
## Troubleshooting
### "I don't see the post-action output"
- Make sure you selected **Gaze** in "Provide build settings from"
- Check View → Navigators → Show Report Navigator (⌘9)
- Select the Archive action to see full logs
### "Sparkle is still in the archive"
- Verify `./switch_to status` shows "App Store" for all items
- Check the script exactly matches (copy/paste the entire script)
- Try cleaning: Product → Clean Build Folder (⌘⇧K)
### "Script says 'Sparkle framework not found'"
- This means Sparkle wasn't embedded (good!)
- Continue with distribution - validation should pass
### "Archive Post-Action section doesn't exist"
- Make sure you're editing the **Archive** section, not Run or Test
- Click the triangle next to "Archive" to expand it
---
## Optional: Remove Old Build Phase Script
If you previously added a Build Phase script (which doesn't work due to file locking), you can remove it:
1. Gaze target → Build Phases
2. Find "Remove Sparkle for App Store" or "Run Script"
3. Click the **X** to delete it
The Archive Post-Action is the correct and only solution needed.
---
## Why This Is Required
Even though:
- ✅ Sparkle code is disabled via `#if !APPSTORE`
- ✅ Info.plist has no Sparkle keys
- ✅ Entitlements have no Sparkle exceptions
App Store validation **still checks** for the physical presence of unsandboxed executables in frameworks. Sparkle contains XPC services that aren't App Store compatible, so the entire framework must be removed.
---
## For Self-Distribution
When building for self-distribution (`./switch_to self`), the script detects the absence of the APPSTORE flag and leaves Sparkle intact. You don't need to change anything!
```bash
./switch_to self
./self_distribute # Sparkle is retained and works normally
```
---
## Summary
**One-time setup:** Add Archive Post-Action script
**Works automatically:** Removes Sparkle only for App Store builds
**Zero maintenance:** Once configured, runs automatically forever
This is the **correct and only working solution** for removing Sparkle from App Store builds!

View File

@@ -8,7 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; }; 275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
275915902F132B0000D0E60D /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B22F10B20000E00DBC /* Sparkle */; }; 275915902F132B0000D0E60D /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B22F10B20000E00DBC /* Sparkle */; settings = {ATTRIBUTES = (Weak, ); }; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B3B2F0F69DC0018C4F3"
BuildableName = "Gaze.app"
BlueprintName = "Gaze"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B482F0F69DD0018C4F3"
BuildableName = "GazeTests.xctest"
BlueprintName = "GazeTests"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B522F0F69DD0018C4F3"
BuildableName = "GazeUITests.xctest"
BlueprintName = "GazeUITests"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B3B2F0F69DC0018C4F3"
BuildableName = "Gaze.app"
BlueprintName = "Gaze"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B3B2F0F69DC0018C4F3"
BuildableName = "Gaze.app"
BlueprintName = "Gaze"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
<PostActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "if [[ &quot;${OTHER_SWIFT_FLAGS}&quot; == *&quot;APPSTORE&quot;* ]]; then&#10; echo &quot;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&quot;&#10; echo &quot;&#x1f5d1;&#xfe0f; Removing Sparkle from archived app...&quot;&#10; echo &quot;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&quot;&#10; &#10; SPARKLE_PATH=&quot;${ARCHIVE_PATH}/Products/Applications/Gaze.app/Contents/Frameworks/Sparkle.framework&quot;&#10; &#10; if [ -d &quot;$SPARKLE_PATH&quot; ]; then&#10; echo &quot;&#x1f4c2; Found Sparkle at: $SPARKLE_PATH&quot;&#10; &#10; # Make writable and remove&#10; chmod -R u+w &quot;$SPARKLE_PATH&quot; 2&gt;/dev/null || true&#10; chflags -R nouchg &quot;$SPARKLE_PATH&quot; 2&gt;/dev/null || true&#10; rm -rf &quot;$SPARKLE_PATH&quot;&#10; &#10; if [ ! -d &quot;$SPARKLE_PATH&quot; ]; then&#10; echo &quot;&#x2705; Sparkle framework removed successfully!&quot;&#10; else&#10; echo &quot;&#x274c; ERROR: Could not remove Sparkle framework&quot;&#10; echo &quot; This will cause App Store validation to fail&quot;&#10; exit 1&#10; fi&#10; else&#10; echo &quot;&#x2139;&#xfe0f; Sparkle framework not found (already removed)&quot;&#10; fi&#10; &#10; echo &quot;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&quot;&#10; echo &quot;&#x2705; Archive ready for App Store distribution&quot;&#10; echo &quot;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&quot;&#10;else&#10; echo &quot;&#x2713; Self-distribution archive - Sparkle retained&quot;&#10;fi&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27A21B3B2F0F69DC0018C4F3"
BuildableName = "Gaze.app"
BlueprintName = "Gaze"
ReferencedContainer = "container:Gaze.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PostActions>
</ArchiveAction>
</Scheme>

View File

@@ -10,5 +10,23 @@
<integer>0</integer> <integer>0</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>27A21B3B2F0F69DC0018C4F3</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>27A21B482F0F69DD0018C4F3</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>27A21B522F0F69DD0018C4F3</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict> </dict>
</plist> </plist>

View File

@@ -20,28 +20,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private var hasStartedTimers = false private var hasStartedTimers = false
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
print("🚀 Gaze: applicationDidFinishLaunching")
// Set activation policy to hide dock icon // Set activation policy to hide dock icon
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
print("✓ Activation policy set to accessory")
timerEngine = TimerEngine(settingsManager: settingsManager) timerEngine = TimerEngine(settingsManager: settingsManager)
print("✓ TimerEngine initialized")
// Initialize update manager after onboarding is complete // Initialize update manager after onboarding is complete
if settingsManager.settings.hasCompletedOnboarding { if settingsManager.settings.hasCompletedOnboarding {
print("✓ Onboarding completed, initializing UpdateManager")
updateManager = UpdateManager.shared updateManager = UpdateManager.shared
} else {
print(" Onboarding not completed, skipping UpdateManager")
} }
// Detect App Store version asynchronously at launch // Detect App Store version asynchronously at launch
Task { Task {
await settingsManager.detectAppStoreVersion() do {
print("🔍 Detecting App Store version...")
await settingsManager.detectAppStoreVersion()
print("✓ App Store detection complete: \(settingsManager.settings.isAppStoreVersion)")
} catch {
print("⚠️ Failed to detect App Store version: \(error)")
}
} }
setupLifecycleObservers() setupLifecycleObservers()
print("✓ Lifecycle observers set up")
observeSettingsChanges() observeSettingsChanges()
print("✓ Settings change observer set up")
// Start timers if onboarding is complete // Start timers if onboarding is complete
if settingsManager.settings.hasCompletedOnboarding { if settingsManager.settings.hasCompletedOnboarding {
print("▶️ Starting timers (onboarding complete)")
startTimers() startTimers()
} else {
print("⏸️ Timers not started (onboarding incomplete)")
} }
print("✅ Gaze: Launch complete")
} }
func onboardingCompleted() { func onboardingCompleted() {

View File

@@ -13,12 +13,18 @@ struct GazeApp: App {
@StateObject private var settingsManager = SettingsManager.shared @StateObject private var settingsManager = SettingsManager.shared
init() { init() {
print("🚀 GazeApp: init")
// Handle test launch arguments // Handle test launch arguments
if TestingEnvironment.shouldSkipOnboarding { if TestingEnvironment.shouldSkipOnboarding {
print(" Test mode: Skipping onboarding")
SettingsManager.shared.settings.hasCompletedOnboarding = true SettingsManager.shared.settings.hasCompletedOnboarding = true
} else if TestingEnvironment.shouldResetOnboarding { } else if TestingEnvironment.shouldResetOnboarding {
print(" Test mode: Resetting onboarding")
SettingsManager.shared.settings.hasCompletedOnboarding = false SettingsManager.shared.settings.hasCompletedOnboarding = false
} }
print("✓ GazeApp initialized")
} }
var body: some Scene { var body: some Scene {

View File

@@ -17,30 +17,55 @@ enum AppStoreDetector {
/// This method is asynchronous due to the use of StoreKit's async API. /// This method is asynchronous due to the use of StoreKit's async API.
static func isAppStoreVersion() async -> Bool { static func isAppStoreVersion() async -> Bool {
#if DEBUG #if DEBUG
print("🔍 AppStoreDetector: DEBUG build, returning false")
return false return false
#else #else
print("🔍 AppStoreDetector: Checking App Store status...")
if #available(macOS 15.0, *) { if #available(macOS 15.0, *) {
print(" Using macOS 15+ AppTransaction API")
do { do {
_ = try await AppTransaction.shared let transaction = try await AppTransaction.shared
print(" ✅ AppTransaction found: This is an App Store version")
return true return true
} catch { } catch {
print(" ⚠️ AppTransaction error: \(error.localizedDescription)")
print(" → Assuming NOT an App Store version")
return false return false
} }
} else { } else {
// Fallback for older macOS: use legacy receipt check // Fallback for older macOS: use legacy receipt check
print(" Using legacy receipt check (macOS <15)")
guard let receiptURL = Bundle.main.appStoreReceiptURL else { guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print(" ⚠️ No receipt URL available")
return false return false
} }
guard FileManager.default.fileExists(atPath: receiptURL.path) else { print(" 📄 Receipt URL: \(receiptURL.path)")
do {
let fileExists = FileManager.default.fileExists(atPath: receiptURL.path)
guard fileExists else {
print(" ⚠️ Receipt file does not exist")
return false
}
print(" ✓ Receipt file exists")
guard let receiptData = try? Data(contentsOf: receiptURL),
receiptData.count > 2
else {
print(" ⚠️ Receipt file is empty or unreadable")
return false
}
print(" ✓ Receipt data loaded (\(receiptData.count) bytes)")
let bytes = [UInt8](receiptData.prefix(2))
let isValid = bytes[0] == 0x30 && bytes[1] == 0x82
print(" \(isValid ? "" : "⚠️") Receipt validation: \(isValid)")
return isValid
} catch {
print(" ❌ Receipt check error: \(error.localizedDescription)")
return false return false
} }
guard let receiptData = try? Data(contentsOf: receiptURL),
receiptData.count > 2
else {
return false
}
let bytes = [UInt8](receiptData.prefix(2))
return bytes[0] == 0x30 && bytes[1] == 0x82
} }
#endif #endif
} }