py2exe+inno setup集成打包python程序
版权所有,转载请注明出处:http://guangboo.org/2013/03/16/build-package-with-py2exe-inno-setup
在使用python开发windows程序时,我们都会对程序进行打包,而对于使用python语言编写的windows程序,包括窗体程序和控制台程序,通常使用py2exe或pyinstaller来进行打包。由于我没有使用过pyinstaller,因此本文所使用的打包库或工具是py2exe。然而,这个打包过程只是将诸多python文件打包成一个.exe文件,并且将python运行环境,及必要的python库一并打包,这样的打包过程会生成一个.exe文件(默认情况还会有一个w9xpopen.exe文件,该文件是为win98系统使用的),和python27.dl(根据所使用Python版本,文件名会有所不同),很多.pyd文件,及其他文件。这样打包的结果不是我们想象中的windows下的安装文件,本文结合使用py2exe和inno setup,对Python编写的windows程序打包成安装文件。
py2exe打包由于本文示例代码是基于Python 2.7版本开发的,因此需要将VC2008的运行时打包进去,因为python 2.7是基于VC 2008编译的,可以参考:http://www.py2exe.org/index.cgi/Tutorial#Step521。本文示例打包的程序是使用wxPython开发的windows窗体程序,因此,我们的打包需要将wx库添加进来,并且程序不考虑windows98系统,因此希望把w9xponen.exe文件排除掉,并希望将python基础的类库打包进一个shared.zip文件中。setup.py示例代码如下:
inno setup打包windows下有很多打包工具,如setup factory, inno setup等,这两种方式我都使用过,但是inno setup相对根据容易一下,只需要一个.iss文件即可实现打包过程,并且.iss文件的格式和.ini文件非常相似,并且可以在.iss文件中直接编写打包逻辑,其编写脚本是使用Pascal语言进行编写的。如下为简单的打包脚本:
另外,py2exe的版本我们可以通过变量VERSION来定义,在inno setup脚本中,我们发现版本号是写死的,这样对于inno setup打包就比较麻烦,每次版本变化就要编写一个新的脚本文件,虽然内容只有版本号不同。因此我们也希望能有一个变量,像py2exe打包一样有一个VERSION变量来定义。
py2exe, inno setup是否可以集成一方面由于版本的变化会带来inno setup打包要编写新的脚本文件的麻烦,另一方面,这两个打包过程还是分开的,打包必须先执行setup脚本,使用py2exe生成可执行文件,然后在编写iss脚本,使用inno setup compiler来打包成安装文件。这个的过程本身也很麻烦,每次版本的变化都要这么麻烦的打包过程,可能觉得麻烦。理想的方案就是,执行一个setup.py脚本即可将两个打包过程都完成。
其实这个过程也不难实现,因为py2exe库提供了一个接口,允许我们添加自己的打包过程,或在打包完成后执行自定义的功能。我们的需求就是在py2exe打包完成后,自动根据当前版本号生成iss脚本,并且使用inno setup compiler来执行新生成的iss脚本文件。py2exe提供了这样的接口:
from py2exe.build_exe import py2exefrom distutils.dir_util import remove_treeclass build_installer(py2exe): # This class first builds the exe file(s), then creates a Windows installer. # You need InnoSetup for it. def run(self): # First, let py2exe do it's work. py2exe.run(self) # your own business codes.上面的代码是定义了一个新的打包过程的类,继承了默认的py2exe类,我们可以在py2exe.run(self)代码后添加自己的代码,如删除py2exe打包过程中生成的build临时目录,根据当前版本生成iss脚本,并调用inno setup compiler执行该脚本,生成安装文件。
py2exe提供了扩展接口,那么现在的问题就是inno setup compiler进程的是否支持启动参数呢,即是否可以传递一个iss文件地址给该进程执行。当然有,inno setup compiler支持这样的参数,其格式是:
compil32.exe /cc 'iss file name'现在问题都得到答案了,py2exe和inno setup两个打包过程是可以集成的,那么下面就是怎么集成的问题了。
自动生成ISS文件集成的方案已经定下来了,并且可能遇到的问题也已经有了方案。下面还有一个工作没有做,就是根据当前版本生成iss脚本文件的过程。这个过程其实简单,因为有了上面的脚本示例,每次生成的脚本只有版本号不同而已,其他都可以直接输出。需要注意与版本相关的目录名和文件名。如下给出的示例代码:
# -*- coding:utf-8 -*-import appimport osclass InnoScript: def __init__(self, output, input): self._output = os.path.join(output, app.VERSION_TEXT) self._input = input self._script_file = os.path.join(self._output, 'setupscript-%s.iss' % app.VERSION_TEXT) self._exename = 'app.exe' def _create_script_file(self): scf = os.path.dirname(self._script_file) exec_path = os.path.abspath(os.path.dirname(__file__)) if not os.path.exists(scf): os.mkdir(scf) f = open(self._script_file, 'w') print >> f, "[Setup]" print >> f, "AppId={{75BB853E-503D-416A-873E-844CDBB95B22}" print >> f, "AppName=%s" % app.NAME print >> f, "AppVersion=%s" % app.VERSION_TEXT print >> f, "AppVerName=%s(%s)" % (app.NAME, app.VERSION_TEXT) print >> f, "VersionInfoDescription=%s" % app.COMPANY print >> f, "VersionInfoProductName=%s" % app.NAME print >> f, "VersionInfoProductVersion=%s" % app.VERSION_TEXT print >> f, "VersionInfoVersion=%s" % app.VERSION_TEXT print >> f, "VersionInfoTextVersion=%s %s" % (app.VERSION_TEXT, 'Alpha') print >> f, "VersionInfoCompany=%s" % app.COMPANY print >> f, "VersionInfoCopyright=%s" % app.COPYRIGHT print >> f, "AppPublisher=%s" % app.COMPANY print >> f, "AppCopyright=%s" % app.COPYRIGHT print >> f, "AppPublisherURL=%s" % app.COMPANY_SITE print >> f, "DefaultDirName={pf}\XXX/test app" print >> f, "DefaultGroupName=%s" % app.NAME print >> f, "LicenseFile=%s" % app.LICENSE_FILE print >> f, "OutputDir=%s" % self._output print >> f, "SetupIconFile=%s" % os.path.join(exec_path, 'app.ico') print >> f, "OutputBaseFilename=setup-%s" % app.VERSION_TEXT print >> f, "Compression=lzma" print >> f, "SolidCompression=yes" print >> f, "" print >> f, "[Languages]" print >> f, 'Name: "english"; MessagesFile: "compiler:Default.isl"' print >> f, "" print >> f, "[Tasks]" print >> f, 'Name: desktopicon; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone' print >> f, 'Name: quicklaunchicon; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked' print >> f, "" print >> f, "[Files]" print >> f, 'Source: "../vcredist_x86.exe"; DestDir:"{tmp}"; Check:NeedInstallVC9' print >> f, 'Source: "%s\%s"; DestDir: "{app}"; Flags: ignoreversion' % (self._input, self._exename) print >> f, 'Source: "%s\python27.dll"; DestDir: "{app}"; Flags: ignoreversion' % self._input print >> f, 'Source: "%s\app.ico"; DestDir: "{app}"; Flags: ignoreversion' % self._input print >> f, 'Source: "%s\lib\*"; DestDir: "{app}/lib"; Flags: ignoreversion recursesubdirs createallsubdirs' % self._input print >> f, "" print >> f, "[Icons]" print >> f, 'Name: "{group}\%s"; Filename: "{app}\app.exe"' % app.NAME print >> f, 'Name: "{group}\{cm:ProgramOnTheWeb,%s}"; Filename: "%s"' % (app.NAME, app.COMPANY_SITE) print >> f, 'Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\%s"; Filename: "{app}\%s"; Tasks: quicklaunchicon' % (app.NAME, self._exename) print >> f, "" print >> f, "[Run]" print >> f, 'Filename: "{tmp}/vcredist_x86.exe"; Parameters: /q; WorkingDir: {tmp}; Flags: skipifdoesntexist; StatusMsg: "Installing Microsoft Visual C++ Runtime ..."; Check: NeedInstallVC9' print >> f, '''Filename: "{app}\%s"; Description: "{cm:LaunchProgram,%s}"; Flags: nowait postinstall skipifsilent''' % (self._exename, self._exename) print >> f, "" print >> f, '''[Code]var vc9Missing: Boolean; function NeedInstallVC9(): Boolean;begin Result := vc9Missing;end; function InitializeSetup(): Boolean;var version: Cardinal;begin if RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{FF66E9F6-83E7-3A3E-AF14-8DE9A809A6A4}', 'Version', version) = false then begin vc9Missing := true; end; result := true;end;''' f.close() def compile(self): self._create_script_file() import subprocess proc = subprocess.Popen('compil32.exe /cc "%s"' % self._script_file) proc.wait()上面的代码中有一个app模块,该模块定义了一些常量,主要有COMPANY, NAME等。该类的compile方法用调用了_create_script_file来生成iss脚本文件,然后调用inno setup的编译器compil32.exe,来编译刚生成的iss脚本。
打包集成现在一切准备工作都做好了,下面一步就是将这些过程集成起来,使一次执行可以生成可执行文件和安装文件。前面介绍了py2exe的扩展接口,下面我们就将自定义的代码添加进去,代码如下:
from py2exe.build_exe import py2exefrom distutils.dir_util import remove_treeclass build_installer(py2exe): # This class first builds the exe file(s), then creates a Windows installer. # You need InnoSetup for it. def run(self): # First, let py2exe do it's work. py2exe.run(self) remove_tree(os.path.join(cw, 'build')) setup_dir = os.path.join(self.dist_dir, '../../setup') import inno_script script = inno_script.InnoScript(setup_dir, self.dist_dir) script.compile()相比之前的代码,我们添加了删除py2exe打包过程生成的build临时目录的代码和生成安装文件的代码(包含生成iss脚本和编译两个过程)。然而只有新的py2exe类的定义还不够,需要在setup方法中使用它才行。使用方法也非常简单,只要在setup方法中添加一个cmdclass参数,其值为{"py2exe": build_installer},即可。这里只贴出setup方法的后半部分代码,前半部分和之前一样:
setup( ..., windows = [{'script':'app.py', 'icon_resources':[(1, 'icons\\app.ico')], 'copyright':'guangboo49@gmail.com', 'company_name':'Jeff zhang', 'name':'test name', 'version':VERSION + '.' + str(build)}], cmdclass = {"py2exe": build_installer},)另外,如果找不到compil32.exe命令时,请确认正确安装了inno setup,并且将inno setup根目录添加到PATH环境变量中。
总结本文是为在使用python编写windows程序时,对繁琐的打包过程提出的一种解决方案,以便简化打包过程,使专注于程序开发,而不是打包。本文中的打包过程与版本管理有一定的关联,本文中的版本号保存在app.py模块,VERSION常量中,如版本发生变化,可以修改该值,然后重新执行setup.py脚本。