乐于分享
好东西不私藏

Bazel C++ 构建系列文档(四):依赖管理与第三方库

Bazel C++ 构建系列文档(四):依赖管理与第三方库

1. Bazel 依赖管理概述

C++ 项目通常依赖大量第三方库(如 protobuf、gtest、abseil、boost 等)。Bazel 提供了多种方式来管理这些依赖:

方式
适用场景
推荐程度
Bzlmod
(MODULE.bazel)
Bazel 6+ 新项目
⭐⭐⭐⭐⭐
WORKSPACE
传统方式,兼容旧项目
⭐⭐⭐
本地路径引用
子项目/monorepo
⭐⭐⭐⭐
Git 子模块 + local_repository
需要 vendor 模式
⭐⭐

2. Bzlmod:现代依赖管理(推荐)

Bzlmod 是 Bazel 6.0 引入的新依赖管理系统,通过 MODULE.bazel 文件声明模块依赖,替代传统的 WORKSPACE 方式。

2.1 为什么选择 Bzlmod?

WORKSPACE 方式的痛点                Bzlmod 的改进───────────────────────           ────────────────────依赖传递需要手动传播              自动传递依赖(像 npm/cargo)依赖冲突难以解决                  支持版本解析(MVS 算法)宏污染全局命名空间                模块化隔离依赖声明冗长复杂                  简洁声明式无法区分开发依赖和运行依赖        支持 dev_dependency

2.2 启用 Bzlmod

在 .bazelrc 中添加:

common --enable_bzlmod

2.3 MODULE.bazel 基础语法

# MODULE.bazelmodule(    name = "my_project",    version = "1.0.0",    compatibility_level = 1,)# 声明依赖bazel_dep(name = "googletest", version = "1.14.0")bazel_dep(name = "abseil-cpp", version = "20230802.1")bazel_dep(name = "protobuf", version = "24.4")# 开发依赖(仅当前模块使用,不传播给依赖者)bazel_dep(name = "rules_proto", version = "5.3.0", dev_dependency = True)

2.4 关键概念

  • 模块(Module)
    :一个可被 Bazel 项目复用的功能单元,有唯一的名称和版本
  • 模块扩展(Module Extension)
    :用于引入非 Bazel 原生仓库的机制
  • 仓库(Repo)
    :模块提供的可被 deps 引用的外部仓库
  • 兼容性级别(Compatibility Level)
    :语义版本之外的破坏性变更标记

2.5 完整示例

# MODULE.bazelmodule(    name = "my_cpp_project",    version = "2.0.0",    compatibility_level = 1,)# ── 核心依赖 ────────────────────────────bazel_dep(name = "abseil-cpp", version = "20230802.1")bazel_dep(name = "protobuf", version = "24.4")# ── 测试依赖 ────────────────────────────bazel_dep(name = "googletest", version = "1.14.0", dev_dependency = True)bazel_dep(name = "benchmark", version = "1.8.3", dev_dependency = True)# ── 规则依赖 ────────────────────────────bazel_dep(name = "rules_proto", version = "5.3.0")bazel_dep(name = "rules_cc", version = "0.0.9")

BUILD 文件中使用:

# src/BUILDcc_library(    name = "string_utils",    srcs = ["string_utils.cc"],    hdrs = ["string_utils.h"],    deps = [        "@abseil-cpp//absl/strings",       # 通过 Bzlmod 引用        "@protobuf//:protobuf",    ],)cc_test(    name = "string_utils_test",    srcs = ["string_utils_test.cc"],    deps = [        ":string_utils",        "@googletest//:gtest_main",          # 测试依赖    ],)

3. WORKSPACE:传统依赖管理

虽然 Bzlmod 是推荐方式,但许多现有项目仍在使用 WORKSPACE 方式,理解它仍然重要。

3.1 http_archive — 下载发布包

最常用的依赖引入方式,从 URL 下载压缩包:

# WORKSPACEload("@bazel_tools//tools/build_defs/repo:http.bzl""http_archive")# Google Testhttp_archive(    name = "com_google_googletest",    urls = [        "https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz",    ],    strip_prefix = "googletest-1.14.0",    sha256 = "8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7",)# Abseilhttp_archive(    name = "com_google_absl",    urls = [        "https://github.com/abseil/abseil-cpp/archive/refs/tags/20230802.1.tar.gz",    ],    strip_prefix = "abseil-cpp-20230802.1",    sha256 = "f50e5ac3a4e287a2286637d218e9bb3e2b0307c5d2a4e7c3ef8c6694f1c93c8f",)# Protocol Buffershttp_archive(    name = "com_google_protobuf",    urls = [        "https://github.com/protocolbuffers/protobuf/archive/refs/tags/v24.4.tar.gz",    ],    strip_prefix = "protobuf-24.4",    sha256 = "6d0ebd6a8a15ea7564eb7c6ea4a2e4e210a5ef5c0e3c2d054c7e9cc0e1b2f5e3",)

关键参数说明

参数
说明
name
仓库名称,通过 @name// 引用
urls
下载地址列表(支持镜像)
sha256
校验和,确保下载内容一致
strip_prefix
解压后去除的前缀目录
type
压缩格式(自动检测,通常不需要)
patches
补丁文件列表
patch_args
补丁参数

3.2 git_repository — 从 Git 仓库获取

load("@bazel_tools//tools/build_defs/repo:git.bzl""git_repository")git_repository(    name = "com_github_mylib",    remote = "https://github.com/example/mylib.git",    branch = "main",    # 或使用特定的 commit/tag    # commit = "a1b2c3d4e5f6...",    # tag = "v1.2.3",)# 也可以使用 new_git_repository(需要指定 BUILD 文件)new_git_repository(    name = "com_github_simplelib",    remote = "https://github.com/example/simplelib.git",    tag = "v2.0.0",    build_file = "//third_party:simplelib.BUILD",      # 指定 BUILD 文件    # 或使用 build_file_content    # build_file_content = """cc_library(name = "simplelib", ...)""",)

最佳实践:优先使用 http_archive 而不是 git_repository,因为前者更快速且可重现。

3.3 local_repository — 引用本地项目

load("@bazel_tools//tools/build_defs/repo:local.bzl""local_repository")# 引用本地另一个 Bazel 项目local_repository(    name = "my_local_lib",    path = "../my-local-lib",   # 相对于 WORKSPACE 的路径)

3.4 传递依赖问题

WORKSPACE 方式的最大痛点是传递依赖需要手动声明。如果库 A 依赖库 B,在 WORKSPACE 中必须同时声明两者:

# ❌ 错误:只声明了 A,缺少 Bhttp_archive(name = "com_example_A", ...)# ✅ 正确:必须同时声明 A 的所有传递依赖http_archive(name = "com_example_B", ...)http_archive(name = "com_example_A", ...)

这也是 Bzlmod 被推荐的主要原因——它自动处理传递依赖。


4. 为非 Bazel 项目编写 BUILD 文件

很多 C++ 第三方库并不自带 BUILD 文件。Bazel 提供了 new_http_archive 和 build_file 来解决这个问题。

4.1 使用 new_http_archive + build_file

load("@bazel_tools//tools/build_defs/repo:http.bzl""http_archive")http_archive(    name = "spdlog",    urls = [        "https://github.com/gabime/spdlog/archive/refs/tags/v1.12.0.tar.gz",    ],    strip_prefix = "spdlog-1.12.0",    sha256 = "4dccf2d10f410c1e2c73a028c7a0c4d5c527e7fd0c7bedb4b3f47d2a1cb5b2f0",    build_file = "//third_party:spdlog.BUILD",   # 指定自定义 BUILD 文件)

4.2 编写第三方库的 BUILD 文件

创建 third_party/spdlog.BUILD

# third_party/spdlog.BUILDpackage(default_visibility = ["//visibility:public"])cc_library(    name = "spdlog",    hdrs = glob([        "include/spdlog/**/*.h",    ]),    includes = ["include"],    defines = [        "SPDLOG_COMPILED_LIB",        "FMT_HEADER_ONLY=1",    ],    deps = [        "@fmt",  # 如果 spdlog 依赖 fmt 库    ],)

4.3 常见 C++ 库的 BUILD 文件模板

Header-only 库

cc_library(    name = "header_only_lib",    hdrs = glob(["include/**/*.h"]),    includes = ["include"],    visibility = ["//visibility:public"],)

源码库

cc_library(    name = "source_lib",    srcs = glob(["src/**/*.cc"]),    hdrs = glob(["include/**/*.h"]),    includes = ["include"],    visibility = ["//visibility:public"],)

带配置的库

cc_library(    name = "configured_lib",    srcs = glob(["src/**/*.cc"]) + ["config.h"],  # config.h 由 genrule 生成    hdrs = glob(["include/**/*.h"]),    includes = ["include""."],                   # "." 用于找到 config.h    defines = ["HAVE_CONFIG_H=1"],    copts = ["-w"],                                # 抑制第三方库的警告    visibility = ["//visibility:public"],)

5. 使用 Bzlmod 的模块扩展引入非 Bazel 依赖

Bzlmod 通过模块扩展来引入不支持 Bazel 的第三方依赖。这是 WORKSPACE 方式在新体系下的替代方案。

5.1 基本结构

my-project/├── MODULE.bazel├── extensions.bzl          ← 模块扩展定义├── third_party/│   ├── spdlog.BUILD        ← 第三方库 BUILD 文件│   └── fmt.BUILD└── ...

5.2 定义模块扩展

创建 extensions.bzl

# extensions.bzlload("@bazel_tools//tools/build_defs/repo:http.bzl""http_archive")def _third_party_impl(module_ctx):    # 所有使用此扩展的模块声明的依赖    for mod in module_ctx.modules:        for dep in mod.tags.http_archive:            http_archive(                name = dep.name,                urls = dep.urls,                strip_prefix = dep.strip_prefix,                sha256 = dep.sha256,                build_file = dep.build_file,            )# 定义标签(tag)用于声明依赖_http_archive_tag = tag_class(    attrs = {        "name": attr.string(mandatory = True),        "urls": attr.string_list(mandatory = True),        "strip_prefix": attr.string(),        "sha256": attr.string(),        "build_file": attr.label(),    },)third_party = module_extension(    implementation = _third_party_impl,    tag_classes = {        "http_archive": _http_archive_tag,    },)

5.3 在 MODULE.bazel 中使用扩展

# MODULE.bazelmodule(name = "my_project", version = "1.0.0")# 声明模块扩展third_party = use_extension("//:extensions.bzl""third_party")# 通过扩展引入第三方依赖third_party.http_archive(    name = "spdlog",    urls = ["https://github.com/gabime/spdlog/archive/refs/tags/v1.12.0.tar.gz"],    strip_prefix = "spdlog-1.12.0",    sha256 = "4dccf2d10f410c1e2c73a028c7a0c4d5c527e7fd0c7bedb4b3f47d2a1cb5b2f0",    build_file = "//third_party:spdlog.BUILD",)# 使用依赖bazel_dep(name = "googletest", version = "1.14.0")

6. 常见 C++ 第三方库引入示例

6.1 Google Test / Google Mock

Bzlmod 方式

bazel_dep(name = "googletest", version = "1.14.0")

WORKSPACE 方式

http_archive(    name = "com_google_googletest",    urls = ["https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz"],    strip_prefix = "googletest-1.14.0",    sha256 = "8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7",)

使用

cc_test(    name = "my_test",    srcs = ["my_test.cc"],    deps = [        "//src:my_lib",        "@googletest//:gtest_main",        # Bzlmod        # "@com_google_googletest//:gtest_main",  # WORKSPACE    ],)

6.2 Abseil

Bzlmod 方式

bazel_dep(name = "abseil-cpp", version = "20230802.1")

使用

cc_library(    name = "my_lib",    deps = [        "@abseil-cpp//absl/strings",        "@abseil-cpp//absl/types:optional",        "@abseil-cpp//absl/container:flat_hash_map",    ],)

6.3 Protocol Buffers

Bzlmod 方式

bazel_dep(name = "protobuf", version = "24.4")bazel_dep(name = "rules_proto", version = "5.3.0")bazel_dep(name = "rules_cc", version = "0.0.9")

使用

proto_library(    name = "person_proto",    srcs = ["person.proto"],)cc_proto_library(    name = "person_proto_cc",    deps = [":person_proto"],)cc_library(    name = "person_utils",    deps = [":person_proto_cc"],)

6.4 fmt / spdlog

Bzlmod 方式(如已注册到 BCR):

bazel_dep(name = "fmt", version = "10.1.1")

手动方式

# extensions.bzl 或 WORKSPACEhttp_archive(    name = "fmt",    urls = ["https://github.com/fmtlib/fmt/archive/refs/tags/10.1.1.tar.gz"],    strip_prefix = "fmt-10.1.1",    sha256 = "...此处填入校验和...",)

7. 依赖版本锁定

7.1 锁定文件

Bzlmod 会自动生成锁定文件 MODULE.bazel.lock,确保可重现构建:

# 此文件由 Bazel 自动生成,不要手动编辑# 提交到版本控制中以确保团队构建一致性

7.2 更新依赖

# 更新所有依赖到最新兼容版本bazel clean --expungebazel deps update# 更新特定依赖bazel deps update --repo=@abseil-cpp

7.3 检查依赖树

# 查看依赖树bazel query "deps(//src:my_lib)" --output=label# 查看外部依赖bazel query "kind(http_archive, //external:*)"# 使用 Bzlmod 查看模块依赖图bazel mod graphbazel mod show_repo @abseil-cpp

8. 依赖管理最佳实践

8.1 通用原则

✅ 推荐做法                                  ❌ 避免做法─────────────────────────                  ─────────────────────────优先使用 Bzlmod                             继续新建 WORKSPACE 项目始终指定 sha256 校验和                       使用不可信来源的依赖固定版本号(tag/commit)                     使用浮动版本(branch)将锁定文件提交到版本控制                      忽略 MODULE.bazel.lock统一团队依赖版本                             各自使用不同版本使用镜像 URL                                只使用单一源

8.2 第三方库 BUILD 文件管理

third_party/├── BUILD                    # 空文件或 exports├── spdlog.BUILD             # spdlog 的构建定义├── fmt.BUILD                # fmt 的构建定义└── README.md                # 第三方依赖说明

为每个第三方库维护独立的 BUILD 文件:

  • 记录版本号和来源
  • 记录引入的补丁和修改
  • 保持 BUILD 文件简洁

8.3 依赖隔离

# 通过 visibility 控制第三方依赖的传播范围# 第三方库 BUILDcc_library(    name = "spdlog",    hdrs = glob(["include/**/*.h"]),    includes = ["include"],    visibility = ["//visibility:public"],)# 项目内部封装cc_library(    name = "logger",              # 封装 spdlog,对外只暴露 logger    hdrs = ["logger.h"],    deps = ["@spdlog"],           # 内部使用 spdlog    visibility = ["//visibility:public"],)

9. 小结

本篇详细介绍了 Bazel 的依赖管理:

  • ✅ Bzlmod 现代依赖管理系统(推荐)
  • ✅ WORKSPACE 传统依赖管理方式
  • ✅ http_archive、git_repository、local_repository
  • ✅ 为非 Bazel 项目编写 BUILD 文件
  • ✅ Bzlmod 模块扩展机制
  • ✅ 常见 C++ 第三方库引入示例
  • ✅ 依赖版本锁定与最佳实践