6730
Comment: Added comment regarding shipping runtime DLL with single file executable
|
12055
Compiled example with NSIS 2.46, needed extra lines to compile successfully
|
Deletions are marked like this. | Additions are marked like this. |
Line 72: | Line 72: |
!include FileFunc.nsh !insertmacro GetParameters |
|
Line 150: | Line 152: |
== More comprehensive example == This is based on the Inno sample code in the py2exe distribution. It has worked successfully for a rather complicated PyGTK/Twisted app requiring extra data at runtime (GTK runtime data, GtkBuilder files, images, text data) that just wouldn't work with `bundle_files`. The exe file it produces will: * Display a dialog while extracting the data to a temporary directory * Hide itself when the app launches * Automatically clean up and close afterwards Sanity check: If your app is large and might be used repeatedly, you should probably think about just making a proper installer. This is for those times when you '''know''' someone just needs to "fire and forget"! '''setup.py''' {{{ import os, os.path import subprocess from distutils.core import setup from py2exe.build_exe import py2exe NSIS_SCRIPT_TEMPLATE = r""" !define py2exeOutputDirectory '{output_dir}\' !define exe '{program_name}.exe' ; Uses solid LZMA compression. Can be slow, use discretion. SetCompressor /SOLID lzma ; Sets the title bar text (although NSIS seems to append "Installer") Caption "{program_desc}" Name '{program_name}' OutFile ${{exe}} Icon '{icon_location}' ; Use XPs styles where appropriate XPStyle on ; You can opt for a silent install, but if your packaged app takes a long time ; to extract, users might get confused. The method used here is to show a dialog ; box with a progress bar as the installer unpacks the data. ;SilentInstall silent AutoCloseWindow true ShowInstDetails nevershow Section DetailPrint "Extracting application..." SetDetailsPrint none InitPluginsDir SetOutPath '$PLUGINSDIR' File /r '${{py2exeOutputDirectory}}\*' GetTempFileName $0 ;DetailPrint $0 Delete $0 StrCpy $0 '$0.bat' FileOpen $1 $0 'w' FileWrite $1 '@echo off$\r$\n' StrCpy $2 $TEMP 2 FileWrite $1 '$2$\r$\n' FileWrite $1 'cd $PLUGINSDIR$\r$\n' FileWrite $1 '${{exe}}$\r$\n' FileClose $1 ; Hide the window just before the real app launches. Otherwise you have two ; programs with the same icon hanging around, and it's confusing. HideWindow nsExec::Exec $0 Delete $0 SectionEnd """ class NSISScript(object): NSIS_COMPILE = "makensis" def __init__(self, program_name, program_desc, dist_dir, icon_loc): self.program_name = program_name self.program_desc = program_desc self.dist_dir = dist_dir self.icon_loc = icon_loc self.pathname = "setup_%s.nsi" % self.program_name def create(self): contents = NSIS_SCRIPT_TEMPLATE.format( program_name = self.program_name, program_desc = self.program_desc, output_dir = self.dist_dir, icon_location = os.path.join(self.dist_dir, self.icon_loc)) with open(self.pathname, "w") as outfile: outfile.write(contents) def compile(self): subproc = subprocess.Popen( # "/P5" uses realtime priority for the LZMA compression stage. # This can get annoying though. [self.NSIS_COMPILE, self.pathname, "/P5"], env=os.environ) subproc.communicate() retcode = subproc.returncode if retcode: raise RuntimeError("NSIS compilation return code: %d" % retcode) class build_installer(py2exe): # This class first builds the exe file(s), then creates an NSIS installer # that runs your program from a temporary directory. def run(self): # First, let py2exe do it's work. py2exe.run(self) lib_dir = self.lib_dir dist_dir = self.dist_dir # Create the installer, using the files py2exe has created. script = NSISScript(PROGRAM_NAME, PROGRAM_DESC, dist_dir, os.path.join('path', 'to, 'my_icon.ico')) print "*** creating the NSIS setup script***" script.create() print "*** compiling the NSIS setup script***" script.compile() zipfile = r"lib\shardlib" setup( name = 'MyApp', description = 'My Application', version = '1.0', window = [ { 'script': os.path.join('path','to','my_app.py'), 'icon_resources': [(1, os.path.join('path', 'to', 'my_icon.ico'))], 'dest_base': PROGRAM_NAME, }, ] options = { 'py2exe': { # Py2exe options... } }, zipfile = zipfile, data_files = # etc... cmdclass = {"py2exe": build_installer}, ) }}} Notes: * I couldn't figure out how to use `pywin32` or `ctypes` to invoke the NSIS compiler, so I used `subprocess` * That means that you need to add the NSIS compiler dir to your `PATH` * The icon location could probably be deduced from the `py2exe` class * This produces the `setup_program_name.nsi` file and the `program_name.exe` in the working directory, not in `dist\` |
The "extending" example that comes with Py2Exe shows a nicely integrated approach for using Inno Setup to create single file executables. This example isn't so nicely integrated, but it uses NSIS instead of Inno Setup in case you prefer that.
Drop a copy of this script in your source directory alongside setup.py and modify the first two lines. The first points to py2exe's output directory and the second is the name of the executable in that directory as well as the name of the executable that NSIS will create. You can also select compression behavior - NSIS' LZMA compression (based on 7-Zip) is pretty impressive - wxPython applications start at about 3.5 - 4 MB instead of 10 - 12 MB. Compression may slow startup time for your executable somewhat.
Once you've built your executable with py2exe, then compile the installer script with NSIS and an executable will be created in the same folder as the script. When run, that single file executable will expand the original executable created by py2exe along with all the dll, pyd, and data files for your application into a temporary directory and run it. When your application exits, the temp folder will be deleted automatically.
Command line parameters for your executable are not supported.
The executables produced have only been tested on Windows XP. Please update this page if you try them on other platforms.
setup.nsi:
!define py2exeOutputDirectory 'dist\Calculator' !define exe 'calculator.exe' ; Comment out the "SetCompress Off" line and uncomment ; the next line to enable compression. Startup times ; will be a little slower but the executable will be ; quite a bit smaller SetCompress Off ;SetCompressor lzma Name 'Calculator' OutFile ${exe} SilentInstall silent ;Icon 'icon.ico' Section InitPluginsDir SetOutPath '$PLUGINSDIR' File '${py2exeOutputDirectory}\*.*' GetTempFileName $0 DetailPrint $0 Delete $0 StrCpy $0 '$0.bat' FileOpen $1 $0 'w' FileWrite $1 '@echo off$\r$\n' StrCpy $2 $TEMP 2 FileWrite $1 '$2$\r$\n' FileWrite $1 'cd $PLUGINSDIR$\r$\n' FileWrite $1 '${exe}$\r$\n' FileClose $1 nsExec::Exec $0 Delete $0 SectionEnd
The version above seems to work fine on Win9x. Even with the batch file that is lauched (and DOS boxes do not close automaticaly, by default, on those OS). The startup time of a wxPython app with lzma compression becomes very long (1+ minute) on a machinne with 64MB RAM and a Pentium 200MHz, but it's usable on faster machines that are common today It also works without a temp batchfile. The current directory is also in the temp dir.
There is a streamlined version below. The sample python script 'simple.py' pops up an dialog box with the paths to important directories. uncompressed size is around 1.5MB, "lzma" compression leads to approx. 500kB. Just save the three files below in a directory then run setup.py and then makensis setup.nsi. Compare the outputs of the exe to directly running single.py. (ctypes has to be installed to create the exe)
I also added a "/r" so that subdirectories are also packed into the installer as i tend to keep images and other data as external files in a subdir, even with py2exe.
Commenting out compressor disables compression.
cliechti
Enhanced to use the correct working directory, pass through command line parameters and the exit code.
setup.nsi - the Nullsoft installer script
!define py2exeOutputDir 'dist' !define exe 'single.exe' !define icon 'C:\python23\py.ico' !define compressor 'lzma' ;one of 'zlib', 'bzip2', 'lzma' !define onlyOneInstance !include FileFunc.nsh !insertmacro GetParameters ; - - - - do not edit below this line, normaly - - - - !ifdef compressor SetCompressor ${compressor} !else SetCompress Off !endif Name ${exe} OutFile ${exe} SilentInstall silent !ifdef icon Icon ${icon} !endif ; - - - - Allow only one installer instance - - - - !ifdef onlyOneInstance Function .onInit System::Call "kernel32::CreateMutexA(i 0, i 0, t '$(^Name)') i .r0 ?e" Pop $0 StrCmp $0 0 launch Abort launch: FunctionEnd !endif ; - - - - Allow only one installer instance - - - - Section ; Get directory from which the exe was called System::Call "kernel32::GetCurrentDirectory(i ${NSIS_MAX_STRLEN}, t .r0)" ; Unzip into pluginsdir InitPluginsDir SetOutPath '$PLUGINSDIR' File /r '${py2exeOutputDir}\*.*' ; Set working dir and execute, passing through commandline params SetOutPath '$0' ${GetParameters} $R0 ExecWait '"$PLUGINSDIR\${exe}" $R0' $R2 SetErrorLevel $R2 SectionEnd
single.py - the example script
setup.py - just run it to make the sample
A NSI along these lines works unter Win NT 4 too.
More comprehensive example
This is based on the Inno sample code in the py2exe distribution. It has worked successfully for a rather complicated PyGTK/Twisted app requiring extra data at runtime (GTK runtime data, GtkBuilder files, images, text data) that just wouldn't work with bundle_files.
The exe file it produces will:
- Display a dialog while extracting the data to a temporary directory
- Hide itself when the app launches
- Automatically clean up and close afterwards
Sanity check: If your app is large and might be used repeatedly, you should probably think about just making a proper installer. This is for those times when you know someone just needs to "fire and forget"!
setup.py
import os, os.path import subprocess from distutils.core import setup from py2exe.build_exe import py2exe NSIS_SCRIPT_TEMPLATE = r""" !define py2exeOutputDirectory '{output_dir}\' !define exe '{program_name}.exe' ; Uses solid LZMA compression. Can be slow, use discretion. SetCompressor /SOLID lzma ; Sets the title bar text (although NSIS seems to append "Installer") Caption "{program_desc}" Name '{program_name}' OutFile ${{exe}} Icon '{icon_location}' ; Use XPs styles where appropriate XPStyle on ; You can opt for a silent install, but if your packaged app takes a long time ; to extract, users might get confused. The method used here is to show a dialog ; box with a progress bar as the installer unpacks the data. ;SilentInstall silent AutoCloseWindow true ShowInstDetails nevershow Section DetailPrint "Extracting application..." SetDetailsPrint none InitPluginsDir SetOutPath '$PLUGINSDIR' File /r '${{py2exeOutputDirectory}}\*' GetTempFileName $0 ;DetailPrint $0 Delete $0 StrCpy $0 '$0.bat' FileOpen $1 $0 'w' FileWrite $1 '@echo off$\r$\n' StrCpy $2 $TEMP 2 FileWrite $1 '$2$\r$\n' FileWrite $1 'cd $PLUGINSDIR$\r$\n' FileWrite $1 '${{exe}}$\r$\n' FileClose $1 ; Hide the window just before the real app launches. Otherwise you have two ; programs with the same icon hanging around, and it's confusing. HideWindow nsExec::Exec $0 Delete $0 SectionEnd """ class NSISScript(object): NSIS_COMPILE = "makensis" def __init__(self, program_name, program_desc, dist_dir, icon_loc): self.program_name = program_name self.program_desc = program_desc self.dist_dir = dist_dir self.icon_loc = icon_loc self.pathname = "setup_%s.nsi" % self.program_name def create(self): contents = NSIS_SCRIPT_TEMPLATE.format( program_name = self.program_name, program_desc = self.program_desc, output_dir = self.dist_dir, icon_location = os.path.join(self.dist_dir, self.icon_loc)) with open(self.pathname, "w") as outfile: outfile.write(contents) def compile(self): subproc = subprocess.Popen( # "/P5" uses realtime priority for the LZMA compression stage. # This can get annoying though. [self.NSIS_COMPILE, self.pathname, "/P5"], env=os.environ) subproc.communicate() retcode = subproc.returncode if retcode: raise RuntimeError("NSIS compilation return code: %d" % retcode) class build_installer(py2exe): # This class first builds the exe file(s), then creates an NSIS installer # that runs your program from a temporary directory. def run(self): # First, let py2exe do it's work. py2exe.run(self) lib_dir = self.lib_dir dist_dir = self.dist_dir # Create the installer, using the files py2exe has created. script = NSISScript(PROGRAM_NAME, PROGRAM_DESC, dist_dir, os.path.join('path', 'to, 'my_icon.ico')) print "*** creating the NSIS setup script***" script.create() print "*** compiling the NSIS setup script***" script.compile() zipfile = r"lib\shardlib" setup( name = 'MyApp', description = 'My Application', version = '1.0', window = [ { 'script': os.path.join('path','to','my_app.py'), 'icon_resources': [(1, os.path.join('path', 'to', 'my_icon.ico'))], 'dest_base': PROGRAM_NAME, }, ] options = { 'py2exe': { # Py2exe options... } }, zipfile = zipfile, data_files = # etc... cmdclass = {"py2exe": build_installer}, )
Notes:
I couldn't figure out how to use pywin32 or ctypes to invoke the NSIS compiler, so I used subprocess
That means that you need to add the NSIS compiler dir to your PATH
The icon location could probably be deduced from the py2exe class
This produces the setup_program_name.nsi file and the program_name.exe in the working directory, not in dist\
Using "bundle_files" and "zipfile"
An easier (and better) way to create single-file executables is to set bundle_files to 1 or 2, and to set zipfile to None. This approach does not require extracting files to a temporary location, which provides much faster program startup.
Valid values for bundle_files are:
3 (default) |
don't bundle |
2 |
bundle everything but the Python interpreter |
1 |
bundle everything, including the Python interpreter |
If zipfile is set to None, the files will be bundle within the executable instead of library.zip. Note: you will still be required to include the MSVC runtime DLL with your application, which should be located in the dist directory along side the executable (named MSVCRXX.dll, where XX = revision number).
Here is a sample setup.py: