robin

robin / auto-update-options.md

Last active 2 hours ago

Like 0

robin revised this gist 2 hours ago · 2290812

1 file changed, 44 insertions

auto-update-options.md (file created)
@@ -0,0 +1,44 @@
1 + # Auto-update options for D-Patch (macOS, Swift, distributed as bare DMGs)
2 +
3 + Short answer: **Sparkle 2** — the de-facto standard, and basically the only serious native option for a non-sandboxed, non-App-Store app.
4 +
5 + ## The options
6 +
7 + | Option | What it is | Used by |
8 + |---|---|---|
9 + | **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 |
10 + | **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 |
11 + | **Homebrew cask** | Zero code — users get `brew upgrade` | Complements either, for the CLI crowd |
12 + | Squirrel.Mac | Electron's updater guts | Electron apps; effectively dead as a native Swift choice |
13 + | App Store / Software Update | Apple's own path | Not applicable — a patcher app will never pass review/sandboxing |
14 +
15 + ## Sparkle 2 specifics for D-Patch
16 +
17 + - SwiftPM dependency, `Sparkle` module, ~10 lines of app code (`SPUStandardUpdaterController` + a Check-for-Updates menu item).
18 + - **Keys**: one-time `generate_keys` → EdDSA keypair; public key in Info.plist, private key in CI secret.
19 + - **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.
20 + - **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.
21 + - **release.yml gets one step**: sign the DMG, append the appcast entry.
22 +
23 + ### The ad-hoc signing question (verify up front)
24 +
25 + 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.
26 +
27 + ### Why it's disproportionately nice here
28 +
29 + 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.
30 +
31 + 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.
32 +
33 + ## Recommendation
34 +
35 + - **Minimum viable**: hand-rolled checker — an afternoon, zero infra, users still manually install.
36 + - **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.
37 +
38 + ## Concrete changes if taking the Sparkle route
39 +
40 + 1. `Package.swift`: add `sparkle-project/Sparkle` (2.x).
41 + 2. `build.sh`: copy the two XPC services into `Contents/XPCServices` before codesign.
42 + 3. `DPatchApp`: wire `SPUStandardUpdaterController`, add Check-for-Updates menu item; Info.plist keys `SUFeedURL` + `SUPublicEDKey`.
43 + 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`.
44 + 5. Spike: end-to-end test update on a throwaway install before wiring it into a real release.