Python 打包工具是一种用于将 Python 程序打包成可执行文件的工具,它可以帮助开发者将 Python 程序打包成独立的可执行文件,使得程序更易于部署和分发。
Python 打包工具有很多种,比如 cx_Freeze、py2exe、pyinstaller 等。cx_Freeze 是一个用于将 Python 应用程序转换为独立的可执行文件的工具,它能够将 Python 应用程序转换为 Windows 上的 exe 文件;py2exe 是一个用于将 Python 应用程序转换为 Windows 上的 exe 文件的工具;pyinstaller 是一个用于将 Python 应用程序转换为独立的可执行文件的工具,它能够将 Python 应用程序转换为 Windows 上、Linux 上和 Mac OS X 上的 exe 文件。
# 使用 cx_Freeze 进行打包 import sys from cx_Freeze import setup, Executable setup(name = "hello", version = "0.1", description = "Hello World", executables = [Executable("hello.py")])
作者:Tarek Ziadé,翻译:张吉
原文:http://www.aosabook.org/en/packaging.html
对于如何安装软件,目前有两种思想流派。第一种是说软件应该自给自足,不依赖于其它任何部件,这点在Windows和Mac OS X系统中很流行。这种方式简化了软件的管理:每个软件都有自己独立的“领域”,安装和卸载它们不会对操作系统产生影响。如果软件依赖一项不常见的类库,那么这个类库一定是包含在软件安装包之中的。
第二种流派,主要在类Linux的操作系统中盛行,即软件应该是由一个个独立的、小型的软件包组成的。类库被包含在软件包中,包与包之间可以有依赖关系。安装软件时需要查找和安装它所依赖的其他特定版本的软件包。这些依赖包通常是从一个包含所有软件包的中央仓库中获取的。这种理念也催生了Linux发行版中那些复杂的依赖管理工具,如dpkg
和RPM
。它们会跟踪软件包的依赖关系,并防止两个软件使用了版本相冲突的第三方包。
以上两种流派各有优劣。高度模块化的系统可以使得更新和替换某个软件包变的非常方便,因为每个类库都只有一份,所有依赖于它的应用程序都能因此受益。比如,修复某个类库的安全漏洞可以立刻应用到所有程序中,而如果应用程序使用了自带的类库,那安全更新就很难应用进去了,特别是在类库版本不一致的情况下更难处理。
不过这种“模块化”也被一些开发者视为缺点,因为他们无法控制应用程序的依赖关系。他们希望提供一个独立和稳定的软件运行环境,这样就不会在系统升级后遭遇各种依赖方面的问题。
在安装程序中包含所有依赖包还有一个优点:便于跨平台。有些项目在这点上做到了极致,它们将所有和操作系统的交互都封装了起来,在一个独立的目录中运行,甚至包括日志文件的记录位置。
Python的打包系统使用的是第二种设计思想,并尽可能地方便开发者、管理员、用户对软件的管理。不幸的是,这种方式导致了种种问题:错综复杂的版本结构、混乱的数据文件、难以重新打包等等。三年前,我和其他一些Python开发者决定研究解决这个问题,我们自称为“打包别动队”,本文就是讲述我们在这个问题上做出的努力和取得的成果。
在Python中, 包 表示一个包含Python文件的目录。Python文件被称为 模块 ,这样一来,使用“包”这个单词就显得有些模糊了,因为它常常用来表示某个项目的 发行版本 。
Python开发者有时也对此表示不能理解。为了更清晰地进行表述,我们用“Python包(package)”来表示一个包含Python文件的目录,用“发行版本(release)”来表示某个项目的特定版本,用“发布包(distribution)”来表示某个发行版本的源码或二进制文件,通常是Tar包或Zip文件的形式。
大多数Python开发者希望自己的程序能够在任何环境中运行。他们还希望自己的软件既能使用标准的Python类库,又能使用依赖于特定系统类型的类库。但除非开发者使用现有的各种打包工具生成不同的软件包,否则他们打出的软件安装包就必须在一个安装有Python环境的系统中运行。这样的软件包还希望做到以下几点:
要做到以上几点往往是不可能的。举例来说,Plone这一功能全面的CMS系统,使用了上百个纯Python语言编写的类库,而这些类库并不一定在所有的打包系统中提供。这就意味着Plone必须将它所依赖的软件包都集成到自己的安装包中。要做到这一点,他们选择使用zc.buildout
这一工具,它能够将所有的依赖包都收集起来,生成一个完整的应用程序文件,在独立的目录中运行。它事实上是一个二进制的软件包,因为所有C语言代码都已经编译好了。
这对开发者来说是福音:他们只需要描述好依赖关系,然后借助zc.buildout
来发布自己的程序即可。但正如上文所言,这种发布方式在系统层面构筑了一层屏障,这让大多数Linux系统管理员非常恼火。Windows管理员不会在乎这些,但CentOS和Debian管理员则会,因为按照他们的管理原则,系统中的所有文件都应该被注册和归类到现有的管理工具中。
这些管理员会想要将你的软件按照他们自己的标准重新打包。问题在于:Python有没有这样的打包工具,能够自动地按照新的标准重新打包?如果有,那么Python的任何软件和类库就能够针对不同的目标系统进行打包,而不需要额外的工作。这里,“自动”一词并不是说打包过程可以完全由脚本来完成——这点上RPM
和dpkg
的使用者已经证实是不可能的了,因为他们总会需要增加额外的信息来重新打包。他们还会告诉你,在重新打包的过程中会遇到一些开发者没有遵守基本打包原则的情况。
我们来举一个实际例子,如何通过使用现有的Python打包工具来惹恼那些想要重新打包的管理员:在发布一个名为“MathUtils”的软件包时使用“Fumanchu”这样的版本号名字。撰写这个类库的数学家想用自家猫咪的名字来作为版本号,但是管理员怎么可能知道“Fumanchu”是他家第二只猫的名字,第一只猫叫做“Phil”,所以“Fumanchu”版本要比“Phil”版本来得高?
可能这个例子有些极端,但是在现有的打包工具和规范中是可能发生的。最坏的情况是easy_install
和pip
使用自己的一套标准来追踪已安装的文件,并使用字母顺序来比较“Fumanchu”和“Phil”的版本高低。
另一个问题是如何处理数据文件。比如,如果你的软件使用了SQLite数据库,安装时被放置在包目录中,那么在程序运行时,系统会阻止你对其进行读写操作。这样做还会破坏Linux系统的一项惯例,即/var
目录下的数据文件是需要进行备份的。
在现实环境中,系统管理员需要能够将你的文件放置到他们想要的地方,并且不破坏程序的完整性,这就需要你来告诉他们各类文件都是做什么用的。让我们换一种方式来表述刚才的问题:Python是否有这样一种打包工具,它可以提供各类信息,足以让第三方打包工具能据此重新进行打包,而不需要阅读软件的源码?
Python标准库中提供的Distutils
打包工具充斥了上述的种种问题,但由于它是一种标准,所以人们要么继续忍受并使用它,或者转向更先进的工具Setuptools
,它在Distutils之上提供了一些高级特性。另外还有Distribute
,它是Setuptools
的衍生版本。Pip
则是一种更为高级的安装工具,它依赖于Setuptools
。
但是,这些工具都源自于Distutils
,并继承了它的种种问题。有人也想过要改进Distutils
本身,但是由于它的使用范围已经很广很广,任何小的改动都会对Python软件包的整个生态系统造成冲击。
所以,我们决定冻结Distutils
的代码,并开始研发Distutils2
,不去考虑向前兼容的问题。为了解释我们所做的改动,首先让我们近距离观察一下Distutils
。
Distutils
由一些命令组成,每条命令都是一个包含了run
方法的类,可以附加若干参数进行调用。Distutils
还提供了一个名为Distribution
的类,它包含了一些全局变量,可供其他命令使用。
当要使用Distutils
时,Python开发者需要在项目中添加一个模块,通常命名为setup.py
。这个模块会调用Distutils
的入口函数:setup
。这个函数有很多参数,这些参数会被Distribution
实例保存起来,供后续使用。下面这个例子中我们指定了一些常用的参数,如项目名称和版本,它所包含的模块等:
from distutils.core import setup
setup(name="MyProject", version="1.0", py_modules=["mycode.py"])
这个模块可以用来执行Distutils
的各种命令,如sdist
。这条命令会在dist
目录中创建一个源代码发布包:
$ python setup.py sdist
这个模块还可以执行install
命令:
$ python setup.py install
Distutils
还提供了一些其他命令:
upload
将发布包上传至在线仓库register
向在线仓库注册项目的基本信息,而不上传发布包bdist
创建二进制发布包bdist_msi
创建.msi
安装包,供Windows系统使用我们还可以使用其他一些命令来获取项目的基本信息。
所以在安装或获取应用程序信息时都是通过这个文件调用Distutils
实现的,如获取项目名称:
$ python setup.py --name
MyProject
setup.py
是一个项目的入口,可以通过它对项目进行构建、打包、发布、安装等操作。开发者通过这个函数的参数信息来描述自己的项目,并使用它进行各种打包任务。这个文件同样用于在目标系统中安装软件。
这里声明了对ldap
模块的依赖,这种依赖并没有实际效力,因为没有安装工具会保证这个模块真实存在。如果说Python代码中会使用类似Perl的require
关键字来定义依赖关系,那还有些作用,因为这时安装工具会检索PyPI上的信息并进行安装,其实这也就是CPAN的做法。但是对于Python来说,ldap
模块可以存在于任何项目之中,因为Distutils
是允许开发者发布一个包含多个模块的软件的,所以这里的元信息字段并无太大作用。
Metadata
的另一个缺点是,因为它是由Python脚本创建的,所以会根据脚本执行环境的不同而产生特定信息。比如,运行在Windows环境下的一个项目会在setup.py
文件中有以下描述:
from distutils.core import setup
setup(name="foo", version="1.0", requires=["win32com"])
这样配置相当于是默认该项目只会运行在Windows环境下,即使它可能提供了跨平台的方案。一种解决方法是根据不同的平台来指定requires
参数:
from distutils.core import setup
import sys
if sys.platform == "win32":
setup(name="foo", version="1.0", requires=["win32com"])
else:
setup(name="foo", version="1.0")
但这种做法往往会让事情更糟。要注意,这个脚本是用来将项目的源码包发布到PyPI上的,这样写就说明它向PyPI上传的Metadata
文件会因为该脚本运行环境的不同而不同。换句话说,这使得我们无法在元信息文件中看出这个项目依赖于特定的平台。
这样一来,客户端还可以根据域名的IP地址来决定连接最近的镜像服务器,或者在服务器发生故障时自动重连到新的地址。镜像协议本身要比rsync更复杂一些,因为我们需要保证下载统计量的准确性,并提供最基本的安全性保障。
镜像必须尽可能降低和主服务器之间的数据交换,要达到这个目的,就必须在PyPI的XML-RPC接口中加入changelog
信息,以保证只获取变化的内容。对于每个软件包“P”,镜像必须复制/simple/P/
和/serversig/P
这两组信息。
如果中央服务器中删除了一个软件包,它就必须删除所有和它有关的数据。为了检测软件包文件的变动,可以缓存文件的ETag信息,并通过If-None-Match
头来判断是否可以跳过传输过程。当同步完成后,镜像就将/last-modified
文件设置为当前的时间。
当用户在镜像中下载一个软件包时,镜像就需要将这个事件报告给中央服务器,继而广播给其他镜像服务器。这样就能保证下载工具在任意镜像都能获得正确的下载量统计信息。
统计信息以CSV文件的格式保存在中央服务器的stats
目录中,按照日和周分隔。每个镜像服务器需要提供一个local-stats
目录来存放它自己的统计信息。文件中保存了每个软件包的下载数量,以及它们的下载工具。中央服务器每天都会从镜像服务器中获取这些信息,将其合并到全局的stats
目录,这样就能保证镜像服务器中的local-stats
目录中的数据至少是每日更新的。
在分布式的镜像系统中,客户端需要能够验证镜像服务器的合法性。如果不这样做,就可能产生以下威胁:
对于第一种攻击,软件包的作者就需要使用自己的PGP密钥来对软件包进行加密,这样其他用户就能判断他所下载的软件包是来自可信任的作者的。镜像服务协议中只对第二种攻击做了预防,不过有些措施也可以预防拦截攻击。
中央服务器会在/serverkey
这个URL下提供一个DSA密钥,它是用opensll dsa-pubout
3生成的PEM格式的密钥。这个URL不能被镜像服务器收录,客户端必须从主服务器中获取这个serverkey密钥,或者使用PyPI客户端本身自带的密钥。镜像服务器也是需要下载这个密钥的,用来检测密钥是否有更新。
对于每个软件包,/serversig/package
中存放了它们的镜像签名。这是一个DSA签名,和URL/simple/package
包含的内容对等,采用DER格式,是SHA-1和DSA的结合4。
客户端从镜像服务器下载软件包时必须经过以下验证:
/simple
页面,计算它的SHA-1
哈希值。/serversig
,将它和第二步中生成的签名进行比对。/simple
页面中的内容对比)。在从中央服务器下载软件包时不需要进行上述验证,客户端也不应该进行验证,以减少计算量。
这些密钥大约每隔一年会被更新一次。镜像服务器需要重新获取所有的/serversig
页面内容,使用镜像服务的客户端也需要通过可靠的方式获取新密钥。一种做法是从https://pypi.python.org/serverkey
下载。为了检测拦截攻击,客户端需要通过CAC认证中心验证服务端的SSL证书。
上文提到的大多数改进方案都在Distutils2
中实现了。setup.py
文件已经退出历史舞台,取而代之的是setup.cfg
,一个类似.ini
类型的文件,它描述了项目的所有信息。这样做可以让打包人员方便地改变软件包的安装方式,而不需要接触Python语言。以下是一个配置文件的示例:
[metadata]
name = MPTools
version = 0.1
author = Tarek Ziade
author-email = tarek@mozilla.com
summary = Set of tools to build Mozilla Services apps
description-file = README
home-page = http://bitbucket.org/tarek/pypi2rpm
project-url: Repository, http://hg.mozilla.org/services/server-devtools
classifier = Development Status :: 3 - Alpha
License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)
[files]
packages =
mopytools
mopytools.tests
extra_files =
setup.py
README
build.py
_build.py
resources =
etc/mopytools.cfg {confdir}/mopytools
Distutils2
会将这个文件用作于:
META-1.2
格式的元信息,可以用作多种用途,如在PyPI上注册项目。sdist
。Distutils2
为基础的项目。Distutils2
还通过version
模块实现了VERSION
元信息。
对INSTALL-DB
元信息的实现会被包含在Python3.3的pkgutil
模块中。在过度版本中,它的功能会由Distutils2
完成。它所提供的API可以让我们浏览系统中已安装的项目。
以下是Distutils2
提供的核心功能:
要改变像Python打包系统这样庞大和复杂的架构必须通过谨慎地修改PEP标准来进行。据我所知,任何对PEP的修改和添加都要历经一年左右的时间。
社区中一直以来有个错误的做法:为了改善某个问题,就肆意扩展项目元信息,或是修改Python程序的安装方式,而不去尝试修订它所违背的PEP标准。
换句话说,根据你所使用的安装工具的不同,如Distutils
和Setuptools
,它们安装应用程序的方式就是不同的。这些工具的确解决了一些问题,但却会引发一连串的新问题。以操作系统的打包工具为例,管理员必须面对多个Python标准:官方文档所描述的标准,Setuptools
强加给大家的标准。
但是,Setuptools
能够有机会在实际环境中大范围地(在整个社区中)进行实验,创新的进度很快,得到的反馈信息也是无价的。我们可以据此撰写出更切合实际的PEP新标准。所以,很多时候我们需要能够察觉到某个第三方工具在为Python社区做出贡献,并应该起草一个新的PEP标准来解决它所提出的问题。
这个标题是援引Guido van Rossum的话,而事实上,Python的这种战争式的哲学也的确冲击了我们的努力成果。
Distutils
是Python标准库之一,将来Distutils2
也会成为标准库。一个被纳入标准库的项目很难再对其进行改造。虽然我们有正常的项目更新流程,即经过两个Python次版本就可以对某个API进行删改,但一旦某个API被发布,它必定会持续存在多年。
因此,对标准库中某个项目的一次修改并不是简单的bug修复,而很有可能影响整个生态系统。所以,当你需要进行重大更新时,就必须创建一个新的项目。
我之所以深有体会,就是因为在我对Distutils
进行了超过一年的修改后,还是不得不回滚所有的代码,开启一个新的Distutils2
项目。将来,如果我们的标准又一次发生了重大改变,很有可能会产生Distutils3
项目,除非未来某一天标准库会作为独立的项目发行。
要改变Python项目的打包方式,其过程是非常漫长的:Python的生态系统中包含了那么多的项目,它们都采用旧的打包工具管理,一定会遇到诸多阻力。(文中一些章节描述的问题,我们花费了好几年才达成共识,而不是我之前预想的几个月。)对于Python3,可能会花费数年的时间才能将所有的项目都迁移到新的标准中去。
这也是为什么我们做的任何修改都必须兼容旧的打包工具,这是Distutils2
编写过程中非常棘手的问题。
例如,一个以新标准进行打包的项目可能会依赖一个尚未采用新标准的其它项目,我们不能因此中断安装过程,并告知用户这是一个无法识别的依赖项。
举例来说,INSTALL-DB
元信息的实现中会包含那些用Distutils
、Pip
、Distribution
、或Setuptools
安装的项目。Distutils2
也会为那些使用Distutils
安装的项目生成新的元信息。
本文的部分章节直接摘自PEP文档,你可以在http://python.org
中找到原文:
在这里我想感谢所有为打包标准的制定做出贡献的人们,你可以在PEP中找到他们的名字。我还要特别感谢“打包别动队”的成员们。还要谢谢Alexis Metaireau、Toshio Kuratomi、Holger Krekel、以及Stefane Fermigier,感谢他们对本文提供的反馈。
本章中讨论的项目有: