Hello World!

Distributing Python Programs, Part 2: The Harder Stuff

In Part 1, we looked at using PyInstaller to package up very simple programs into executables. For more complex programs that use other Python modules like PyGame or PyQt, the process is similar, but there are a few other things you need to do.  Let’s talk about Pygame first.

PyInstaller and Pygame

The best way to show this is with an example.  Let’s make LunarLander (from Ch 24)  into a shareable program.  For this one, we’ll make a single-folder shareable version, instead of a single-file executable  (it simplifies it a little.)  That means users will have a folder which contains the executable, LunarLander.exe, as well as a bunch of supporting files.  You can zip this folder up and send people the .zip file.  They will just need to extract it somewhere on their computer in order to run it.

We’ll start by making a new folder for our sharable LunarLander.  Call it ‘LunarLanderSharable’  We need to put the LunarLander.py file there, as well as the two images that the program uses:  lunarlander.png (the rocket) and moonsurface.png (the moonscape).

The first “tricky” thing to know is that, for some reason, when using the Font object, using the default font in Pygame doesn’t work with PyInstaller.  You have to specify the font name.  Also, you need to have a copy of the font file in a place where the LunarLander sharable program can find it when it runs.

So, every place we had this:

pygame.font.Font(None, 26)

We need to replace it with this:

pygame.font.Font('freesansbold.ttf', 14)

You can find the license-free font ‘freesansbold.ttf’ in the Pygame folder (in Windows, that’s usually c:\python27\lib\site-packages\pygame\).  Copy that to the LunarLanderSharable folder.

Notice that we also changed the font size.  That’s because freesansbold renders larger than the default font.  We need to adjust the sizes of all the font objects.

Those are the only changes we need to make to the code.

To get a spec file, I first ran PyInstaller with the ‘—noconsole’ option. 

pyinstaller --noconsole lunarlander.py

This generated a basic spec file.  Then I edited it so that it would bundle the image and font files. Here’s what the spec file looks like.  The highlighted parts are what I added manually.

# -*- mode: python -*-
a = Analysis(['LunarLander.py'],
             pathex=['E:\\Warren\\PythonScripts\\LunarLanderSharable'],
             hiddenimports=[],
             hookspath=None,
             runtime_hooks=None)

pygame_files = [('freesansbold.ttf', 'E:\\Warren\\PythonScripts\\LunarLanderSharable\\freesansbold.ttf', 'DATA')] 

game_files =  [('lunarlander.png', 'E:\\Warren\\PythonScripts\\LunarLanderSharable\\lunarlander.png', 'DATA')]
game_files += [('moonsurface.png', 'E:\\Warren\\PythonScripts\\LunarLanderSharable\\moonsurface.png', 'DATA')] 

pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='LunarLander.exe',
          debug=False,
          strip=None,
          upx=True,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas + pygame_files + game_files,
               strip=None,
               upx=True,
               name='LunarLander')

If we invoke pyinstaller like this,

pyinstaller LunarLander.spec

it will create the ‘dist’ folder with the LunarLander.exe and the supporting files.  We can give that (as a .zip file, for example) to anyone who has Windows, even if they don’t have Python and Pygame installed, and they will be able to run it.

PyInstaller and PyQt

Now let’s see what is needed to make a shareable version of a PyQt program.  The ideas are the same as we saw with Pygame.  We just need to make sure the shareable version has all the supporting files it needs.

We’ll  use the Temperature Converter from Chapter 20 as our example.  Once again, we’ll make a one-folder shareable version, instead of a single-file EXE.  Remember, TempConv looks like this:

tempconv

Again, we’ll start by making a new folder for our shareable program.  Call it ‘TempConvShareable’.  In that folder, we’ll put the main code, as well as the .ui file.  If we just run PyInstaller with no spec file, it will create one for us.

pyinstaller –noconsole TempConv.py

But it will be missing a couple things we need, and the shareable version of TempConv that it makes won’t work.

Below is the final spec file for TempConv.  The highlighted parts are what we added.

 # -*- mode: python -*-
a = Analysis(['TempConv.py'],
             pathex=['E:\\Warren\\PythonScripts\\TempConvShareable'],
             hiddenimports=[],
             hookspath=None,
             runtime_hooks=None)

ui_file =  [('tempconv_menu.ui', 'E:\\Warren\\PythonScripts\\TempConvShareable\\tempconv_menu.ui', 'DATA')]
             
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='TempConv.exe',
          debug=False,
          strip=None,
          upx=True,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas + ui_file,
               strip=None,
               upx=True,
               name='TempConv')

Now we can run PyInstaller with this spec file, and it will create the shareable folder with TempConv.exe inside it.

A couple more tricks for single-file EXE’s

If you want to turn programs like LunarLander or TempConv into single-file executables, there’s one more thing you need to know about, and some changes you need to make to the code.

When PyInstaller makes a single-file executable, what it’s really making is a self-extracting zip file with all the necessary supporting files inside it.  Graphics, font files, .UI files, etc.  When it runs, that gets unzipped to a temporary folder and run from there.  When the program stops, the temporary folder gets deleted.  It’s all transparent to the user.  This means two things:

  1. A single-file executable will usually take a little longer to start up, because it has to extract the zip file first.
  2. The python code needs to know where that temp folder is, so it can find the files it needs (fonts, graphics, etc.).

The temporary folder location that PyInstaller uses can be found with ‘sys._MEIPASS’.  So, to make sure your program can find what it needs, you can use something like this:

if hasattr(sys, '_MEIPASS'):
    ui_path = os.path.join(sys._MEIPASS, "tempconv_menu.ui")
else:
    ui_path = "tempconv_menu.ui"

form_class = uic.loadUiType(ui_path)[0]     # Load the UI

That’s an example from TempConv.  Note that you need to import both sys and os modules.  Similarly, if we want to make a single-file version of LunarLander, we have to use sys._MEIPASS to get the font and the image files:

 if hasattr(sys, '_MEIPASS'):
    font_path = os.path.join(sys._MEIPASS, "freesansbold.ttf")
    ship = pygame.image.load(os.path.join(sys._MEIPASS, 'lunarlander.png'))
    moon = pygame.image.load(os.path.join(sys._MEIPASS, 'moonsurface.png'))
else:
    font_path = "freesansbold.ttf"
    ship = pygame.image.load('lunarlander.png')
    moon = pygame.image.load('moonsurface.png')

The if hasattr() part makes it so that this code will run as a single-file exe, or the normal way (e.g. running it from IDLE). We also need to make sure that when the LunarLander code is trying to load the font, it looks in the right place. So, now we’re going to replace this:

pygame.font.Font('freesansbold.ttf', 14)

with this:

pygame.font.Font(font_path, 14)

in every place where we use fonts.

There’s one more thing I discovered when making the single-file shareable version of LunarLander.  There appears to be a bug in PyInstaller, such that it tries to put 2 copies of a file called ‘pyconfig.h’ in two different paths.  The paths are named the same, except one path uses mixed case and one uses only lowercase.  In Windows, paths are case-insensitive, so this causes an error like this:

“Warning: file already exists but should not:
C:\Users\wsande\AppData\Local\Temp\_MEI38122\include\pyconfig.h”

warning_file_already_Exists

To fix this, we can put some extra code in the spec file so that it removes the redundant paths:

 for d in a.datas:                  #remove redundant paths to pyconfig.h.
    if 'pyconfig' in d[0]:         # This is a workaround to fix a bug 
        a.datas.remove(d)          # in PyInstaller
        break

That’s it!

While that’s certainly not everything there is to know about PyInstaller, I hope this gives you a good start on using PyInstaller to make shareable versions of your Python programs. If you run into problems, you can usually find answers by looking at the PyInstaller documentation or doing a web search. (Chances are, someone else has run into the same problem before you.) Good luck!