When I first started to write Hatta, I made it a single Python script, because I wanted it to be just a single file you can drop into your project and run it. That turned out to be increasingly harder to maintain as the application grew – I even included some data in form of strings in the source file!
Then I discovered that 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.
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
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!
As mentioned, you can include all your pure-python dependencies in the archive. Unfortunately, you cannot do that with any binary libraries – the users still need to have them installed to run your application. In the Jelly example above, the users would still need to have PyGame installed, for example.