MicroPython 清单(manifest)文件

概述

MicroPython 提供了一项功能,可以将 Python 代码“冻结”到固件中,作为从文件系统加载代码的替代方案。

这样做有以下好处:

  • 代码会被预编译为字节码,从而无需在加载时再编译 Python 源代码。

  • 字节码可以直接从 ROM(即闪存)中执行,而不必复制到 RAM 中。同样,任何常量对象(字符串、元组等)也会从 ROM 中加载。这可以为你的应用程序释放出明显更多的可用内存。

  • 在没有文件系统的设备上,这是加载 Python 代码的唯一方式。

在开发过程中,通常不建议使用冻结,因为它会显著拖慢你的开发周期——每次更新都需要重新刷写整个固件。不过,有选择地冻结一些很少改动的依赖项(例如第三方库)仍然是有用的。

列出要冻结到固件中的 Python 文件的方式是通过一个“清单(manifest)”,它是一个由构建过程解释执行的 Python 文件。通常你会把清单文件作为板卡定义的一部分来编写,但你也可以编写一个独立的清单文件,并将其与现有的板卡定义配合使用。

清单文件可以定义对 micropython-lib 中库的依赖,也可以定义对文件系统上 Python 文件的依赖,还可以定义对其他清单文件的依赖。

编写清单文件

清单文件是一个包含一系列函数调用的 Python 文件。请参阅下面定义的可用函数。

清单文件中使用的任何路径都可以包含以下变量。它们都会解析为绝对路径。

  • $(MPY_DIR) —— micropython 仓库的路径。

  • $(MPY_LIB_DIR) —— micropython-lib 子模块的路径。优先使用 require()

  • $(PORT_DIR) —— 当前 port 的路径(例如 ports/stm32

  • $(BOARD_DIR) —— 当前板卡的路径(例如 ports/stm32/boards/OPENMV4

自定义清单文件不应放在 MicroPython 主仓库中。你应该把它们与项目的其余部分一起纳入版本控制。

用于编译固件的清单通常需要包含 port 清单,其中可能包含板卡正常工作所必需的冻结模块。如果你只是想为现有板卡添加额外的模块,那么可以包含板卡清单(板卡清单又会进一步包含 port 清单)。

使用自定义清单进行构建

你的清单可以在 make 命令行中通过以下方式指定:

$ make BOARD=MYBOARD FROZEN_MANIFEST=/path/to/my/project/manifest.py

这适用于所有 port,包括基于 CMake 的 port(例如 rp2),因为 Makefile 包装器会将其传递给 CMake 构建。

向板卡定义添加清单

如果你有自定义的板卡定义,可以让它自动包含你的自定义清单。在基于 make 的 port(大多数 port)上,在你的 mpconfigboard.mk 中设置 FROZEN_MANIFEST 变量。

FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py

在基于 CMake 的 port(例如 rp2)上,则改用 mpconfigboard.cmake

set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

高级函数

这些是你通常会使用的函数。它们会把代码加入到将被预编译为字节码并冻结进固件镜像的集合中:

  • modulepackage 用于冻结你自己的本地源代码——分别对应单个文件或整个软件包目录。

  • require 用于按名称冻结来自 micropython-lib已发布软件包(及其依赖项)。

  • include 用于引入另一个清单,从而把它的冻结模块也一并加入。

  • add_librarymetadata 是辅助函数(分别用于为 require 注册额外的搜索路径,以及声明软件包元数据)。

一个典型的固件清单首先会 include port 或板卡清单(这样板卡所需的模块仍保持冻结),然后再添加它自己的 module/package/require 行。

注意:可以在各个函数上设置 opt 关键字参数,它用于控制交叉编译器所使用的优化级别。参见 micropython.opt_level()

add_library(library, library_path, prepend=False)

注册一个外部命名 library 的路径。

当你希望 requiremicropython-lib 以外的目录解析软件包时使用此函数——例如你自己的一批驱动程序,或某个第三方库的检出(checkout)。

在使用 require 时会自动搜索路径 library_path。默认情况下,新增的库会被加到待搜索库列表的末尾。传入 True 可将其前置,即加到列表的开头。

此外,还可以通过使用 require("name", library="library") 来显式请求新增的库。

package(package_path, files=None, base_path='.', opt=None)

冻结整个软件包——一个包含 .py 文件(可选地带有子软件包)的目录——以便可以通过 import <package> 来导入它。对于单个独立文件,请改用 module

这等同于将“package_path”目录复制到设备上(区别在于这是作为冻结代码)。

在最简单的情况下,冻结当前目录中的软件包“foo”:

package("foo")

会递归地包含 foo 中的所有 .py 文件,并以 foo/**/*.py 的形式冻结。

如果软件包与清单文件不在同一目录中,请使用 base_path

package("foo", base_path="path/to/libraries")

你可以在 base_path 中使用上述变量,例如 $(PORT_DIR)

若要限定为软件包中的某些文件,请使用 files(注意:路径应相对于该软件包):package("foo", files=["bar/baz.py"])

module(module_path, base_path='.', opt=None)

冻结单个独立的 .py 文件,以便可以按其名称导入(module("foo.py") 会让 import foo 生效)。对于目录/软件包,请使用 package

如果文件在当前目录中:

module("foo.py")

否则使用 base_path 来定位文件:

module("foo.py", base_path="src/drivers")

你可以在 base_path 中使用上述变量,例如 $(PORT_DIR)

require(name, library=None)

按名称要求(require)来自 micropython-lib 的一个软件包(及其依赖项)。

标准库扩展和社区驱动程序就是通过这种方式被冻结进去的:指定名称的软件包会从 micropython-lib 子模块中获取,并连同它所依赖的一切一起冻结。若要冻结你自己的源代码而非已发布的软件包,请改用 modulepackage

可选地指定 library(一个字符串),以引用某个之前已通过 add_library 注册的库中的软件包。否则将使用库路径列表。

include(manifest_path)

包含另一个清单。这就是清单的组合方式:自定义固件清单应当 include port(或板卡)清单,以便板卡所需的模块保持冻结,然后再添加它自己的条目。

用于编译固件的清单通常需要包含 port 清单,其中可能包含板卡正常工作所必需的冻结模块。

manifest 参数可以是一个字符串(文件名),也可以是一个字符串的可迭代对象。

相对路径会相对于当前清单文件进行解析。

如果该路径指向一个目录,则它会隐式地包含该目录内的 manifest.py 文件。

你可以在 manifest_path 中使用上述变量,例如 $(PORT_DIR)

metadata(description=None, version=None, license=None, author=None)

为此清单文件定义元数据。这对于 micropython-lib 软件包的清单很有用。

当某个软件包通过 mip 发布到 micropython-lib 或从中安装时,会使用到这些字段;在板卡固件清单中并不需要它们。

低级函数

为完整起见对这些函数进行了说明,但除 freeze_as_str 之外,所有功能都可以通过高级函数来访问。

各个 freeze* 函数仅在代码如何存储这一点上有所不同:

  • freeze_as_mpy / freeze_mpy 将预编译的字节码.mpy)存储在闪存中。代码直接从闪存运行,占用极少的 RAM,并且导入速度快。这正是 modulepackagerequire 在内部所使用的方式。

  • freeze_as_str 则冻结 Python 源代码,源代码会在导入时编译为字节码(占用 RAM,并需要设备端的编译器)。这是高级函数未暴露出来的唯一一项能力,这也是上文中将其列为例外的原因。

freeze(path, script=None, opt=0)

高级函数所基于的底层原语;请优先使用那些高级函数。冻结由 path 指定的输入,并自动判断其类型。.py 脚本会先编译为 .mpy 再冻结,而 .mpy 文件则会被直接冻结。

path 必须是一个目录,它是开始搜索文件的基目录。在导入由此产生的冻结模块时,模块名称从 path 之后开始,也就是说模块名称中不包含 path

如果 path 是相对路径,则会相对于当前的 manifest.py 进行解析。

如果 script 为 None,则会冻结 path 中的所有文件。

如果 script 是一个可迭代对象,则会对该可迭代对象中的所有项调用 freeze()(并传入相同的 pathopt)。

如果 script 是一个字符串,则它指定要冻结的文件或目录,并且可以在该文件或最后一级目录之前包含额外的目录。该文件或目录将在 path 中进行搜索。如果 script 是一个目录,则会冻结该目录中的所有文件。

opt 是在将 .py 编译为 .mpy 时传给 mpy-cross 的优化级别。这些级别在 micropython.opt_level() 中有说明。

freeze_as_str(path)

将给定的 path 及其内部的所有 .py 脚本作为字符串冻结,这些脚本将在导入时编译。仅当冻结的代码必须保留为 Python 源代码时才使用此方式;与 .mpy 变体相比,它会消耗导入时的 RAM。

freeze_as_mpy(path, script=None, opt=0)

通过先将 .py 脚本编译为 .mpy 文件、再冻结所得的 .mpy 文件来冻结输入。这正是 modulepackage 在底层所做的工作。有关参数的更多细节,请参见 freeze()

freeze_mpy(path, script=None, opt=0)

冻结输入,输入必须是会被直接冻结的 .mpy 文件(无编译步骤)。有关参数的更多细节,请参见 freeze()

示例

若要从当前目录冻结单个文件,使其可通过 import mydriver 使用,请使用:

module("mydriver.py")

若要冻结当前目录下子目录“mydriver”中的一批文件,使其可通过 import mydriver 使用,请使用:

package("mydriver")

若要冻结来自 micropython-lib 的“hmac”库,请使用:

require("hmac")

一个更完整的自定义 manifest.py 文件示例(针对一块拥有自身默认清单的板卡)如下:

# Include the board's default manifest.
include("$(BOARD_DIR)/manifest.py")
# Add a custom driver
module("mydriver.py")
# Add aiorepl from micropython-lib
require("aiorepl")

然后该板卡可以通过以下方式编译

$ cd ports/stm32
$ make BOARD=MYBOARD FROZEN_MANIFEST=~/src/myproject/manifest.py

请注意,大多数板卡并没有自己的 manifest.py,而是直接使用 port 的清单,在这种情况下,你的清单应当只写 include("$(PORT_DIR)/boards/manifest.py")