robin

robin / auto-update-options.md

Last active 2 hours ago

Like 0

Revision 229081270e555a6f0edd957f89304670fbba8dd6

auto-update-options.md Raw

Auto-update options for D-Patch (macOS, Swift, distributed as bare DMGs)

Short answer: Sparkle 2 — the de-facto standard, and basically the only serious native option for a non-sandboxed, non-App-Store app.

The options

Option What it is Used by
Sparkle 2 Framework: checks an appcast.xml feed, downloads, EdDSA-verifies, installs & relaunches. Delta updates, "install to /Applications", Check-for-Updates menu UX The default for indie/OSS Mac apps for ~20 years
Hand-rolled GitHub Releases check GET /repos/<owner>/<repo>/releases/latest, compare against CFBundleVersion, alert + link to DMG Small OSS apps that don't want the machinery
Homebrew cask Zero code — users get brew upgrade Complements either, for the CLI crowd
Squirrel.Mac Electron's updater guts Electron apps; effectively dead as a native Swift choice
App Store / Software Update Apple's own path Not applicable — a patcher app will never pass review/sandboxing

Sparkle 2 specifics for D-Patch

  • SwiftPM dependency, Sparkle module, ~10 lines of app code (SPUStandardUpdaterController + a Check-for-Updates menu item).
  • Keys: one-time generate_keys → EdDSA keypair; public key in Info.plist, private key in CI secret.
  • Appcast: appcast.xml with per-release entries; at release time sign_update <dmg> produces the signature + length for the entry. Can be committed to a branch and served via raw.githubusercontent/Pages — no server needed.
  • build.sh gets ~5 lines: Sparkle ships two XPC services (Autoupdate, Updater) that must be copied into Contents/XPCServices before signing. Framework + XPC, no Bundle.module accessor — doesn't touch the resource-bundle accessor minefield escaped in the 0.4.4 fix.
  • release.yml gets one step: sign the DMG, append the appcast entry.

The ad-hoc signing question (verify up front)

D-Patch is ad-hoc signed. Sparkle's integrity story with ad-hoc hosts rides on the EdDSA key rather than matching code signatures — OSS projects (IINA et al.) ship Sparkle this way. Still worth a 30-minute spike: test appcast, 0.4.4 → fake-0.4.5 update on a throwaway copy under /var/folders, confirm it installs cleanly.

Why it's disproportionately nice here

Sparkle's installer replaces the bundle itself — the downloaded update never goes through first-launch Gatekeeper assessment, so the "app is damaged / remove quarantine attr" dance users currently do disappears for every future update. That alone justifies it for an app distributed as bare DMGs.

Parallel unlock: a real Developer ID certificate ($99/yr Apple Developer Program) fixes the first-install Gatekeeper experience too, and makes Sparkle's validation story trivial. Nice-to-have, not a prerequisite.

Recommendation

  • Minimum viable: hand-rolled checker — an afternoon, zero infra, users still manually install.
  • The proper answer: Sparkle 2 — about half a day including the build.sh/release.yml changes and a dry-run update test. This is the one "most people" use.

Concrete changes if taking the Sparkle route

  1. Package.swift: add sparkle-project/Sparkle (2.x).
  2. build.sh: copy the two XPC services into Contents/XPCServices before codesign.
  3. DPatchApp: wire SPUStandardUpdaterController, add Check-for-Updates menu item; Info.plist keys SUFeedURL + SUPublicEDKey.
  4. CI: store the EdDSA private key as a repo secret; release.yml runs sign_update on the DMG and appends the entry to appcast.xml.
  5. Spike: end-to-end test update on a throwaway install before wiring it into a real release.