Monday, July 16, 2007

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"

13 comments:

Tiago Cogumbreiro said...

Great post!

Michael said...

I have a question:
I am trying to program a proprietary python app, that would require pygtk. Would using py2exe violate GTK+'s LGPL? Especially since it creates, as I understand, a single (statically linked?) exe.

if you could help me clarify this, that'd be great!

you can reach me at mccanna (at) gmail dot com

Michael McCanna said...

Also, it says here:http://www.sqlalchemy.org/trac/wiki/FAQ#py2exe
that special steps need to be taken for SQLAlchemy to work, but in your setup.py file, you have sqlalchemy. Am I missing something here? (I *am* new to python.) Or do special steps need to be taken for SQLAlchemy?

Ali said...

@Michael

1. It is not a statically linked exe, so this is not a violation of the license and perfectly acceptable.

2. I also read that in the sqlalchemy FAQ. It's basically not true. I have written to the mailing list suggesting that they change it, but that was a while ago. I guess it got forgotten.

Michael McCanna said...

@Ali: thanks for the clarification...by any chance do you know if this is the case with python freeze (http://wiki.python.org/moin/Freeze)?
If you don't know offhand, don't worry, I'll try to figure it out myself.

Right now I will probably use wxPython, which should get rid of any licensing concerns I might have.

I'm glad that SQLAlchemy works without a hitch, too!

Christian said...

Is it possible to build a binary file that runs without installing anything (e.g. GTK, Python, Python packages, application itself)?

E.g. executing app.exe from any location starts the application although GTK, Python etc. is not installed on the user's computer (but is included in the binary).

If the above mentioned is not possible:
What happens if I build a binary according to your tutorial, a Windows user runs the setup successfully and now wants to install a newer version. Will dialogs pop up and ask him again to confirm the installation of GTK, Python, etc.?

Ali said...

Christian,

Yes and no. I suspect I have not explained properly. The installer doesn't install GTK etc on the user's machine with the normal install dialog etc. It just re-packages GTK (and all other dependencies as if they were support files for your application. So you would never get the popups as you describe "confirming installation" because installers of those things are never used.

On the other hand, I have never tried for a single target binary, it's not necessary in my opinion, and if you take the approach in the blog post, and stop after the py2exe stage you have something that will run, entirely contained from a single directory.

I hope that has made things clearer.

Anonymous said...

I have everything set up and ready to go (single simple python / gtk2 sourcecode file which runs fine via the interpreter) but my simple question is.. how do I run py2exe?

Neil

Anonymous said...

ah, found my answer here:

http://www.py2exe.org/index.cgi/Tutorial

just one other Q, although my app built & ran just fine, the text on it's button interfaces was all square boxes. I'm wild-guessing it's some sort of character set issue, is this something configurable somewhere along the line..

neil

Anonymous said...

fixed it after a fashion. something called pango wasn't set up right somehow, this windows batch script got it working manually:

[code]
cd\
C:
md Python25\dist\etc\pango\
cd GTK
cd bin\
pango-querymodules.exe > C:\Python25\dist\etc\pango\pango.modules
[/code]

not a proper solution I know but might help someone.

app builds perfick now, thanks for the article :)

neil

google said...

ps.. the batch script above assumes you're building directly in the /Python25/ directory (I was too lazy to set up my Path)

neil

Kopfgeldjaeger said...

Nice tutorial! But I think an addiotional minimalistic example would be a very good idea.

Anonymous said...

Nvin Installer is a Free, open source installer. I found it really interesting !!!