Codesigning and automatic updates for PyQt apps

When developing a PyQt app, you will need to codesign it in order to avoid the following warning on your users' machines:

app can't be opened because it is from an unidentified developer.

What's more, you often want your app to be able to automatically update itself. This post shows how you can implement both auto-updating and codesigning in PyQt-based apps on OS X.

Esky is not the answer

Esky is an open-source auto-update framework for Python apps. It has a nice API and makes it seemingly easy to have your app update itself automatically. The problem is, it does not really work with codesigning because it does not conform to OS X's required bundle structure. What's more, development of Esky seems to be borderline inactive.

Use PyInstaller

There are a couple of tools for turning Python code into deployable applications, a process called "freezing". I looked at the following options:

  • bbfreeze does not support Python 3 and is unmaintained.
  • py2app is "not moving forward" because the author lacks the time.
  • cx_Freeze was last updated 18 months ago.
  • pyqtdeploy involves a Qt-based build process. Its GUI helper crashed when I tried to set up the (comprehensive) configuration.
  • PyInstaller seems to be the most actively developed, with the last release from 2 months ago.

I have used cx_Freeze, py2app and PyInstaller extensively in the past two weeks. Esky (which I originally wanted to use) only supports cx_Freeze and py2app. But I've had immense trouble with the two, probably because they don't support the latest versions of PyQt. I gave up on py2app after not being able to find out why it made my app crash with message Abort trap: 6. If you found this page on Google searching for this error, I recommend you use PyInstaller. Despite being cross-platform, it can output OS X .app bundles with the required directory structure and supports PyQt5 out of the box.

Sparkle for PyQt apps

Sparkle is an auto-update framework for OS X applications. You normally configure it using Xcode. But as it turns out, it's also possible to use it with PyQt applications. You need the pip dependency pyobjc-core (the whole pyobjc is not required) and the following code:

# Your Qt QApplication instance
QT_APP = ...
# URL to Appcast.xml, eg. https://yourserver.com/Appcast.xml
APPCAST_URL = '...'
# Path to Sparkle's "Sparkle.framework" inside your app bundle
SPARKLE_PATH = '/path/to/Sparkle.framework'

from objc import pathForFramework, loadBundle
sparkle_path = pathForFramework(SPARKLE_PATH)
objc_namespace = dict()
loadBundle('Sparkle', objc_namespace, bundle_path=sparkle_path)
def about_to_quit():
	# See https://github.com/sparkle-project/Sparkle/issues/839
	objc_namespace['NSApplication'].sharedApplication().terminate_(None)

QT_APP.aboutToQuit.connect(about_to_quit)
sparkle = objc_namespace['SUUpdater'].sharedUpdater()
sparkle.setAutomaticallyChecksForUpdates_(True)
sparkle.setAutomaticallyDownloadsUpdates_(True)
NSURL = objc_namespace['NSURL']
sparkle.setFeedURL_(NSURL.URLWithString_(APPCAST_URL))
sparkle.checkForUpdatesInBackground()

This is the absolute core of the Python part of the solution. For more information, please consult the Sparkle Documentation.

If you want to use Sparkle's Delta Update mechanism, you also need to move the file your.app/Contents/MacOS/base_library.zip which is created by PyInstaller to your.app/Contents/Resources/base_library.zip. Then create a symlink to ../Resources/base_library.zip at your.app/Contents/MacOS/base_library.zip so PyInstaller can still find the file.

If you are a programmer, you may be interested in fman. It's a modern file manager that can save you a lot of time in your daily work.

Michael started fman in 2016, convinced that we deserve a better file manager. fman's launch in 2017 was a huge success. But despite full-time work, it only makes $500 per month. The goal is to fix this.