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

就像 “大” 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 适配端 上分发(作为冻结字节码)。至少,upip 在 MicroPython 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__.py 和 utils.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 资源模块,基于在 MANIFEST 或 MANIFEST.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

参考