Party TOML 配置体系:通过 NIMMAKE.toml 实现组件级声明式构建
Nimmake 允许在每个 Party(组件)目录下放置 NIMMAKE.toml 文件,由 Party 自己声明源文件列表、头文件目录和宏定义,实现「组件自治」的构建模式。
目录
- 4.1 自动发现模式(无 TOML)
- 4.2 声明式模式(有 TOML)
- 4.3 模式对比
1. 概述
核心思想
传统的构建系统(如 CMake、Makefile)在中心脚本中声明所有源文件:
CMakeLists.txt:
add_executable(test main.c driver.c hal.c …)
当项目膨胀到数十个组件、上千个源文件时,中心脚本变得难以维护。
Nimmake 的 Party TOML 方案将「源文件清单」下放给每个组件自己管理:
Core/NIMMAKE.toml ← Core 组件自己说「我有这些源文件」
Drivers/NIMMAKE.toml ← Drivers 组件自己说「我有这些源文件」
App/NIMMAKE.toml ← App 组件自己说「我有这些源文件」
Nimmake.py ← 主脚本只关心「依赖关系」和「构建目标」
关键代码:GenericParty.setup()
位于 [ThirdParty.py](file:///d:/MyCodeNew/nimmake/src/nimmake/thirdParties/ThirdParty.py#L521-L597):
class GenericParty(ThirdParty):
def setup(self) –> None:
pymake_toml = self.dir / TOML_PYMAKEX # "NIMMAKE.toml"
if pymake_toml.exists():
with open(pymake_toml, "rb") as f:
data = tomllib.load(f)
tgt = data.get("target", [])
if tgt and isinstance(tgt, dict):
srcs = tgt.get("srcs", [])
incs = tgt.get("incs", [])
defines = tgt.get("defines", [])
include_macros = tgt.get("include_macros", [])
# 如果 build_type 不是 HEADER, 添加源文件
if self.build_type != BuildType.HEADER.name:
for src in srcs:
self.srcs.append(self.dir / src)
for inc in incs:
self.incs.append(self.dir / inc)
# 解析 defines:["DEBUG=1"] → {"DEBUG": "1"}
if defines and isinstance(defines, list):
for df in defines:
if not df: continue
df_lst = df.split("=")
if len(df_lst) != 2: continue
self.defines.update({df_lst[0]: df_lst[1]})
# 解析 include_macros
if include_macros and isinstance(include_macros, list):
for macro in include_macros:
if not macro: continue
macro_lst = macro.split("=")
if len(macro_lst) != 2: continue
self.include_macros.update({macro_lst[0]: macro_lst[1]})
else:
# 无 TOML → 自动发现文件
self.incs = self.include_dirs(...)
if self.build_type != BuildType.HEADER.name:
self.srcs = self.sources(...)
2. Party TOML 工作机制
Nimmake.py
│
hlp.Parties("CORE", "Core")
│
▼
GenericParty("CORE", root="Core")
│
▼
GenericParty.setup()
│
┌─────────┴──────────┐
▼ ▼
Core/NIMMAKE.toml 没有 TOML 文件
存在? │
│ ▼
▼ 自动发现(目录扫描)
读取 TOML 配置 │
│ ▼
▼ srcs = 扫描所有
srcs = 明确的列表 .c/.h/.s 文件
incs = 明确的目录
defines = 明确的宏 incs = 自动收集
│ include 目录
▼
┌─────────────────┐
│ Party 构建数据 │
│ srcs, incs, │
│ defines, deps │
└────────┬────────┘
▼
ParseParties 处理依赖
▼
编译 → 归档 → 链接
关键设计
| 文件命名 | NIMMAKE.toml(常量 TOML_PYMAKEX 定义在 [configs.py](file:///d:/MyCodeNew/nimmake/src/nimmake/configs.py#L53)) |
| 旧名兼容 | PYMAKEX.toml 同样有效(示例中两者共存) |
| 发现时机 | GenericParty.__init__() → setup() 中自动检测 |
| 优先级 | TOML 配置完全替代自动发现(有 TOML 就不扫描目录) |
| 解析引擎 | Python 3.11+ 内置 tomllib,无外部依赖 |
3. Nimmake.toml 配置格式
基础结构
[target]
srcs = [
"Src/main.c",
"Src/stm32f4xx_hal_msp.c",
"Src/stm32f4xx_it.c",
"Src/system_stm32f4xx.c",
"Src/syscalls.c",
"Src/sysmem.c",
]
incs = ["Inc"]
defines = ["DEBUG=1", "USE_HAL_DRIVER=1"]
include_macros = ["BOARD_NAME=\\"STM32F407\\""]
字段说明
| srcs | string[] | 否 | 源文件列表(相对于 TOML 所在目录) | ["Src/main.c", "Src/hal.c"] |
| incs | string[] | 否 | 头文件搜索目录 | ["Inc", "Inc/config"] |
| defines | string[] | 否 | C 宏定义,= 分割键值 | ["DEBUG=1", "USE_HAL_DRIVER=1"] |
| include_macros | string[] | 否 | 包含宏(同 defines,语义更明确) | ["BOARD_ID=345"] |
路径规则
- 所有路径相对于 TOML 文件所在目录
- 路径分隔符使用 /(跨平台兼容)
- 最终路径会被解析为 self.dir / "Src/main.c"
实际文件示例
samples/09_arm_llvm/Core/NIMMAKE.toml 文件内容:
[target]
srcs = [
"Src/main.c",
"Src/stm32f4xx_hal_msp.c",
"Src/stm32f4xx_it.c",
"Src/system_stm32f4xx.c",
"Src/syscalls.c",
"Src/sysmem.c",
]
incs = ["Inc"]
可选的注释行
# defines = ["DEBUG=1", "USE_HAL_DRIVER=1", "BOARD_ID=345", "xxxx"]
# include_macros = ["DEBUG=1", "USE_HAL_DRIVER=1", "BOARD_ID=345", "xxxx"]
TOML 支持 # 注释,可以保留常用配置模板以备启用。
4. 两种工作模式
4.1 自动发现模式(无 TOML)
当 Party 目录没有 NIMMAKE.toml 时:
core = hlp.Parties("CORE", "Core")
# → GenericParty.setup()
# → 无 TOML 文件
# → 自动扫描 Core/ 下所有 .c/.h/.s 文件
# → 自动收集 include 目录
# → 使用 hlp.Parties() 传入的 defines
优点:
- 零配置,新建文件自动纳入构建
- 适合快速原型和文件较少的组件
缺点:
- 目录扫描耗时(大型 Library 如 HAL 有几百个文件)
- 无法控制文件加入顺序
- 可能误扫不需要的文件
4.2 声明式模式(有 TOML)
当 Party 目录存在 NIMMAKE.toml 时:
core = hlp.Parties("CORE", "Core")
# → GenericParty.setup()
# → 找到 Core/NIMMAKE.toml
# → 读取 [target].srcs → 精确的源文件列表
# → 读取 [target].incs → 精确的头文件目录
# → 读取 [target].defines → Party 专属宏定义
# → 完全跳过目录扫描
优点:
- 构建速度更快:跳过目录扫描,大型项目节省数秒
- 精确控制:只编译需要的文件,排除测试、示例文件
- 可复现性:源文件列表是版本化的,不依赖目录状态
- IDE 友好:TOML 语法高亮,非开发者也可编辑
缺点:
- 新增文件需要手动更新 TOML
- 少量样板文件维护成本
4.3 模式对比
| 配置位置 | Nimmake.py(中心化) | NIMMAKE.toml(分散化) |
| 新增文件 | 自动纳入 | 需手动添加 |
| 删除文件 | 自动排除 | 需手动删除 |
| 构建速度 | 慢(扫描磁盘 IO) | 快(零扫描) |
| 可复现性 | 低(依赖目录状态) | 高(列表版本化) |
| 维护成本 | 初期低,后期高 | 初期低,后期低 |
| 大型项目 | 不推荐 | 推荐 |
| CI 确定性 | 低 | 高 |
5. 完整实战示例
项目结构
my_project/
├── Nimmake.py # 主构建脚本
├── NIMMAKE_CFG.toml # 编译配置(工具链/芯片/优化)
├── Core/
│ ├── NIMMAKE.toml # Core 组件的构建参数
│ ├── Inc/
│ │ └── main.h
│ └── Src/
│ ├── main.c
│ ├── stm32f4xx_hal_msp.c
│ └── system_stm32f4xx.c
├── Drivers/
│ ├── NIMMAKE.toml # Drivers 组件的构建参数
│ ├── Inc/
│ │ └── stm32f4xx_hal.h
│ └── Src/
│ ├── stm32f4xx_hal_adc.c
│ ├── stm32f4xx_hal_dma.c
│ ├── stm32f4xx_hal_gpio.c
│ ├── stm32f4xx_hal_rcc.c
│ └── stm32f4xx_hal_uart.c
└── startup_stm32f407xx.s
Core/NIMMAKE.toml
[target]
srcs = [
"Src/main.c",
"Src/stm32f4xx_hal_msp.c",
"Src/system_stm32f4xx.c",
]
incs = ["Inc"]
defines = ["USE_HAL_DRIVER=1"]
Drivers/NIMMAKE.toml
[target]
srcs = [
"Src/stm32f4xx_hal_adc.c",
"Src/stm32f4xx_hal_dma.c",
"Src/stm32f4xx_hal_gpio.c",
"Src/stm32f4xx_hal_rcc.c",
"Src/stm32f4xx_hal_uart.c",
]
incs = ["Inc"]
defines = ["STM32F407xx="]
Nimmake.py
from nimmake.Helper import Helper
from nimmake.configs import BuildType
toolpath = r"D:\\LLVM\\arm-none-eabi-gcc14\\bin"
hlp = Helper()
# ── 全局编译配置 ──
hlp.TOML() # 读取 NIMMAKE_CFG.toml
hlp.Update({"TOOLPATH": toolpath, "TOOL": "gcc", "TOOL_PREFIX": "arm-none-eabi-"})
hlp.Refresh()
# ── 组件定义 ──
# Core 组件:从 Core/NIMMAKE.toml 自动获取 srcs/incs/defines
core = hlp.Parties("CORE", "Core")
# Drivers 组件:从 Drivers/NIMMAKE.toml 自动获取,编译为静态库
driver = hlp.Parties(
"Driver", "Drivers",
build_type=BuildType.STATIC.name,
)
# 也可以不写 defines,因为已经在 Drivers/NIMMAKE.toml 中声明
# ── 依赖关系 ──
core.DependOn(driver)
# ── 目标 ──
t = hlp.Program("test", sources=["startup_stm32f407xx.s"])
hlp.DefaultTarget(t)
# ── 命令 ──
bin = hlp.Command("BIN", [
f"{hlp['OBJCOPY']} -O binary BUILD/test.ELF build/test.bin",
])
hlp.Phony("all", ["test", "BIN"])
执行
# 方式一:完整构建
nimmake all
# 方式二:单独构建某个 Party(需在 Nimmake.py 中指定命令行参数支持)
# nimmake –party Driver
6. 与 CMake 的对比
CMake 的做法
# CMakeLists.txt(中心化)
add_library(driver STATIC
Drivers/Src/stm32f4xx_hal_adc.c
Drivers/Src/stm32f4xx_hal_dma.c
Drivers/Src/stm32f4xx_hal_gpio.c
# … 几十个文件
)
target_include_directories(driver PUBLIC Drivers/Inc)
target_compile_definitions(driver PUBLIC STM32F407xx)
add_executable(test
Core/Src/main.c
Core/Src/system_stm32f4xx.c
startup_stm32f407xx.s
)
target_link_libraries(test driver)
Nimmake 的做法
分散到组件自身:
# Drivers/NIMMAKE.toml(去中心化)
[target]
srcs = [
"Src/stm32f4xx_hal_adc.c",
"Src/stm32f4xx_hal_dma.c",
# …
]
incs = ["Inc"]
defines = ["STM32F407xx="]
# Nimmake.py(只关心依赖关系)
driver = hlp.Parties("Driver", "Drivers")
core.DependOn(driver)
t = hlp.Program("test", sources=["startup_stm32f407xx.s"])
关键对比维度
| 配置方式 | 中心化 CMakeLists.txt | 分散化 NIMMAKE.toml 在每个组件目录 |
| 源文件声明 | 在 CMakeLists.txt 中列出 | 在组件自己的 NIMMAKE.toml 中列出 |
| 编译命令生成 | CMake 生成 Makefile/Ninja | Nimmake 直接生成 Ninja 或 Shell |
| 语法 | CMake 专有语言 | TOML(声明式)+ Python(逻辑) |
| 学习曲线 | 陡峭(CMake 语言特性多) | 平缓(TOML 简单、Python 熟悉) |
| 增量构建 | 依赖 Make/Ninja | 内置 Tracker + BuildCache |
| 文件扫描 | file(GLOB …) 不推荐 | 自动发现 + TOML 声明双模 |
| 跨平台 | 优秀(全平台支持) | 良好(Windows/Linux/Mac) |
| IDE 集成 | 优秀(CLion、VS、VSCode) | 基础(命令行 + Ninja) |
详细对比
6.1 源文件管理
| 新增 .c 文件 | 修改 CMakeLists.txt 添加 target_sources() | 修改 NIMMAKE.toml 在 srcs 列表追加 |
| 删除 .c 文件 | 从 CMakeLists.txt 移除 | 从 NIMMAKE.toml 移除 |
| 文件组织 | 按目标聚合(add_library) | 按目录聚合(每个目录一个 TOML) |
| GLOB 风险 | file(GLOB) 不检测新文件 | 无 TOML 时自动发现;有 TOML 时精确列表 |
6.2 依赖管理
| 目标间依赖 | target_link_libraries(A B) | A.DependOn(B) |
| 传递性 | PUBLIC/PRIVATE/INTERFACE | 依赖被继承(源文件级) |
| 循环依赖 | 不允许 | 在 DependOn() 中可声明(由用户保证无环) |
| 外部库 | find_package() | third_party="HAL" 参数指定预设 |
6.3 构建速度
| 配置阶段 | CMake 配置耗时 | 几乎零配置(直接执行 Python) |
| 构建阶段 | 生成器(Ninja 快,Make 慢) | 内置 Ninja 生成 / Shell 模式 |
| 增量检查 | 文件时间戳 | Tracker(哈希 + 时间戳 + 编译命令变化) |
| 大型项目 | 优秀(Ninja 并行) | 良好(当前单线程执行,可并行) |
6.4 代码简洁性
CMake(中心化,20 个组件):
# CMakeLists.txt
add_subdirectory(drivers)
add_subdirectory(core)
add_subdirectory(freertos)
add_subdirectory(lwip)
add_subdirectory(fatfs)
# … 20 个 add_subdirectory
add_executable(firmware main.c startup.s)
target_link_libraries(firmware drivers core freertos lwip fatfs …)
Nimmake(去中心化,20 个组件):
# Nimmake.py
driver = hlp.Parties("Driver", "Drivers")
core = hlp.Parties("CORE", "Core")
freertos = hlp.Parties("FreeRTOS", "FreeRTOS", third_party="FREERTOS")
lwip = hlp.Parties("LwIP", "LwIP", third_party="LWIP")
core.DependOn(driver)
core.DependOn(freertos)
freertos.DependOn(lwip)
t = hlp.Program("firmware", sources=["main.c", "startup.s"])
hlp.DefaultTarget(t)
每个组件的 NIMMAKE.toml 由组件作者维护,主脚本只关心依赖拓扑。
6.5 Nimmake 的独特优势
优势 1:组件自治 + 去中心化
每个组件目录就是一个自描述的构建单元:
# Components/Lvgl/NIMMAKE.toml
[target]
srcs = [
"src/lv_core/lv_obj.c",
"src/lv_draw/lv_draw_rect.c",
# … 其他源文件
]
incs = ["src"]
defines = ["LV_CONF_PATH=\\"lv_conf.h\\""]
组件复用 = 复制目录 + 在 Nimmake.py 中 hlp.Parties()。无需修改任何中心配置文件。
优势 2:构建参数可编程
Python 是图灵完备的语言,可以在构建脚本中做任意逻辑:
for chip in ["STM32F407", "STM32F429", "STM32F103"]:
cfg = get_chip_config(chip)
hlp.Config(cfg)
core = hlp.Parties("CORE", "Core") # 同一个 Core 目录
# … 同时构建三个芯片版本
CMake 虽然也支持循环,但语法远不如 Python 直观。
优势 3:零外部依赖
import tomllib # Python 3.11+ 内置
# 无需安装任何包
Nimmake + Python 标准库 = 完整构建系统。CMake 需要安装 CMake 二进制。
优势 4:构建缓存内置
CMake 依赖 Make/Ninja 的增量构建。Nimmake 内置 [Tracker](file:///d:/MyCodeNew/nimmake/src/nimmake/builders/Tracker.py) 和 [BuildCache](file:///d:/MyCodeNew/nimmake/src/nimmake/builders/buildCache.py#L50-L113):
- 哈希比较编译命令是否变化
- 时间戳比较源文件是否更新
- 编译命令变化时自动重新编译
- 缓存命中时零耗时跳过
6.6 CMake 的相对优势
| 生态成熟 | CMake 有 20+ 年历史,无数第三方模块 |
| IDE 支持 | CLion、VS、VSCode 原生支持 CMake |
| 包管理器 | find_package() + Conan/vcpkg 集成 |
| 测试框架 | enable_testing() + add_test() 内置 |
| 安装规则 | install() 命令成熟 |
| 全平台 | Windows/MSVC、Linux/GCC、Mac/Xcode 全支持 |
| 语言标准 | 支持几乎全部编译器和语言 |
6.7 适用场景建议
| 嵌入式 MCU 项目(STM32/ESP32 等) | Nimmake(轻量、组件自治、零依赖) |
| 大型桌面/服务器应用 | CMake(生态成熟、工具链全) |
| 快速原型、个人项目 | Nimmake(配置简单、上手快) |
| 团队协作、企业级项目 | CMake(IDE 支持好,新人熟悉) |
| 混合方案 | CMake 构建主程序,Nimmake 管理 MCU 固件 |
7. 最佳实践
✅ 推荐做法
1. 为稳定的组件编写 TOML
# ✅ 推荐:稳定的大库用 TOML
# Drivers/NIMMAKE.toml(HAL 库很少改动)
srcs = ["Src/stm32f4xx_hal_adc.c", "Src/stm32f4xx_hal_dma.c", …]
# ❌ 不必要:频繁改动的文件不需要 TOML
# App/NIMMAKE.toml(每天都在改)
srcs = ["app.c"] # 只有一个文件,自动发现即可
2. 自动发现 + TOML 混合使用
# 大库用 TOML 声明(跳过目录扫描,加快构建)
driver = hlp.Parties("Driver", "Drivers", build_type=BuildType.STATIC.name)
# 小库自动发现(无需 TOML)
app = hlp.Parties("APP", "App")
3. 利用 TOML 生成命令导出配置
nimmake –party Core
# 自动生成:
# [target]
# srcs = […]
# incs = […]
该功能由 [__toml_party()](file:///d:/MyCodeNew/nimmake/src/nimmake/builders/parseParties.py#L722-L743) 实现,自动将目录扫描结果输出为 TOML 格式,复制到文件即可。
4. 保持路径简洁
# ✅ 推荐:相对于 TOML 目录
srcs = ["Src/main.c"]
# ❌ 不推荐:绝对路径
srcs = ["/absolute/path/to/Src/main.c"]
# ❌ 不推荐:相对项目根
srcs = ["../../Core/Src/main.c"]
5. 使用注释模板
# 保留注释模板,方便快速启用
# defines = ["DEBUG=1", "USE_HAL_DRIVER=1"]
# include_macros = ["BOARD_ID=345"]
⚠️ 注意事项
| TOML 不存在 | setup() 自动回退到目录扫描,无报错 | 可选择性使用 |
| 路径格式 | Windows 使用 / 而非 \\ | TOML 中使用 "Src/main.c" |
| 宏格式 | defines 使用 "KEY=VALUE" 字符串 | 参考示例:"STM32F407xx=" 表示纯定义 |
| d 后缀 | "STM32F407xx=" 会被解析为 {"STM32F407xx": ""} → -DSTM32F407xx | 留空值在 = 后即可 |
| TOML 优先级 | TOML 中的 defines 会与 hlp.Parties(defines=…) 中的合并 | 两者同名时以 hlp.Parties() 为准 |
总结
Party TOML 配置体系是 Nimmake 实现"组件自治"构建的核心机制:
| 声明式 | 组件自己声明源文件/头文件/宏 |
| 去中心化 | 每个目录一个 NIMMAKE.toml |
| 高性能 | 跳过目录扫描,直接读取文件列表 |
| 可复现 | 源文件列表版本化,CI 一致 |
| 零依赖 | Python 3.11+ 内置 tomllib |
与 CMake 的本质区别:
CMake 是中心化的——一个 CMakeLists.txt 统治所有组件; Nimmake 是去中心化的——每个组件有自己的 NIMMAKE.toml,主脚本只编排依赖。
对于 嵌入式 MCU 项目,Nimmake 的 Party TOML 方案提供了更轻量、更直观、更容易维护的构建体验。

![打卡信奥刷题(3584)用C++实现信奥题 P11523 [THUPC 2025 初赛] 摊位分配-171主机测评](https://www.171host.com/wp-content/uploads/2026/09/20260922020544-6ab1e2783b78e-220x150.png)
