Making Py2Exe Play Nice With Pygame

Edit: I’ve updated this script to incorporate some changes that are found to help correct a few other problems with Pygame DLLs not being treated properly. The approach I use appears to work for all versions of py2exe rather than any specific one. If anyone has any problems with it, let me know.

It’s always a good idea to make sure your entire development toolchain works correctly from beginning to end.  This includes the package builder and installer. Even though I’m still a little while out from releasing Star Merchant 2, I spent the better part of Wednesday digging into py2exe to make sure it would, in fact, produce a solid, working Windows distribution package for the game.  And then I found out how it didn’t like to play so nice with pygame.

For those who have done the searches like I have and tried to piece together a solution, I’ll spare you some of the bugs and gotchas you might encounter by putting them all here.

First, there seems to be a problem with how pygame’s architecture works with py2exe, specifically with how it interfaces with SDL_Mixer.  Admittedly I haven’t done a lot of homework on the exact cause of the problem, but I know what happens as a result.

When you write your setup.py script (or whatever you choose to call it), if the py2exe options aren’t specified correctly, it will miss the bindings for pygame.mixer.music, resulting in SDL_Mixer’s music playback ceasing to operate.  Sound playback is fine, but play a music track (i.e., an OGG file) and your game’s executable will bomb.

Considering that this is really the only big show-stoppers I’m impressed.  And the fix itself is actually quite trivial to implement.

The trick I have found is in how you configure py2exe to work on a pygame-based game.  I’ve found a good balance between size, speed, and compatibility with pygame by using the following in the call to setup:

options = {"py2exe": {
                          "optimize": 2,
                          "includes": INCLUDE_STUFF,
                          "compressed": 1,
                          "bundle_files": 2}
              },

The two options that are of note are compressed and bundle_files.  By adding another line later (not in options, but as another argument to setup(), is zipfile = None.  These three options, when combined, will compress all of the necessary files for your game’s operation together into the executable itself.  This allows for faster initial load times, and with compression doesn’t lead to an out-of-control bloated executable.

In experimenting around before arriving at this nice combination of options my curiosity got the better of me, so I went puttering around in the zip file of Python modules, the default of which is called library.zip.  Opening it up revealed a ton of extra libraries which simply were not necessary for Star Merchant 2.  Checking the total size revealed that SM2, in its current infant stage was weighing in at close to 30MB, the majority of which was comprised of Python libraries that simply weren’t needed.

There had to be a way to reduce it, and with a little digging around in py2exe’s documentation, I found the options: excludes and includes.  I went through the zip file, removed a library, tried the executable, and found it kept working (except for the pesky music error, which I fixed above.)  Through trial and error, and a little bit of common sense, I excluded enough modules to choke a small server and get it down to a more reasonable 18.5MB.

Still, there was more I could slice out.  So I went after the encodings.  Now, I’m not trying to be biased here, but every piece of text in Star Merchant 2 is ASCII.  So why do I need to add encodings for Big-5 Chinese and Japanese symbols?  Truthfully, they aren’t necessary at this time.  I may later, but that’s an easy fix on the dist-build end.  Another option to py2exe that does this easily is ‘ascii’ : 1 in the setup options.  This takes the guesswork out of knowing which ones to discard as a blacklist and take a whitelist approach.  And if you’ve ever looked in the encodings directory, you’ll see that this can be a real time saver.

There’s also a few “red herrings” that py2exe will give you at the end of the build process, namely regarding some missing modules named AppKit and Foundation.  Rather than look at the warnings, I put them in the ignores list option, which tells py2exe to just, well, ignore them if it can’t find them.

The actual call to setup() now looks like this:

setup(windows=[{'script': SCRIPT_MAIN,
                        'other_resources': [(u"VERSIONTAG",1,VERSIONSTRING)],
                                          'icon_resources': [(1,ICONFILE)]}],
                        options = {"py2exe": {
                                       "optimize": 2,
                                       "includes": INCLUDE_STUFF,
                                       "compressed": 1,
                                       "ascii": 1,
                                       "bundle_files": 2,
                                       "ignores": ['tcl','AppKit','Numeric','Foundation'],
                                       "excludes": MODULE_EXCLUDES} },
                        name = PRODUCT_NAME,
                        version = VERSION,
                        data_files = extra_files,
                        zipfile = None,
                        author = AUTHOR_NAME,
                        author_email = AUTHOR_EMAIL,url = AUTHOR_URL)

Now you’re probably also familiar with pygame2exe, as am I.  After looking through it I found that, while it does do a good job of some of this, it doesn’t seem to want to get down into the nitty-gritty and do a really close examination of libraries to keep out of the /dist subdirectory.  If you really truly want to get a working dist-build for Windows for your game, you’re going to have to put a little elbow grease in to make it work for you.  Frankly, it shouldn’t take that much more time for this step of the process.

Here’s what my script looks like right now to build Star Merchant 2, all lean and mean:

# py2exe setup program
from distutils.core import setup
import py2exe
import sys
import os
import glob, shutil
sys.argv.append("py2exe")
 
VERSION = '1.0'
AUTHOR_NAME = 'Your Name'
AUTHOR_EMAIL = 'your_email@somewhere.com'
AUTHOR_URL = "http://www.urlofyourgamesite.com/"
PRODUCT_NAME = "Your Game's Game"
SCRIPT_MAIN = 'main_script.py'
VERSIONSTRING = PRODUCT_NAME + " ALPHA " + VERSION
ICONFILE = 'icon.ico'
 
# Remove the build tree on exit automatically
REMOVE_BUILD_ON_EXIT = True
PYGAMEDIR = os.path.split(pygame.base.__file__)[0]
 
SDL_DLLS = glob.glob(os.path.join(PYGAMEDIR,'*.dll'))
 
if os.path.exists('dist/'): shutil.rmtree('dist/')
 
extra_files = [ ("",[ICONFILE,'icon.png','readme.txt']),
                   ("data",glob.glob(os.path.join('data','*.dat'))),
                   ("gfx",glob.glob(os.path.join('gfx','*.jpg'))),
                   ("gfx",glob.glob(os.path.join('gfx','*.png'))),
                   ("fonts",glob.glob(os.path.join('fonts','*.ttf'))),
                   ("music",glob.glob(os.path.join('music','*.ogg'))),
                   ("snd",glob.glob(os.path.join('snd','*.wav')))]
 
# List of all modules to automatically exclude from distribution build
# This gets rid of extra modules that aren't necessary for proper functioning of app
# You should only put things in this list if you know exactly what you DON'T need
# This has the benefit of drastically reducing the size of your dist
 
MODULE_EXCLUDES =[
'email',
'AppKit',
'Foundation',
'bdb',
'difflib',
'tcl',
'Tkinter',
'Tkconstants',
'curses',
'distutils',
'setuptools',
'urllib',
'urllib2',
'urlparse',
'BaseHTTPServer',
'_LWPCookieJar',
'_MozillaCookieJar',
'ftplib',
'gopherlib',
'_ssl',
'htmllib',
'httplib',
'mimetools',
'mimetypes',
'rfc822',
'tty',
'webbrowser',
'socket',
'hashlib',
'base64',
'compiler',
'pydoc']
 
INCLUDE_STUFF = ['encodings',"encodings.latin_1",]
 
setup(windows=[
             {'script': SCRIPT_MAIN,
               'other_resources': [(u"VERSIONTAG",1,VERSIONSTRING)],
               'icon_resources': [(1,ICONFILE)]}],
         options = {"py2exe": {
                             "optimize": 2,
                             "includes": INCLUDE_STUFF,
                             "compressed": 1,
                             "ascii": 1,
                             "bundle_files": 2,
                             "ignores": ['tcl','AppKit','Numeric','Foundation'],
                             "excludes": MODULE_EXCLUDES} },
          name = PRODUCT_NAME,
          version = VERSION,
          data_files = extra_files,
          zipfile = None,
          author = AUTHOR_NAME,
          author_email = AUTHOR_EMAIL,
          url = AUTHOR_URL)
 
# Create the /save folder for inclusion with the installer
shutil.copytree('save','dist/save')
 
if os.path.exists('dist/tcl'): shutil.rmtree('dist/tcl') 
 
# Remove the build tree
if REMOVE_BUILD_ON_EXIT:
     shutil.rmtree('build/')
 
if os.path.exists('dist/tcl84.dll'): os.unlink('dist/tcl84.dll')
if os.path.exists('dist/tk84.dll'): os.unlink('dist/tk84.dll')
 
for f in SDL_DLLS:
    fname = os.path.basename(f)
    try:
        shutil.copyfile(f,os.path.join('dist',fname))
    except: pass

There’s a few other things in that script you’re probably scratching your head about.  I append “py2exe” into the arguments list for the script so it automatically does what I want it to.  Yeah, it means I’m being a bit lazy, but I only have to type

python -OO setup.py

at the command line to kick off the entire build process.

Py2exe chokes on my machine if I already have a /dist directory present, so the script checks first to see if it’s already there and removes it prior to running setup(), ensuring a smooth run.  One line of defensive coding that saves me headaches and manual deleting later, especially if I’m testing dozens of builds in a single day.

The MODULE_EXCLUDES list is all of the modules I found were unnecessary (does a game really need the gopher protocol?), which brought the whole /dist folder down to a more sane 10MB.  20MB removed just from a few hours of hacking around.  There may still be more room in the future to remove more unnecessary cruft in the build.  With hard drive sizes these days measuring closer to 1 terabyte in size, I might be crazy to even think about this.  But programmers should take pride that they put out a game that’s lean, mean, and easily distributable on any kind of media.

Also, I’m a bit of a neat freak, so there’s an option in the script to remove the /build folder after the call to setup().  It also does some extra housecleaning to make sure Tcl/Tk’s DLLs aren’t snuck in there by mistake.

There’s still plenty of room to expand on this, but it should do the job for now.  The next step would be integrating NSIS into the mix with a custom build class that assembles a working installer.  All from the same smart script.

Your mileage may vary, but hopefully this will help you get past any problems you’re having with your own setup.py scripts.

If you find this useful or have any suggestions or questions, leave a comment below.

18 thoughts on “Making Py2Exe Play Nice With Pygame

  1. I’m currently developing an adventure game using Python and Pygame, and for the last few days I’ve been struggling with getting py2exe to work properly. While I’ve managed to fix the problems with fonts not being bundled correctly etc. I’m completely stuck with trying to get SDL_mixer and py2exe to play nice. I’ve read in some forums and mailing lists that py2exe is known to throw a fit if you’re using pygame.mixer.Music, but we’re using pygame.mixer.Sound, which should work.

    It seems you’ve had similar problems, so would you please share what you did to solve the problem?

    If you have a few minutes to spare, I would be so incredibly grateful if you could check out the source at http://github.com/kallepersson/subterranean/tarball/master. All the sound stuff is contained within the file main.py, and the py2exe setup script is simply called setup.py.

  2. Thank you so much for posting this! I was fortunate to find this early in my search for the pygame issues and it worked perfectly, as well as quickly helping me get from 20mb to 9 in a few minutes. Thanks again.

  3. Hi

    This script is excellent and has got me past some problems I was having trying to include extra modules however I get this error when I try to run my exe

    “NotImplementedError: mixer module not available”

    I think it’s a similar problem to Tommy Brunn’s

    Would you be willing to share the solution for you sent to him?

    Thanks

    Nick

    1. Sure. What we found was that Tommy was using a more recent version of py2exe. When he rolled back py2exe to version 0.6.6 it was able to include all of the Pygame modules.

  4. Hi. I just stumbled upon your informative page trying to find info for pygame + py2exe. I used your script to prepare an executable for a little pygame app I made (using Python 2.6 – Py2Exe 0.6.9 – PyGame 1.9.1), but I failed. When the script tries to locate the necessary DLLs (*** finding dlls needed ***), it terminates with a message saying that it can’t locate python25.dll, which is to be expected of course, since I use Python 2.6.

    Do you have any hints on the direction I should take to solve this?

    Regards,
    Spyros

    1. That particular step that says “*** finding dlls needed **” is buried inside of py2exe itself. I’m running Python 2.6 now with py2exe 0.6.9 as well and haven’t run into any problems with it confusing python25.dll with python26.dll. However, that doesn’t mean it isn’t a problem worth investigating.

      My first guess would be to check py2exe itself and see if it was built for Python 2.5 instead of 2.6. Also, have you upgraded recently from 2.5 to 2.6 on your development machine?

      There might be a way to force py2exe to override your settings and package python26.dll instead, but as with most things Python there is always a simpler way to go about it.

  5. Well, the problem seems to happen on a particular project. I have successfully created executables of console and pygame apps, but this one doesn’t want to cooperate!

    It does use the paintbrush lib, which you may know ( http://arainyday.se/projects/python/PaintBrush/ ), but this is a source distribution, surely there should be no bond to Python25 in it.

    My 2.6 installation is a clean one, not an upgrade.

    Well, I have uploaded the problematic app on my webspace at http://www.paraschis.gr/files/kidspaint.zip – it would be appreciated if you can take a look.

    1. Okay, that answers a few questions but also raises a new one. Is the inclusion of paintbrush lib the only difference between this project and the others that do compile successfully?

    2. I downloaded your project and was able to get it compiled using the setup script above. However, there were some errors, mainly with psyco. Commenting out those at the bottom of the source code seemed to help. Also added in the standard pygame icon and referenced it in setup.py on the ICONFILE line at the top. If you want optimized performance, run “python -OO setup.py” from the command line to get an optimized build.

  6. I had forgotten psyco – it’s so transparent in its operation that I get used to just include it and expect better performance.

    Still, the fact that you managed to compile it makes it even more strange for me. Could the fact that I use a 64-bit OS (Win 7) have anything to do with it? When I get home I’ll try again after commenting out the Psyco references.

  7. Hi again. Removing the importing of psyco, my setup script works correctly now. I’ll contact the psyco developers to present this wird, in my mind at least, problen.

    Thanks for taking the time to look into this problem.

  8. @Spyros
    Spyros, just so that you know, Psyco does *not* work on 64bit machines–at all. Says so on the homepage, and I quote:
    “Just for reference, Psyco does not work on any 64-bit systems at all.”

Comments are closed.

%d bloggers like this: