分发包、包管理和部署应用程序

就像 “大” Python 一样, MicroPython支持创建、分发、并可以很轻松地在每个用户的环境中安装 ”第三方“ 包。本章讨论如何实现这些操作。我们推荐你熟悉一些 Python 的包管理知识。

概述

下述步骤代表了创建和使用包的高级工作流程:

  1. Python 模块和包被转换成分发包归档,并在 Python 包索引 (PyPI) 上发布。

  2. upip 包管理器可用于在具备网络功能的 MicroPython 适配端 (例如在 Unix 适配端上)上安装分发包。

  3. 对于没有网络功能的适配端,可以在 Unix 适配端上准备一个 “安装镜像”,并通过适当的方式传输到设备上。

  4. 对于低内存适配端,可以将安装镜像冻结为 MicroPython 可执行模块,从而最大限度地减少内存开销。

下面的章节详细描述了这个过程。

分发包

Python 模块和包可以被打包成适合在系统之间传输的归档,并存储在知名的地方(PyPI),实现可按需下载后部署。这些归档称之为 “分发包”(以将它们与 Python 包(用于组织 Python 源代码)区分)。

MicroPython 分发包的格式是众所周知的 tar.gz 格式,但有一些调整。因为用于 tar 归档的外部包装器是 Gzip 压缩器,默认使用 32KB 字典大小,这意味着需要分配 32KB 连续的内存用于解压压缩流。这个要求可能在低内存设备上无法满足,因为这些设备可能拥极少的内存,以至于连这些连续的内存块都难以分配。为了解决这些限制,MicroPython 分发包所使用的 Gzip 压缩,只使用 4K 字典大小,这应该是一个合适的折中方案,使得在内存较小的设备上仍然可以解压。

除了减小了压缩字典大小,MicroPython 分发包还有其他优化,比如移除归档中不用于安装过程的任何文件。例如常见地,upip 包管理器在安装过程中并不执行 setup.py(详见下文),因此不会包含在归档中。

与此同时,这些优化使得 MicroPython 分发包与 CPython 的包管理器 pip 不兼容。因为:

  1. 可以使用 upip 安装的包,也可以用于 CPython(如果它们与 CPython 兼容)。

  2. 但另一方面,大多数 CPython 包都是不兼容 MicroPython 的,首当其冲的问题就是它们所依赖的一些特性在 MicroPython 并未实现。

总而言之,MicroPython 分发包针对 MicroPython 目标环境(资源高度受限的设备)进行了高度的优化。

upip 包管理器

MicroPython 分发包旨在通过 upip 包管理器安装。upip 是一个 Python 应用程序,通常用于网络可用的 MicroPython 适配端 上分发(作为冻结字节码)。至少,upipMicroPython Unix 适配端 上是可用的。

在任何 upip 可用的 MicroPython 适配端 上,可以通过如下方式访问:

import upip
upip.help()
upip.install(package_or_package_list, [path])

其中, package_or_package_list 是要安装的分发包的名称,或者一个包含分发包名称的列表用于安装多个分发包。 path 参数是可选的,表示指定安装的文件系统位置,默认为标准库位置(详见下文)。

安装指定的分发包后,如何使用它:

>>> import upip
>>> upip.install("micropython-pystone_lowmem")
[...]
>>> import pystone_lowmem
>>> pystone_lowmem.main()

请注意,一般情况下同样功能的 Python 包和 MicroPython 分发包的名称在不一定相同,且通常不相同,因为 PyPI 提供了所有不同 Python 实现和版本的中心包库,因此分发包名称可能需要在特定实现下指定命名空间。例如,所有来自 micropython-lib 的包都遵循这样的命名规则:对于名为 foo 的 Python 模块或包,MicroPython 分发包名称为 micropython-foo

对于可从操作系统命令提示符中运行 MicroPython 的适配端(如 Unix 适配端),upip 也可以通过命令行运行而不是 MicroPython 的自带 REPL,这些命令对于上述的例子是:

micropython -m upip -h
micropython -m upip install [-p <path>] <packages>...
micropython -m upip install micropython-pystone_lowmem

[TODO: 描述安装路径。]

跨平台安装包

对于没有网络功能的 MicroPython 适配端,建议的安装过程是:在 MicroPython Unix 适配端 中 “跨平台安装” 到一个 “目录镜像”,然后通过适当的方式将这个映像传输到设备中。

安装到一个目录镜像需要在使用 upip 时添加 -p 参数:

micropython -m upip install -p install_dir micropython-pystone_lowmem

执行此命令后,包内容(以及所有依赖的包的内容)将在 install_dir/ 子目录中可见。你需要将这个目录(不包含 install_dir/ 前缀)的内容传输到设备的适当位置,使得这些内容可以通过 Python import 语句找到(请参阅上述的 upip 安装路径讨论)。

跨平台安装包(使用冻结)

对于低内存的 MicroPython 适配端,上述的安装过程不提供最有效的资源使用,因为包在源码中安装,因此需要在每次导入时编译,这需要 RAM,编译后的字节码也存储在 RAM 中,这样会减少存储应用数据的可用空间。此外,上述的安装过程需要设备上存在文件系统,而最资源紧张的设备可能不具备。

字节码的冻结是一个解决上述所有问题的过程:

  • 源代码被预编译为字节码并存储。

  • 字节码存储在 ROM 中,而不是 RAM 中。

  • 冻结包不需要文件系统。

使用冻结字节码需要从 C 源代码中构建一个特定的 MicroPython 适配端 的可执行文件(固件)。因此,这个过程是:

  1. 根据阅读特定适配端的说明,设置工具链和构建适配端,例如,对于 ESP8266 适配端,请阅读 ports/esp8266/README.md 中的相关描述跟着做。在进行下一步之前,请确保你可以成功构建适配端并部署可执行文件/固件。

  2. 构建 MicroPython Unix 适配端,并确保它在你的 PATH 中且可执行 micropython

  3. 切换到适配端的目录(例如,对于 ESP8266,切换到 ports/esp8266/)。

  4. 执行 make clean-frozen。这一步会清除以前安装的冻结包(因此,如果你只是添加额外的模块,而不是从头开始,则可以跳过这一步)。

  5. 执行 micropython -m upip install -p modules <packages>... 安装你想冻结的包。

  6. 执行 make clean

  7. 执行 make

这一步之后,你应当得到了一个包含了冻结包的字节码的可执行文件/固件,你可以像一般情况下对其进行部署。

一些注意事项:

  1. 上面的步骤 5 假设可以从 PyPI 中获取到发行版包。若非如此,你需要手动将 Python 源代码文件复制到适配端的 modules/ 子目录中。(注意,upip 不支持从版本控制仓库中安装)。

  2. 对于裸机设备,通常会有大小限制,因此添加太多的冻结包可能会导致溢出。通常,如果这种情况发生,你会收到一个链接错误。然而,在某些情况下,可能仍会生成一个镜像,但该镜像在设备上不可运行。这种情况通常是一个 bug,应当被报告并进一步调查验证。如果你遇到这种情况,作为一个初始步骤,你可能需要减少冻结包的数量。

创建发行包

MicroPython 的发行包的创建与 CPython 或其他 Python 实现的创建方式相同,参见本章末尾的参考资料。应当使用 setuptools(而不是 distutils),因为 distutils 不支持依赖和其他特性。“源码发行 Source distribution” (sdist) 格式是用于打包过程。上文提到的后处理(及下一章节要讨论的前处理)是通过使用自定义的 sdist 命令来实现的。因此,打包步骤仍然保持了与标准 setuptools 相同,用户只需要通过传递适当的参数来覆盖 sdist 命令的实现调用 setup()

from setuptools import setup
import sdist_upip

setup(
    ...,
    cmdclass={'sdist': sdist_upip.sdist}
)

上文提及的 sdist_upip.py 模块可以在 micropython-lib 中找到: https://github.com/micropython/micropython-lib/blob/master/sdist_upip.py

应用资源

一个完整的应用,除了源代码之外,通常还包括数据文件,例如网页模板、游戏图片等。当应用被手动安装时如何处理这些内容是非常明确的 —— 你只需要将这些数据文件放到文件系统中的某个位置,并使用标准文件访问函数即可。

当从发行包部署应用时,情况就不同了 —— 这是更高级的、更简单的、更灵活的方式,但也需要更高级的方法来访问数据文件。这种方式认为数据文件是“资源”,并且抽象对它们的访问。

通过使用 pkg_resources 模块, Python 支持通过它的 “setuptools” 库来访问资源。MicroPython 按照一贯的做法,实现了这个模块的子集功能,特别是 pkg_resources.resource_stream(package, resource) 函数。这个思路是,应用程序调用这个函数,传递一个资源标识符,代表在指定的包(通常是顶层应用程序包)中的数据文件的相对路径。然后通过 ``resource_stream()``返回一个流对象,使用标准的 open() 来访问资源内容。

实现方面,如果发行包安装在文件系统中, resource_stream() 在底层使用文件操作实现。但是,也支持不依赖文件系统的功能,例如,如果发行包被冻结为字节码。这种情况下,打包应用程序时需要一个额外的中间步骤 —— 创建 “Python 资源模块”。

这个模块的思路是将二进制数据转换为 Python 字节对象,并将其放入字典中,根据资源名称索引。这个转换是通过重写在前面的部分描述的 sdist 命令自动完成。

我们通过下面的示例来纵观完整的过程。假设你的应用程序有如下结构:

my_app/
    __main__.py
    utils.py
    data/
        page.html
        image.png

__main__.pyutils.py 应该使用如下的调用来访问资源:

import pkg_resources

pkg_resources.resource_stream(__name__, "data/page.html")
pkg_resources.resource_stream(__name__, "data/image.png")

你可以像往常一样使用 MicroPython Unix 适配端 正常开发和调试。当到了制作发行包的时候,使用在上文提到的 sdist_upip.py 模块中重写的 “sdist” 命令。

这将创建一个名为 R.py 的 Python 资源模块,基于在 MANIFESTMANIFEST.in 文件中声明的文件(任何非 .py 文件都将被认作资源并加入到 R.py 中),然后,继续正常的打包步骤。

像这样准备好之后,你的应用程序将在文件系统中部署或冻结字节码都能正常工作。

如果你想调试 R.py 的创建过程,你可以运行:

python3 setup.py sdist --manifest-only

另外,你也可以使用 MicroPython 发行版中的 tools/mpy_bin2res.py 脚本,在这里你可以需要传进所有资源文件的路径:

mpy_bin2res.py data/page.html data/image.png

参考