⚠️ 法律与用途声明:本文讨论仓库模板的元数据驱动设计,基于开源工具(Conan 2 / CMake),属于经验分享。所有企业名、项目名、内部脚本与路径命名均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。

一、配置满天飞的日子
做过 C/C++ 项目的人大概都有过这种体验:一个仓库里,包名叫什么、版本是多少、依赖哪些库,这些信息散落在好几个地方。
Conan recipe 里写一遍 name 和 version,CMakeLists 里可能又硬编码了一遍包名,CI workflow 里为了传包名可能还写了一次,文档里介绍这个包时又手敲了一遍版本号。改个版本号,要在四五个文件里同步改,漏一个就出 bug。到最后谁也说不清"真正的版本号到底以哪个为准"。

这不是某个人的粗心,是架构问题——没有指定一个唯一的配置源头,所有消费者各自维护一份副本,必然漂移。
B 系列开篇这篇,就讲怎么用一个文件管住所有事。
二、metadata.json:一个文件说清楚"我是谁"
核心思路极简:在仓库根目录放一个 metadata.json,把包的所有身份信息集中在这一个文件里。包名、版本、依赖、C++ 标准、测试开关、文档语言……全都写在这里。

一份脱敏后的 metadata 长这样(只看结构):
{
"name": "<包名>",
"version": "<版本>",
"description": "<一句话描述>",
"license": "Apache-2.0",
"authors": ["<作者>"],
"build_cppstd": "17",
"build_cstd": "11",
"is_shared": false,
"generate_modules_inplace": false,
"dependencies": {
"common": { "<公共依赖>": ["<链接目标>"] },
"c": { "<C 依赖>": ["<链接目标>"] },
"cpp": { "<C++ 依赖>": ["<链接目标>"] },
"test": { "<测试依赖>": ["<链接目标>"] }
},
"trigger_tests": false,
"enable_python_bindings": false,
"doc_languages": ["en", "zh"],
"doc_versions": ["1.0", "2.0"]
}
注意这个文件里有什么、没什么:
有的:包的身份(name/version/license/authors)、构建选项(cppstd/shared/modules)、依赖清单(按 C/C++/test 分组)、开关(测试/Python 绑定/文档)。全是声明式的、结构化的数据。
没有的:任何构建脚本路径、CI 内部细节、平台特定配置。metadata 不知道也不需要知道平台怎么构建它。它是纯粹的"自我描述"。
这个"有与没有"的边界就是 AB1 讲的契约——metadata.json 是模板仓向所有消费者(Conan recipe、CMake、CI、文档)下达的唯一数据源。
三、所有消费者从它继承
metadata 定下来之后,仓库里所有需要这些信息的地方,都从它继承,而不是各自维护副本。

Conan recipe 从 metadata 继承
最典型的是 Conan recipe。传统的 recipe 在类里直接写死 name = "xxx"、version = "1.0.0"。但在模板仓里,recipe 的 init 方法从 metadata 读取所有属性:
class PackageRecipe(ConanFile):
def init(self):
# 从 metadata.json 继承所有身份属性
self.meta = load_json("metadata.json")
self.name = self.meta["name"]
self.version = self.meta["version"]
self.license = self.meta["license"]
self.description = self.meta["description"]
# ... 还有 authors、topics 等
效果是:recipe 里不写死任何 name/version,改 metadata 一处,recipe 自动继承新值。recipe 只负责构建逻辑(怎么编译),不负责身份信息(叫什么、什么版本)。职责清晰。
CMake 从 metadata 读依赖
依赖关系也类似。metadata 的 dependencies 字段按 common/c/cpp/test 四组声明所有依赖。CMake 不自己维护一份依赖列表,而是通过 recipe 从 metadata 生成依赖链接变量。加一个依赖,只改 metadata 一处,recipe 和 CMake 都自动拿到。
CI 从 metadata 读开关
CI 要不要跑测试?recipe 里有个 trigger_tests 开关,它也是从 metadata 读的。trigger_tests: false 时,CI 跳过测试步骤。改这个开关不用改 CI workflow,只改 metadata。
文档从 metadata 读版本
文档系统展示的版本号也从 metadata 来。文档里不手敲版本,而是从 metadata.json 读 version 字段自动填充。改版本号,文档同步更新。
四、一处改,处处变
把所有消费者都接到 metadata 之后,最直观的好处是:改一处,所有地方自动变。

# 改版本号:只改 metadata.json 一处
# Before: "version": "1.0.0"
# After: "version": "1.1.0"
# 下游消费者自动拿到新值:
# conan recipe → name/version = <包名>/1.1.0
# CMake → 链接目标名含 1.1.0
# CI → 构建产物包名含 1.1.0
# 文档 → 展示版本 1.1.0
这个"一处改处处变"不是什么魔法,就是把所有消费者指向同一个数据源。它的反面是"各写各的副本"——改了 recipe 忘改 CMake,改了 CMake 忘改 CI,改了 CI 忘改文档。metadata 驱动消除了这种"同步漂移"。
五、为什么是 JSON 不是别的
有人可能会问:为什么用 JSON,不用 YAML 或 TOML?
答案不是"JSON 最好",而是"JSON 最通用"。Conan recipe 的 init 方法要读这个文件,CMake 要读它,Python 脚本要读它,甚至 CI 可能也要解析它。JSON 是几乎所有编程语言都有标准库解析的格式。Python 有 json 模块、CMake 可以通过 recipe 间接读、CI 的脚本语言也都能解析 JSON。
YAML 固然更人性化(支持注释、多行字符串),但它的解析器在不同语言间行为不完全一致(比如锚点、标签这些高级特性)。对于一个要被多个异构消费者读取的"单一数据源"来说,格式的通用性和解析一致性比人的书写体验更重要。
所以选 JSON 不是因为它是最好的配置格式,而是因为它是最不会出问题的配置格式——谁都能读,读出来都一样。
六、几条攒下来的判断
这篇是 B 系列的开篇,也是模板仓设计哲学的基石。收束成几条:
一个文件管所有事。name/version/依赖/开关,全集中在 metadata.json 一个文件里。不散落在 recipe、CMake、CI 各处。
所有消费者继承,不复制。Conan recipe 从 metadata 读 name/version,CMake 从 metadata 读依赖,CI 从 metadata 读开关。谁都不自己维护副本。
一处改处处变。改 metadata 一处,所有消费者自动拿到新值。消除"改了 A 忘改 B"的同步漂移。
JSON 不是最好,是最通用。选它不是因为语法优雅,是因为几乎所有语言都能可靠解析。单一数据源的格式,通用性大于美观。
metadata 是 AB1 讲的两个契约之一。它是模板仓向平台和自身构建系统下达的唯一数据源。这条契约稳定,整套体系才能各自演进。

这套"metadata 驱动一切"的设计,是模板仓所有能力的地基。后面 B1 到 B11 讲的每一个能力(Conan 包规范、版本管理、双接口、baremetal 兼容、C++23 Modules、文档、测试、benchmark),都会回到这一篇——它们全都是从 metadata.json 继承或派生出来的。
下一篇,我们讲模板仓的另一半基础设施:template repo 与软件组织管理——怎么用 GitHub 的 template repository 机制,让新仓库一键从模板创建。

参考链接:
[1] Conan 2:recipe 属性与动态设置 https://docs.conan.io/2/reference/conanfile/attributes.html
[2] Conan 2:init 方法与动态配置 https://docs.conan.io/2/reference/conanfile/methods.html
[3] CMake:外部数据读取与变量生成 https://cmake.org/cmake/help/latest/command/configure_file.html
[4] JSON 数据格式规范 https://www.json.org/
[5] Single Source of Truth 概念(Martin Fowler) https://martinfowler.com/bliki/SingleSourceOfTruth.html
[6] GitHub Actions:从仓库文件读取配置 https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-your-workflow-does/workflow-commands