From 9e28ad593909e36b15bd37d5156c2d6f52cbf9f7 Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Fri, 18 May 2012 20:49:43 +0200 Subject: [PATCH] Add py2exe installer support. --- setup.py | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/setup.py b/setup.py index 59e1066..15c06ce 100644 --- a/setup.py +++ b/setup.py @@ -22,19 +22,209 @@ import sys if not hasattr(sys, "version_info") or sys.version_info < (2, 4, 0, "final", 0): raise SystemExit("This program requires Python 2.4 or later.") import os +import shutil +import glob +import subprocess from distutils.core import setup +try: + # py2exe monkey-patches the distutils.core.Distribution class + # So we need to import it before importing the Distribution class + import py2exe + has_py2exe = True +except ImportError: + # py2exe is not installed + has_py2exe = False +from distutils.core import Distribution +from distutils import util AppName = "patool" AppVersion = "0.17" MyName = "Bastian Kleineidam" MyEmail = "calvin@users.sourceforge.net" +# basic excludes for py2exe and py2app +# py2exe options for windows .exe packaging +py2exe_options = dict( + packages=["encodings"], + excludes=['doctest', 'unittest', 'optcomplete', 'Tkinter'], + # silence py2exe error about not finding msvcp90.dll + dll_excludes=['MSVCP90.dll'], + compressed=1, + optimize=2, +) + +# Microsoft Visual C++ runtime version (tested with Python 2.7.2) +MSVCP90Version = '9.0.21022.8' +MSVCP90Token = '1fc8b3b9a1e18e3b' + + data_files = [] if os.name == 'nt': data_files.append(('share', ['doc/patool.txt'])) else: data_files.append(('share/man/man1', ['doc/patool.1'])) + +def get_nt_platform_vars (): + """Return program file path and architecture for NT systems.""" + platform = util.get_platform() + if platform == "win-amd64": + # the Visual C++ runtime files are installed in the x86 directory + progvar = "%ProgramFiles(x86)%" + architecture = "amd64" + elif platform == "win32": + progvar = "%ProgramFiles%" + architecture = "x86" + else: + raise ValueError("Unsupported platform %r" % platform) + return os.path.expandvars(progvar), architecture + + +def add_msvc_files (files): + """Add needed MSVC++ runtime files. Only Version 9.0.21022.8 is tested + and can be downloaded here: + http://www.microsoft.com/en-us/download/details.aspx?id=29 + """ + prog_dir, architecture = get_nt_platform_vars() + dirname = "Microsoft.VC90.CRT" + version = "%s_%s_x-ww_d08d0375" % (MSVCP90Token, MSVCP90Version) + args = (architecture, dirname, version) + path = r'C:\Windows\WinSxS\%s_%s_%s\*.*' % args + files.append((dirname, glob.glob(path))) + # Copy the manifest file into the build directory and rename it + # because it must have the same name as the directory. + path = r'C:\Windows\WinSxS\Manifests\%s_%s_%s.manifest' % args + target = os.path.join(os.getcwd(), 'build', '%s.manifest' % dirname) + shutil.copy(path, target) + files.append((dirname, [target])) + + +if 'py2exe' in sys.argv[1:]: + if not has_py2exe: + raise SystemExit("py2exe module could not be imported") + add_msvc_files(data_files) + + +class MyDistribution (Distribution, object): + """Custom distribution class generating config file.""" + + def __init__ (self, attrs): + """Set console and windows scripts.""" + super(MyDistribution, self).__init__(attrs) + self.console = ['patool'] + + +class InnoScript: + """Class to generate INNO script.""" + + def __init__(self, lib_dir, dist_dir, windows_exe_files=[], + console_exe_files=[], service_exe_files=[], + comserver_files=[], lib_files=[]): + """Store INNO script infos.""" + self.lib_dir = lib_dir + self.dist_dir = dist_dir + if not self.dist_dir[-1] in "\\/": + self.dist_dir += "\\" + self.name = AppName + self.version = AppVersion + self.windows_exe_files = [self.chop(p) for p in windows_exe_files] + self.console_exe_files = [self.chop(p) for p in console_exe_files] + self.service_exe_files = [self.chop(p) for p in service_exe_files] + self.comserver_files = [self.chop(p) for p in comserver_files] + self.lib_files = [self.chop(p) for p in lib_files] + self.icon = os.path.abspath(r'doc\icon\favicon.ico') + + def chop(self, pathname): + """Remove distribution directory from path name.""" + assert pathname.startswith(self.dist_dir) + return pathname[len(self.dist_dir):] + + def create(self, pathname=r"dist\omt.iss"): + """Create Inno script.""" + self.pathname = pathname + self.distfilebase = "%s-%s" % (self.name, self.version) + self.distfile = self.distfilebase + ".exe" + with open(self.pathname, "w") as fd: + self.write_inno_script(fd) + + def write_inno_script (self, fd): + """Write Inno script contents.""" + print >> fd, "; WARNING: This script has been created by py2exe. Changes to this script" + print >> fd, "; will be overwritten the next time py2exe is run!" + print >> fd, "[Setup]" + print >> fd, "AppName=%s" % self.name + print >> fd, "AppVerName=%s %s" % (self.name, self.version) + print >> fd, r"DefaultDirName={pf}\%s" % self.name + print >> fd, "DefaultGroupName=%s" % self.name + print >> fd, "OutputBaseFilename=%s" % self.distfilebase + print >> fd, "OutputDir=.." + print >> fd, "SetupIconFile=%s" % self.icon + print >> fd + # List of source files + files = self.windows_exe_files + \ + self.console_exe_files + \ + self.service_exe_files + \ + self.comserver_files + \ + self.lib_files + print >> fd, '[Files]' + for path in files: + print >> fd, r'Source: "%s"; DestDir: "{app}\%s"; Flags: ignoreversion' % (path, os.path.dirname(path)) + # Set icon filename + print >> fd, '[Icons]' + for path in self.windows_exe_files: + print >> fd, r'Name: "{group}\%s"; Filename: "{app}\%s"' % \ + (self.name, path) + print >> fd, r'Name: "{group}\Uninstall %s"; Filename: "{uninstallexe}"' % self.name + print >> fd + # Uninstall optional log files + print >> fd, '[UninstallDelete]' + print >> fd, r'Type: files; Name: "{pf}\%s\patool*.exe.log"' % self.name + print >> fd + + def compile (self): + """Compile Inno script with iscc.exe.""" + progpath = get_nt_platform_vars()[0] + cmd = r'%s\Inno Setup 5\iscc.exe' % progpath + subprocess.check_call([cmd, self.pathname]) + + def sign (self): + """Sign InnoSetup installer with local self-signed certificate.""" + pfxfile = r'C:\certificate.pfx' + if os.path.isfile(pfxfile): + cmd = ['signtool.exe', 'sign', '/f', pfxfile, self.distfile] + subprocess.check_call(cmd) + else: + print "No signed installer: certificate %s not found." % pfxfile + +try: + from py2exe.build_exe import py2exe as py2exe_build + + class MyPy2exe (py2exe_build): + """First builds the exe file(s), then creates a Windows installer. + Needs InnoSetup to be installed.""" + + def run (self): + """Generate py2exe installer.""" + # First, let py2exe do it's work. + py2exe_build.run(self) + print "*** preparing the inno setup script ***" + lib_dir = self.lib_dir + dist_dir = self.dist_dir + # create the Installer, using the files py2exe has created. + script = InnoScript(lib_dir, dist_dir, self.windows_exe_files, + self.console_exe_files, self.service_exe_files, + self.comserver_files, self.lib_files) + print "*** creating the inno setup script ***" + script.create() + print "*** compiling the inno setup script ***" + script.compile() + script.sign() +except ImportError: + class MyPy2exe: + """Dummy py2exe class.""" + pass + + setup ( name = AppName, version = AppVersion, @@ -78,4 +268,11 @@ installed. 'Programming Language :: Python', 'Operating System :: OS Independent', ], + distclass = MyDistribution, + cmdclass = { + 'py2exe': MyPy2exe, + }, + options = { + "py2exe": py2exe_options, + }, )