胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-08-02 / 1 阅读
0
0

NO.B2 metadata.json 每个字段都有主

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


一、B0 讲了思路,这篇拆每个字段

B0 讲了 metadata.json 怎么驱动一切——一个文件管住 name/version/依赖/开关,所有消费者从它继承。但那篇是"为什么",这篇是"每个字段具体怎么用"。

如果你打开模板仓的 metadata.json 仔细看,会发现它的字段不是随便拍的——每个字段都有明确的消费者(谁读它)、明确的用途(读了干什么)。这篇就逐组拆开,说清楚每个字段为什么存在、被谁消费。

二、传统 recipe 的写法:写死一切

先看反面教材。传统的 Conan recipe 长这样:

class MyPackage(ConanFile):
    name = "my-package"           # 写死
    version = "1.2.0"            # 写死
    license = "Apache-2.0"       # 写死
    description = "一个算法库"     # 写死
    settings = "os", "compiler", "build_type", "arch"

    def requirements(self):
        self.requires("eigen/3.4.0")   # 写死
        self.requires("fmt/10.1.0")   # 写死

问题在哪?包的身份信息和构建逻辑搅在了同一个文件里。改版本号要去 recipe 里改 name/version,改依赖要去 requirements 里加减行,改 license 又要去改属性。这些身份信息(叫什么、什么版本、依赖什么)和构建逻辑(怎么编译)是两个维度,硬塞一起就是"改一处忘三处"的根源。

metadata 驱动的思路是:把所有身份信息抽出来放进 metadata.json,recipe 只保留构建逻辑

三、字段消费矩阵:谁读什么

把 metadata.json 的所有字段和它们的消费者列成一张表,一眼看清每个字段的归属:

字段组 关键字段 谁消费 消费来干什么
身份 name / version / license / description / authors Conan recipe init() 设置 recipe 的 name/version 等属性
构建选项 build_cppstd / build_cstd / is_shared / generate_modules_inplace Conan recipe configure() 设 C++ 标准、动态静态、模块开关
依赖 dependencies.common / .c / .cpp / .test Conan recipe requirements() + CMake 声明依赖 + 生成链接目标
开关 trigger_tests / enable_python_bindings Conan recipe + CI 决定跑不跑测试、要不要 Python 绑定
文档 doc_languages / doc_versions / doc_doxygen_folders 文档构建脚本 多语言多版本文档配置

这张表的设计原则只有一条:没有孤儿字段——每个字段至少有一个明确的消费者。如果某个字段没人读,它就不该存在。

四、身份字段:recipe 的 init() 从这里继承

身份字段是最基础的一组。recipe 的 init() 方法读它们,逐条设到 Conan 属性上:

def init(self):
    meta = load_json(Path(self.recipe_folder) / "metadata.json")
    # 身份属性从 metadata 继承,不写死
    self.name = meta["name"]
    self.version = meta["version"]
    self.license = meta["license"]
    self.description = meta["description"]
    self.authors = meta.get("authors", [])
    self.topics = tuple(meta.get("topics", []))
    # URL 类属性也继承
    for key in ("url", "homepage"):
        if key in meta:
            setattr(self, key, meta[key])

这段代码的核心是 setattr 循环——metadata 里有什么字段,recipe 就自动继承什么。加一个新字段(比如 maintainers),metadata 里写一行,recipe 的 init 自动继承(因为 for key in [...] 会扫到它),不需要改 recipe 代码。

这就是"单一数据源"在代码层面的体现:recipe 不维护任何身份信息的副本,它只从 metadata 读

五、构建选项:configure() 读它们

构建选项字段控制的是编译行为:

def configure(self):
    meta = self.meta  # init() 已加载
    # C++ 标准
    cppstd = meta.get("build_cppstd", "17")
    self.settings.compiler.cppstd = cppstd
    # 动态/静态库
    if meta.get("is_shared"):
        self.options.rm_safe("fPIC")
    # C++23 Modules 开关
    if meta.get("generate_modules_inplace"):
        self._modules_preprocessing()

注意 cppstd 有个默认值兜底("17")——如果 metadata 里没写这个字段,不报错,用默认。这是防御性设计:新增字段时老仓库的 metadata 里没有它,不应该崩。

六、依赖四分组:为什么是 common / c / cpp / test

metadata 的依赖不是一坨平铺的列表,而是分了四组:

"dependencies": {
    "common": { "<公共库>":  ["<链接目标>"] },
    "c":      { "<C 库>":    ["<链接目标>"] },
    "cpp":    { "<C++ 库>":  ["<链接目标>"] },
    "test":   { "<测试库>":  ["<链接目标>"] }
}

为什么分四组?因为这个模板仓的产物是双接口的——它同时产出 C 接口库和 C++ 接口库(B5 会专门讲双接口设计)。C 接口只链接 C 依赖和公共依赖,C++ 接口链接 C++ 依赖和公共依赖。测试依赖只在测试时链接,不进产物。

所以分组不是装饰,是给 CMake 生成链接目标用的。recipe 里有个方法把这四组合并、生成 CMake 需要的链接变量:

def _preparing_deps_links(self):
    common = self.meta["dependencies"]["common"]
    c_deps = self.meta["dependencies"]["c"]
    cpp_deps = self.meta["dependencies"]["cpp"]
    test_deps = self.meta["dependencies"]["test"]

    # C 接口 = common + c
    # C++ 接口 = common + cpp
    # 测试 = test(不进产物)
    c_links = merge(common, c_deps)
    cpp_links = merge(common, cpp_deps)
    return c_links, cpp_links

加一个依赖时,你不用想"这个依赖要链接到 C 接口还是 C++ 接口",只要在 metadata 里把它放进对应的组——放 c 组就进 C 接口,放 cpp 组就进 C++ 接口,放 common 组两个都进。分组即声明意图

七、开关字段:一个布尔值控制一个流程

开关字段是最简单的一类,但效果很实际:

def requirements(self):
    for req in self.meta["dependencies"]:
        # trigger_tests=false 时跳过测试依赖
        if "gtest" in req and not self.meta.get("trigger_tests"):
            continue
        # enable_python_bindings=false 时跳过 pybind11
        if "pybind11" in req and not self.meta.get("enable_python_bindings"):
            continue
        self.requires(req)

trigger_tests: false —— recipe 不声明 gtest 依赖,CI 跳过测试步骤,产物不含测试库。一个布尔值,关闭整条测试链路。

enable_python_bindings: false —— recipe 不声明 pybind11 依赖,不构建 Python 绑定。又一个布尔值,关闭整条绑定链路。

这种"一个布尔值控制一个流程"的设计,让模板仓可以灵活裁剪——不需要测试的仓库关掉 trigger_tests,不需要 Python 绑定的仓库关掉 enable_python_bindings,不需要改任何代码。

八、几条攒下来的判断

每个字段都有消费者。metadata 里的每个字段至少被一个消费者读取。没消费者的孤儿字段不该存在。

身份和构建逻辑要分离。name/version/license 是身份信息,放 metadata;compile/link 是构建逻辑,留 recipe。两边不混。

依赖分四组不是装饰。common/c/cpp/test 对应双接口的链接需求,分组即声明依赖该链接到哪个产物。

开关字段一个布尔控一条链路。trigger_tests、enable_python_bindings,一个布尔值关闭整条流程,不用改代码。

防御性默认值。新加字段时老仓库没这个字段不该崩——给默认值兜底。这让 metadata schema 可以平滑演进。

这套 schema 设计是模板仓所有能力的"中央配置台"。B3 到 B11 讲的每个能力(包规范、版本管理、双接口、baremetal 兼容、C++23 Modules、文档、测试、benchmark),它们的配置入口最终都会回到这份 metadata.json——因为它是唯一数据源。

下一篇,我们站在 Conan 包规范的视角,讲企业自有包的 name/version 规范——一个包该怎么命名、版本怎么管,才能既对齐 ConanCenter 又适合企业内部消费。


参考链接:

[1] Conan 2:recipe 属性参考 https://docs.conan.io/2/reference/conanfile/attributes.html

[2] Conan 2:init / configure / requirements 方法 https://docs.conan.io/2/reference/conanfile/methods.html

[3] Conan 2:options 与 settings https://docs.conan.io/2/reference/config_files/settings.yml.html

[4] CMake:target_link_libraries 与依赖分组 https://cmake.org/cmake/help/latest/command/target_link_libraries.html

[5] JSON schema 与数据驱动配置 https://json-schema.org/

[6] Conan 2:cpp_info 组件与双接口 https://docs.conan.io/2/reference/conanfile/methods.html#package-info


评论