24 May 2011

Distributing PyQt4 applications in a single .exe file (with py2exe)

I rarely need to run my Python code on Windows -- the few cases are usually my university projects. Not long ago I had to distribute a standalone package in Windows executable format. What I mean by that, no additional dependencies were to be installed. It took many tries before I ended up with an acceptable result.
Generally the problem was the final package size reaching up to 20MiB. So my aim was to lower this number as much as I can. The following guide is not meant to be comprehensive but it can save a lot of time if you have a similar use case. I've read considerable amount of docs and written this piece so next time I don't have to do it again.


First of all you should satisfy main CPython dependency that is Microsoft Visual C Runtime. To keep it brief: just make sure you have this package installed if you run Python 2.6 or newer. Of course you need full Python, Qt4 and PyQt4 install. PySide should work as well.

Secondly, the most important part, you have to make a setup.py script and place it in the same directory as your application. Probably there's a way to keep these two separate but it wasn't that important to me.
Now I'm going to describe the aforementioned script piece-by-piece. Line numbers are for reference and do not correspond to lines in a finished script.
1
2
3
4
5
6
7
from distutils.core import setup
import py2exe
from glob import glob

data_files = [
    ("Microsoft.VC90.CRT", glob(r'Microsoft.VC90.CRT\*.*')),
]
The path passed to glob in line 7 should point to a place where you installed MSVCR package, usually it's somewhere around: C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\x86. If you are 100% sure people running your application have this package installed you can comment this line out.

Side note: if you need to distribute some files not bound by dependencies (docs, graphics, license, etc) you can add to the list your customized entries:
1
2
3
4
data_files = [
    ("Microsoft.VC90.CRT", glob(r'Microsoft.VC90.CRT\*.*')),
    ("docs", ["README.TXT", "AUTHORS.TXT", "LICENSE.TXT"]), 
]
The first value of this exemplary tuple is the target directory for your files (use empty string for main application folder). The second value is a list of paths to file(s) you want to distribute additionally.

Back to the main thread:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
options = {
    "py2exe": {
        "includes": ["sip"],
        "excludes": [
            "pywin", "pywin.debugger", 
            "pywin.debugger.dbgcon", "pywin.dialogs", 
            "pywin.dialogs.list", "Tkconstants",
            "Tkinter", "tcl", "select",

            "_ssl", "doctets", "pdb", "unittest", 
            "difflib", "inspect",
        ],
        "optimize": 2,
        "compressed": True,
        "bundle_files": 1,
    }
}
This one dict declares options for distutils. In our case we only need to configure py2exe options so there's another dict (lines 2. to 16.) which does exactly that. There are some things demanding explanation.
The first option, includes, specifies which Python packages to pack with your application inside special library zipfile. Usually you don't need to add anything to this list as all dependencies will be automatically satisfied, but if you encounter an error in later stages (building process) try to add here missing packages' names. Next, excludes allows us to strip many unnecessary Python packages out of our bundle. The list provided by me is inspired by py2exe documentation and works for me. However, you have to be careful not to exclude packages you actually need -- check your source code for imports.

Finally there are some bundle size tweaks. optimize tells Python interpreter to do some basic optimizations. You have to be aware that setting this option to the value of 2 removes docstrings from your source files. This is not always desirable. compressed decreases the library zipfile size by adding compression. As I'm concerned with size and not with speed I advise you to leave this on.
The last option is the most interesting as it sets the bundling level. If you set it to 1 you receive a whole package in a single .exe file (with everything included). This makes a perfectly valid option if you only need one executable Python script. Otherwise you should change the value to 2 so you'll end up with Python libraries separate from your executable files. If you didn't change this, every .exe file would be embedded with a interpreter and a standard library -- a significant total size increase.

Another side note: there's also a dll_excludes option. If you notice that some unwanted library is getting bundled you can override this decision by adding following line:
1
"dll_excludes": ["my_lib.dll", "second.dll"],        

The last part of setup.py:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
setup(
    windows=[
        {"script": "application.py"}
    ], 
    data_files=data_files,
    options=options,
    version='1.0',
    description="My Application",
    author="A Serious Programmer",
    zipfile="python-libs.zip",
)
This piece does the bundling itself. To further customize final binary (parameters like version, description, author etc.) you should consider reading distutils documentation.
windows is a list containing your desired binaries -- every element will end up as a separate Windows GUI executable.
zipfile specifies the (arbitrary) name of Python libraries bundle. It's only observable if you don't include it inside the executable file. You can as well replace it with a path to keep it in a sub-directory.

Final step -- the real work.
When you are finished with the distutils script you can prepare your application bundle by calling from the console (cmd or PowerShell):
python setup.py py2exe

A number of directories will be created. The most interesting is dist, it contains your long anticipated application bundle. You are ready to distribute it to your users, just make sure you don't omit any of the generated files (although w9xpopen.exe is only needed for older versions of MS Windows).

That would be it.

No comments:

Post a Comment