A PyInstaller Tutorial – Build a Binary Series!

In our previous article on building binaries, we learned a little about py2exe. This time around, we will be focusing our collective attention on the ins and outs of PyInstaller. We’ll use the same lame wxPython script from the last article for one of our examples, but we’ll also try a normal console script to see what the differences are, if any. In case you didn’t know, PyInstaller works on Linux, Windows and Mac (experimental) and works with Python 1.5-2.6 (except on Windows, where there’s a caveat for 2.6 – see below). PyInstaller supports code-signing (Windows), eggs, hidden imports, single executable, single directory, and lots more!

Getting Started with PyInstaller

Note on Python 2.6 on Windows: If you read the PyInstaller website closely, you will see a warning about Python 2.6+ not being fully supported. The note says that you will currently need to have the Microsoft CRT installed for your executable to work. What this is probably referring to is the side-by-side assemblies / manifest issues that were introduced with Python 2.6 vis-a-vis Microsoft Visual Studio 2008. We already mentioned this problem in the first article. If you don’t know anything about it, please check out the py2exe website, the wxPython wiki or Google for the issue.

Anyway, let’s get on with the show. After you download PyInstaller, you just unzip the archive somewhere convenient. The follow these three simple steps:

  1. Run Configure.py to save some basic configuration data to a “.dat” file. This saves some time since PyInstaller won’t have to recalculate the configuration on the fly.
  2. Run the following command on the command line: python Makespec.py [opts] where scriptname is the name of the main Python file you use to run your program.
  3. Finally, run the following command via the command line: python Build.py specfile to build your executable.

Let’s run through this with a real script now. We’ll start with a simple console script that creates a faux configuration file. Here’s the code:

import configobj

#----------------------------------------------------------------------
def createConfig(configFile):
    """
    Create the configuration file
    """
    config = configobj.ConfigObj()
    inifile = configFile
    config.filename = inifile
    config['server'] = "http://www.google.com"
    config['username'] = "mike"
    config['password'] = "dingbat"
    config['update interval'] = 2
    config.write()
    
#----------------------------------------------------------------------
def getConfig(configFile):
    """
    Open the config file and return a configobj
    """    
    return configobj.ConfigObj(configFile)

def createConfig2(path):
    """
    Create a config file
    """
    config = configobj.ConfigObj()
    config.filename = path
    config["Sony"] = {}
    config["Sony"]["product"] = "Sony PS3"
    config["Sony"]["accessories"] = ['controller', 'eye', 'memory stick']
    config["Sony"]["retail price"] = "$400"
    config.write()

if __name__ == "__main__":
    createConfig2("sampleConfig.ini")

Now let’s make a spec file:


c:\Python25\python c:\Users\Mike\Desktop\pyinstaller-1.4\Makespec.py config_1.py

On my test machine, I have 3 different Python versions installed, so I had to specify the Python 2.5 path explicitly (or set Python 2.5 as the default). Anyway, this should create a file similar to the following (which was named “config_1.spec”):

# -*- mode: python -*-
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'config_1.py'],
             pathex=['C:\\Users\\Mike\\Desktop\\py2exe_ex', r'C:\Python26\Lib\site-packages'])
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=1,
          name=os.path.join('build\\pyi.win32\\config_1', 'config_1.exe'),
          debug=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT( exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name=os.path.join('pyInstDist2', 'config_1'))

For the Python script we’re using, we need to add an explicit path to the location of configobj.py in the Analysis section of the spec file in the pathex parameter. If you do not do this, when you run the resulting executable, it will open and close a console window really fast and you won’t be able to tell what it says unless you run the exe from the command line. I did the latter to find out what was wrong and discovered it could not find the configobj module. You can also specify the output path for your exe in the COLLECT function’s name parameter. In this case, we put PyInstaller’s output in the “config_1” subfolder of “pyInstDist2”, which should be a folder alongside your original script. There are a ton of options when configuring your spec file, which you can read about here.

To build the executable based on the spec file, do the following on the command line:


c:\Python25\python c:\Users\Mike\Desktop\pyinstaller-1.4\Build.py config_1.spec

On my machine, I ended up with a folder that had 25 files in it that totaled 6.7 MB. You should be able to reduce the size using the Analysis section’s excludes parameter and/or compression.

PyInstaller and wxPython

Now let’s try creating a binary from a simple wxPython script. Here’s the Python script:

import wx

########################################################################
class DemoPanel(wx.Panel):
    """"""
    
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent)
        
        labels = ["Name", "Address", "City", "State", "Zip",
                  "Phone", "Email", "Notes"]
        
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        lbl = wx.StaticText(self, label="Please enter your information here:")
        lbl.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD))
        mainSizer.Add(lbl, 0, wx.ALL, 5)
        for lbl in labels:
            sizer = self.buildControls(lbl)
            mainSizer.Add(sizer, 1, wx.EXPAND)
        self.SetSizer(mainSizer)
        mainSizer.Layout()
        
    #----------------------------------------------------------------------
    def buildControls(self, label):
        """"""
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        size = (80,40)
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD)
        
        lbl = wx.StaticText(self, label=label, size=size)
        lbl.SetFont(font)
        sizer.Add(lbl, 0, wx.ALL|wx.CENTER, 5)
        if label != "Notes":
            txt = wx.TextCtrl(self, name=label)
        else:
            txt = wx.TextCtrl(self, style=wx.TE_MULTILINE, name=label)
        sizer.Add(txt, 1, wx.ALL, 5)
        return sizer
    
    

########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "PyInstaller Tutorial",
                          size=(600,400)
                          )
        panel = DemoPanel(self)        
        self.Show()
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DemoFrame()
    app.MainLoop()

Since this is a GUI, we’ll create our spec file slightly differently:


c:\Python25\python c:\Users\Mike\Desktop\pyinstaller-1.4\Makespec.py -F -w sampleApp.py

Note the -F and -w parameters. The -F command tells PyInstaller to create just one executable whereas the -w tells PyInstaller to hide the console window. Here’s the resulting spec file:

# -*- mode: python -*-
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'sampleApp.py'],
             pathex=['C:\\Users\\Mike\\Desktop\\py2exe_ex'])
pyz = PYZ(a.pure)
exe = EXE( pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name=os.path.join('pyInstDist', 'sampleApp.exe'),
          debug=False,
          strip=False,
          upx=True,
          console=False )

Note that the last line has the “console” parameter set to “False”. If you build this like you did with the console script you should end up with one file in the “pyInstDist” folder that is approximately 7.1 MB in size.

Wrapping Up

This ends our quick tour of PyInstaller. I hope you found this helpful in your Python binary-making endeavors. There is much more information on the PyInstaller website and it’s pretty well documented, although the website is pretty plain. Be sure to give it a try and see just how much easy PyInstaller is to use!

Note: I tested all this using PyInstaller 1.4 and Python 2.5 on Windows 7 Home Premium (32-bit).

Further Reading