Introduction

This document is targeted at developers who have successfully completed a project based on PythonCard and now wish to distribute their work to a wider audience. The information given here was gathered with reference to PythonCard Version 0.7.2. It is inevitable that some (or all!) of this document will go out of date. If so, please either post a message to the PythonCard users mailing list or send email to phil at linux2000 dot com, and I'll update it.

My motivation in writing this document is fairly straightforward. Having got my PIMP application working pretty much the way I wanted it, I needed to look into how to turn it into a single executable, with an easy-to-use install and un-install routine. My ultimate goal was to produce something so straightforward that my mum could install it on her Windows XP computer without any problems. :-)

Tools Required - Hardware

As a PythonCard developer, you will already have a computer with Python, wxPython and PythonCard itself already installed. This will be the machine you use for coding and testing your PythonCard applications. This machine will be of no use whatsoever to you in testing your application as a standalone piece of software.

The reason for this is precisely because it already has everything installed that is required to run your application. In order to be able to properly test a standalone PythonCard application, you will need access to a machine which explicitly does not have the above installed - you need to be confident that the behaviour you are seeing from your app is because you have packaged it correctly, rather than just being as a result of it being run on a machine with all the necessary components already installed.

The only 100% reliable way of doing this is to have a separate machine for testing your application and its installer. I achieved this by using VMWare workstation (http://www.vmware.com) on my Linux PC and setting up 2 'guest' machines, both running Windows XP - one with Python and the other bits installed for doing the development and debugging work, the other set aside purely for testing the results of my work.

In the remainder of this document, where I talk about the development machine, I'll be referring to the PC where you do your coding and the building of your standalone application. If you see me talking about the testing machine, I'm referring to the machine that doesn't have Python, etc installed.

Tools Required - Software

In addition to the software you already have on your development machine, you will also need to install these packages:

At the time of writing (March 18 2004) the McMillan website has been down for some time. It may or may not come back up again, in the meantime, you can download a copy of McMillan Installer version 5b5 from http://www.linux2000.com/downloads/installer_5b5_5.zip

Configuring McMillan Installer

Before you can do anything useful with the McMillan Installer, it needs to be configured. To do this, click Start --> run and type cmd to open a command line session on your development machine. Type the following commands:

cd C:\Python23\Installer

Configure.py

In the rest of this document, I'll be using my PIMP application as my example of building a standalone. The directory names will therefore reflect the way I personally like to work, your mileage may vary. Specifically, I have a directory C:\Python23\projects\pimp where I do all my coding, and everything else will be relative to this 'root' directory.

Generating A 'Spec' File For Your Application

The specification, or 'spec', file is the information used by McMillan Installer to compile and build the standalone version of your application. It's basically a list of the Python source files, Pythoncard resource files and any other stuff that your Pythoncard app needs in order to run properly. To generate the initial spec file, type these commands in a Command Prompt window:

cd C:\Python23\Installer

Makespec.py --icon ..\projects\pimp\pixmaps\pimp.ico --out ..\projects\pimp ..\projects\pimp\pimp.py

You should replace pimp.py with the name of the Python script that corresponds to the main part of your application. The icon file isn't compulsory, but taking the time to select an icon makes your app look a little more polished.

You may find at this point that you wind up with an error message saying TypeError: unpack non-sequence. At the time of writing, the jury is still out on whether this is a bug in the Installer or in wxPython. The recommended solution is to comment out the call to RegistryImportDirector() on line 225 of the iu.py file in c:\python23\installer. Do the same on line 251 of the mf.py file and then re-run the Makespec.py command from above. This issue has been reported to Gordon as a bug, you can check the status of this item on the McMillan web site, the URL is http://mcmillan-inc.com/cgi-bin/BTSCGI.py/BTS/editbugs?bugid=73. Thanks to Brent Fentem <bfentem (at) sbcglobal.net> for the fix.

At this point, you'll have a basic spec file in your app's root directory, that probably looks something like this:

a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'),
             os.path.join(HOMEPATH,'support\\useUnicode.py'),
             '..\\projects\\pimp\\pimp.py'],
             pathex=['C:\\Python23\\Installer'])
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=1,
          name='buildpimp/pimp.exe',
          debug=0,
          strip=0,
          upx=0,
          console=1 , icon='..\\projects\\pimp\\pixmaps\\pimp.ico')
coll = COLLECT( exe,
               a.binaries,
               strip=0,
               upx=0,
               name='distpimp')
    

The observant reader will have noticed (quite correctly) that this file seems to consist of Python source code! This helps us immensely in simplifying the file contents for ease of maintenance. Firstly add a line at the top of the file to define the root directory for your application, then modify the remainder of the spec file so it looks like this:

p = 'c:/python23/projects/pimp/' # defines project 'root' directory
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'),
             os.path.join(HOMEPATH,'support\\useUnicode.py'),
             p + 'pimp.py'],
             pathex=['C:/Python23/Installer'])
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=1,
          name='buildpimp/pimp.exe',
          debug=0,
          strip=0,
          upx=0,
          console=1 , icon = p + 'pixmaps/pimp.ico')
coll = COLLECT( exe,
               a.binaries + \
               [('pimp.rsrc.py', p + 'pimp.rsrc.py', 'DATA')],
               strip=0,
               upx=0,
               name='distpimp')
    

Go back to your command prompt window, and issue this command:

Build.py ..\projects\pimp\pimp.spec

The build process should now run to completion with no fatal error messages. You will find 2 new folders have been created in your application root directory. buildpimp is used purely as a temporary work area when (re-)building the app. distpimp is the directory containing the standalone version of your program. It is the contents of this directory which need to be packaged up using Inno Setup and distributed, more on that part of the process later.

You should note the line in the spec file containing console=1. This causes your app to be run from a command-prompt style window, allowing you to see any unexpected error messages. When you're happy that the app builds and runs the way you want, you can change this to read console=0.

Resolving import errors

The Installer does its best to analyse your source code to see what modules you've used via Python import statements. For the most part, it does an excellent job, but it needs a little help where the Pythoncard part is concerned. Open a new Command Prompt window and type the following:

cd c:\python23\projects\pimp\distpimp

pimp

The program will now abort with an error message, complaining that it was unable to import some module or other. The precise one will depend on the Pythoncard components that your app uses, the first one mine complained about was statictext. This is where the first bit of tedious source code editing comes in. You need to explicitly import all of the Pythoncard components that your application uses. I did this the lazy way, by adding in each component in turn, rebuilding the standalone and re-running it, until I had no more import errors. The lines I ended up with at the top of my code looked like this:

# imports required by mcmillan installer

from PythonCardPrototype.components import statictext, imagebutton, textfield, \ textarea, list, staticbox, checkbox, passwordfield, radiogroup, spinner, \ combobox, choice, htmlwindow, bitmapcanvas

Finalizing the spec file

Having resolved all the issues with the components, you now need to add all the other parts of your application to the spec file, i.e. the Pythoncard resource files for any child windows, any images used for buttons, documentation files, etc. This is an extremely tedious task, and has already got me thinking about writing a Pythoncard project manager app to do all this stuff automatically...the final version of my spec file looks like this:

p = 'c:/python23/projects/pimp/' # defines project 'root' directory
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'),
             os.path.join(HOMEPATH,'support\\useUnicode.py'),
             p + 'pimp.py'],
             pathex=['C:/Python23/Installer'])
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=1,
          name='buildpimp/pimp.exe',
          debug=0,
          strip=0,
          upx=0,
          console=1 , icon = p + 'pixmaps/pimp.ico')
coll = COLLECT( exe,
               a.binaries + \
               [('pimp.rsrc.py', p + 'pimp.rsrc.py', 'DATA')] + \
               [('advancedPrefsDialog.rsrc.py', p + 'advancedPrefsDialog.rsrc.py', 'DATA')] + \
               [('albumDialog.rsrc.py', p + 'albumDialog.rsrc.py', 'DATA')] + \
               [('albumEdit.rsrc.py', p + 'albumEdit.rsrc.py', 'DATA')] + \
               [('filterBatch.rsrc.py', p + 'filterBatch.rsrc.py', 'DATA')] + \
               [('filterInfo.rsrc.py', p + 'filterInfo.rsrc.py', 'DATA')] + \
               [('editFilter.rsrc.py', p + 'editFilter.rsrc.py', 'DATA')] + \
               [('helpAbout.rsrc.py', p + 'helpAbout.rsrc.py', 'DATA')] + \
               [('imageExportDialog.rsrc.py', p + 'imageExportDialog.rsrc.py', 'DATA')] + \
               [('pandlDialog.rsrc.py', p + 'pandlDialog.rsrc.py', 'DATA')] + \
               [('passwdDialog.rsrc.py', p + 'passwdDialog.rsrc.py', 'DATA')] + \
               [('picPreview.rsrc.py', p + 'picPreview.rsrc.py', 'DATA')] + \
               [('prefsDialog.rsrc.py', p + 'prefsDialog.rsrc.py', 'DATA')] + \
               [('slideShow.rsrc.py', p + 'slideShow.rsrc.py', 'DATA')] + \
               [('changelog.txt', p + 'changelog.txt', 'DATA')] + \
               [('readme.txt', p + 'readme.txt', 'DATA')] + \
               [('doc/gpl.txt', p + 'doc/gpl.txt', 'DATA')] + \
               [('doc/about.html', p + 'doc/about.html', 'DATA')] + \
               [('doc/author.html', p + 'doc/author.html', 'DATA')] + \
               [('doc/license.html', p + 'doc/license.html', 'DATA')] + \
               [('doc/recursion.txt', p + 'doc/recursion.txt', 'DATA')] + \
               [('pixmaps/album.png', p + 'pixmaps/album.png', 'DATA')] + \
               [('pixmaps/blank.png', p + 'pixmaps/blank.png', 'DATA')] + \
               [('pixmaps/camera.png', p + 'pixmaps/camera.png', 'DATA')] + \
               [('pixmaps/delete.png', p + 'pixmaps/delete.png', 'DATA')] + \
               [('pixmaps/down.png', p + 'pixmaps/down.png', 'DATA')] + \
               [('pixmaps/exit.png', p + 'pixmaps/exit.png', 'DATA')] + \
               [('pixmaps/export.png', p + 'pixmaps/export.png', 'DATA')] + \
               [('pixmaps/filter.png', p + 'pixmaps/filter.png', 'DATA')] + \
               [('pixmaps/finepix2800.png', p + 'pixmaps/finepix2800.png', 'DATA')] + \
               [('pixmaps/forward.png', p + 'pixmaps/forward.png', 'DATA')] + \
               [('pixmaps/fullscreen.png', p + 'pixmaps/fullscreen.png', 'DATA')] + \
               [('pixmaps/import.png', p + 'pixmaps/import.png', 'DATA')] + \
               [('pixmaps/info.png', p + 'pixmaps/info.png', 'DATA')] + \
               [('pixmaps/ledgreen.png', p + 'pixmaps/ledgreen.png', 'DATA')] + \
               [('pixmaps/ledred.png', p + 'pixmaps/ledred.png', 'DATA')] + \
               [('pixmaps/left.png', p + 'pixmaps/left.png', 'DATA')] + \
               [('pixmaps/locations.png', p + 'pixmaps/locations.png', 'DATA')] + \
               [('pixmaps/mkalbum.png', p + 'pixmaps/mkalbum.png', 'DATA')] + \
               [('pixmaps/new.png', p + 'pixmaps/new.png', 'DATA')] + \
               [('pixmaps/nofullscreen.png', p + 'pixmaps/nofullscreen.png', 'DATA')] + \
               [('pixmaps/open.png', p + 'pixmaps/open.png', 'DATA')] + \
               [('pixmaps/options.png', p + 'pixmaps/options.png', 'DATA')] + \
               [('pixmaps/padlock.png', p + 'pixmaps/padlock.png', 'DATA')] + \
               [('pixmaps/password-big.png', p + 'pixmaps/password-big.png', 'DATA')] + \
               [('pixmaps/password.png', p + 'pixmaps/password.png', 'DATA')] + \
               [('pixmaps/pause.png', p + 'pixmaps/pause.png', 'DATA')] + \
               [('pixmaps/people.png', p + 'pixmaps/people.png', 'DATA')] + \
               [('pixmaps/play.png', p + 'pixmaps/play.png', 'DATA')] + \
               [('pixmaps/reverse.png', p + 'pixmaps/reverse.png', 'DATA')] + \
               [('pixmaps/right.png', p + 'pixmaps/right.png', 'DATA')] + \
               [('pixmaps/rotatecw.png', p + 'pixmaps/rotatecw.png', 'DATA')] + \
               [('pixmaps/rotateccw.png', p + 'pixmaps/rotateccw.png', 'DATA')] + \
               [('pixmaps/save.png', p + 'pixmaps/save.png', 'DATA')] + \
               [('pixmaps/slideshow.png', p + 'pixmaps/slideshow.png', 'DATA')] + \
               [('pixmaps/stop.png', p + 'pixmaps/stop.png', 'DATA')] + \
               [('pixmaps/up.png', p + 'pixmaps/up.png', 'DATA')] + \
               [('pixmaps/view.png', p + 'pixmaps/view.png', 'DATA')] + \
               [('themes/default/caption.gif', p + 'themes/default/caption.gif', 'DATA')] + \
               [('themes/default/hide.gif', p + 'themes/default/hide.gif', 'DATA')] + \
               [('themes/default/notescript.js', p + 'themes/default/notescript.js', 'DATA')] + \
               [('themes/default/tech.gif', p + 'themes/default/tech.gif', 'DATA')],
               strip=0,
               upx=0,
               name='distpimp')
    

You can now rebuild your standalone and check that it runs without any error messages in the console window. The installer generated the standalone version of my app in the ./projects/distpimp on my machine. The folder name will be whatever you called your app with 'dist' on the front. I also found a ./projects/buildpimp directory had been created - according to the installers docs, this is used to store transient files during the build process and can be ignored.

Using Inno Setup

Inno Setup is a lovely piece of software that allows you to package up the contents of a directory into a single .EXE file that can easily be distributed. When someone downloads this and double-clicks, it launches the familiar series of dialogs through which a new program can be installed. It also takes care of adding an item in Control Panel --> Add and remove programs which allows your application to be removed just as easily.

Launch Inno Setup, and select the option to Create a new script file using the Script Wizard. Click OK, then Next to launch the wizard. I set the options on the application information section as follows:

Click Next to get to the Application directory section. For most people, the defaults that Inno offers should be okay, but feel free to make amendments if required.

Click Next. You'll now see the Application files section. This is probably the most important part of the process, as this is where you tell Inno which specific files to include in the executable. Luckily, if you've done the part with McMillan Installer correctly, everything you need should already be there. Make sure you only include the files that your app really needs - for example, whilst putting this document together, I couldn't work out why PIMP kept having problems after I had installed it on the test machine. It took me a while to figure out that I'd included a pimp.ini file from my development machine in the SETUP.EXE produced by Inno. Taking this out cured the problem!

Click Next to get to the Application icons section. You should select whichever options you want the user to have.

Click Next. On the Application documentation section, I included a copy of the GPL in text format as the license file, and a copy of the PIMP changelog as the file to display before installation. This leads to the interesting situation of the end user having to agree to the terms of the GPL before being allowed to install the software! Irony is such a wonderful thing...

Click Next a couple more times and your SETUP.EXE file will be generated. Inno will put it in a ./Output directory off your main project root folder. You can now copy this file over to your test machine and check that it does actually install and run the way you expect it to.

The final task in this section is to ensure you save the Inno Setup script file that you just generated. For the record, my PIMP application generated the following:

; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

[Setup]
AppName=PIMP
AppVerName=PIMP 0.7
AppPublisher=Phil Edwards
AppPublisherURL=http://www.linux2000.com/pimp.html
AppSupportURL=http://www.linux2000.com/pimp.html
AppUpdatesURL=http://www.linux2000.com/pimp.html
DefaultDirName={pf}\PIMP
DefaultGroupName=PIMP
AllowNoIcons=yes
LicenseFile=C:\Python23\projects\pimp\distpimp\doc\gpl.txt
InfoBeforeFile=C:\Python23\projects\pimp\distpimp\changelog.txt

[Tasks]
; NOTE: The following entry contains English phrases ("Create a desktop icon"
; and "Additional icons"). You are free to translate them into another language
; if required.
Name: "desktopicon"; Description: "Create a &desktop icon"; GroupDescription: "Additional icons:";
	Flags: unchecked

[Files]
Source: "C:\Python23\projects\pimp\distpimp\pimp.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\_socket.pyd"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\_sre.pyd"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\_ssl.pyd"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\_winreg.pyd"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\advancedPrefsDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\albumDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\albumEdit.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\changelog.txt"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\editFilter.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\filterBatch.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\filterInfo.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\helpAbout.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\imageExportDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\pandlDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\passwdDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\picPreview.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\pimp.exe"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\pimp.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\prefsDialog.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\pyexpat.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\python23.dll"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\pywintypes23.dll"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\readme.txt"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\slideShow.rsrc.py"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\win32api.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\wxmsw24h.dll"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\wxPython.helpc.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\wxPython.htmlc.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\wxPython.stc_c.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\wxPython.wxc.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\zlib.pyd"; DestDir: "{app}";
	Flags: ignoreversion
Source: "C:\Python23\projects\pimp\distpimp\doc\*"; DestDir: "{app}\doc";
	Flags: ignoreversion recursesubdirs
Source: "C:\Python23\projects\pimp\distpimp\pixmaps\*"; DestDir: "{app}\pixmaps";
	Flags: ignoreversion recursesubdirs
Source: "C:\Python23\projects\pimp\distpimp\themes\*"; DestDir: "{app}\themes";
	Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Icons]
Name: "{group}\PIMP"; Filename: "{app}\pimp.exe"
Name: "{userdesktop}\PIMP"; Filename: "{app}\pimp.exe"; Tasks: desktopicon
    

Preparing to distribute the finished app

We're almost ready to send the finished Windows version of PIMP out into the big bad world, but there are still a couple of sharp edges that need rubbing down.

Firstly, you will recall that when we built the standalone using McMillan Installer, we set the following options in the pimp.spec file:

exe = EXE(pyz,
          a.scripts,
          exclude_binaries=1,
          name='buildpimp/pimp.exe',
          debug=0,
          strip=0,
          upx=0,
          console=1 , icon = p + 'pixmaps/pimp.ico')
          ^^^^^^^^^
    

This causes the program to run in a command prompt style window, so we can see any run-time error messages produced. Obviously, the program will look pretty lame if we allow it to run this way on the end-users systems. You need to remember to change the console=1 to read console=0 before doing a final build of your program.

Secondly, the output executable will always be called SETUP.EXE, which i think is too generic. I prefer the file to include the name, version number and preferably the build number of the program in question. Open up the pimp.iss file that Inno saved and add the following line to the end of the [Setup] section:

OutputBaseFilename=pimp-0.7-1

There doesn't appear to be any way at present to force Inno to take this information from an external file, so you just have to remember to update it manually each time you release a new build of the program. Another job for the vapor-ware Pythoncard project manager, I think!

As a final check immediately before releasing a new version of PIMP, I try to remember to go also through the following checklist:

Vaporware alert! PythonCard project manager

In the process of preparing these notes, it's become painfully obvious to me that there's a need for a project manager of some sort to assist with the whole process of managing a PythonCard project from initial coding all the way through to the finished Inno-based installer binary. I've made a start on coding this, see the screenshot below for what the user interface will (maybe) look like:

Project Manager Screenshot

I'm grateful for the code that Lawrie Abott originally wrote, which has proven to be the inspiration for this project. It's probably about time I left PIMP alone to fester for a bit, anyway. :-)

Page last updated: March 3rd 2004 03:00 UTC
That's all for today, folks!


SourceForge Logo Valid XHTML 1.0! Valid CSS!

$Revision: 1.4 $ : $Author: andy47 $ : Last updated $Date: 2004/04/23 19:57:47 $