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:
- McMillan Installer (http://www.mcmillan-inc.com) - download this and install it under C:\Python23\Installer on your development machine.
- Inno Setup (http://www.jrsoftware.org/isinfo.php) - download and follow the prompts in the installation wizard
- Python for Windows Extensions (http://starship.python.net/crew/mhammond/) - as above
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:
- Application name: PIMP
- Application name including version:PIMP 0.7
- Appplication publisher: Phil Edwards
- Application website: http://www.linux2000.com
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:
- Go through all of the .rsrc.py files and reset the position parameter to '-1, -1' - the Pythoncard resource editor will almost always set this to wherever the panel happened to be on our screen when you saved it, which isn't always what you want.
- If (like me!) you like to show off with screen shots of your app, ensure that the ones on your web site reflect the latest version.
- Check that the version and build numbers are correct in the various files.
- If you distribute a changelog and/or READMEfile with your app, check that you've updated it so that the release date is correct and it's up to date with the latest changes.
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:
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!
$Revision: 1.4 $ : $Author: andy47 $ : Last updated $Date: 2004/04/23 19:57:47 $