卷1:第14章 Python打包工具

2018-02-24 15:55 更新

作者:Tarek Ziadé,翻译:张吉

原文:http://www.aosabook.org/en/packaging.html

14.1 简介

对于如何安装软件,目前有两种思想流派。第一种是说软件应该自给自足,不依赖于其它任何部件,这点在Windows和Mac OS X系统中很流行。这种方式简化了软件的管理:每个软件都有自己独立的“领域”,安装和卸载它们不会对操作系统产生影响。如果软件依赖一项不常见的类库,那么这个类库一定是包含在软件安装包之中的。

第二种流派,主要在类Linux的操作系统中盛行,即软件应该是由一个个独立的、小型的软件包组成的。类库被包含在软件包中,包与包之间可以有依赖关系。安装软件时需要查找和安装它所依赖的其他特定版本的软件包。这些依赖包通常是从一个包含所有软件包的中央仓库中获取的。这种理念也催生了Linux发行版中那些复杂的依赖管理工具,如dpkgRPM。它们会跟踪软件包的依赖关系,并防止两个软件使用了版本相冲突的第三方包。

以上两种流派各有优劣。高度模块化的系统可以使得更新和替换某个软件包变的非常方便,因为每个类库都只有一份,所有依赖于它的应用程序都能因此受益。比如,修复某个类库的安全漏洞可以立刻应用到所有程序中,而如果应用程序使用了自带的类库,那安全更新就很难应用进去了,特别是在类库版本不一致的情况下更难处理。

不过这种“模块化”也被一些开发者视为缺点,因为他们无法控制应用程序的依赖关系。他们希望提供一个独立和稳定的软件运行环境,这样就不会在系统升级后遭遇各种依赖方面的问题。

在安装程序中包含所有依赖包还有一个优点:便于跨平台。有些项目在这点上做到了极致,它们将所有和操作系统的交互都封装了起来,在一个独立的目录中运行,甚至包括日志文件的记录位置。

Python的打包系统使用的是第二种设计思想,并尽可能地方便开发者、管理员、用户对软件的管理。不幸的是,这种方式导致了种种问题:错综复杂的版本结构、混乱的数据文件、难以重新打包等等。三年前,我和其他一些Python开发者决定研究解决这个问题,我们自称为“打包别动队”,本文就是讲述我们在这个问题上做出的努力和取得的成果。

术语

在Python中,  表示一个包含Python文件的目录。Python文件被称为 模块 ,这样一来,使用“包”这个单词就显得有些模糊了,因为它常常用来表示某个项目的 发行版本 。

Python开发者有时也对此表示不能理解。为了更清晰地进行表述,我们用“Python包(package)”来表示一个包含Python文件的目录,用“发行版本(release)”来表示某个项目的特定版本,用“发布包(distribution)”来表示某个发行版本的源码或二进制文件,通常是Tar包或Zip文件的形式。

14.2 Python开发者的困境

大多数Python开发者希望自己的程序能够在任何环境中运行。他们还希望自己的软件既能使用标准的Python类库,又能使用依赖于特定系统类型的类库。但除非开发者使用现有的各种打包工具生成不同的软件包,否则他们打出的软件安装包就必须在一个安装有Python环境的系统中运行。这样的软件包还希望做到以下几点:

  • 其他人可以针对不同的目标系统对这个软件重新打包;
  • 软件所依赖的包也能够针对不同的目标系统进行重新打包;
  • 系统依赖项能够被清晰地描述出来。

要做到以上几点往往是不可能的。举例来说,Plone这一功能全面的CMS系统,使用了上百个纯Python语言编写的类库,而这些类库并不一定在所有的打包系统中提供。这就意味着Plone必须将它所依赖的软件包都集成到自己的安装包中。要做到这一点,他们选择使用zc.buildout这一工具,它能够将所有的依赖包都收集起来,生成一个完整的应用程序文件,在独立的目录中运行。它事实上是一个二进制的软件包,因为所有C语言代码都已经编译好了。

这对开发者来说是福音:他们只需要描述好依赖关系,然后借助zc.buildout来发布自己的程序即可。但正如上文所言,这种发布方式在系统层面构筑了一层屏障,这让大多数Linux系统管理员非常恼火。Windows管理员不会在乎这些,但CentOS和Debian管理员则会,因为按照他们的管理原则,系统中的所有文件都应该被注册和归类到现有的管理工具中。

这些管理员会想要将你的软件按照他们自己的标准重新打包。问题在于:Python有没有这样的打包工具,能够自动地按照新的标准重新打包?如果有,那么Python的任何软件和类库就能够针对不同的目标系统进行打包,而不需要额外的工作。这里,“自动”一词并不是说打包过程可以完全由脚本来完成——这点上RPMdpkg的使用者已经证实是不可能的了,因为他们总会需要增加额外的信息来重新打包。他们还会告诉你,在重新打包的过程中会遇到一些开发者没有遵守基本打包原则的情况。

我们来举一个实际例子,如何通过使用现有的Python打包工具来惹恼那些想要重新打包的管理员:在发布一个名为“MathUtils”的软件包时使用“Fumanchu”这样的版本号名字。撰写这个类库的数学家想用自家猫咪的名字来作为版本号,但是管理员怎么可能知道“Fumanchu”是他家第二只猫的名字,第一只猫叫做“Phil”,所以“Fumanchu”版本要比“Phil”版本来得高?

可能这个例子有些极端,但是在现有的打包工具和规范中是可能发生的。最坏的情况是easy_installpip使用自己的一套标准来追踪已安装的文件,并使用字母顺序来比较“Fumanchu”和“Phil”的版本高低。

另一个问题是如何处理数据文件。比如,如果你的软件使用了SQLite数据库,安装时被放置在包目录中,那么在程序运行时,系统会阻止你对其进行读写操作。这样做还会破坏Linux系统的一项惯例,即/var目录下的数据文件是需要进行备份的。

在现实环境中,系统管理员需要能够将你的文件放置到他们想要的地方,并且不破坏程序的完整性,这就需要你来告诉他们各类文件都是做什么用的。让我们换一种方式来表述刚才的问题:Python是否有这样一种打包工具,它可以提供各类信息,足以让第三方打包工具能据此重新进行打包,而不需要阅读软件的源码?

14.3 现有的打包管理架构

Python标准库中提供的Distutils打包工具充斥了上述的种种问题,但由于它是一种标准,所以人们要么继续忍受并使用它,或者转向更先进的工具Setuptools,它在Distutils之上提供了一些高级特性。另外还有Distribute,它是Setuptools的衍生版本。Pip则是一种更为高级的安装工具,它依赖于Setuptools

但是,这些工具都源自于Distutils,并继承了它的种种问题。有人也想过要改进Distutils本身,但是由于它的使用范围已经很广很广,任何小的改动都会对Python软件包的整个生态系统造成冲击。

所以,我们决定冻结Distutils的代码,并开始研发Distutils2,不去考虑向前兼容的问题。为了解释我们所做的改动,首先让我们近距离观察一下Distutils

14.3.1 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是一个项目的入口,可以通过它对项目进行构建、打包、发布、安装等操作。开发者通过这个函数的参数信息来描述自己的项目,并使用它进行各种打包任务。这个文件同样用于在目标系统中安装软件。

图14.2:PyPI仓库

你可以通过Classifies(类别)来浏览,获取项目作者的名字和主页。同时,Requires可以用来定义Python模块的依赖关系。requires选项可以向元信息文件的Requires字段添加信息:

from distutils.core import setup

setup(name='foo', version='1.0', requires=['ldap'])

这里声明了对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文件会因为该脚本运行环境的不同而不同。换句话说,这使得我们无法在元信息文件中看出这个项目依赖于特定的平台。

14.3.3 PyPI的架构设计

http://python.org/peps/pep-0214.html

  • PEP 314: Metadata for Python Software Packages 1.1: http://python.org/peps/pep-0314.html
  • PEP 345: Metadata for Python Software Packages 1.2: http://python.org/peps/pep-0345.html
  • PEP 376: Database of Installed Python Distributions: http://python.org/peps/pep-0376.html
  • PEP 381: Mirroring infrastructure for PyPI: http://python.org/peps/pep-0381.html
  • PEP 386: Changing the version comparison module in Distutils: http://python.org/peps/pep-0386.html
  • 在这里我想感谢所有为打包标准的制定做出贡献的人们,你可以在PEP中找到他们的名字。我还要特别感谢“打包别动队”的成员们。还要谢谢Alexis Metaireau、Toshio Kuratomi、Holger Krekel、以及Stefane Fermigier,感谢他们对本文提供的反馈。

    本章中讨论的项目有:

    脚注

    1. 文中引用的Python改进提案(Python Enhancement Proposals,简称PEP)会在本文最后一节整理。
    2. 过去被命名为CheeseShop
    3. 即RFC 3280 SubjectPublishKeyInfo中定义的1.3.14.3.2.12算法。
    4. 即RFC 3279 Dsa-Sig-Value中定义的1.2.840.10040.4.3算法。
    以上内容是否对您有帮助:
    在线笔记
    App下载
    App下载

    扫描二维码

    下载编程狮App

    公众号
    微信公众号

    编程狮公众号