1. Radomir Dopieralski
  2. pyg.exe

Wiki

Clone wiki

pyg.exe / Single ZIP file

Python can easily run zipped code -- there is a nice howto about it. And using pkgutil.get_data you can even load all the data files you need from the same zip file. Oh, and you can include all the pure-python dependencies in the zip.

Moreover, you can add a hashbang and a .py extension, so that it runs with Python by default both on POSIX systems and Windows. Of course, your users still need to have Python installed, and possibly all the non-pure libraries.

Technical details

So how do you exactly do it? Let me show you using my game Jelly as an example. The directory structure looks something like this:

__main__.py
jelly/__init__.py
jelly/game.py
[... more .py files here ...]
jelly/jelly.png
[... more .png files here ...]
jelly/pf_ronda_seven_bold.ttf

In the __main__.py is very simple:

#!/usr/bin/env python

from jelly import game

game.Game().run()

Now, in the game itself I need to load the images and fonts into memory. As mentioned before, I do that using pkgutil.get_data:

def load_image(filename):
    f = StringIO.StringIO(pkgutil.get_data('jelly', filename))
    return pygame.image.load(f, filename).convert()

Unfortunately the trick with StringIO leads to a segmentation fault on some versions of PyGame when we try to load fonts that way. We need a workaround:

def load_font(filename, size=8):
    with tempfile.NamedTemporaryFile() as f:
        data = pkgutil.get_data('jelly', filename)
        f.write(data)
        f.seek(0)
        font = pygame.font.Font(f.name, size)
    return font

Unfortunately again, that workaround will not work on Windows, due to non-POSIX file operations semantics. Long story short, you need to use this:

def load_font(filename, size=8):
    tmpdir = tempfile.mkdtemp()
    fname = os.path.join(tmpdir, filename)
    try:
        with open(fname, 'wb') as f:
            data = pkgutil.get_data('jelly', filename)
            f.write(data)
        font = pygame.font.Font(fname, size)
    finally:
        try:
            os.remove(fname)
            os.rmdir(tmpdir)
        except:
            pass
    return font

Building the package

Now we can make the zip file:

zip -r jelly.zip jelly/ __main__.py --exclude='*.pyc'

Then we add the hashbang at the beginning. We make a file hashbang.txt with following contents:

#!/usr/bin/env python


(Make sure to have an empty line at the end.) Now just join the two:

cat hashbang.txt jelly.zip > jelly.zip.py
chmod +x jelly.zip.py

And voila! We have our application in a single runnable file! You can run it with pyg.exe:

pyg.exe jelly.zip.py

Updated