zipapp —— 管理可执行的 Python zip 打包文件

3.5 新版功能.

源代码: Lib/zipapp.py


本模块提供了一套管理工具,用于创建包含 Python 代码的压缩文件,这些文件可以 直接由 Python 解释器执行。 本模块提供 命令行界面Python API

简单示例

下述例子展示了用 命令行界面 根据含有 Python 代码的目录创建一个可执行的打包文件。 运行后该打包文件时,将会执行 myapp 模块中的 main 函数。

$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>

命令行界面

若要从命令行调用,则采用以下形式:

$ python -m zipapp source [options]

如果 source 是个目录,将根据 source 的内容创建一个打包文件。如果 source 是个文件,则应为一个打包文件,将会复制到目标打包文件中(如果指定了 -info 选项,将会显示 shebang 行的内容)。

可以接受以下参数:

-o <output>, --output=<output>

将程序的输出写入名为 output 的文件中。若未指定此参数,输出的文件名将与输入的 source 相同,并添加扩展名 .pyz。如果显式给出了文件名,将会原样使用(因此必要时应包含扩展名 .pyz)。

如果 source 是个打包文件,必须指定一个输出文件名(这时 output 必须与 source 不同)。

-p <interpreter>, --python=<interpreter>

给打包文件加入 #! 行,以便指定 解释器 作为运行的命令行。另外,还让打包文件在 POSIX 平台上可执行。默认不会写入 #! 行,也不让文件可执行。

-m <mainfn>, --main=<mainfn>

在打包文件中写入一个 __main__.py 文件,用于执行 mainfnmainfn 参数的形式应为 “pkg.mod:fn”,其中 “pkg.mod”是打包文件中的某个包/模块,“fn”是该模块中的一个可调用对象。__main__.py 文件将会执行该可调用对象。

在复制打包文件时,不能设置 --main 参数。

-c, --compress

利用 deflate 方法压缩文件,减少输出文件的大小。默认情况下,打包文件中的文件是不压缩的。

在复制打包文件时,--compress 无效。

3.7 新版功能.

--info

显示嵌入在打包文件中的解释器程序,以便诊断问题。这时会忽略其他所有参数,SOURCE 必须是个打包文件,而不是目录。

-h, --help

打印简短的用法信息并退出。

Python API

该模块定义了两个快捷函数:

zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)

source 创建一个应用程序打包文件。source 可以是以下形式之一:

  • 一个目录名,或指向目录的 path-like object ,这时将根据目录内容新建一个应用程序打包文件。

  • 一个已存在的应用程序打包文件名,或指向这类文件的 path-like object,这时会将该文件复制为目标文件(会稍作修改以反映出 interpreter 参数的值)。必要时文件名中应包括 .pyz 扩展名。

  • 一个以字节串模式打开的文件对象。该文件的内容应为应用程序打包文件,且假定文件对象定位于打包文件的初始位置。

target 参数定义了打包文件的写入位置:

  • 若是个文件名,或是 path-like object,打包文件将写入该文件中。

  • 若是个打开的文件对象,打包文件将写入该对象,该文件对象必须在字节串写入模式下打开。

  • 如果省略了 target (或为 None),则 source 必须为一个目录,target 将是与 source 同名的文件,并加上 .pyz 扩展名。

参数 interpreter 指定了 Python 解释器程序名,用于执行打包文件。这将以 “释伴(shebang)”行的形式写入打包文件的头部。在 POSIX 平台上,操作系统会进行解释,而在 Windows 平台则会由 Python 启动器进行处理。省略 interpreter 参数则不会写入释伴行。如果指定了解释器,且目标为文件名,则会设置目标文件的可执行属性位。

参数 main 指定某个可调用程序的名称,用作打包文件的主程序。仅当 source 为目录且不含 __main__.py 文件时,才能指定该参数。main 参数应采用 “pkg.module:callable”的形式,通过导入“pkg.module”并不带参数地执行给出的可调用对象,即可执行打包文件。如果 source 是目录且不含``__main__.py`` 文件,省略 main 将会出错,生成的打包文件将无法执行。

可选参数 filter 指定了回调函数,将传给代表被添加文件路径的 Path 对象(相对于源目录)。在文件添加完成后,应返回 True

可选参数 compressed 指定是否要压缩打包文件。若设为 True,则打包中的文件将用 deflate 方法进行压缩;否则就不会压缩。本参数在复制现有打包文件时无效。

sourcetarget 指定的是文件对象,则调用者有责任在调用 create_archive 之后关闭文件。

当复制已有的打包文件时,提供的文件对象只需 readreadline 方法,或 write 方法。当由目录创建打包文件时,若目标为文件对象,将会将其传给 类,且必须提供 zipfile.ZipFile 类所需的方法。

3.7 新版功能: 加入了 filtercompressed 参数。

zipapp.get_interpreter(archive)

返回打包文件开头的 行指定的解释器程序。如果没有 #! 行,则返回 None。参数 archive 可为文件名或在字节串模式下打开以供读取的文件类对象。#! 行假定是在打包文件的开头。

例子

将目录打包成一个文件并运行它。

$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>

同样还可用 create_archive() 函数完成:

>>> import zipapp
>>> zipapp.create_archive('myapp', 'myapp.pyz')

要让应用程序能在 POSIX 平台上直接执行,需要指定所用的解释器。

$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>

若要替换已有打包文件中的释伴行,请用 create_archive() 函数另建一个修改好的打包文件:

>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')

若要原地更新打包文件,可用 BytesIO 对象在内存中进行替换,然后再覆盖源文件。注意,原地覆盖文件会有风险,出错时会丢失原文件。这里没有考虑出错情况,但生产代码则应进行处理。另外,这种方案仅当内存足以容纳打包文件时才有意义:

>>> import zipapp
>>> import io
>>> temp = io.BytesIO()
>>> zipapp.create_archive('myapp.pyz', temp, '/usr/bin/python2')
>>> with open('myapp.pyz', 'wb') as f:
>>>     f.write(temp.getvalue())

指定解释器程序

注意,如果指定了解释器程序再发布应用程序打包文件,需要确保所用到的解释器是可移植的。Windows 的 Python 启动器支持大多数常见的 POSIX #! 行,但还需要考虑一些其他问题。

  • 如果采用“/usr/bin/env python”(或其他格式的 python 调用命令,比如“/usr/bin/python”),需要考虑默认版本既可能是 Python 2 又可能是 Python 3,应让代码在两个版本下均能正常运行。

  • 如果用到的 Python 版本明确,如“/usr/bin/env python3”,则没有该版本的用户将无法运行应用程序。(如果代码不兼容 Python 2,可能正该如此)。

  • 因为无法指定“python X.Y以上版本”,所以应小心“/usr/bin/env python3.4”这种精确版本的指定方式,因为对于 Python 3.5 的用户就得修改释伴行,比如:

通常应该用“/usr/bin/env python2”或“/usr/bin/env python3”的格式,具体根据代码适用于 Python 2 还是 3 而定。

用 zipapp 创建独立运行的应用程序

利用 zipapp 模块可以创建独立运行的 Python 程序,以便向最终用户发布,仅需在系统中装有合适版本的 Python 即可运行。操作的关键就是把应用程序代码和所有依赖项一起放入打包文件中。

创建独立运行打包文件的步骤如下:

  1. 照常在某个目录中创建应用程序,于是会有一个 myapp 目录,里面有个``__main__.py`` 文件,以及所有支持性代码。

  2. 用 pip 将应用程序的所有依赖项装入 myapp 目录。

    $ python -m pip install -r requirements.txt --target myapp
    

    (这里假定在 requirements.txt 文件中列出了项目所需的依赖项,也可以在 pip 命令行中列出依赖项)。

  3. pip 在 myapp 中创建的 .dist-info 目录,是可以删除的。这些目录保存了 pip 用于管理包的元数据,由于接下来不会再用到 pip,所以不是必须存在,当然留下来也不会有什么坏处。

  4. 用以下命令打包:

    $ python -m zipapp -p "interpreter" myapp
    

这会生成一个独立的可执行文件,可在任何装有合适解释器的机器上运行。详情参见 指定解释器程序。可以单个文件的形式分发给用户。

在 Unix 系统中,myapp.pyz 文件将以原有文件名执行。如果喜欢 “普通”的命令名,可以重命名该文件,去掉扩展名 .pyz 。在 Windows 系统中,myapp.pyz[w] 是可执行文件,因为 Python 解释器在安装时注册了扩展名``.pyz`` 和 .pyzw

制作 Windows 可执行文件

在 Windows 系统中,可能没有注册扩展名 .pyz,另外有些场合无法“透明”地识别已注册的扩展(最简单的例子是,subprocess.run(['myapp']) 就找不到——需要明确指定扩展名)。

因此,在 Windows 系统中,通常最好 由zipapp 创建一个可执行文件。虽然需要用到 C 编译器,但还是相对容易做到的。基本做法有赖于以下事实,即 zip 文件内可预置任意数据,Windows 的 exe 文件也可以附带任意数据。因此,创建一个合适的启动程序并将 .pyz 文件附在后面,最后就能得到一个单文件的可执行文件,可运行 Python 应用程序。

合适的启动程序可以简单如下:

#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    return Py_Main(__argc+1, myargv);
}

若已定义了预处理器符号 WINDOWS,上述代码将会生成一个 GUI 可执行文件。若未定义则生成一个可执行的控制台文件。

直接使用标准的 MSVC 命令行工具,或利用 distutils 知道如何编译 Python 源代码,即可编译可执行文件:

>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path

>>> def compile(src):
>>>     src = Path(src)
>>>     cc = new_compiler()
>>>     exe = src.stem
>>>     cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>>     cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>>     # First the CLI executable
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe)
>>>     # Now the GUI executable
>>>     cc.define_macro('WINDOWS')
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe + 'w')

>>> if __name__ == "__main__":
>>>     compile("zastub.c")

生成的启动程序用到了 “受限 ABI”,所以可在任意版本的 Python 3.x 中运行。只要用户的 PATH 中包含了 Python(python3.dll)路径即可。

若要得到完全独立运行的发行版程序,可将附有应用程序的启动程序,与“内嵌版” Python 打包在一起即可。这样在架构匹配(32位或64位)的任一 PC 上都能运行。

注意事项

要将应用程序打包为单个文件,存在一些限制。大多数情况下,无需对应用程序进行重大修改即可解决。

  1. 如果应用程序依赖某个带有 C 扩展的包,则此程序包无法由打包文件运行(这是操作系统的限制,因为可执行代码必须存在于文件系统中,操作系统才能加载)。这时可去除打包文件中的依赖关系,然后要求用户事先安装好该程序包,或者与打包文件一起发布并在 __main__.py 中增加代码,将未打包模块的目录加入 sys.path 中。采用增加代码方式时,一定要为目标架构提供合适的二进制文件(可能还需在运行时根据用户的机器选择正确的版本加入 sys.path)。

  2. 若要如上所述发布一个 Windows 可执行文件,就得确保用户在 PATH 中包含``python3.dll`` 的路径(安装程序默认不会如此),或者应把应用程序与内嵌版 Python 一起打包。

  3. 上述给出的启动程序采用了 Python 嵌入 API。 这意味着应用程序将会是 sys.executable ,而*不是*传统的 Python 解释器。代码及依赖项需做好准备。例如,如果应用程序用到了 multiprocessing 模块,就需要调用 multiprocessing.set_executable() 来让模块知道标准 Python 解释器的位置。

Python 打包应用程序的格式

自 2.6 版开始,Python 即能够执行包含 文件的打包文件了。为了能被 Python 执行,应用程序的打包文件必须为包含 __main__.py 文件的标准 zip 文件,__main__.py 文件将作为应用程序的入口运行。类似于常规的 Python 脚本,父级(这里指打包文件)将放入 sys.path ,因此可从打包文件中导入更多的模块。

zip 文件格式允许在文件中预置任意数据。利用这种能力,zip 应用程序格式在文件中预置了一个标准的 POSIX “释伴”行(#!/path/to/interpreter)。

因此,Python zip 应用程序的格式会如下所示:

  1. 可选的释伴行,包含字符 b'#!',后面是解释器名,然后是换行符 (b'\n')。 解释器名可为操作系统 “释伴”处理所能接受的任意值,或为 Windows 系统中的 Python 启动程序。解释器名在 Windows 中应用 UTF-8 编码,在 POSIX 中则用 sys.getfilesystemencoding()

  2. 标准的打包文件由 zipfile 模块生成。其中 必须 包含一个名为``__main__.py`` 的文件(必须位于打包文件的“根”目录——不能位于某个子目录中)。打包文件中的数据可以是压缩或未压缩的。

如果应用程序的打包文件带有释伴行,则在 POSIX 系统中可能需要启用可执行属性,以允许直接执行。

不一定非要用本模块中的工具创建应用程序打包文件,本模块只是提供了便捷方案,上述格式的打包文件可用任何方式创建,均可被 Python 接受。