2. 编写安装脚本

备注

这篇文档是历史遗留文档,在 https://setuptools.readthedocs.io/en/latest/setuptools.html 上的 setuptools 文档独立涵盖此处包含的所有相关信息之后,将不再单独作为正式文档保留。

安装脚本是使用 Distutils 进行构建、分发和安装模块的的入口。设置脚本的主要目的是向 Distutils 描述如何分发您的模块,以便对您的模块进行操作的各种命令正确运行。正如我们在上面的 一个简单的例子 部分中看到的,安装脚本主要包含对 setup() 的调用,并且模块开发人员提供给 Distutils 的大部分信息都通过关键字参数提供给 setup()

这是一个略微复杂一点的例子,我们将在接下来的几节中继续追踪它:Distutils 本身的安装脚本。 (请记住,虽然 Distutils 被包括在 Python 1.6 及之后的版本中,但它们也仍保存独立存在因此 Python 1.5.2 用户可以使用它们来安装其他模块分发版。 这里所演示的 Distutils 本身的安装脚本,是用来将该软件包安装到 Python 1.5.2 中。)

#!/usr/bin/env python

from distutils.core import setup

setup(name='Distutils',
      version='1.0',
      description='Python Distribution Utilities',
      author='Greg Ward',
      author_email='gward@python.net',
      url='https://www.python.org/sigs/distutils-sig/',
      packages=['distutils', 'distutils.command'],
     )

这里与 一个简单的例子 一节中介绍的最简版单文件分发包只有两处差异:更多的元数据,以及按包列出纯 Python 模块,而非按模块列出。 这一点很重要因为 Distutils 是由(目前)分成两个包的几十个模块组成的;显式地列出每个模块将十分繁琐并且难以维护。 有关额外元数据的更多信息,请参阅 附加元数据 一节。

请注意在 setup 脚本中提供的任何路径名称(文件或目录)都应当使用 Unix 约定来书写,即以斜杠分隔。 Distutils 会负责在实际使用该路径名称之前将这个与平台无关的表示形式转换为适用于你的当前平台的形式 。 这使得你的 setup 脚本可以在跨操作系统进行移植,这当然是 Distutils 的主要目标之一。 基于此精神,在本文档中的所有路径名称都是以斜杠分隔的。

当然,这仅适用于提供给 Distutils 函数的路径名称。 举例来说,如果你使用标准 Python 函数例如 glob.glob()os.listdir() 来指定文件,则你应当小心地编写可移植的代码而不是硬编码路径分隔符:

glob.glob(os.path.join('mydir', 'subdir', '*.html'))
os.listdir(os.path.join('mydir', 'subdir'))

2.1. 列出全部的包

packages 选项通知 Distutils 处理(构建、分发、安装等)在 packages 列表中提及的每个包中找到的所有纯 Python 模块。 当然,为做到这一点,包名称和文件系统的目录之间必须保持对应关系。 默认的对应关系是很显然的,即 distutils 包即相对于分发包根目录的 distutils 目录。 因此,当你在你的 setup 脚本中写 packages = ['foo'] 时,你就承诺了 Distutils 将在相对于你的 setup 脚本所在目录下找到一个文件 foo/__init__.py (在你的目录上可能会有不同的拼写形式,但意思已经很明白了)。 如果你违反此承诺,Distutils 将发出警告但无论如何仍会处理损坏的包。

如果你使用不同的约定来布局你的源目录,那也没有问题:你只需提供 package_dir 选项来告知 Distutils 你的约定。 例如,假设你将所有For example, say you keep all Python 源代码保存在 lib 下,以便“根包”中的模块(即不属于任何包的模块)位于 lib 中,foo 包中的模块位于 lib/foo 中,依此类推。 那么你应当编写:

package_dir = {'': 'lib'}

在你的 setup 脚本中。 这个字典的键是包名,空的包名代表根包。 字典的值是相对于你的发布包根目录的目录名。 在本例中,当你声明 packages = ['foo'] 时,你将承诺存在 lib/foo/__init__.py 这个文件。

另一种可能的约定是将 foo 包直接放在 lib 中,将 foo.bar 包放在 lib/bar 中等等。 这可以在 setup 脚本中写为

package_dir = {'foo': 'lib'}

package_dir 字典中的 package: dir 条目隐式地应用于 package 以下的所有包,因而在这里 foo.bar 的情况会被自动地处理。 在这个示例中,使用 packages = ['foo', 'foo.bar'] 告知 Distutils 要查找 lib/__init__.pylib/bar/__init__.py。 (请记住虽然 package_dir 会递归地应用,但你必须在 packages 中显式地列出所有的包:Distutils 不会 递归地扫描你的源代码树来查找任何带有 __init__.py 文件的目录。)

2.2. 列出单独的模块

对于一个小型的模块分发包,你可能会倾向于列出所有的模块而不是只列出包 --- 特别是在“根包”中只有一个模块(即根本不存在包)的情况下。 这种最简单的情况见 一个简单的例子 一节;下面是一个稍微复杂一点的例子:

py_modules = ['mod1', 'pkg.mod2']

这描述了两个模块,其中一个是在 "root" 包中,另一个是在 pkg 包中。 同样地,默认的包/目录布局表明这两个模块位于 mod1.pypkg/mod2.py 中,并且也存在 pkg/__init__.py。 同样地,你可以使用 package_dir 选项来重载包/目录的对应关系。

2.3. 描述扩展模块

正如编写Python扩展模块比编写纯Python模块要复杂一些一样,在Distutils中描述它们也要复杂一些。与纯模块不同,仅仅列出模块或包并期望Distutils出去找到正确的文件是不够的;您必须指定扩展名、源文件和任何编译/链接要求(包括要链接的目录、库等)。

所有这些都是通过 setup() 的另一个关键字参数 ext_modules 选项实现的。 ext_modules 是 由 Extension 实例组成的列表,每个实例描述一个扩展模块。 假设你的发行版包括一个扩展模块,名为 foo 并由 foo.c 实现。 如果不需要编译器/链接器的额外指令,描述这个扩展很简单:

Extension('foo', ['foo.c'])

Extension 类可以与 setup() 一起从 distutils.core 导入。 因此,只包含这一个扩展而不包含其他扩展的模块分发版的安装脚本可能是:

from distutils.core import setup, Extension
setup(name='foo',
      version='1.0',
      ext_modules=[Extension('foo', ['foo.c'])],
      )

Extension 类(实际上,由 build_ext 命令实现的底层扩展构建机制)在描述 Python 扩展时支持很大的灵活性,这将在以下部分中进行解释。

2.3.1. 扩展名和软件包

Extension 构造器的第一个参数始终是扩展的名称,包括任何包名称。 例如,:

Extension('foo', ['src/foo1.c', 'src/foo2.c'])

描述了根包中的一个扩展,而::

Extension('pkg.foo', ['src/foo1.c', 'src/foo2.c'])

描述了 pkg 包中的相同扩展。 在这两种情况下,源文件和生成的目标代码是相同的;唯一的区别是所产生的扩展处在文件系统中的何处(相应地在 Python 的命名空间层级结构中处在何处)。

如果在同一个包中(或在同一基本包下)有多个扩展,请使用 setup() 的关键字参数 ext_package。 例如,:

setup(...,
      ext_package='pkg',
      ext_modules=[Extension('foo', ['foo.c']),
                   Extension('subpkg.bar', ['bar.c'])],
     )

将把 foo.c 编译为扩展 pkg.foo,把 bar.c 编译为 pkg.subpkg.bar

2.3.2. 扩展的源文件

Extension 构造器的第二个参数是源文件列表。 由于 Distutils 目前只支持 C、C++ 和 Objective-C 扩展,因此这些扩展通常是 C/C++/Objective-C 源文件。 (请确保使用适当的扩展名来区分 C++ 源文件: .cc.cpp 看来能够同时被 Unix 和 Windows 编译器所识别。)

但是,您也可以在列表中包括 SWIG 接口 (.i) 文件;build_ext 命令知道如何处理 SWIG 扩展:它将在接口文件上运行 SWIG,并将生成的 C/C++ 文件编译为你的扩展。

尽管有此警告,SWIG的选项目前可以这样传递

setup(...,
      ext_modules=[Extension('_foo', ['foo.i'],
                             swig_opts=['-modern', '-I../include'])],
      py_modules=['foo'],
     )

或者在命令行上,像这样

> python setup.py build_ext --swig-opts="-modern -I../include"

在某些平台上,可以包含由编译器处理并包含在扩展中的非源文件。目前,这只是指Visual C++的Windows消息文本(.mc)文件和资源定义(.rc)。这些文件将被编译为二进制资源(.res)文件,并链接到可执行文件中。

2.3.3. 预处理器选项

如果你需要指定要搜索的include目录或预处理器宏来define/undefine,那么:Extension`的三个可选参数将有所帮助:``include_dirs`define_macros``和``undef_macros

例如,如果你的扩展需要分发根目录下的 include 目录中的头文件,请使用 include_dirs 选项:

Extension('foo', ['foo.c'], include_dirs=['include'])

你可以在那里指定绝对目录;如果你知道你的扩展只能在安装到 /usr 的 X11R6 的 Unix 系统上构建,那么你就可以:

Extension('foo', ['foo.c'], include_dirs=['/usr/include/X11'])

如果你计划分发代码,应该避免这种不可移植的用法:最好像这样编写C代码

#include <X11/Xlib.h>

如果你需要包含来自其它 Python 扩展的头文件,你可以利用这样一个事实,即 Distutils install_headers 命令以一致的方式安装头文件。 例如,Numerical Python 头文件 (在标准 Unix 安装版上) 会安装到 /usr/local/include/python1.5/Numerical (具体位置将根据你的平台和 Python 安装版而有所不同)。 由于在构建Python 扩展时,Python include 目录 --- 在本例中为 /usr/local/include/python1.5 --- 总是会包含在搜索路径中,因此最好的做法是像这样编写 C 代码:

#include <Numerical/arrayobject.h>

但是,如果必须将 Numerical include 目录放在头文件搜索路径中,则可以使用 Distutils distutils.sysconfig 模块找到该目录:

from distutils.sysconfig import get_python_inc
incdir = os.path.join(get_python_inc(plat_specific=1), 'Numerical')
setup(...,
      Extension(..., include_dirs=[incdir]),
      )

尽管这是非常可移植的——无论平台如何,它都可以在任何Python安装中工作——但以合理的方式编写C代码可能更容易。

您可以使用``define_macros``和``undef_macros``选项 define 和 undefine 预处理器宏。define_macros``采用一个``(name, value)``元组列表,其中``name``是要定义的宏的名称(字符串),``value``是其值:字符串或“``None。(将宏``FOO``定义为``None``相当于C源代码中的空``#define FOO``:对于大多数编译器,这会将``FOO``设置为字符串``1``。)``undef_macros``只是一个undefine的宏列表。

例如:

Extension(...,
          define_macros=[('NDEBUG', '1'),
                         ('HAVE_STRFTIME', None)],
          undef_macros=['HAVE_FOO', 'HAVE_BAR'])

相当于将其放在每个C源文件的顶部

#define NDEBUG 1
#define HAVE_STRFTIME
#undef HAVE_FOO
#undef HAVE_BAR

2.3.4. 库选项

你还可以指定在构建扩展时要链接的库,以及搜索这些库的目录。 libraries 选项是要链接的库列表,library_dirs 是要在链接时搜索库的目录列表,runtime_library_dirs 是要在运行时搜索共享(动态加载)库的目录的列表。

例如,如果你需要链接到目标系统上标准库搜索路径中已知的库

Extension(...,
          libraries=['gdbm', 'readline'])

如果你需要链接到非标准位置的库,则必须将该位置包含在 library_dirs 中:

Extension(...,
          library_dirs=['/usr/X11R6/lib'],
          libraries=['X11', 'Xt'])

(同样,如果你打算分发代码,应该避免这种不可移植的构造。)

2.3.5. 其它选项

还有一些其它选项可以用于处理特殊情况。

optional 选项是一个布尔值;如果它为真值,则扩展中的生成失败不会中止生成过程,而是简单地不安装失败的扩展。

extra_objects 选项是要传递给链接器的对象文件列表。 这些文件不能有扩展名,因为使用的是编译器的默认扩展名。

extra_compile_argsextra_link_args 可用于为相应的编译器和链接器命令行指定额外的命令行选项。

export_symbols 仅在 Windows 上适用。 它可以包含要导出的符号(函数或变量)列表。 生成编译的扩展时不需要此选项:Distutils 会自动将 initmodule 添加到导出的符号列表中。

depends 选项是扩展所依赖的文件列表(例如头文件)。 build 命令将调用源上的编译器以重新生成扩展(如果自上一次生成以来此文件上的任何扩展已被修改)。

2.4. 分发和软件包之间的关系

一个分发可能以三种特定方式与软件包相关:

  1. 它可能需要软件包或模块。

  2. 它可能提供软件包或模块。

  3. 它可能废弃软件包或模块。

可以使用 distutils.core.setup() 函数的关键字参数来指定这些关系。

可以通过将 requires 关键字参数提供给 setup() 来指定对其他 Python 模块和包的依赖关系。 其值必须是字符串列表。 每个字符串都指定了所需的软件包,并可以选择哪些版本满足要求。

要指定需要模块或软件包的任何版本,字符串应该完全由模块或软件包的名称组成。 示例包括 'mymodule''xml.parsers.expat'

如果需要特定的版本,可以在括号中提供一系列限定符。每个限定符可以由一个比较运算符和一个版本号组成。可接受的比较运算符为:

<    >    ==
<=   >=   !=

这些可以通过使用逗号分隔的多个限定符(以及可选的空格)进行组合。在这种情况下,所有限定符都必须匹配;使用逻辑AND来组合评估。

让我们看一堆例子:

Requires 表达式

说明

==1.0

只有``1.0``版本兼容

>1.0, !=1.5.1, <2.0

1.0 之后和 2.0 之前的任何版本都是兼容的,1.5.1 除外

既然我们可以指定依赖项,我们还需要能够指定我们提供的其它发行版可能需要的内容。这是通过将 provides 关键字参数用于 setup() 来完成的。 该关键字的值是一个字符串列表,每个字符串都命名一个Python模块或软件包,并可选择标识版本。 如果未指定版本,则假定与该发行版的版本相匹配。

一些例子:

Provides 表达式

说明

mypkg

Provide mypkg, 使用该分发版的版本

mypkg (1.1)

提供版本为 1.1的``mypkg``,不管分发版的版本

一个软件包可以使用 obsoletes 关键字参数声明它已经废弃了其它软件包。它的值类似于 requires 关键字的值:一个给出模块或软件包说明符的字符串列表。每个说明符由一个模块或软件包名称组成,后面可选地跟有一个或多个版本限定符。版本限定符在模块或软件包名称后面的括号中给出。

限定符标识的版本是那些被所描述的分发版本废弃的版本。如果没有给出限定符,那么指定的模块或软件包的所有版本都被认为是废弃的。

2.5. 安装脚本

到目前为止,我们一直在处理纯Python模块和非纯Python模块,它们通常不是自己运行的,而是通过脚本导入的。

脚本是包含Python源代码的文件,旨在从命令行启动。脚本不需要Distutils来做任何非常复杂的事情。唯一聪明的功能是,如果脚本的第一行以``#!``开头并且包含单词“python”,Distutils将调整第一行以引用当前解释器位置。默认情况下,它被替换为当前解释器位置。选项:option:!--executable`(或:option:!-e`)将允许显式覆盖解释器路径。

scripts 选项只是要以这种方式处理的文件列表。 从 PyXML 安装脚本

setup(...,
      scripts=['scripts/xmlproc_parse', 'scripts/xmlproc_val']
      )

在 3.1 版本发生变更: 如果没有提供模板,所有脚本也将被添加到 MANIFEST 文件中。请参见 指定要分发的文件

2.6. 安装软件包数据

通常,需要将额外的文件安装到软件包中。这些文件通常是与软件包的实现密切相关的数据,或者是包含使用软件包的程序员可能感兴趣的文档的文本文件。这些文件被称为 package data

可以使用 setup() 函数的关键字参数 package_data 将软件包数据添加到软件包中。 该值必须是从软件包名到应该复制到软件包中的相对路径名列表的映射。 路径被解释为相对于包含软件包的目录(如果合适,将使用来自 package_dir 映射的信息);也就是说,这些文件应该是软件包源目录中的一部分。 它们也可能包含 glob 模式。

路径名称可以包含目录部分;任何必要的目录都将在安装过程中创建。

例如,如果一个软件包应该包含一个有多个数据文件的子目录,那么这些文件可以在源树中按如下方式排列

setup.py
src/
    mypkg/
        __init__.py
        module.py
        data/
            tables.dat
            spoons.dat
            forks.dat

setup() 的相应调用可能是

setup(...,
      packages=['mypkg'],
      package_dir={'mypkg': 'src/mypkg'},
      package_data={'mypkg': ['data/*.dat']},
      )

在 3.1 版本发生变更: 如果没有提供模板,则所有与 package_data 匹配的文件都将添加到 MANIFEST 文件中。 请参见 指定要分发的文件

2.7. 安装其它文件

data_files 选项可用于指定模块分发所需的其它文件:配置文件、消息目录、数据文件,以及任何不属于前述类别的文件。

data_files 以如下方式指定 (directory, files) 对的序列:

setup(...,
      data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
                  ('config', ['cfg/data.cfg'])],
     )

序列中的每队(directoryfiles)指定安装目录和要在其中安装的文件。

files 中的每个文件名都是相对于软件包源发行版顶部的 setup.py 脚本进行解释的。 请注意,您可以指定安装数据文件的目录,但不能重命名数据文件本身。

directory 应该是一个相对路径。它是相对于安装前缀来解释的(Python 的 sys.prefix 用于系统安装;site.USER_BASE 用于用户安装)。 Distutils 允许将 directory 作为一个绝对的安装路径,但这是不鼓励的,因为它与 wheel 封装格式不兼容。不使用来自 files 的目录信息来确定已安装文件的最终位置;只使用文件的名称。

你可以将 data_files 选项指定为一个简单的文件序列,而无需指定目标目录,但不建议这样做,在这种情况下,install 命令将打印警告。 要将数据文件直接安装到目标目录中,应提供一个空字符串作为目录。

在 3.1 版本发生变更: 如果没有提供模板,则所有与 data_files 匹配的文件都将添加到 MANIFEST 文件中。 请参见 指定要分发的文件

2.8. 附加元数据

安装脚本可以包括除了名称和版本之外的附加元数据。这些信息包括:

元数据

描述

备注

name

包名称

短字符串

(1)

version

此发布的版本

短字符串

(1)(2)

author

软件包作者的姓名

短字符串

(3)

author_email

软件包的作者的电子邮件地址

电子邮件地址

(3)

maintainer

软件包维护者的名字

短字符串

(3)

maintainer_email

软件包维护者的电子邮件地址

电子邮件地址

(3)

url

软件包的网址

网址

(1)

description

软件包的简短摘要说明

短字符串

long_description

软件包的详细说明

长字符串

(4)

download_url

可以下载软件包的网址

网址

classifiers

分类列表

字符串列表

(6)(7)

platforms

平台清单

字符串列表

(6)(8)

keywords

关键字列表

字符串列表

(6)(8)

license

软件包许可证

短字符串

(5)

注释:

  1. 这些字段是必需的。

  2. 建议版本采用 major.minor[.patch[.sub]] 的形式。

  3. 必须确定作者或维护者。如果提供了维护者,distutils 会在 PKG-INFO 中将其列为作者。

  4. PyPI在发布软件包时使用``long_description``字段来构建其项目页面。

  5. license 字段是一个文本,指示软件包中的许可证,其中该许可证不是从 “license” Trove分类器中选择的。请参见 Classifier 字段。 请注意,有一个 licence 分发选项,该选项已被弃用,但仍充当 license 字段的别名。

  6. 此字段必须是一个列表。

  7. 有效的分类器在 PyPI 上列出。

  8. 为了保持向后兼容性,此字段还接受一个字符串。如果传递逗号分隔的字符串``'foo, bar',它将被转换为``['foo', 'bar'],否则,它将转换为一个字符串列表。

'短字符串'

一行文字,不超过200个字符。

'长字符串'

reStructuredText格式的多行纯文本(请参阅https://docutils.sourceforge.io/).

'字符串列表'

请参见下文。

对版本信息进行编码本身就是一门艺术。Python包通常遵循版本格式 major.minor[.patch][sub] 。初始的、实验性的软件版本的主数字是0。对于表示包中主要里程碑的版本,主版本会递增。当重要的新特性添加到包中时,次版本编号会增加。当发布错误修复程序时,补丁编号会增加。附加的尾随版本信息有时用于指示子版本。这些是“a1,a2,…,aN”(对于alpha版本,功能和API可能会更改),“b1,b2,…,bN”(针对beta版本,它只修复了错误)和“pr1,pr2,……,prN”(用于最终的预发布测试)。一些例子:

0.1.0

一个软件包的第一个实验性发布

1.0.1a2

1.0的第一个补丁版本的第二个alpha版本

classifiers 必须在列表中指定

setup(...,
      classifiers=[
          'Development Status :: 4 - Beta',
          'Environment :: Console',
          'Environment :: Web Environment',
          'Intended Audience :: End Users/Desktop',
          'Intended Audience :: Developers',
          'Intended Audience :: System Administrators',
          'License :: OSI Approved :: Python Software Foundation License',
          'Operating System :: MacOS :: MacOS X',
          'Operating System :: Microsoft :: Windows',
          'Operating System :: POSIX',
          'Programming Language :: Python',
          'Topic :: Communications :: Email',
          'Topic :: Office/Business',
          'Topic :: Software Development :: Bug Tracking',
          ],
      )

在 3.7 版本发生变更: 现在 setupclassifierskeywordsplatforms 字段未指定为列表或字符串时发出警告。

2.9. 调试安装脚本

有时会出现问题,并且安装脚本不能执行开发人员想要的操作。

Distutils在运行安装脚本时捕获任何异常,并在脚本终止前打印一条简单的错误消息。这种行为的动机是为了不让那些对Python不太了解并试图安装软件包的管理员感到困惑。如果他们从Distutils的内部深处得到了一个很长的追溯,他们可能会认为这个软件包或Python安装坏了,因为他们没有一直读到底部,看到这是一个权限问题。

另一方面,这并不能帮助开发人员找到失败的原因。 为此,DISTUTILS_DEBUG 环境变量可以设置为除空字符串之外的任何值,distutils 现在将打印有关其正在执行的操作的详细信息,在发生异常时转储完整的回溯,并在外部程序(如 C 编译器)失败时打印整个命令行。