惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
MongoDB | Blog
MongoDB | Blog
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
O
OpenAI News
酷 壳 – CoolShell
酷 壳 – CoolShell
The GitHub Blog
The GitHub Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 聂微东
Engineering at Meta
Engineering at Meta
W
WeLiveSecurity
Hacker News: Ask HN
Hacker News: Ask HN
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
D
Docker
F
Full Disclosure
AI
AI
罗磊的独立博客
博客园 - 【当耐特】
U
Unit 42
S
SegmentFault 最新的问题
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Palo Alto Networks Blog
博客园_首页
H
Help Net Security
量子位
月光博客
月光博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
F
Fortinet All Blogs
D
DataBreaches.Net
B
Blog RSS Feed
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
S
Secure Thoughts
爱范儿
爱范儿
I
InfoQ
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
CERT Recently Published Vulnerability Notes
Martin Fowler
Martin Fowler
Blog — PlanetScale
Blog — PlanetScale
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Securelist

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger Vibe Meter: Monitor Your AI Costs | Peter Steinberger Claude Code is My Computer | Peter Steinberger Stop Over-thinking AI Subscriptions | Peter Steinberger Introducing Demark: HTML in. MD out. Blink-fast. | Peter Steinberger The Future of Vibe Coding: Building with AI, Live and Unfiltered | Peter Steinberger MCP Best Practices | Peter Steinberger Finding My Spark Again | Peter Steinberger Top-Level Menu Visibility in SwiftUI for macOS | Peter Steinberger Fixing keyboardShortcut in SwiftUI | Peter Steinberger Supporting Both Tap and Long Press on a Button in SwiftUI | Peter Steinberger On Using Apple Silicon Mac Mini for Continuous Integration | Peter Steinberger Apple Silicon M1: A Developer's Perspective | Peter Steinberger Gardening Your Twitter: Curating Your Timeline | Peter Steinberger Gardening Your Twitter: Growing Your Followers | Peter Steinberger Forbidden Controls in Catalyst: Optimize Interface for Mac | Peter Steinberger Disabling Keyboard Avoidance in SwiftUI's UIHostingController | Peter Steinberger The State of SwiftUI | Peter Steinberger Logging in Swift | Peter Steinberger Building with Swift Trunk Development Snapshots | Peter Steinberger Calling Super at Runtime in Swift | Peter Steinberger zld — A Faster Version of Apple's Linker | Peter Steinberger How to Fix LLDB: Couldn't IRGen Expression | Peter Steinberger Updating macOS on a Hackintosh | Peter Steinberger InterposeKit — Elegant Swizzling in Swift | Peter Steinberger The Great Mac Catalyst Text Input Crash Hunt | Peter Steinberger Jailbreaking for iOS Developers | Peter Steinberger Network Kernel Core Dump | Peter Steinberger How to macOS Core Dump | Peter Steinberger Kernel Panics and Surprise boot-args | Peter Steinberger The LG UltraFine 5K, kernel_task, and Me | Peter Steinberger Let's Try This Again | Peter Steinberger How We Work at PSPDFKit | Peter Steinberger Swizzling in Swift | Peter Steinberger WWDC for First-Timers, 2019 Edition | Peter Steinberger Challenges of Adopting Drag and Drop | Peter Steinberger Marzipan: Porting iOS Apps to the Mac | Peter Steinberger How to Use Slack and Not Go Crazy | Peter Steinberger Hardcore Debugging - Heavy Weapons for Hard Bugs | Peter Steinberger Binary Frameworks in Swift | Peter Steinberger Even Swiftier Objective-C | Peter Steinberger The Case for Deprecating UITableView | Peter Steinberger Running tests with Clang Address Sanitizer | Peter Steinberger UI testing on iOS, without busy waiting | Peter Steinberger Hiring a distributed team | Peter Steinberger Writing Good Bug Reports | Peter Steinberger Real-time collaboration, Apple, and you | Peter Steinberger Converting Xcode Test Runs to JUnit, the Fast Way | Peter Steinberger Efficient iOS Version Checking | Peter Steinberger Investigating Thread Safety of UIImage | Peter Steinberger Swifty Objective-C | Peter Steinberger Running UI Tests on iOS With Ludicrous Speed | Peter Steinberger A Pragmatic Approach to Cross-Platform | Peter Steinberger Surprises with Swift Extensions | Peter Steinberger Using ccache for Fun and Profit | Peter Steinberger UITableViewController designated initializer woes | Peter Steinberger Researching ResearchKit | Peter Steinberger The curious case of rotation with multiple windows on iOS 8 | Peter Steinberger UIKit Debug Mode | Peter Steinberger Retrofitting containsString: on iOS 7 | Peter Steinberger A Story About Swizzling "the Right Way™" and Touch Forwarding | Peter Steinberger Hacking with Aspects | Peter Steinberger Fixing UITextView On iOS 7 | Peter Steinberger Fixing What Apple Doesn't | Peter Steinberger How To Inspect The View Hierarchy Of Third-Party Apps | Peter Steinberger Fixing UISearchDisplayController On iOS 7 | Peter Steinberger Smart Proxy Delegation | Peter Steinberger Adding Keyboard Shortcuts To UIAlertView | Peter Steinberger How To Center Content Within UIScrollView | Peter Steinberger UIAppearance for Custom Views | Peter Steinberger Hacking Block Support Into UIMenuItem | Peter Steinberger
Code Signing and Notarization: Sparkle and Tears | Peter Steinberger
Peter Steinberger · 2025-06-05 · via Peter Steinberger

Or: How I Learned to Stop Worrying and Love the XPC Services

If you’ve ever tried to implement automatic updates in a sandboxed macOS app using Sparkle, you know it can feel like trying to solve a Rubik’s cube while wearing oven mitts. After creating way too many beta releases and spending countless hours debugging cryptic authorization errors, I finally cracked the code. Here’s my step-by-step guide to code signing, notarization, and Sparkle auto-updates in sandboxed macOS apps.

The Setup: Vibe Meter Meets Sparkle

Vibe Meter is a sandboxed macOS menu bar app that tracks AI service spending. When I decided to add automatic updates using Sparkle 2.x, I thought it would be straightforward. After all, Sparkle is the de facto standard for macOS app updates, right?

Oh, sweet summer child.

I’d already mastered code signing and notarization (its own nightmare), so I just told Claude: “Look at this repository and do what we did there.” It nailed the signing and notarization process.

My first attempts seemed promising. The app built, signed, and notarized successfully. But when users tried to update, they were greeted with:

Error: Failed to gain authorization required to update target

This error is Sparkle’s polite way of saying “I can’t talk to my XPC services, and I have no idea why.”

Act 2: The Entitlements Enigma

After digging through Sparkle’s documentation and Console logs, I discovered my first issue: missing mach-lookup entitlements. In a sandboxed app, Sparkle uses XPC services to perform privileged operations, and these services need special permissions to communicate.

The Missing Piece

My entitlements file was missing a critical entry:

<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
    <string>com.steipete.vibemeter-spks</string>
    <string>com.steipete.vibemeter-spki</string>
</array>

But here’s the kicker - I initially only added -spks, thinking it stood for “Sparkle Server.” Turns out, you need BOTH:

  • -spks: Sparkle Server (for the InstallerLauncher.xpc service)
  • -spki: Sparkle Installer (for the Installer.xpc service)

Missing either one results in the dreaded authorization error.

Act 3: The Code Signing Circus

Next came the code signing adventures. My notarization script was doing what seemed logical:

codesign --deep --force --sign "Developer ID" VibeMeter.app

But Sparkle’s documentation specifically warns against using --deep. Why? Because it can mess up the XPC services’ signatures. Instead, you need to sign components in a specific order.

Here’s the correct approach from my codesign script:

# Sign XPC services first
codesign -f -s "$SIGN_IDENTITY" -o runtime \
    "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"

# Preserve entitlements for Downloader.xpc (Sparkle 2.6+)
codesign -f -s "$SIGN_IDENTITY" -o runtime --preserve-metadata=entitlements \
    "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"

# Sign other Sparkle components
codesign -f -s "$SIGN_IDENTITY" -o runtime \
    "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"

# Then sign the framework
codesign -f -s "$SIGN_IDENTITY" -o runtime \
    "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework"

# Finally, sign the app WITHOUT --deep
codesign --force --sign "$SIGN_IDENTITY" --entitlements VibeMeter.entitlements \
    --options runtime VibeMeter.app

Act 4: The Bundle ID Bamboozle

At one point, I thought I was being clever (or rather, Claude Code thought…) by trying to manually modify the XPC services’ bundle identifiers. Big mistake.

For sandboxed apps, Sparkle requires specific bundle ID suffixes:

  • Use -spks suffix for the InstallerLauncher service
  • Use -spki suffix for the Installer service

The framework expects these exact suffixes. Use anything else, and you’ll get cryptic XPC connection errors that will make you question your career choices.

Act 5: The Build Number Blues

Even after fixing all the sandboxing issues, I hit another snag. Users were seeing “You’re up to date!” when updates were clearly available. The culprit? My appcast1 generation script was defaulting build numbers to “1”.

Sparkle uses build numbers (CFBundleVersion), not version strings, to determine if an update is available. If your build numbers don’t increment, Sparkle thinks there’s nothing new.

My appcast generation script now properly handles this:

# Extract build number from Info.plist
BUILD_NUMBER=$(plutil -extract CFBundleVersion raw "$INFO_PLIST")

# Ensure build numbers increment correctly
if [[ "$BUILD_NUMBER" -le "$PREVIOUS_BUILD_NUMBER" ]]; then
    echo "Error: Build number must be greater than previous release"
    exit 1
fi

The Grand Finale: It Works!

Sparkle update dialog working perfectly

After two days of intense debugging, I finally had a working setup. My complete automation pipeline is now rock-solid, with comprehensive scripts that handle every aspect of the process.

The Magic Recipe

  1. Entitlements: Include BOTH -spks and -spki mach-lookup exceptions
  2. Bundle IDs: Never change Sparkle’s XPC service bundle IDs
  3. Code Signing: Sign XPC services individually, never use --deep
  4. Build Numbers: Always increment them, and verify your appcast
  5. Info.plist: Set SUEnableInstallerLauncherService = true and SUEnableDownloaderService = false2

The Working Configuration

The XPC services themselves keep their original Sparkle bundle IDs (org.sparkle-project.InstallerLauncher and org.sparkle-project.Downloader). What we configure in the entitlements are the mach-lookup communication channels:

<!-- VibeMeter.entitlements -->
<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>com.steipete.vibemeter-spks</string>
    <string>com.steipete.vibemeter-spki</string>
</array>

The Scripts That Saved My Sanity

My complete build pipeline consists of several specialized scripts:

Look at this beauty! Now even Claude can do releases without messing up 🎉 I just tell it “Create a new beta release, see release.md” and it handles everything.

# Now create the release
./scripts/release.sh stable        # For stable 1.1.0
# OR
./scripts/release.sh beta 1        # For 1.1.0-beta.1

# Use the version script to bump versions
./scripts/version.sh --minor       # 1.0.0 -> 1.1.0
./scripts/version.sh --major       # 1.0.0 -> 2.0.0
./scripts/version.sh --set 1.1.0   # Set specific version

Everything happens automatically - from building to GitHub release creation. These scripts are tuned for Tuist and can easily be adapted for simpler setups.

The Notarization Nightmare

Apple’s notarization process adds another layer of complexity. Notarization verifies your app is safe - you upload it to Apple’s servers for scanning, and if approved, they “staple” a ticket to your app. The process typically takes a few minutes in Apple’s notarization queue (officially called the “processing queue”). The notarization script uses the modern notarytool approach:

# Create ZIP for notarization (not DMG!)
ditto -c -k --keepParent "$APP_BUNDLE" "$ZIP_PATH"

# Submit for notarization
xcrun notarytool submit "$ZIP_PATH" \
    --key "$API_KEY_PATH" \
    --key-id "$API_KEY_ID" \
    --issuer "$ISSUER_ID" \
    --wait

# Staple the ticket
xcrun stapler staple "$APP_BUNDLE"

# Verify everything worked
spctl -a -t exec -vv "$APP_BUNDLE"
xcrun stapler validate "$APP_BUNDLE"

Of course this needs certificates from Apple. Luckily Claude is smart enough to guide you through the whole process of downloading those files and adding them to the keychain and your environment.

Pretty Changelogs in Sparkle

One challenge I hadn’t anticipated was making the update dialogs actually useful. Sparkle can display rich HTML changelogs, but getting from my Markdown changelog to properly formatted HTML required some creativity.

The changelog-to-html.sh script extracts version-specific sections from CHANGELOG.md and converts them to HTML with what I call the “Poor Man’s Markdown Parser”:

# Extract version section and convert Markdown to HTML
awk "/^## \\[$version\\]/{flag=1;next}/^## \\[/{flag=0}flag" CHANGELOG.md | \
sed 's/^### \(.*\)/<h3>\1<\/h3>/' | \
sed 's/^- \(.*\)/<li>\1<\/li>/' | \
sed 's/\*\*\([^*]*\)\*\*/<strong>\1<\/strong>/g'

The result? Users see properly formatted changelogs with headers, lists, and styled text instead of raw Markdown. Note: This parser has limitations - it doesn’t handle nested lists, images, or links. Keep your changelogs simple or extend the parser for more complex formatting.

The Complete Architecture

What emerged is a surprisingly elegant zero-infrastructure solution that leverages GitHub’s existing services:

GitHub-Centric Distribution

  • Releases: GitHub releases host the actual DMG files
  • Appcast Hosting: Raw GitHub URLs serve the XML feeds
  • Dual Feeds: Separate appcasts for stable and pre-release channels
  • Version Control: Appcast files are versioned alongside the code

Dynamic Channel Switching

The app includes runtime logic to switch between update channels without reinstallation. Users can choose “stable” for production releases or “pre-release” for beta access, and the app dynamically points to the appropriate appcast URL.

Vibe Meter settings showing update channel options

Automated Everything

The release.sh script orchestrates the entire pipeline:

  1. Build and sign the app
  2. Create GitHub release with DMG
  3. Generate both appcast files with proper signatures
  4. Commit and push everything

No separate hosting, no Jekyll setup, no additional infrastructure - just GitHub doing what it does best. The key is that release.sh is the master orchestrator that calls most other scripts in sequence for a complete automated release.

View the complete script flow architecture
🚀 Main Release Flow (release.sh)

release.sh
├── 1. preflight-check.sh ────────────── Validates everything ready
├── 2. generate-xcproj.sh ────────────── Generates Xcode project
├── 3. build.sh ──────────────────────── Builds app with IS_PRERELEASE_BUILD flag
├── 4. sign-and-notarize.sh ──────────── Signs & notarizes app
│   ├── codesign-app.sh ─────────────── Code signs app bundle
│   └── notarize-app.sh ─────────────── Notarizes signed app
├── 5. create-dmg.sh ─────────────────── Creates & signs DMG
├── 6. GitHub CLI (gh) ───────────────── Creates GitHub release
├── 7. generate-appcast.sh ───────────── Updates appcast XML files
└── 8. verify-appcast.sh ─────────────── Validates appcast (optional)

✅ Verification Flow

preflight-check.sh ───────────────────── Pre-release validation
└── verify-prerelease-system.sh ──────── IS_PRERELEASE_BUILD system check

verify-app.sh ────────────────────────── Post-build verification
verify-appcast.sh ────────────────────── Appcast validation

🔐 Manual Signing Flow

sign-and-notarize.sh
├── codesign-app.sh ──────────────────── Code sign app
├── notarize-app.sh ──────────────────── Notarize app
└── create-dmg.sh ────────────────────── Create distribution DMG

🛠️ Utility Scripts (Called by others)

- changelog-to-html.sh ← Called by update-appcast.sh
- version.sh ← Standalone version management

Lessons Learned

  1. Read the documentation carefully - But also know that it might not cover every edge case
  2. Console.app is your friend - Filter by your process name and watch for XPC errors
  3. Don’t be clever - Follow Sparkle’s conventions exactly
  4. Test updates, not just builds - A successful build doesn’t mean updates will work
  5. Automate everything - Manual processes lead to human errors
  6. Version control your scripts - Build automation is as important as your app code

Final Thoughts

Implementing Sparkle in a sandboxed app is like solving a puzzle where the pieces keep changing shape. But once you understand the rules - respect the XPC services, get your entitlements right, and sign everything properly - it works beautifully.

The irony? The final solution is actually quite simple. It’s getting there that’s the adventure. I don’t know how anyone manages to ship working macOS apps at all, honestly.

Now go download Vibe Meter and read my dev diary on how I built it with Claude Code: Vibe Meter: Monitor Your AI Costs.

Resources


Special thanks to the Sparkle team and Claude Code - without them, this automation pipeline wouldn’t exist.

P.S. Just steal my scripts. It’ll save you days of debugging. ☕️

  1. An appcast is an RSS-like XML feed that Sparkle uses to check for updates. It contains information about available versions, download URLs, and release notes. See Sparkle’s documentation on appcasts for more details. ↩

  2. The Downloader service is only needed if your app doesn’t have network access. Since Vibe Meter includes the com.apple.security.network.client entitlement, we can disable SUEnableDownloaderService. This simplifies the setup and avoids the Downloader service’s limitations. See Sparkle’s sandboxing documentation for details. ↩