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:
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.
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
pyobjc-core (the whole
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
your.app/Contents/MacOS/base_library.zip which is created
by PyInstaller to
Then create a symlink to
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.