Deploying PyQt applications on Windows and Mac OS
时间:2009-04-06 来源:cobrawgl
The open source Qt development toolkit is a popular choice for cross-platform development. It provides native-looking widgets and tight integration with the underlying platform on Windows, Linux, and Mac OS X. Qt applications that are written in C++ are easy to compile and deploy across all three platforms, but what if you don't like C++? I prefer Python, a dynamic programming language with a richly expressive syntax and exceptionally powerful support for introspection.
Fortunately, there are cross-platform Python bindings for Qt. The downside, however, is that packaging PyQt applications so that they can be deployed to users on Windows and Mac OS X is an immensely frustrating and arcane process. I declared victory last week after spending several hours battling with MacPorts and distutils. Now that I have unlocked the toolkit's dark mysteries, I can show you the hidden secrets that will allow you to achieve mastery of the alchemical art of cross-platform PyQt application deployment.
First, you'll need access to each platform for which you want to build redistributable packages. The easiest way to accomplish this is to use a Mac and either triple-boot or virtualize Windows and Linux. The initial setup process for Mac OS X will require a lot of very heavy compilation, so you are going to be in for a world of pain and a very long wait if you try to do this on a Mac mini.
My test application
My computing environment is a quad core Mac Pro configured to dual-boot OS X and openSUSE 11.1. For Windows, I'm running XP in VirtualBox. I do most of my actual development in Linux, but you can do it pretty comfortably on any of the platforms.
My test application, which I call Orbital Liftr, is a simple utility that I made for batch uploading graphics to Ars Technica's content management system. The Ars CMS is built on Movable Type, which means that it supports the MetaWeblog XML-RPC API, and my app lets you upload images to any standard Movable Type or Wordpress blog that supports the API. The app has a few simple features like support for receiving images via drag-and-drop, and it can proportionally resize them before uploading.
The program consists of one module of Python code which contains the application logic and a few basic user interface forms that I made with the Qt Designer program. I have published the complete source code of the program on Launchpad. You can use it to follow along with this tutorial, or you can use your own code.
PyQt on Windows
To build a distributable PyQt package for Windows, you first need to set up a working PyQt execution environment. Start by downloading and installing the standard Qt SDK from the Qt Software website. Next, you will need to install Python 2.6.1. Use the binary installer for Windows from the Python website.
The next step is installing the Python bindings, which can be obtained from the download page at the PyQt website. You'll want to get the Windows installer that is compatible with Python 2.6; it's listed at the bottom of the Binary Packages section.
These components should be enough to give you a fully functional environment for running PyQt applications. You can test it by making a simple PyQt application with a few widgets in a single .pyw file. If your PyQt environment installed correctly, you should be able to run the program by double-clicking the .pyw file in the file manager. There are several example scripts that come bundled with the PyQt installation. These can be found in the site-packages\PyQt4\examples folder.
Now that you have a working PyQt environment, you need to package up the application so that you can distribute it to users and make it possible for them to run it without having to install all of the dependencies. This is done with a utility called py2exe that leverages Python's distutils framework. An installer for py2exe is available from the SourceForge website.
You will need to adapt your setup.py script so that it can provide proper instructions to py2exe. If your program is simple and you already know how to use distutils, this shouldn't be terribly hard. The following example shows my setup.py file:
view plain- from distutils.core import setup
- import py2exe
- setup(name="liftr",
- version="0.1",
- author="Ryan Paul",
- author_email="[email protected]",
- url="https://launchpad.net/liftr",
- license="GNU General Public License (GPL)",
- packages=['liftr'],
- package_data={"liftr": ["ui/*"]},
- scripts=["bin/liftr"],
- windows=[{"script": "bin/liftr"}],
- options={"py2exe": {"skip_archive": True, "includes": ["sip"]}})
from distutils.core import setup import py2exe setup(name="liftr", version="0.1", author="Ryan Paul", author_email="[email protected]", url="https://launchpad.net/liftr", license="GNU General Public License (GPL)", packages=['liftr'], package_data={"liftr": ["ui/*"]}, scripts=["bin/liftr"], windows=[{"script": "bin/liftr"}], options={"py2exe": {"skip_archive": True, "includes": ["sip"]}})
Most of that is pretty much standard distutils. The last two lines were added to accommodate py2exe. The "windows" parameter specifies the script that py2exe should use to launch the actual program. As you can see, for a simple program that is being ported from Linux, it's the same thing that you already have in your "scripts" parameter.
The "options" parameter allows you to pass specific instructions to py2exe. For PyQt applications, you will need to tell it to include sip, a fundamental component of the PyQt binding system.
The py2exe tool will typically compress all of your required Python library modules into a single zip file in order to reduce space and keep your redistributable package clean. I disable that with the skip_archive option. My program dynamically loads the user interface description XML files at runtime, but it can't read those files when they are bundled up in the zip archive.
When you are building PyQt applications with py2exe, you need to either statically generate your user interface modules from the XML description files in advance, disable archiving with the skip_archive option, or structure your program so that the UI files will not end up in the archive.
After you finish making your setup.py script, you can build your redistributable package by running it from the command line:
$ python setup.py py2exe
The terminal will display a lot of messages as it byte-compiles your modules and copies all of the necessary dll files and other dependency components. The automated setup process will take place in the "build" directory and everything that your users need will be copied into the "dist" directory. If the script won't execute, make sure that Python is in your PATH environment variable.
To deploy your application to users, ship them everything that is in the "dist" directory. This adds up to roughly 25 MB for a simple program. It includes the executable and all of the runtime dependencies, which means that users will be able to run the program without having to install the other components.
You can just zip it up and ship it out that way, but it will be more convenient for your users if you give them a single self-standing executable with a standard installer wizard. A lot of PyQt developers seem to like Inno Setup, a free installer maker.
Although your users will not have to manually install Qt or the PyQt bindings, they might still have to install the VC++ 2008 Redistributable Package. This package is available directly from Microsoft.
Deploying PyQt on Windows works reasonably well. Inno Setup uses good compression, so you can get your final package down to an acceptable size and make it easy for users to install. The biggest challenge is getting py2exe to deal appropriately with certain kinds of corner cases.
If your application is complex or structured in an unusual way, you might run into problems. For example, py2exe doesn't respect the distutils package_data option. There are a few workarounds for problems of that nature, and you can get more details about py2exe from the project's website.
PyQt on Mac OS X
Deploying PyQt on Mac OS X was much harder than I initially expected. There are several tutorials out there that explain how to deal with specific problems that people commonly encounter during the setup process. After much struggling, I managed to get a working PyQt build environment set up on OS X by using information I discovered in several of the tutorials.
The goal is to produce a totally self-contained app bundle that will allow users to drag your program into their /Applications folder and treat it like any other piece of Mac software. You can do this with py2app, a utility that is very similar to py2exe. Unfortunately, you will encounter serious versioning problems if you try to make a PyQt app bundle with the native Python installation that is included with the operating system.
After several false starts, I discovered that the only way to get a PyQt environment that works properly with py2app is to compile the whole bloody stack from MacPorts. This is especially tricky because you have to make sure to configure your environment to use the MacPorts version of Python before you use MacPorts to compile the PyQt bindings. If you do this step at the wrong time, then nothing will work. I had to try three times before I got it right.
The basic steps for using MacPorts to get a PyQt environment were published by Aral Balkan in his py2app tutorial. The order in which he lists the steps did not work for me because his instructions tell you switch to the MacPorts version of Python at the wrong time. The result was that I consistently got a "Bus Error" when I attempted to run my program.
I'm apparently not the first person to encounter this error. When I Googled the message, I discovered a really helpful blog entry by Horacio Mijail Antón Quiles. As he explains, the problem is caused when MacPorts uses the system's native Python installation to build the PyQt bindings. It's absolutely imperative that you configure the environment to use the right version of Python before you build the Python-based dependencies.
To switch to the MacPorts Python installation, you have to run the python_select command. After you use python_select, the desired Python environment will be used every time you invoke Python for the rest of the shell session. The following are exactly the steps that I took at the command line to get a working PyQt setup on Mac OS X:
$ sudo port install python25 $ sudo port install python_select $ sudo python_select python25 $ sudo port install py25-macholib-devel $ sudo port install py25-sip $ sudo port install py25-pyqt4 $ sudo port install py25-py2app-devel $ sudo port install py25-pyqt
This will take a very long time to compile, even on a fast computer. After it finishes, you will be able to test it by opening a terminal, running the "python_select python25" command, and then running a PyQt script from the command line.
The process above will automatically compile and install py2app from MacPorts, so you won't have to manually do so with additional steps. After this process is complete, you'll be ready to build your app bundle. For py2app, you will use setuptools instead of distutils. This means that you have to create a separate setup.py file. I call mine setup-mac.py.
The py2app suite comes with a utility called py2applet that you can use to automatically generate the setup.py file, but it's pretty easy to write one by hand. The following is my setup-mac.py file:
view plain- from setuptools import setup
- setup(
- app=["liftr/liftr.py"],
- options={"py2app":
- {"argv_emulation": True, "includes": ["sip", "PyQt4._qt"]}
- },
- setup_requires=["py2app"])
from setuptools import setup setup( app=["liftr/liftr.py"], options={"py2app": {"argv_emulation": True, "includes": ["sip", "PyQt4._qt"]} }, setup_requires=["py2app"])
The app parameter is the main Python module that you want the bundle to run when the program launches. The includes are the high-level components that you want the program to bundle; we need the sip binding layer and the actual PyQt4 bindings.
The argv_emulation option is a nifty hack that will improve native platform integration. When the user puts your app in their dock, they will be able to launch it by clicking it or by dragging and dropping files onto the icon. The argv_emulation option will take the full paths of the files that are dropped onto the icon and translate them into argv parameters so that your application can process the files.
After you finish writing your setup-mac.py file, you can run it from the command line:
$ python setup-mac.py py2app
This will byte-compile the modules and generate your app bundle. It will use the build directory for the construction process and then it will put the output in dist, just like py2exe. The resulting app bundle is a soul-crushing 85MB. I tried several things to reduce the total size, but I didn't really have much luck.
I used the ditto tool to strip out PPC code, but that didn't decrease the size at all. Next, I tried Trimmit, which was able to scrape off about 10MB, but broke the program in the process. The only thing you can really do to ameliorate the large executable size is compress the app bundle when you make it available for download. Zipping the app bundle reduced its size to about 30MB.
My problems didn't end there. Although the application worked fine on my own computer, it didn't entirely work properly on other computers. The main problem is that jpeg thumbnails didn't render at all. After some troubleshooting, I discovered that Qt's jpeg image support is implemented in a plugin. I tried to put the Qt plugin directory into my app bundle, but I couldn't get it to work.
I did some searching and I found a recent blog entry about this problem written by Tory Hoke. She apparently never found a solution to the problem and decided instead to work around it by converting jpeg images to PNG on the fly. This is probably what I will do too, because my program is already using PIL for some image transformations anyway. The lack of support for plugins in py2app applications is a pretty big problem because it means that a lot of other critical functionality, such as Phonon, isn't going to be accessible.
It's worth noting that this same jpeg problem also impacts Windows builds in some cases, but there is a working solution that will allow programs to use plugins on Windows.
PyQt is clearly unsuitable for building distributable Mac applications, and you're probably better off taking a more native approach for Mac ports. For instance, you can use Apple's Cocoa bindings for Python to build a Mac GUI on top of your Python code. Apple provides full support for this throughout the native Mac development tool stack, so you can do it with XCode and Interface Builder.
Some closing thoughts about PyQt
My development framework of choice has traditionally been PyGTK+, so I'm still mostly at the toe-dipping stage with PyQt. My initial sense is that there are a lot of advantages to using PyQt and a lot of disadvantages.
Qt itself is a significantly richer and more powerful toolkit than GTK+. It offers really robust and mature implementations of important features that the GTK+ developers are only just now beginning to experiment with, such as comprehensive CSS theming, a powerful canvas with input redirection, tightly integrated support for the WebKit HTML renderer, and scriptable extensibility with JavaScript. Qt is also more conducive to portability and provides a more native look and feel on supported platforms.
Although I'm pretty much sold on Qt, the Python bindings leave a lot to be desired. PyQt is not developed or supported by Nokia. It's a third-party commercial project (developed and maintained by Riverbank Computing) that is basically one big hack. There are some weird limitations in the APIs and the bindings have a lot of really grotesque C++ idioms that seep through to the Python side. The most hideous wart is that you have to put the entire C++ method type signature in a string when you bind a signal. If you put in the type data wrong, then the connection will silently fail.
The other downside of the PyQt bindings is that they are still distributed under the GPL, unlike Qt, which was recently moved to the more permissive LGPL. This means that you have to buy a commercial PyQt license from Riverbank Computing if you want to build proprietary software with the bindings.
There are several useful tools that might be of value to PyQt application developers. There is a complete integrated development environment called Eric4 that is built with PyQt and is designed specifically for PyQt development. It has refactoring tools, a built-in graphical breakpoint debugger, basic autocompletion support, and a lot of other goodies.
After spending several days working with PyQt and packaging the applications for deployment on Windows and Mac OS X, I feel that—despite its significant limitations—it's an acceptable solution for a certain class of applications. If you are looking for a toolkit for building a GUI on top of some existing Python code and you want to be able to deploy the program on Windows and Linux, then PyQt might do the trick.