MicroPython 条件编译组件。提取自 pyrite-cli 的条件编译功能,已解除对 pyrite 内部模块的依赖,可独立安装、独立测试、被任意工具复用。
把 @feature/@target 装饰器与 with feature()/target() 上下文管理器在源码级重写为 if True/False 块,配合 manifest 文件级过滤与标签解析,实现「一份源码、按目标板/特性裁剪后再刷入」。
┌──────────────┐ preprocess (libcst) ┌──────────────────┐ mpy-cross ┌─────────────┐
│ 带宏的源码 │ ──────────────────────→ │ 展开为 if True/ │ ─────────────→ │ 精简后的 │
│ feature/target│ │ False,死代码消除 │ 死代码消除 │ 字节码 │
└──────────────┘ └──────────────────┘ └─────────────┘
pip install -e ".[dev]"唯一运行时依赖:libcst>=1.1.0(Python 3.10 额外需要 tomli 以读取 pyproject.toml)。
from mpymacro import (
preprocess, # 源码级转换
load_manifest, # manifest 文件级过滤
build_project, # 按 manifest 离线裁剪整个项目到输出目录
resolve_active_tags, # target/feature/no_feature 合并为最终标签集
resolve_board_tags, # 设备 machine/platform 字符串 → 板级标签
load_board_tags, # 读 pyproject.toml 的 board_tags(与 pyrite-cli 兼容)
DEFAULT_BOARD_TAGS, # 默认板级标签表
)对单个 .py 源码执行条件编译转换。active_tags 为 set[str];传 None 时保留全部块(不做裁剪)。
src = '@feature("wifi")\ndef connect():\n import network\n'
preprocess(src, {"wifi"}) # 保留 connect(),去掉装饰器
preprocess(src, {"ble"}) # 不匹配 → 包进 if False:转换规则:
with feature("x"):/with target("X"):→if True:(命中)或if False:(未命中)@feature("x")/@target("X")装饰的函数 → 命中则去掉装饰器保留函数;未命中则整体包进if False:- 同时做静态分析:被裁掉的函数若存在未被 guard 保护的「裸调用」,发出 warning(运行时会 NameError)
安全解析 manifest.py(AST 解析,非 exec),仅允许 module() / package() 字面量调用,支持 remote=(路径重映射)与 features=[...](标签过滤)。
# manifest.py
module("main.py")
module("wifi_manager.py", features=["wifi"]) # 仅 wifi 激活时刷入
package("drivers/", remote="/lib", features=["sdcard"])合并出最终标签集,收敛了原 pyrite-cli 中分散多处的逻辑:
- 显式
target→ 查board_tags取标签列表并包含 target 自身(大写);否则用detected - 并入
feature、差去no_feature feature/no_feature/detected均接受逗号分隔字符串或集合
resolve_active_tags(target="ESP32", feature="ble") # {"ESP32", "wifi", "ble"}
resolve_active_tags(detected={"RP2040"}, no_feature="debug")将设备 os.uname().machine / sys.platform 字符串映射为板级标签(纯逻辑,不含串口 I/O,设备读取由调用方负责)。
把「manifest 文件级过滤」与「逐文件 preprocess 代码级裁剪」串成一步,将裁剪后的副本写到 out_dir(按条目 remote 相对路径建目录;非 .py 文件原样拷贝)。纯文件操作,零设备依赖。
build_project("manifest.py", "dist/", {"wifi"}, base_dir="src/")从 start_dir(默认 cwd)向上搜索 pyproject.toml,读取并合并 board_tags:
[tool.pyrite.board_tags]—— 与 pyrite-cli 完全兼容(同一份配置两边等价)[tool.mpymacro.board_tags]—— mPyMacro 自有命名空间,优先级更高
key 统一大写。未找到或解析失败时回退到 defaults。
安装后提供 mpymacro 命令,全程离线、零设备依赖(设备标签只能由 --detected 显式传入)。所有子命令都会向上搜索 pyproject.toml 加载 board_tags(--config-dir 指定起点),与 pyrite-cli 项目配置保持一致。
# 解析并打印 active_tags(逐行,便于 CI / $(...) 取用)
mpymacro tags --target ESP32 --feature ble # → ESP32 / ble / wifi
# 单文件条件编译(默认输出到 stdout)
mpymacro expand main.py --tag wifi -o main.built.py
# 按 manifest 裁剪整个项目到 dist/
mpymacro build -m manifest.py -o dist/ --target ESP32 --feature sdcard
# 导出 feature/target 类型存根
mpymacro stub -o feature_stub.pyi通用标签选项:--target、--feature a,b、--no-feature x、--tag T(可重复)、--detected a,b(调用方从设备读出的标签)、--config-dir DIR。
mPyMacro 的 preprocess / load_manifest 与 pyrite-cli 内部实现 API 同构,manifest 格式、board_tags 配置均互相兼容。pyrite-cli 可将其作为 package 依赖引入、删除内部重复实现,同时保留板型自动识别、自动编译等设备侧能力。详见 docs/pyrite-cli-integration.md。
from mpymacro import resolve_board_tags, resolve_active_tags, preprocess, load_manifest
# 1. 从设备读取 machine/platform(你自己的串口逻辑),映射板级标签
detected = resolve_board_tags(machine_str, platform_str)
# 2. 合并 CLI 选项得到 active_tags
active_tags = resolve_active_tags(detected=detected, feature="sdcard", no_feature="debug")
# 3. 文件级过滤
for local, remote in load_manifest("manifest.py", active_tags):
src = open(local).read()
# 4. 代码级转换后再刷入
out = preprocess(src, active_tags, local)
flash(remote, out) # 你自己的刷入逻辑mpymacro/stubs/feature_stub.pyi 提供 feature / target 的类型存根。把它拷到你的工程目录(或加入 stub 搜索路径),即可在编辑器中获得 @feature(...) / with feature(...): 的类型提示——这些符号仅用于预处理标记,运行时不会保留。
python -m pytest -vtarget |
feature |
|
|---|---|---|
| 语义 | 硬件平台(互斥) | 功能特性(可组合) |
| 来源 | 设备自动检测 / target= |
board_tags 隐式 / feature= |
| 示例 | ESP32、RP2040、STM32 |
wifi、ble、sdcard、debug |
两者语法完全相同,仅语义区分。标签匹配为精确字符串匹配;target 自动大写,feature 原样传递。