Let the app install its own updates - #6
Merged
Merged
Conversation
Loadout 0.3.2 could tell you a new version existed and then send you to a download page. That is the whole of what it did: every update ended in somebody dragging an app over another one in Applications, and plenty of people simply never did it. Sparkle replaces that. It finds the release, checks it was signed with the key this copy was built with, verifies it before unpacking, installs it and reopens. Pinned to 2.9.6 rather than a range, because this is the one dependency that installs executable code on somebody's disk. The old checker is deleted rather than left beside it. Two mechanisms mean two schedules, two preferences and two answers to "am I current?" — and the first time they disagree, one of the places you looked was lying. Its tests go with it: they described a GitHub API call and a version comparison that Sparkle now does itself. The preference survives the swap. Somebody who turned checks off in 0.3.2 asked not to be checked, so that answer is moved into Sparkle's own preference before the updater starts, and the dead keys are cleared. Settings › Updates stays, and now reads Sparkle instead of keeping its own copy of the same facts.
The framework is embedded in Contents/Frameworks with ditto, because a framework is a tree of symlinks and cp -R flattens it into something that looks right and will not load. Sparkle's XPC services are dropped: they only exist to carry the installer across a sandbox boundary, and Loadout is not sandboxed, so keeping them ships two executables that can never run. Signing is inside out and never with --deep. --deep is deprecated and hands the outer bundle's options to nested code, which is exactly wrong here: Autoupdate and Updater.app are separate programs that each need their own signature and the hardened runtime. Signing the app before the framework would be pointless anyway — changing anything inside a bundle breaks the signature wrapped around it. None of this fails loudly. A wrongly signed Sparkle builds, notarises and installs happily, and only shows up months later when somebody accepts an update and nothing happens. Hence Scripts/test-update.sh, which asks the assembled bundle the questions swift test cannot: is the framework there, is it signed, can the app find it, is the feed address right, is the public key in place. CI runs it after the build.
An update feed is only worth anything signed: the app is built with SURequireSignedFeed, so an unsigned one is treated as no feed at all and every installed copy quietly stops finding updates. Scripts/appcast.sh signs the image and the feed with the private key in this machine's login keychain — never in this repository — and refuses to run if that key and the public half baked into the app have drifted apart, which would make every installed copy reject the release as forged. The feed points at a second, byte-identical copy of the image published as Loadout-<version>-update.dmg. GitHub counts downloads per asset and nothing else, so this is the only way to tell an update from a first install. Both have to go up or the updater follows the feed to a 404. release.sh writes the feed after stapling rather than before — stapling changes the bytes the signature covers — clears the previous release's feed and update copy before building, and only prints the publish command when a signed feed is actually sitting in dist/. Otherwise it says so, because a release published without one breaks the updater for everybody who already has the app.
The changelog entry is about the thing somebody notices: no more dragging an app into Applications. The README says what a release now consists of, since it is three files and leaving one out is silent. The spec gains the two criteria that were missing — the bundle checks, and the one nobody can automate: that an installed copy really does install what it is offered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
0.3.2 could tell you a new version existed. It could not do anything about it — you still had to open the page, download a disk image and drag the app over the old one. This replaces that with Sparkle, set up the same way Imark is.
The app now downloads the update, checks its signature and installs it. Automatic checks are on and run once a day; automatic installs are off, so nothing is replaced without you saying yes.
How it is wired. Sparkle 2.9.6, pinned exactly. The framework is embedded in
Contents/Frameworksand signed inside-out with the hardened runtime, includingAutoupdateandUpdater.app. The feed isappcast.xml, published as a release asset and signed with an EdDSA key whose private half lives only in the login keychain; the app carries the public half and refuses an unsigned feed.Scripts/appcast.shwrites and signs the feed, andScripts/release.shrefuses to publish without one.The feed points at a second, identically-built
-update.dmg, because GitHub counts downloads per asset and that is the only way to tell an update apart from a first install.What goes away.
UpdateCheck.swift,UpdateNotice.swiftand their tests — they only described asking the GitHub API and comparing version strings, which is now Sparkle's job. Settings › Updates stays, driven by the updater: the running version, a toggle for automatic checks, and a button to check now. 311 → 299 tests, 0 failures.Not verifiable before this ships. That an update installs end to end needs two notarised releases: install this one, publish the next, watch it replace itself. Everything up to that point is verified — signature, feed, guards.