分发包、包管理和部署应用程序¶
就像 “大” Python 一样, MicroPython支持创建、分发、并可以很轻松地在每个用户的环境中安装 ”第三方“ 包。本章讨论如何实现这些操作。我们推荐你熟悉一些 Python 的包管理知识。
概述¶
下述步骤代表了创建和使用包的高级工作流程:
Python 模块和包被转换成分发包归档,并在 Python 包索引 (PyPI) 上发布。
upip 包管理器可用于在具备网络功能的 MicroPython 适配端 (例如在 Unix 适配端上)上安装分发包。
对于没有网络功能的适配端,可以在 Unix 适配端上准备一个 “安装镜像”,并通过适当的方式传输到设备上。
对于低内存适配端,可以将安装镜像冻结为 MicroPython 可执行模块,从而最大限度地减少内存开销。
下面的章节详细描述了这个过程。
分发包¶
Python 模块和包可以被打包成适合在系统之间传输的归档,并存储在知名的地方(PyPI),实现可按需下载后部署。这些归档称之为 “分发包”(以将它们与 Python 包(用于组织 Python 源代码)区分)。
MicroPython 分发包的格式是众所周知的 tar.gz 格式,但有一些调整。因为用于 tar 归档的外部包装器是 Gzip 压缩器,默认使用 32KB 字典大小,这意味着需要分配 32KB 连续的内存用于解压压缩流。这个要求可能在低内存设备上无法满足,因为这些设备可能拥极少的内存,以至于连这些连续的内存块都难以分配。为了解决这些限制,MicroPython 分发包所使用的 Gzip 压缩,只使用 4K 字典大小,这应该是一个合适的折中方案,使得在内存较小的设备上仍然可以解压。
除了减小了压缩字典大小,MicroPython 分发包还有其他优化,比如移除归档中不用于安装过程的任何文件。例如常见地,upip 包管理器在安装过程中并不执行 setup.py(详见下文),因此不会包含在归档中。
与此同时,这些优化使得 MicroPython 分发包与 CPython 的包管理器 pip
不兼容。因为:
可以使用 upip 安装的包,也可以用于 CPython(如果它们与 CPython 兼容)。
但另一方面,大多数 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 适配端 的可执行文件(固件)。因此,这个过程是:
根据阅读特定适配端的说明,设置工具链和构建适配端,例如,对于 ESP8266 适配端,请阅读
ports/esp8266/README.md
中的相关描述跟着做。在进行下一步之前,请确保你可以成功构建适配端并部署可执行文件/固件。构建 MicroPython Unix 适配端,并确保它在你的 PATH 中且可执行
micropython
。切换到适配端的目录(例如,对于 ESP8266,切换到
ports/esp8266/
)。执行
make clean-frozen
。这一步会清除以前安装的冻结包(因此,如果你只是添加额外的模块,而不是从头开始,则可以跳过这一步)。执行
micropython -m upip install -p modules <packages>...
安装你想冻结的包。执行
make clean
。执行
make
。
这一步之后,你应当得到了一个包含了冻结包的字节码的可执行文件/固件,你可以像一般情况下对其进行部署。
一些注意事项:
上面的步骤 5 假设可以从 PyPI 中获取到发行版包。若非如此,你需要手动将 Python 源代码文件复制到适配端的
modules/
子目录中。(注意,upip 不支持从版本控制仓库中安装)。对于裸机设备,通常会有大小限制,因此添加太多的冻结包可能会导致溢出。通常,如果这种情况发生,你会收到一个链接错误。然而,在某些情况下,可能仍会生成一个镜像,但该镜像在设备上不可运行。这种情况通常是一个 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
参考¶
Python 包管理用户指引:https://packaging.python.org/
Setuptools 文档:https://setuptools.readthedocs.io/
Distutils 文档:https://docs.python.org/3/library/distutils.html