It’s no secret that the Mac App Store is a terrific new distribution channel for developers. Apple also provides plenty of documentation on how to prepare your app for submission.
Unfortunately, there’s not much information on how to create a product that can also be distributed through more traditional channels, such as your own product website. This guide will help you update your Xcode projects to make it as simple as possible to create products for both channels simultaneously.
Introduction
The basic strategy is to create two build targets: one that creates everything for your own website, and another that creates the stuff Apple requires for the Mac App Store. The examples were all written using Xcode 3, but can be adapted to newer versions as needed.
The build target for your own website will include the Sparkle framework for doing updates. The one for the Mac App Store will create signed code that does a cryptographic check of the license receipt.
Note that these techniques can be used even if you’re only doing distribution on the Mac App Store: beta testers will benefit from builds that don’t require a receipt before launching.
To take you through the entire process, I’m going to use a real world example: I originally wrote these instructions while preparing our Flare product for release. When you see “Flare”, think “MyApp”. Likewise, “Iconfactory” will be “MyCompany.”
Certificate Setup
Before setting things up, make sure that you have two certificates in your keychain:
3rd Party Mac Developer Installer: The Iconfactory
3rd Party Mac Developer Application: The Iconfactory
If you don’t already have these installed, you can get them from the Developer Certificate Utility in the Member Center on the Apple Developer website.
Project Settings
In Project Build Settings, under Packaging, enable Info.plist preprocessing with:
Info.plist Other Preprocessor Flags: -C Preprocess Info.plist File: Checked
Do this for both the Debug and Release configurations.
Why do you want to preprocess your Info.plist file? Because there are subtle differences between the settings required for the Mac App Store and your own website. You could, of course, solve the problem by having two Info.plist files, but then you run the risk of them getting out of sync: what happens when you change the version number in one file and forget to do it in the other? Keeping things like document types consistent is tedious work: it’s much easier to configure your app with a single file.
Since things like checking license files and expiration dates are a pain when you’re debugging code, we’ll also define a DEBUG flag in the Debug configuration:
Other C Flags: -DDEBUG
Target Settings
You’ll want to create two build targets:
Flare
Flare App
It’s likely that you already have one target and can just duplicate it. The target “Flare” will be for your website, while “Flare App” is destined for the Mac App Store.
Make sure that both targets are using the same Info.plist source file for both the Debug and Release configurations:
Info.plist File: Flare-Info.plist
When you duplicate a target, Xcode “helpfully” creates a new Info.plist file for you. Since you’re going to be using a single file that’s customized using the preprocessor, you don’t need this second file and can delete it from the project folder.
It’s also important to remember that any new files that get added to the project now must be added to both targets. The goal is to keep both of them in sync.
Flare
In the target’s Link Binary With Libraries build phase, make sure that Sparkle.framework is included. If you’re following these steps just so you can have a beta build, leave this framework out.
Flare App
In Flare App’s Link Binary With Libraries build phase, make sure that Sparkle.framework is not included. You’ll want to make sure that IOKit.framework and Security.framework are included in the list (since they are used to determine the MAC address and check certificates during license verification.)
While you’re at it, make sure that Sparkle.framework is not in the target’s Copy Files build phase. Even if your app is not linking to Sparkle, a rejection awaits if the App Review team finds the framework in the application bundle.
Since we’re going to be adding code that checks which version is being built, add a new preprocessor flag to both the Debug and Release configurations for the target:
Other C Flags: -DMAC_APP_STORE
In your Objectve-C source code, you can now do things like this:
#ifdef MAC_APP_STORE BOOL validLicense = AppStoreLicenseCheck(); #else BOOL validLicense = MySuperSekretLicenseCheck(); #endif
App Store submissions must use signed code, so you’ll need set the following build setting in the Code Signing section:
Code Signing Identity: 3rd Party Mac Developer Application: The Iconfactory
You may also want to do this for the Flare target as well.
In the Linking section, you need to add the OpenSSL cryptographic library so the App Store license can be checked:
Other Linker Flags: -lcrypto
Do this for both the Release and Debug configurations since you’ll want to use Apple’s test_receipt while debugging your license verification code.
Finally, in the Target Build Settings, add the following under Packaging:
Info.plist Preprocessor Definitions: MAC_APP_STORE
Info.plist
Now you can update your Info.plist to work with preprocessor definitions. For example, the Flare-Info.plist is:
#define BUILD_VERSION 1.0 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleExecutable</key> <string>${EXECUTABLE_NAME}</string> <key>CFBundleIconFile</key> <string>app.icns</string> <key>CFBundleIdentifier</key> #ifndef MAC_APP_STORE <string>com.artissoftware.mac.flare</string> #else <string>com.artissoftware.flare</string> #endif <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>${PRODUCT_NAME}</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>BUILD_VERSION</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>BUILD_VERSION</string> <key>LSApplicationCategoryType</key> <string>public.app-category.photography</string> <key>LSMinimumSystemVersion</key> #ifndef MAC_APP_STORE <string>${MACOSX_DEPLOYMENT_TARGET}</string> #else <string>10.6.6</string> #endif <key>NSHumanReadableCopyright</key> <string>Copyright © 2006-2011 The Iconfactory & Artis Software</string> <key>NSMainNibFile</key> <string>MainMenu</string> <key>NSPrincipalClass</key> <string>NSApplication</string> #ifndef MAC_APP_STORE <key>SUFeedURL</key> <string>http://iconfactory.com/appcasts/Flare/appcast.xml</string> <key>SUExpectsDSASignature</key> <true/> <key>SUPublicDSAKeyFile</key> <string>dsa_pub.pem</string> #endif </dict> </plist>
Note that after adding the preprocessor definitions, the file will no longer open as a property list. You’ll need to right-click on the item in the Groups & Files list and select Open As > Source Code File.
The MAC_APP_STORE preprocessor definition allows a different bundle identifier to be selected, forces 10.6.6 as a minimum OS version, and removes the Sparkle configuration information.
Distribution Targets
It’s now time to automate the build of these two targets. This will be done with two aggregate build targets. Create the following:
Distribution_Iconfactory
Distribution_App_Store
The first will be used for your own website and the second is for the App Store.
For both build targets, make sure to define the following build setting for the release configuration:
PRODUCT_NAME: Flare
This value gets used in the run scripts to generate file names.
Distribution_Iconfactory
The first thing in the “Distribution_Iconfactory” target should be “Flare”. After that, create a new run script build phase (right-click on “Distribution_Iconfactory” and select Add > New Build Phase > New Run Script Build Phase. Name it “Package Release”.
This script will create several things:
- A ZIP file with build results. A version number is also added to the file name.
- A signature for the file that Sparkle will use to verify a new update.
- An appcast file that can be uploaded to your website.
The contents of the run script are as follows:
set -o errexit [ $CONFIGURATION = Release ] || { echo Distribution target requires "'Release'" build configuration; false; } VERSION=$(defaults read "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Info" CFBundleVersion) DOWNLOAD_BASE_URL="http://iconfactory.s3.amazonaws.com/flare" RELEASENOTES_URL="http://iconfactory.com/appcasts/Flare/release_notes.html#version-$VERSION" PACKAGE_NAME=`echo "$PRODUCT_NAME" | sed "s/ /_/"` ARCHIVE_FILENAME="$PACKAGE_NAME-$VERSION.zip" DOWNLOAD_URL="$DOWNLOAD_BASE_URL/$ARCHIVE_FILENAME" KEYCHAIN_PRIVKEY_NAME="Sparkle Private Key" WD=$PWD cd "$BUILT_PRODUCTS_DIR" rm -f "$PRODUCT_NAME"*.zip ditto -ck --keepParent "$PRODUCT_NAME.app" "$ARCHIVE_FILENAME" SIZE=$(stat -f %z "$ARCHIVE_FILENAME") PUBDATE=$(LC_TIME=en_US date +"%a, %d %b %G %T %z") PRIVATE_KEY=$(security find-generic-password -g -s "$KEYCHAIN_PRIVKEY_NAME" 2>&1 1>/dev/null | /usr/bin/perl -pe '($_) = /"(.+)"/; s/\\012/\n/g' | /usr/bin/perl -MXML::LibXML -e 'print XML::LibXML->new()->parse_file("-")->findvalue(q(//string[preceding-sibling::key[1] = "NOTE"]))') echo "$PRIVATE_KEY" > /tmp/sparkle.key [ -n "$PRIVATE_KEY" ] || { echo Unable to load signing private key with name "'$KEYCHAIN_PRIVKEY_NAME'" from keychain; false; } SIGNATURE=$(openssl dgst -sha1 -binary < "$ARCHIVE_FILENAME" | openssl dgst -dss1 -sign /tmp/sparkle.key | openssl enc -base64) echo $SIGNATURE > /tmp/sig.out rm /tmp/sparkle.key [ -n "$SIGNATURE" ] || { echo Unable to create signature for "'$ARCHIVE_FILENAME'"; false; } cat > "$BUILT_PRODUCTS_DIR/appcast.xml" <<EOF <?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>Flare Release</title> <link>http://iconfactory.com/appcasts/Flare/appcast.xml</link> <description>Most recent version of Flare.</description> <language>en</language> <item> <title>Version $VERSION</title> <sparkle:releaseNotesLink>$RELEASENOTES_URL</sparkle:releaseNotesLink> <pubDate>$PUBDATE</pubDate> <enclosure url="$DOWNLOAD_URL" sparkle:version="$VERSION" type="application/octet-stream" length="$SIZE" sparkle:dsaSignature="$SIGNATURE" /> </item> </channel> </rss> EOF
(Thanks to Marc Liyange for the original idea.)
Before this script will work, you need to create a secure note in your Keychain named “Sparkle Private Key”. Contents should be the “DSA PRIVATE KEY” text from the dsa_priv.pem file you created when setting up Sparkle.
Distribution_App_Store
The first thing in this aggregate target is “Flare App”. After that build step, add this run script build phase. Name it “Package Release”:
set -o errexit [ $CONFIGURATION = Release ] || { echo Distribution target requires "'Release'" build configuration; false; } VERSION=$(defaults read "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Info" CFBundleVersion) PACKAGE_NAME=`echo "$PRODUCT_NAME" | sed "s/ /_/"` ARCHIVE_FILENAME="$PACKAGE_NAME-$VERSION.pkg" cd "$BUILT_PRODUCTS_DIR" rm -f "$PACKAGE_NAME"*.pkg productbuild --component "$PRODUCT_NAME.app" /Applications \ --sign "3rd Party Mac Developer Installer: The Iconfactory" "$ARCHIVE_FILENAME"
This script produces a .pkg installer file that’s signed with the Iconfactory’s certificate.
Distribution Build Script
Since both of the distribution builds create a binary in your Project’s build/Release folder, you need to remember to clean the target before building. To make this easy, just create the following shell script named “build_release”:
#!/bin/sh current_tools=`xcode-select -print-path` build_tools="/Developer" switch=0 if [ $current_tools != $build_tools ]; then switch=1 fi if [ $switch -ne 0 ]; then echo "Switching from $current_tools to $build_tools..." sudo xcode-select -switch $build_tools else echo "Using $build_tools..." fi configuration="Release" target="Distribution_App_Store" xcodebuild \ -configuration "$configuration" \ -target "$target" \ clean build target="Distribution_Iconfactory" xcodebuild \ -configuration "$configuration" \ -target "$target" \ clean build if [ $switch -ne 0 ]; then echo "Restoring $current_tools..." sudo xcode-select -switch $current_tools fi
This script also ensures that you’re building with a released version of the Xcode developer tools (installed in /Developer). This is important if you do iOS development, as you’re likely to have a beta versions installed as well.
Changes to App Delegate
So far, we’ve been focused on building and packaging your release. You’ll also need to change your source code to accommodate the distribution channel.
If you haven’t done so already, make sure that there are no Sparkle objects in your MainMenu.xib file (as is suggested by the documentation.) The reason for this is simple: the App Review team does a grep for SUUpdater on binaries are submitted. If you have an archived object in the NIB file you’ll be rejected.
Since you’ll still need an SUUpdater object, you’ll need to create an instance of the object manually as you awake from the NIB. Since the application delegate is also on the responder chain, it can also handle the -checkForUpdates: message:
#ifndef MAC_APP_STORE #import "Sparkle/SUUpdater.h" #endif - (void)awakeFromNib { #ifdef MAC_APP_STORE [[checkForUpdatesMenuItem menu] removeItem:checkForUpdatesMenuItem]; #else [[SUUpdater alloc] init]; #endif } #ifndef MAC_APP_STORE - (IBAction)checkForUpdates:(id)sender { [[SUUpdater sharedUpdater] checkForUpdates:sender]; } #endif
You’ll also need to create a NIB outlet for the “Check for Updates…” menu item so that it can be removed from the Mac App Store version.
(If you have your own purchasing and registration menu items, you can handle them in a similar manner. I can guarantee you won’t get through App Review with a menu item titled “Purchase Flare…”.)
There are two different bundle identifiers for a single application named “Flare” (one for the version from the website and another for the Mac App Store). A user could get confused if they have both copies installed (especially if one of them is a trial version with limited functionality.) To help users with multiple versions, you should add the following code at launch to check if the one from the web site is installed alongside another from the App Store:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { #ifndef DEBUG #ifndef MAC_APP_STORE // check that a trial version isn't installed NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; NSString *pathToAppStoreVersion = [workspace absolutePathForAppBundleWithIdentifier:@"com.artissoftware.flare"]; if (pathToAppStoreVersion) { NSString *message = [NSString stringWithFormat:@"It looks like you have a copy of Flare from the Mac App Store installed in \"%@\".\n\nThe application you just launched is a trial version and should be removed. Would you like to quit now so you can move this unneeded file to the Trash?\n\nHint: Use the \"Move to Trash\" item in the \"File\" menu after the Finder window appears.", [pathToAppStoreVersion stringByDeletingLastPathComponent]]; NSInteger result = [[NSAlert alertWithMessageText:@"Multiple Copies Installed" defaultButton:@"Quit and Reveal in Finder" alternateButton:@"Continue" otherButton:nil informativeTextWithFormat:message] runModal]; if (result == NSAlertDefaultReturn) { [workspace selectFile:[[NSBundle mainBundle] bundlePath] inFileViewerRootedAtPath:nil]; exit(0); } } #endif #endif … }
If you’re using these techniques to build a beta version in parallel with your Mac App Store build, you’ll want to have some expiration checking code in the app. Otherwise, your final beta will find its way onto the Internet…
// expiration is used for any (beta) builds that aren't for the Mac App Store #ifndef MAC_APP_STORE #define USE_EXPIRATION 1 #else #define USE_EXPIRATION 0 #endif
#if USE_EXPIRATION - (void)checkExpiration { NSDate *today = [NSDate date]; // pick the date to expire on NSDate *expireDate = [NSDate dateWithString:@"2011-04-01 00:00:00 -0800"]; NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [dateFormatter setDateFormat:@"h:mm a 'on' EEEE',' MMMM d',' yyyy"]; NSString *expireDateString = [dateFormatter stringFromDate:expireDate]; if (! [[today laterDate:expireDate] isEqualToDate:expireDate]) { [[NSAlert alertWithMessageText:@"Take Five Beta" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"This beta release of Take Five expired at %@.", expireDateString] runModal]; [[NSApplication sharedApplication] terminate:self]; } else { [[NSAlert alertWithMessageText:@"Take Five Beta" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"This release of Take Five is a beta and will expire at %@.", expireDateString] runModal]; } } #endif
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { … #ifndef DEBUG #if USE_EXPIRATION [self checkExpiration]; #endif #endif … }
Changes to main.m
The license check for the Mac App Store should happen as early as possible in the app. It’s recommended that this happens before NSApplicationMain is called:
#import "ValidateReceipt.h" int main(int argc, char *argv[]) { #if MAC_APP_STORE NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; if (! validReceipt()) { pool = (NSAutoreleasePool *)1; exit(173); } [pool drain]; #endif return NSApplicationMain(argc, (const char **) argv); }
The code that assigns an invalid value to the pool object is done as a simple protection against piracy. If someone tries to patch the exit(173), they’ll just be met with an objc_msgSend exception when the pool is drained.
Of course, you’ll want to add additional code that checks the validity of the receipt elsewhere in your code. The Apple Developer website has additional information on how to validate your store receipts.
Receipt Validation
As far as validating the license is concerned, the bulk of the code to parse and validate the receipt can be gotten from the ValidateStoreReceipt project on github. You’ll need to change the hard-coded bundle identifier, version numbers and paths used in the project as you adapt this code for your own product.
Gatekeeper
You’ll also want to sign code sign the app you distribute outside of the Mac App Store. Apple has a helpful guide on how to make your app compatible with Gatekeeper.
To quickly check that your build is signed correctly with a Developer ID, you can use the spctl
command and make sure it is “accepted”:
$ spctl -a -v Flare.app Flare.app: accepted source=Developer ID
Testing
There’s a lot going on here, so you’ll certainly want to test both the distribution builds. The builds that are going to beta testers or onto your own website are straightforward: it’s just a ZIP file that can be loaded onto the target system. Once loaded, check that menu item for Sparkle updates is present. You’ll also want to check the Package Contents to see that the correct bundle identifier is set.
Things get a little more complicated when you start testing the build for the Mac App Store.
Start by reading Apple’s instructions on testing the installation process. Unfortunately, these instructions only show you how to get the application installed in your /Applications folder:
$ sudo installer -store -pkg Flare-1.0.pkg -target /
The documentation fails to mention that the installation process can fail if duplicate copies of the app are present elsewhere on the system (e.g. in the build folder.) Beta versions that use the same bundle identifier can also be a problem.
Apple’s test documentation also fails to mention that sandbox users accounts can be created in iTunes Connect (under Manage Users.) These user names and passwords can then be entered after launching the installed application and a receipt will be generated in the application’s _MASReceipt folder.
In order for this launch test to work, the app metadata must be defined in iTunes Connect (a binary does not need to be uploaded, it only needs to show up in “Manage Your Apps”.) Also make sure you sign out and quit the App Store app before launching your app to test the receipt processing code.
A bug report has been filed.
Conclusion
Hopefully these long-winded instructions will be helpful as you prepare your release for the Mac App Store. We’ve already used them several times for our own products, so I’m guessing they will be. :-)