ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Protobuf 多构建系统支持设计解析:Bazel 如何作为唯一事实来源驱动 CMake 构建

Protobuf 多构建系统支持设计解析:Bazel 如何作为唯一事实来源驱动 CMake 构建 Protobuf 多构建系统支持设计解析Bazel 如何作为唯一事实来源驱动 CMake 构建【免费下载链接】protobufProtocol Buffers - Googles data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobufProtocol Buffers 的 C 运行时与编译器同时支持 Bazel 和 CMake 两套构建系统二者的构建定义语义完全不同却共享同一份编译运行时需要哪些文件的清单。本文基于官方文档 docs/cpp_build_systems.md 展开结合 pkg/cc_dist_library.bzl、pkg/build_systems.bzl 与 pkg/BUILD.bazel 的源码实现讲清 Protobuf 用 Bazel Aspect 提取源文件清单、用cc_dist_library聚合粗粒度库、再生成 CMake 文件列表这一整套机制的工作原理与维护流程帮助你在为本项目或类似的多构建系统项目新增构建目标时准确理解其背后的约定。背景为什么要维护多套构建定义Protobuf 主要使用 Bazel 构建 C 运行时与编译器protoc。历史上开源项目在发布前使用 Autoconf 构建 C 实现后来随着社区贡献Bazel 等构建系统相继加入。由于 Protobuf 涉及多种语言各语言实现最终都依赖 C 实现的 protocBazel 成为天然的全项目构建选择——它及其前身 Blaze 正是为这类富多语言构建而设计的。当前的现状是C Protobuf 可以用 Bazel 和 CMake 构建。两套系统语义与结构不同但共享一份核心数据——构建运行时和编译器所需的文件列表。文档的设计目标因此很明确每种构建系统的整体结构只定义一次各自的 BUILD / CMake 文件而频繁变化的元数据尤其是源文件列表从 Bazel 以可被其他构建定义引用的形式暴露出来。也就是说Bazel 是文件清单的单一事实来源CMake 构建只是消费 Bazel 生成的变量。从 Bazel 提取文件信息两个 Aspect 与两个 ProviderBazel 的 Starlark API 提供了 Aspect遍历构建图、检查规则、附加动作和 Provider暴露信息机制。例如cc_proto_library规则用 Aspect 遍历proto_library依赖图动态附加用 protoc 生成 C 代码并用 C 编译器编译的动作。多构建系统支持正是复用了这一能力。仓库中定义了两个 Aspectcc_file_list_aspect提取 C 规则的源文件位于 pkg/cc_dist_library.bzlcc_file_list_aspect定义于该文件 L194-L220它从cc_library等带CcInfo的规则中提取srcs、hdrs、textual_hdrs通过名为CcFileList的 Provider 暴露。CcFileList的字段定义L123-L136为hdrs公共头文件含生成代码所需的头文件textual_hdrs被包含但不自包含的文件internal_hdrs出现在srcs中的内部头文件仅用于编译库本身srcs源文件。从实现细节看pkg/cc_dist_library.bzl该 Aspect 直接读取被遍历规则的 attrsrcs按扩展名过滤只有.c/.cc/.cpp/.cxx进入srcs其余文件归入internal_hdrs通过getattr(rule_attr, srcs, [])容错处理暴露了CcInfo但没有srcs属性的依赖_flatten_target_files会过滤掉外部仓库的文件并显式排除third_party/utf8_range该目录目前有自己的 CMake 构建Aspect 声明了attr_aspects [deps]即沿deps遍历。file_list_aspect文档中的proto_file_list_aspect提取 proto 源文件并合成生成文件名位于 pkg/build_systems.bzl。它从proto_library中提取srcs并合成protoc 预期生成的文件名通过ProtoFileListProviderL33-L43暴露字段为proto_srcsproto 源文件srcs预期的.pb.cc生成文件按%s/%s.pb.cc % (src.dirname, src.basename)规则合成见 L64-L65hdrs预期的.pb.h生成文件。文档强调这两个 Aspect 单独使用价值有限真正的用途是被自定义规则实例化——让一个普通的BUILD.bazel目标能基于 Aspect 收集到的信息产出构建文件。分布库Distribution Librarycc_dist_libraryBazel 的原生cc_library是细粒度的——方便编写作用域狭窄的轻量单元测试。虽然 Bazel 也会产出库工件Linux 上的.so、.a但它们对应的是单个cc_library规则。而整个 Protobuf 库由许多cc_library组成因此需要一个能把多个细粒度库合并为单一巨型库的特殊规则——这就是cc_dist_library实现于 pkg/cc_dist_library.bzl。对 Protobuf 项目而言这些分布库的粒度刻意与 CMake 构建保持一致。由于 Bazel 构建的分布库覆盖了其他构建所需的源文件规则cc_dist_library会对输入库调用cc_file_list_aspect结果是一个cc_dist_library目标不仅产出组合库工件静态归档libname.a、PIC 归档.pic.a与动态库还收集并提供其输入的源文件列表。文档中的示例此处完整保留$ cat cc_dist_library_example/BUILD.bazel load(rules_cc//cc:defs.bzl, cc_library) load(//pkg:cc_dist_library.bzl, cc_dist_library) cc_library( name a, srcs [a.cc], ) cc_library( name b, srcs [b.cc], deps [:c], ) # N.B.: not part of the cc_dist_library, even though it is in the deps of b: cc_library( name c, srcs [c.cc], ) cc_dist_library( name lib, deps [ :a, :b, ], visibility [//visibility:public], ) # Note: the output below has been formatted for clarity: $ bazel cquery //cc_dist_library_example:lib \ --outputstarlark \ --starlark:exprproviders(target)[//pkg:cc_dist_library.bzl%CcFileList] struct( hdrs depset([]), internal_hdrs depset([]), srcs depset([ source file cc_dist_library_example/a.cc, source file cc_dist_library_example/b.cc, ]), textual_hdrs depset([]), )注意示例中:c虽然在b的 deps 里却不进入cc_dist_library的清单——这正引出了下一个关键设计点。文件列表 Aspect 不传递do not propagate与大多数 Bazel 规则类型的一个重大区别是文件列表 Aspect 不传递——只暴露直接依赖的源文件而非传递transitive依赖的源文件。文档给出了两条理由直接依赖概念简单若考虑传递依赖就需要某种机制排除不应进入最终库的依赖例如//:protobuf的分布库可以定义为不包含全部//:protobuf_lite。依赖剪除是个有趣的设计问题但 protobuf 库足够小直接列明依赖即可只处理直接依赖能对什么进入组合库获得更细粒度的控制例如 Starlark 的select()可以为部分构建条件性加入某些细粒度库。这一点在源码中可以直接印证cc_dist_library的实现里_collect_inputspkg/cc_dist_library.bzl对每个 dep 取CcFileList中由 Aspect 沿deps收集好的 depset 再合并而链接输入收集函数_collect_linker_input_objects中有一段明确的注释逻辑if link_input.owner ! dep_label: continue # This is a transitive dep: skip itL19-L22即传递依赖的.o文件被显式跳过。此外cc_dist_library还提供dist_deps属性用于显式排除某些cc_dist_library依赖实现上是_subtract_filespkg/cc_dist_library.bzl对deps与dist_deps收集到的文件集合做差集。在 Protobuf 实际使用中大量出现例如 pkg/BUILD.bazel 中protoc的分布库声明cc_dist_library( name protoc, dist_deps [ :protobuf, :protobuf_lite, :upb, ], tags [manual], deps [ //src/google/protobuf/compiler:command_line_interface, //src/google/protobuf/compiler/cpp, //src/google/protobuf/compiler/csharp, //src/google/protobuf/compiler/java, //src/google/protobuf/compiler/kotlin, //src/google/protobuf/compiler/objectivec, //src/google/protobuf/compiler/php:php_generator, //src/google/protobuf/compiler/python, //src/google/protobuf/compiler/ruby, //src/google/protobuf/compiler/rust, ], )即protoc组合库只含编译器各代码生成器而把protobuf/protobuf_lite/upb三个运行时整体排除在外。cc_test的配置冲突陷阱文档还记录了一个由 Bazel 内部机制导致的测试微妙问题Bazel 内部对cc_test与cc_dist_library求值时使用略有不同的配置。如果cc_test目标被放入cc_dist_library规则且两者都被 Bazel 求值可能触发构建期错误——测试所用配置包含告诉 Bazel 如何执行测试的额外选项而cc_file_list_aspect的构建配置没有这些选项Bazel 会将其检测为两个冲突的动作生成相同的输出。最简单的 workaround 是cc_test规则通过filegroup或类似手段提供源文件。文件列表生成gen_cmake_file_lists输入文件列表由 Bazel 生成格式可导入其他构建系统目前只支持生成 CMake 风格的文件。列表来源于 Bazel 构建目标允许的来源类型包括cc_dist_library规则如上所述proto_library规则单个文件filegroup规则rules_pkg 项目中的pkg_files或pkg_filegroup规则。文档中的完整示例$ cat gen_file_lists_example/BUILD.bazel load(protobuf//bazel:proto_library.bzl, proto_library) load(//pkg:build_systems.bzl, gen_cmake_file_lists) filegroup( name doc_files, srcs [ README.md, english_paper.md, ], ) proto_library( name message, srcs [message.proto], ) gen_cmake_file_lists( name source_lists, out source_lists.cmake, src_libs { :doc_files: docs, :message: buff, //cc_dist_library_example:c: distlib, }, ) $ bazel build gen_file_lists_example:source_lists $ cat bazel-bin/gen_file_lists_example/source_lists.cmake # Auto-generated by //gen_file_lists_example:source_lists # # This file contains lists of sources based on Bazel rules. It should # be included from a hand-written CMake file that defines targets. # # Changes to this file will be overwritten based on Bazel definitions. if(${CMAKE_VERSION} VERSION_GREATER 3.10 OR ${CMAKE_VERSION} VERSION_EQUAL 3.10) include_guard() endif() # //gen_file_lists_example:doc_files set(docs_files gen_file_lists_example/README.md gen_file_lists_example/english_paper.md ) # //gen_file_lists_example:message set(buff_proto_srcs gen_file_lists_example/message.proto ) # //gen_file_lists_example:message set(buff_srcs gen_file_lists_example/message.proto.pb.cc ) # //gen_file_lists_example:message set(buff_hdrs gen_file_lists_example/message.proto.pb.h ) # //gen_file_lists_example:message set(buff_files gen_file_lists_example/message-descriptor-set.proto.bin ) # //cc_dist_library_example:c set(distlib_srcs cc_dist_library_example/a.cc cc_dist_library_example/b.cc ) # //cc_dist_library_example:c set(distlib_hdrs )随后手写的 CMake 构建文件只需include该生成文件并用变量定义库include(source_lists.cmake) add_library(distlib ${distlib_srcs} ${buff_srcs})生成逻辑的源码视角gen_cmake_file_lists规则与生成逻辑在 pkg/build_systems.bzl。几个值得注意的实现细节通用实现_create_file_list_implL120-L228是一个工厂接受一个fragment 生成器函数作为参数由调用方决定输出采用何种构建系统语法CMake 的_cmake_var_fragment只生成一段set(varname ...)文本。文档Protobuf usage一节提到为新构建系统加规则类型时说的fragment generator就是指这一层抽象src_libs是{target: libname[,gencode_dir]}形式的 label 键控字符串字典libname用于构造输出文件中变量名前缀C 规则生成{libname}_srcs与{libname}_hdrs后者含textual_hdrsproto_library额外生成{libname}_proto_srcs且_srcs/_hdrs是合成路径pkg_files/pkg_filegroup使用目标路径destination pathsource_prefix属性为每个路径添加前缀。仓库里的宏gen_file_listspkg/build_systems.bzl默认使用${protobuf_SOURCE_DIR}/作为前缀并额外包一层filegroup方便引用生成的文件头部自带include_guard()要求 CMake 3.10保证同一列表文件不会被重复包含。Protobuf 仓库中的真实生成产物上述机制在仓库中的真实产物是 src/file_lists.cmake——一个提交进版本库的生成文件首行即标注# Auto-generated by //pkg:gen_src_file_lists_cmake内容形如# //pkg:protobuf set(libprotobuf_srcs ${protobuf_SOURCE_DIR}/src/google/protobuf/any.pb.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/api.pb.cc ... )而手写的 CMake 定义消费这些变量cmake/libprotobuf.cmake 先include(${protobuf_SOURCE_DIR}/src/file_lists.cmake)再用${libprotobuf_srcs}与${libprotobuf_hdrs}定义add_library(libprotobuf ...)。这正是文档所述手写 CMake 文件引用变量、不直接列文件的落地形态。Protobuf 自身的用法与维护流程主 C 运行时lite 与 full和 Protobuf 编译器都通过对应的cc_dist_library规则生成文件列表proto_library目标由文件列表生成器直接提取源文件其他目标特别是cc_test则通过filegroup规则提供文件。Protobuf 仓库中真正的清单映射在 pkg/BUILD.bazel 的gen_file_lists(name gen_src_file_lists, ...)中src_libs覆盖了:protobuf、:protobuf_lite、:protoc、:upb、各protoc-gen-upb*生成器、descriptor/plugin 等 proto 目标以及test_util、common_test、conformance_cpp等测试库——每个条目都形如:protoc: libprotocBazel 目标 → 生成文件中的变量名。文档同时给出了通用维护指南把新目标加入非 Bazel 构建系统或引入全新构建系统需要的一次性设置定义新构建系统的整体结构。它应当导入文件列表、以变量方式引用文件而不是直接罗列文件仅当构建系统全新时在 pkg/build_systems.bzl即//pkg:build_systems.bzl中添加新的规则类型。大部分实现是共享的但需要一个fragment generator来声明文件列表变量规则类型本身需要定义并调用共享实现。文件增删或 Bazel 结构变化时的典型场景向已有cc_library的srcs添加或移除文件无需改动。只要该cc_library已属于某个cc_dist_library重新生成源列表即会反映变化新增cc_library可能需要把新目标加入 Protobuf 的cc_dist_library目标视情况而定删除cc_library若某个cc_dist_library依赖被删目标会导致构建期错误需要把该库从cc_dist_library中移除新增或删除cc_test测试源文件由与cc_test同包内定义的filegroup规则处理这些filegroup通常命名为test_srcs常使用glob()查找源文件。因此增删测试通常不需要额外工作但应在测试规则所在的包内确认新增仅测试用的 proto 文件可能需要在 pkg/BUILD.bazel 的文件列表映射中加入该proto_library再把文件加入各构建系统。不过多数仅测试用的 proto 已经通过//src/google/protobuf:test_protos之类的库暴露。生成后的回写步骤如果发生了变更需要把重新生成的文件列表复制回仓库。这样对应的构建系统可以直接在 git checkout 上使用而无需先运行 Bazel——这正是 src/file_lists.cmake 被提交进版本库的原因。附发布归档Distribution Archives//pkg目录下还定义了功能非常相似的一组规则用于构建发布用的源码发布归档。除完整源码外Protobuf 发布还包含按语言切片的源码归档例如 Ruby 项目可以只拿到构建 Ruby 运行时所需的源文件每个语言切片都包含构建 protobuf 编译器所需的源因此实际上都包含 C 运行时。这些归档使用 rules_pkg 项目的规则定义。虽然与cc_dist_library和文件列表生成规则相似但目标不同上文构建系统文件列表只针对 C按什么应/不应进入构建的哪一部分组织例如主库不包含测试发布归档则处理 C 之外的语言包含发布需要分发的全部文件对 C 而言也多于 C 源文件本身。理论上可以用CcFileList和ProtoFileListProvider 的信息定义发布文件但发布归档还需要额外文件如各种BUILD.bazel文件。无论如何发布文件列表通常都可以用glob()生成因此与文件列表 Aspect 共享逻辑未必有收益。目前所有文件列表都是提交入库的不过也可以改为在构建时动态生成并直接放进发布归档而不再提交。小结Protobuf 的多构建系统支持本质上是一套构建元数据同步方案Bazel 是唯一事实来源——文件清单从 Bazel 规则经cc_file_list_aspect/file_list_aspect两个 Aspect 与CcFileList/ProtoFileList两个 Provider 提取cc_dist_library将细粒度cc_library合并为与 CMake 构建粒度对齐的分布库同时提供源文件列表且通过不传递 dist_deps差集精确控制成员范围gen_cmake_file_lists及其工厂式实现把手工 CMake 与 Bazel 解耦CMake 只写include()和变量引用清单内容全部自动生成并回写仓库如 src/file_lists.cmake使纯 git checkout 即可脱离 Bazel 构建维护成本被压缩为一次性搭骨架 增量场景对照检查文档中给出的场景清单增删cc_library、cc_test、测试 proto就是日常维护的操作手册。对阅读者而言理解这套机制的价值在于当你想修改 Protobuf 的 CMake 构建、新增构建目标甚至为其他多构建系统项目设计类似的单一事实来源方案时可以直接参考 pkg/ 下的规则定义与 pkg/BUILD.bazel 中的真实配置而非在两套构建系统间手工同步文件清单。【免费下载链接】protobufProtocol Buffers - Googles data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobuf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表