`
bcyy
  • 浏览: 1816554 次
文章分类
社区版块
存档分类
最新评论

py2exe+inno setup集成打包python程序

 
阅读更多

版权所有,转载请注明出处:http://guangboo.org/2013/03/16/build-package-with-py2exe-inno-setup

在使用python开发windows程序时,我们都会对程序进行打包,而对于使用python语言编写的windows程序,包括窗体程序和控制台程序,通常使用py2exepyinstaller来进行打包。由于我没有使用过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脚本。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics