版权所有,转载请注明出处: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示例代码如下:
import sys
import os
from glob import glob
from distutils.core import setup
import py2exe
import app
includes = ['wx', 'wx.html', 'select', 'hashlib']
excludes = ['bz2', 'unicodedata']
packages = []
dll_excludes = ['w9xpopen.exe']
VERSION = app.VERSION
cw = os.path.dirname(__file__)
data_files = [('', ['License.txt']),
('', [os.path.join(cw, 'icons/app.ico')]),
('lib', glob(r'C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\x86\Microsoft.VC90.CRT\*.*'))]
sys.path.append("C:\\Program Files\\Microsoft Visual Studio 9.0\\VC\\redist\\x86\\Microsoft.VC90.CRT")
try:
build_file = open('../dist/build.txt', 'r')
build = int(build_file.readline())
except:
build = 0
setup(
version = VERSION+ '.' + str(build),
name = 'test app',
description = 'app for test.',
long_description = '',
author = 'Jeff Zhang',
author_email = 'guangboo49@gmail.com',
data_files = data_files,
options = {'py2exe':{'compressed':1,
'optimize':2,
'includes':includes,
'excludes':excludes,
'packages':packages,
'dll_excludes':dll_excludes,
'bundle_files':3,
'dist_dir':os.path.join('../dist/',VERSION),
'xref':False,
'skip_archive':False,
'ascii':False,
'custom_boot_script':'',
}
},
zipfile = 'lib/shared.zip',
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)}],
)
open('../dist/build.txt', 'w').write(str(build + 1))
示例中,build是用来作为版本号的最后一位,表示生成的编号,该编号一直往上涨,不予主版本和次版本号的变化而清零。build版本号保存在一个build.txt文件中,每次执行该脚本时都会提前文件中的数字作为build版本号,打包完后,将加一后的新build版本号再保存到build.txt文件中。setup函数制定了一个zipfile参数,其值为lib/shared.zip,表示将Python基础库打包到lib/shared.zip文件中。另外需要注意到是windows参数中icon_resources,因为该参数涉及到的ico文件如果设置不正确,可能在windows 7等系统中显示出现问题,可以参考,之前专门的文章提供的解决方案:http://guangboo.org/2013/01/10/exe-file-packaging-with-py2exe-cant-display-ico-in-vista-win7。
以上脚本是将python程序打包成.exe可执行文件(包括其他dll和pyd等文件),上面的脚本生成的结果,如下图目录结构:
inno setup打包
windows下有很多打包工具,如setup factory, inno setup等,这两种方式我都使用过,但是inno setup相对根据容易一下,只需要一个.iss文件即可实现打包过程,并且.iss文件的格式和.ini文件非常相似,并且可以在.iss文件中直接编写打包逻辑,其编写脚本是使用Pascal语言进行编写的。如下为简单的打包脚本:
[Setup]
AppId={{75BB8432-50eD-436A-873E-844CDC495B22}
AppName=test app
AppVersion=0.17.11
AppVerName=test app(0.17.11)
VersionInfoDescription=XXX Co.,Ltd.
VersionInfoProductName=test app
VersionInfoProductVersion=0.17.11
VersionInfoVersion=0.17.11
VersionInfoTextVersion=0.17.11 Alpha
VersionInfoCompany=XXX Co.,Ltd.
VersionInfoCopyright=Copyright (C) XXX Co.,Ltd. All right reserved.
AppPublisher=XXX Co.,Ltd.
AppCopyright=Copyright (C) XXX Co.,Ltd. All right reserved.
AppPublisherURL=http://www.xxxx.net
DefaultDirName={pf}\test app
DefaultGroupName=test app
LicenseFile=
OutputDir=D:\setup\0.17.11
SetupIconFile=D:\src\app.ico
OutputBaseFilename=setup-0.17.11
Compression=lzma
SolidCompression=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: desktopicon; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone
Name: quicklaunchicon; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "../vcredist_x86.exe"; DestDir:"{tmp}"; Check:NeedInstallVC9
Source: "D:\dist\0.17.11\app.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "D:\dist\0.17.11\python27.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "D:\dist\0.17.11\app.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "D:\dist\0.17.11\lib\*"; DestDir: "{app}/lib"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\test app"; Filename: "{app}\app.exe"
Name: "{group}\{cm:ProgramOnTheWeb, test app}"; Filename: "http://www.xxxx.net"
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\test app"; Filename: "{app}\app.exe"; Tasks: quicklaunchicon
[Run]
Filename: "{tmp}/vcredist_x86.exe"; Parameters: /q; WorkingDir: {tmp}; Flags: skipifdoesntexist; StatusMsg: "Installing Microsoft Visual C++ Runtime ..."; Check: NeedInstallVC9
Filename: "{app}\app.exe"; Description: "{cm:LaunchProgram,app.exe}"; Flags: nowait postinstall skipifsilent
[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;
其中[Code]节定义了一个函数,用于判断是否需要安装VC9运行时。因为我在使用inno setup打包时也把VC9运行时打包进去了,如果客户端没有安装VC9运行时的话,inno setup就会调用VC9的安装包vcredist_x86.exe进行安装。[Files]节表示要打包的文件,第一行包含了vcredist_x86.exe文件。
版本控制
因为我们每次打包时可能会有python代码的修改,有代码修改就有版本的变化,前面py2exe打包时,我们使用build版本号,来表示生成或打包的变化。然而这个版本号一般不是主要的,主要的变化在于主版本和次版本,有时还包括修正版本。为了区别和记录软件打包的版本变化,本文主要区分主版本,次版本和修正版本,当版本号的这三个部分发生变化时,都将生成新的目录,如0.7.11表示0.7.11版本打包的目录,0.7.12表示下一个修正版本的目录。
由于我们采用的是先使用py2exe进行打包生成可执行文件,然后在使用inno setup进行打包生成安装文件。并且py2exe输出目录在dist目录,inno setup的输出目录是setup目录,目录结构如图:
另外,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 py2exe
from distutils.dir_util import remove_tree
class 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 app
import os
class 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 py2exe
from distutils.dir_util import remove_tree
class 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脚本。