Last Friday I felt the time being ripe – after over a month of intense work – to roll out the first 1.0 version of iCatalog Editor. This Mac app is meant to revolutionize the work flow of creating digital catalog editions at my partner International Color Services. All those people who are are tasked with converting the raw material for paper catalogs into their feature-rich interactive digital counterparts where lacking such a tool for the past two years.
Not any more. Two years ago I had promised that there would be a Mac-based editor, but until now I was lacking the guts to dive into Mac development. I was afraid that my iOS development knowledge would not do me any good and that being an iOS development pro wouldn’t do me any good on the big intimidating Mac platform.
It turned out to be an unfounded fear. There are quite a few pitfalls, but I am pretty sure any experienced iOS developer can put together a good-sized Mac app in about a month or so. That’s what I did.
Development goes on, but from here on forth I will bunch fixes and new features together in releases that I need to push out to the guys and girls using the Editor app. Since it is sharply targeted at the unique needs of ICS I abandoned an earlier thought of putting it on the Mac app store. So how would I go about releasing the updates in a way that is as convenient as the app store, but would allow me to supply only the select group of Catalog Editors?
The answer to this question is: the same way most professional apps had been distributed before there ever was a Mac app store: Sparkle.
Sparkle is a framework invented by the now-famous Apple engineer Andy Matuschak. Andy is a sharp and well-dressed guy as we saw when he gave some amazing talks at WWDC. He had this to say about the state of Sparkle.
@cocoanetics It is not really maintained, but it is functional. So long as your app is not in a sandbox. :/
— Andy Matuschak (@andy_matuschak) November 9, 2012
Since I don’t need Sandbox for an in-house app that means I was good to go.
My iCatalog Editor app requires Mac OS X 10.8 because of several nifty components that Apple had introduced in Mountain Lion. For example the NSPageController which I use for the main editing view. This allows to page left and right by swiping a trackpad with two fingers or magic mouse with one. Also it gives me pinch-to-zoom for free.
Because of this requirement and not going through the Mac App Store I need to sign the app with Apple’s Developer ID. The default setting for 10.8 machines is to allow apps signed by Apple or by a Developer ID, but deny apps without signature. Since I didn’t want my users to have to override security settings Developer ID was the clear choice.
How does Sparkle work with Developer ID?
When I asked this question on twitter I didn’t get any useful answer. To make sure that there is no “man in the middle” attack on the app distribution flow Sparkle requires for a download to be cryptographically signed. Because of this I thought that there would be a conflict, would I use Sparkle’s signature or Developer ID?
Turns out that there is no conflict at all.
Sparkle does not actually require a signature of the app itself, but it requires that you generate a signature for a download package (e.g. ZIP) and add that to the app cast. This app cast is an xml file that serves as an index of available downloads. So the process is, you Archive your app, then you create a distribution with Developer ID. The output .app bundle gets zipped and for this zip file you generate a signature. This signature plus a few other details you put into the app cast.
Sparkle and Developer ID coexist peacefully without any problems.
How to Setup
The whole setup process is well documented though it is totally devoid of pictures which made it a bit harder to parse for the more visually oriented people. But if you walk through it step by step you will end up with a working distribution.
Rather let me show you around my successful implementation. I have the Sparkle.framework in the Frameworks folder. Also there is a Copy Files step that copies the framework into the app bundle when building the app.
The main class of Sparkle is the SUUpdater class which you “install” by adding it to a NIB file that are are certain will be loaded on app launch. Andy recommends to use the file that holds your app’s menu, and so I put it there in MainMenu.xib.
Sparkle takes care of most of the update UI automatically, but you probably also want to let the user check for updates manually. So we add a menu option for that right underneath the standard Preferences option. We connect the menu option to the checkForUpdates: IBAction of the Updater object we previously added, another reason why MainMenu.xib is the ideal place for that.
You need to add at least two keys to your app’s info plist.
- SUFeedURL – the URL of the “app cast” xml file on your download server
- SUPublicDSAKeyFile – the name (default is dsa_pub.pem) of the public key to your Sparkle signature
For setup we need to generate a pair of private and public keys for the DSA signature. There is a ruby script for this purposed contained in Extras – Signing Tools. Since a working version of ruby comes preinstalled on Macs, all we need to do is execute:
ruby generate_keys.rb |
As a result we get a dsa_priv.pem (private key) and dsa_pub.pem (public key) file of which we add the public key to our app’s resources. This needs to be included in the app bundle. The private key we have to keep in a safe place. Installed copies of our app require the private key signature to match the public key contained in their resources to be able to validate downloads.
Sparkle uses the bundle version to know if an offered version in the app cast is newer than the installed one. To make it easy on myself I added a Run Script phase to my build process that would use the current revision number from my SVN repository.
This uses /usr/bin/perl as the interpreter. I also modified the $REV to use xcrun so that the svnversion command from inside the Xcode bundle can be used.
# Xcode auto-versioning script for Subversion # by Axel Andersson, modified by Daniel Jalkut to add # "--revision HEAD" to the svn info line, which allows # the latest revision to always be used. use strict; die "$0: Must be run from Xcode" unless $ENV{"BUILT_PRODUCTS_DIR"}; # Get the current subversion revision number and use it to set the CFBundleVersion value my $REV = `/usr/bin/xcrun svnversion -n ./`; my $INFO = "$ENV{BUILT_PRODUCTS_DIR}/$ENV{WRAPPER_NAME}/Contents/Info.plist"; my $version = $REV; # (Match the last group of digits and optional letter M/S): # ugly yet functional (barely) regex by Daniel Jalkut: #$version =~ s/([\d]*:)(\d+[M|S]*).*/$2/; # better yet still functional regex via Kevin "Regex Nerd" Ballard ($version =~ m/\d+[MS]*$/) && ($version = $&); die "$0: No Subversion revision found" unless $version; open(FH, "$INFO") or die "$0: $INFO: $!"; my $info = join("", ); close(FH); $info =~ s/([\t ]+CFBundleVersion<\/key>\n[\t ]+).*?(<\/string>)/$1$version$2/; open(FH, ">$INFO") or die "$0: $INFO: $!"; print FH $info; close(FH); |
Let me repeat that: CFBundleShortVersionString is the human-readable version string like “1.0.1”, CFBundleVersion is meant to be machine-readable and is ideal to have the SVN revision number like “187”.
Now we have all the setup in place. Let’s do a release!
Doing a Release
Everything implemented in the bug tracker and also committed to the SVN repo. So we’re ready for releasing a new version. The short version string is already updated.
First step is to Build&Archive. Product – Archive. In Xcode organizer we see the new archive.
Click on Distribute. Xcode probably has already automatically requested and created a Developer ID for you, if you are a member of the Mac development program. So we choose “Export Developer ID-signed Application” and choose to output the resulting app bundle to the desktop.
The export might take a few seconds. Signing is hard. In the end we have a “iCatalog Editor.app” bundle on the desktop which carries an embedded Developer ID.
We quickly check the info plist to find out the internal build (= SVN revision) number. The short version string (user facing) is “1.01”, the bundle version string (for machines) is “201”.
We compress the app bundle without modifying the app name. It might be superstition, but I think I was having trouble if I modified that. On the resulting ZIP file I replace the space with an underscore because this is easier to handle at the terminal. The name is now “iCatalog_Editor.zip”.
For the app cast I need the exact file size as well as a freshly generated signature. From within the previously mentioned “Signing Tools” folder I create a signature with the sign_update ruby script.
ruby sign_update.rb ~/Desktop/iCatalog_Editor.zip dsa_priv.pem MCwCFFo7d8kVi46LOjeHLQG1MI0aaiezAhRrv6YgOIpWZnesvtNpPLTOoubzbg== |
And the file size:
ls -la ~/Desktop/iCatalog_Editor.zip -rw-r--r-- 1 oliver staff 3982518 11 Nov 14:19 /Users/oliver/Desktop/iCatalog_Editor.zip |
Those infos go into the appcast.xml which we can nicely edit with a text editor of even Xcode.
I’m not quite certain if I really need the sparkle:version and sparkle:shortVersionString, but it does not hurt. You can put the changes either into a CDATA block in the description or into external files.
Now all we need is to upload the new appcast.xml and zip file to our distribution server. A user installing the app for the very first time would still have to download the ZIP, unpack it and put it into his Application folder. But he would be reminded automatically about subsequent updates and the update process happens without user input.
Conclusion
The is a bit of preparation needed to get started with Sparkle and also a few steps are necessary to get a finished update from your Xcode organizer to the machines of your users. But I am certain that it should pose no problem to put these steps into a script so that they can be executed automatically e.g. by your build server.
Of course you could also go and simply provide a hyperlink to the ZIP file with the latest app version, but then users would not get the benefit of automatic update checks. Sparkle is the most ideal solution combining a great user experience with and easy to implement framework.
It is a bit sad that Andy does not have the time (or permission from Apple) to continue work on Sparkle but I am glad to be able to report that the current version is stable and fulfills my needs without problems. It remains the ideal solution for distributing apps that – for whatever reason – cannot be distributed through Apple’s Mac app store.
Categories: Recipes
Hi, exactly followed your process. Check for updates is coming but is grayed out.
This is also throwing the error on 2013-07-24 15:56:50.071 Myapp[7908:707] Unknown class ‘SUUpdater’, using ‘NSObject’ instead. Encountered in Interface Builder file at path /Users/sai/mycode/src/out/Release/Myapp.app/Contents/Versions/27.0.1441.0/Myapp Framework.framework/Resources/MainMenu.nib.
2013-07-24 15:56:50.072 Myapp[7908:707] Could not connect the action checkForUpdates: to target of class NSObject
Where appcast.xml should be loaded? Where you tell the app to read the XML file?
Thanks