Long time ago — in 1984 maybe — Andy Matuschak released a famous open-source framework named Sparkle. Unfortunately Andy left its development after joining Apple, but his successor Kornel Lesiński does a brilliant job of maintaining this important project.
Sparkle vs Sandbox
Eventually Apple introduced Sandbox for Mac apps, and Authorizarion Services became unavailable. In order to enable automatic software updates, Sparkle needs a privileged helper tool with a root access. The branch named 2.x has got a long history and uses a deprecated API, but it does the job.
Nevertheless I decided to avoid using Sparkle framework for NativeConnect. Their Sandbox implementation is still in Beta and it would add 1 MB of XPC helpers to the bundle.
So I wanted to build a custom update engine from the very beginning. This is not easy however, and the app was launched without support for in-app updates. As a temporary workaround, NativeConnect periodically asked the App Center if a newer version is available. Then it added a nice badge to the toolbar, suggesting to download and install an update manually.
The “Shparkle” Project
Once the launch was over, I started the new project. And after weeks in development, a major Update 1.1 was released for public. Among some great features, this new version has finally brought support for automatic in-app updates.
NativeConnect is still sandboxed, but now it includes a primitive non-sandboxed XPC service for command-line tasks of extracting an archive and replacing a bundle in the “Applications” folder. This helper tool is a necessary compromise, and it weights just 100 KB.
I’m not going to open-source “Shparkle” at the moment because such sensitive component surely needs some battle-testing. But in this article I’d like to describe a generic workflow for your consideration. I hope to get some feedback and maybe questions, so feel free to reach me in Twitter.
By the way, if you have an idea for a better name for such framework, I’m all ears. Just let me know!
1. The app is launched
Right after launch, there is no information about a newer version. We could say that the status of in-app updates is unknown, but at least we know the local version number.
This is the initial state of the Updates panel in Preferences.
2. Checking for Updates
NativeConnect is distributed via the App Center and we use their custom JSON feed for updates. This could be your own server however, so for simplicity let’s show it as “Cloud”.
This step is triggered when you hit the main menu item “Check for Updates…” or open the Preferences window for the first time. In our document app, we also check for updates when the current status is requested for a special badge in the toolbar.
Technically we make a URL request and download a JSON or XML feed from the Cloud to retrieve information about the newest available version of the app.
3. No Update is Available
If the local version matches the recent version in the Cloud, we’re up to date. There is no need to show the badge in the toolbar and the command “Check for Updates” becomes available again.
Optionally, we may schedule the next automatic check e.g. after 30 minutes.
4. An Update is Available
If the newer version can be downloaded and installed, we display a badge in the toolbar or a local notification. Now we should have a URL of a ZIP archive with an update. So we replace the button “Check for Updates” in Preferences by “Update Now”.
Optionally, we may have a checkbox with a setting that allows automatic installation.
5. Downloading a ZIP archive
In this state, a preference panel displays a nice progress indicator, while the update is being downloaded into the temporary location. Downloading can be cancelled if it’s too slow.
URLSession asks for a persistent location, we save a ZIP archive to the local folder in
6. Requesting help from the XPC service
Next, we pass both the archive and the main bundle paths to the XPC service which is not sandboxed.
Important: Our implementation does not use Authorization Services i.e. in-app updates will work only if you are logged in as admin.
Modern macOS is so secured that escalating privileges is a bad practice anyway. If you really want to update the app as a regular user, just click the button “More Info…” to download and install an update manually.
7. Extracting and installing an update
Speaking of errors, any issue in the helper tool is returned back to the main app and displayed in the Preferences panel.
First, the XPC service uses a
NSUserUnixTask to unarchive a new version of the app and remove its quarantine flag:
$ unzip "./Updates/NativeConnect-1.1-(11a8d62).zip" -d "./Updates/" $ xattr -d -r com.apple.quarantine "./Updates/NativeConnect.app"
Next, we compare entitlements of the original bundle versus entitlements of the downloaded app. This could be done in code but the command-line tool named
codesign is reliable as well and requires much less work.
$ codesign --display --verbose=2 "/Applications/NativeConnect.app" $ codesign --display --verbose=2 "./Updates/NativeConnect.app"
Next, we rename the outdated bundle by adding a process identifier as a suffix. Then the up-to-date version of the bundle is moved into the Applications folder.
$ mv "/Applications/NativeConnect.app" "/Applications/NativeConnect (12345).app" $ mv "./Updates/NativeConnect.app" "/Applications/NativeConnect.app"
Finally, we put the original bundle into the Trash using
NSFileManager and remove the temporary folder.
$ rm "/Applications/NativeConnect (12345).app" // Pseudo $ rm "./Updates"
8. Relaunching the Mac app
We’re almost done but there’s one neat trick borrowed from the AppMover codebase. A special set of Bash commands allows to schedule one application for launch when another application quits.
$ while /bin/kill -0 12345 >&/dev/null $ do sleep 0.1 $ done $ open "/Applications/NativeConnect.app"
While this script is running, we notify the Mac app about the successful update.
At this moment, the client app is still running even though its bundle is in Trash. Time to save all work and quit.
The magic script scheduled by the XPC will automatically launch the updated bundle from the Applications folder. For a better user experience, we pass a flag as a command-line argument. It can be inspected after relaunch to show Release Notes or the Updates panel.
Congratulations, the in-app update is over!
Our implementation is not feature-complete. For example, there is no support for DMG (not sure if it’s necessary for in-app updates). Nor we have a precise progress bar while a ZIP archive is being extracted and moved into the Applications folder. We also prefer command-line tools for some advanced tasks… This solution does work however, and it can be improved in the future.
The process of updating software is quite complex but also linear. We hope to release the “Shparkle” framework for public after a number of successful updates. So please stay tuned!