Wednesday, March 26, 2014

Getting a Qt/C++ App in the Mac App Store

Short story: yes, despite what you may have heard getting a Qt/C++ application into the Mac App Store is possible.  Its not simple, there are some traps but my app, Plistinator, is proof it can be done even the new world of Mavericks with its increasingly more stringent sandboxing regime.

Update: 29 June 2015 - with Qt 5.4 the macdeployqt application has greatly improved and not only has many of the bugs fixed, it also can help with the actual signing.  I've left the links to scripts and patches on here, but you likely will not need them all.

Mac App Store?


Do you really want your app in the Mac App Store?  If  you can, in my opinion - yes.  The cost in terms of Apple's commission is very reasonable since you get
  • fulfillment
    • download to an end-user's hands in exchange for zapping their credit card
  • licensing
    • get a license receipt for the app so you can deal with usage issues
  • handle reviews, charge-backs, upgrades
    • charge-backs and other post-sale customer issues are a part of e-commerce
  • captive audience
    • App Store is on every Mac
Doing all these things to a commercial grade is Hard(TM).  Sure you can use PotionStore and dump some binaries on Amazon S3 but are you ready to take the phone calls if your instance goes down after the credit card is billed, but before the download is served up?

Matt Gemmell has written an excellent article on the pro's and con's and I agree with pretty much everything he says.  My conclusion was that I wanted to go with the Store still, but I also sell my other versions outside.  If you are not sure about the App Store read it and see where you fall on the issues he raises.

I want to clarify the licensing thing: licenses are not DRM, licenses are not evil, Open Source has licenses.  In the App Store drafting the legalese of the license is done for you, so you don't need to hire a lawyer for that.  When the packagemanager installs it from the store the app is receipted, with the system storing its license key automatically - you don't need to do anything to manage that.  If you want to check the license key (Apple calls it a receipt) you can use their API.

By way of comparison, if you use CocoaFob what you can do is create your own keys and make a nag screen to enter the key if it has not been receipted for that installation.  You'll need to store the receipted key into some prefs file yourself.

I think a nag screen is a good way of dealing with the license key entry issue, because you can just have a "Later" button and if the person has torrented your app they can click "Later", or when they're ready "Buy" to go and get a key.  Without this a person could have a copy of your app and never realise they were even supposed to pay for it, or if they do have no way to complete a transaction to "buy" the app.  License keys give you this.  It means torrents become another marketing channel for you, instead of a dead-end to any revenue from your app.

For my app store binaries I just decided not to bother with checking receipts.  If I get the feeling its becoming an issue I'll deal with it then.  For my Windows binaries I absolutely want to do license keys so I do that with CocoaFob.

Sand-Boxing your App

Before you start to build the .pkg file that you'll be pushing up for App Store review, you'll need to make sure your code is sand-boxing compliant.  I love this article by Cocoa-in-the-Shell which is not so happy about Mac sandboxing.  :-)

I used Qt 5.2 which includes the latest fixes to Qt and also has specific Qt for Mac features.  It seems to work well with Mavericks.

In the Digia article (which I suggest following as a general guide) it seems your only problem is the preferences file location, and the storage of cache files.  As of Qt 5.2 you really don't need to worry about these things.  Just make sure that you have lines like this in your main.cpp:

QApplication::setApplicationName("Plistinator");
QApplication::setOrganizationDomain("smithsoft.com.au");

and Qt will put your preferences and settings in the right place.  Make sure your apps bundle id in the Info.plist file match this.  If you use the new QStandardPath API in Qt you'll get the right paths to things like caches and file stores.

As the Digia article says you may use the Qt File open dialog so that the user can by inference provide permission to access any files that your app wants to open that is outside the sandbox.

The issue with this is that the Qt file dialogs return a QString which means you cannot save the Security Scoped Bookmark.  Converting the QString to an NSURL does not fix this, because the original URL has magic pixie dust embedded in it so as to make the bookmark and the QString returned does not preserve that.

To fix this you'll need to build a small Qt C++ wrapper for NSOpenPanel and NSSavePanel which presents the dialog to get the users response (and sandbox permission) and then saves the Security Scoped Bookmark.

Note that in the case of the NSSavePanel, if the file has not been written to (because you got a name to save a file to but there's no file there yet) you will not be able to save a bookmark.  My workaround is to write a file with a single byte in it as a placeholder, then save the Security Scoped Bookmark, then return from the wrapper and save the users file by over-writing the placeholder back in your main app code.  See my gists for some ideas.

Times you'll need the Security Scoped Bookmarks are:

  • recent files menus
  • opening the last edited file on relaunch
  • re-displaying a directory view after relaunching the app
basically any time you need to open a file or folder that you did not have the user open via a dialog, and can not now logically display an open file dialog for.

How to Build your App for the Store

Follow the pointers in this excellent article by the folks who are now behind Qt, Digia plc.  There are a few gotchas and wrinkles, but unfortunately doing a complete step by step tutorial is not really possible for me right now, and making it very specific would mean it was out of date when Qt or Apple change something anyway.

Here are the gotcha's I am aware of (read the Digia article):
  • I suggest using bash to create a build script
    • instead of trying to put the signing into your pro file
  • Setting flags for debug symbols via your "myapp.pro" file
    • Qt mkspecs are a bit off and I had to patch them to get it to work.  
    • See this Qt-Project bug report for my patches
    • You can probably get it to work more simply by hard coding the switches
    • Use my qmake line as shown below in your build script
    • I create a "appstore" stanza in my .pro, used to switch on App Store specific code
  • Building - as per normal 
  • Running macdeployqt
  • Extract the dsym with the utility
    • see below
  • Sign the macdeployqt bundle - if it helps using my script here
  • Run the productbuild command to create a signed package of your app
    • see snippet below
It's not obvious where the dsym file should go.  It needs to be right next to your app, for productbuild to find it, that is

ls $BUILD
myapp.app/
myapp.app.dSYM

Here is my qmake line from my build script:

qmake $SRC_DIR/${DEV_APP}.pro CONFIG+=release CONFIG+=x86_64 CONFIG+=force_debug_info CONFIG+=appstore

Here is a snippet from my build scripts for making the package:

/usr/bin/productbuild \
    --component $TMP_APP /Applications \
    --sign "$PKG_SIGN_CERT" \
    --version "$VERSION_NO" \
    $PACKAGE

Hope that all helps.  If you get stuck feel free to post a comment here, and I will try to assist.  I'll feel especially motivated to help if you tweet or G+ my little app for me!  :-D

Thanks for reading!

1 comment: