PyGTK, Py2exe, and Inno setup for single-file Windows installers

I am lucky, as part of my job, I get to code PyGTK. The downside is of course
that I have to deploy these applications on Windows. Now these Windows users
(especially at the price they are paying for this stuff) don't install
dependencies, don't understand what a scripting language or interpreter is,
and frankly they should not have to.

Unfortunately, the list of dependencies for a PyGTK application (at the very
minimum) is Python, GTK, PyGTK, PyGObject, PyCairo. 5 installers! So very
early on in this project I found the way to give them exactly what they want
and deserve, a single-file executable installer. There are a few guides online
about how to achieve this, but none seem to work, and none are particularly
new.

How do we achieve this? The toolchain involved consists of two elements:

1. Py2exe website
2. Inno Setup website

NOTE: The documentation for all the tools used is extensive, I will not repeat everything in there.

Py2exe



Py2exe is a distutils extension that searches for Python modules that your
application uses, and other dependent libraries, and copies them all into your
dist/ directory. The pure Python stuff gets zipped up, and the DLLs (Windows
remember) get copied into the directory.

The dist directory would be runnable from say a CDRom at this stage, but of
course this is not enough, we need a single file installer (more later).
Making py2exe work is a bit of an art. There is lots of documentation at the
website linked above, so I don't need to repeat that. What I will do is paste
my setup.py file so that it can be copied if required (just don't tell my
employer).


setup(
name='myapp',
version='1.3',
packages=['myapplib'],
scripts=['myapp.py'],
windows=[
{
'script': 'myapp.py',
'icon_resources': [(1, 'pixmaps/cdm.ico')],
}
],
data_files=[
('data', [
'data/cd_db.csv'
]
),
('glade', [
'glade/main.glade',
'glade/patient_editor.glade',
'glade/prescriber_editor.glade',
'glade/report_chooser.glade',
'glade/supplier_editor.glade',
'glade/user_editor.glade',
'glade/pharmacy_editor.glade',
]
),
('pixmaps', [
'tools/kiwi_requirements/validation-error-16.png',
'tools/kiwi_requirements/plus.png',
'pixmaps/s2icon16.png',
'pixmaps/s2icon24.png',
'pixmaps/s2icon32.png',
'pixmaps/s2icon48.png',
]
),
('catalogs', ['tools/kiwi_requirements/dummy.txt']),
('plugins', ['tools/kiwi_requirements/dummy.txt']),
('resources', ['tools/kiwi_requirements/dummy.txt']),
('pixmaps/kiwi', ['tools/kiwi_requirements/dummy.txt']),
('backups', ['tools/kiwi_requirements/backups_go_here.txt']),
],
options = {
'py2exe' : {
'packages': 'encodings, sqlalchemy',
'includes': 'cairo, pango, pangocairo, atk, gobject',
},
'sdist': {
'formats': 'zip',
}
}
)


Ok, I changed the name of the application. "myapplib" is the name of the
package for the application, and the top-level script that runs is called
"myapp.py".

Note: there are some specific things in there for packaging Kiwi. That is
probably the hardest task (but is not relevant here).

Now there are two tricky things about using py2exe, and they are dependencies,
and data files (the rest is just basic setup.py stuff). Dependencies can be
explicitly set in options['py2exe'], and this is the magical bit. It *should*
find these automatically I think, but sometimes it just doesn't. Sometimes
putting something in "packages" works, but putting it in "includes" doesn't,
and I have to admit that I am not well enough versed with py2exe and the
documentation to know the difference. So, if you want it to work, I would just
copy what I have above, and save yourself the time and pain of making it work.

Data files are the next problem. Eventually you will want this data file
copied firstly to your dist directory, and then secondly when building the
installer into the installer file, and then from there somewhere into Program
Files, where your application will find it. So the above data_files list
demonstrates how I do this.

These data files will be copied into the dist/ directory, and then will be available (once installed) to your application in the Program Files\MY_APP_NAME directory, which should be easy enough to find. Examples are available on request for interested people.

Inno Setup



Inno setup is an application that packages any sort of distribution of files
into a single installer and unpacks it wherever you want. There are oodles of
options, and it does a pretty good job, and it also has pretty good
documentation that will get you on your way.

The important line from my Inno Setup config file is this:


[Files]
Source: "..\dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs


This basically just dumps the entire dist directory into the package, and will
dump it back out in a similar structure on the other side, which is exactly
what we want.

GTK Data files



It is not sufficient to just make py2exe pull in the GTK DLLs for packaging
(which it does pretty successfully). GTK also requires a number of data files
which include themes, translations etc. These will need to be manually copied
into the dist directory so that the application can find them when being run.

If you look inside your GTK runtime directory (usually something like c:\GTK\)
you will find the directories: share, etc, lib. You will need to copy all of
these into the dist directory after running py2exe.

You can prune some of these directories. My main pruning action comes by
removing all the non-English translations (which saves a few megabytes in the
final bundle).

Since I am in to automation (as we will discuss later) I have zipped this lot
up and unzip it from a location after running py2exe.

Putting it all together



Now that we have discussed the tools used, and some specific details about
them, I shall present the specific process involved in the build on a clean Windows system:

  1. Install GTK runtime
  2. Install Python
  3. Install all the dependent Python packages
  4. Run py2exe
  5. Copy GTK data files
  6. Run Inno Setup
  7. Install the application
Wow, that is a lot of steps, especially when you change one line of code on the day before a release. The solution is to use some scripts for automation.

The first "script" will provide a source bundle with everything I need in it to upload onto the Windows computer (since I develop everything on Linux). Luckily distutils has such a thing, and we can use the sdist command to setup.py to achieve it, as long as you have correctly specified the MANIFEST.in file. For added bonus points, you should (and notice setup.py above) make sure you are bundling into a zip, not a tar.gz (unless you have ways of unpacking the tar on Windows). This can be achieved with a command line flag --formats or in the setup.py options dict, or even in a setup.cfg file.

I manually unzip the thing on the other side.

The second script will install all the requirements: GTK, Python etc. To achieve this I have a directory in my source distribution that contains all the Windows binary installers. I had to build some of them (for Python modules) but this is as easy as:

python setup.py bdist_wininst


I change to the directory of installers and I run a batch script. Batch is a pretty evil language once you have used something as nice as Bash(!), but for our purposes it is simple. Here are the contents of that batch file, which lives in the same directory as all the installers:


call gtk-2.10.11-win32-1.exe
call python-2.5.1.msi
call pycairo-1.2.6-1.win32-py2.5.exe
call pygobject-2.12.3-1.win32-py2.5.exe
call pygtk-2.10.4-1.win32-py2.5.exe
call kiwi-1.9.15.win32.exe
call gazpacho-0.7.0.win32.exe
call py2exe-0.6.6.win32-py2.5.exe
call SQLAlchemy-0.3.4.win32.exe
call isetup-5.1.10.exe


There is some [ Ok ] clicking to be done, this won't run unattended, but this script alone has saved me hours in total. Perhaps there are flags for unattended install of these things.

Once the dependency installers are installed, I can set to creating the installer package which are steps 4,5,6 above. Again I automated these with a script. This time the script is in Python, because now we have Python installed! Here it is:


import os, sys, shutil, zipfile

BASE_DIR = os.path.dirname(__file__)
OUT_DIR = os.path.join(BASE_DIR, 'dist')
GTK_ZIP = os.path.join('tools', 'windows', 'gtk', 'gtk_to_copy.zip')
INNO_SCRIPT = os.path.join('tools', 'config-inno.iss')
INNO_EXECUTABLE = '"c:\\Program Files\\Inno Setup 5\\ISCC.exe"'


class Unzip(object):
def __init__(self, from_file, to_dir, verbose = False):
"""Removed for brevity, you can find a recipe similar to this in the Cook Book"""


def delete_old_out_dir():
if os.path.exists(OUT_DIR):
shutil.rmtree(OUT_DIR)


def run_py2exe():
# A hack really, but remember setup.py will run on import
sys.argv.append('py2exe')
import setup


def unzip_gtk():
Unzip(GTK_ZIP, OUT_DIR)


def run_inno():
os.system(INNO_EXECUTABLE + " " + INNO_SCRIPT)


def main():
#Clean any mess we previously made
delete_old_out_dir()
# run py2exe
run_py2exe()
# put the GTK data files in the dist directory
unzip_gtk()
# build the single file installer
run_inno()
# prevent the windows command prompt from just closing
raw_input('Done..')

if __name__ == '__main__':
main()



That produces an installer in the top-level source directory which has the name of the value of the config key setup/OutputBaseFilename, which you can then test to install.

I advise that you remove GTK, Python and all the dependent modules before installing, just so you can be sure that it will work on a clean Windows system.

You may think that including GTK, Python and a whole load of other stuff might make for a really heavy distribution. Actually it's not, it's pretty light. For the application I am referring to we come out at just under 6 megabytes, 11 if you include all the translations. Granted, it is a huge amount when you consider how much non-library code we are using, but by today's standards, no one is going to be upset about having to download that.

Please drop me a line if you have any queries.

Possible future post: "Making Kiwi work with py2exe"

Popular posts from this blog

ESP-IDF for Arduino Users, Tutorials Part 2: Delay

How to install IronPython2 with Mono on Ubuntu