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
__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
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:
(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.