Cargo 与 Bazel:两种构建系统在 Rust 大型项目中的取舍分析
一、Cargo:够用就好
Cargo 是 Rust 的官方构建系统,对纯 Rust 项目来说几乎完美:
# Cargo.toml —— 简洁直观的配置
[package]
name = "my-service"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# 一个 cargo build 搞定一切
Cargo 的优势很明显:
- 零配置入门:
cargo new就能开始写代码。 - crates.io 生态:依赖管理自动、版本解析可靠。
- 项目结构约定:
src/main.rs、src/lib.rs等约定让所有 Rust 项目看起来一样。 - 增量编译:
cargo build自动只编译修改过的部分。
但 Cargo 也有局限:
- 只懂 Rust:无法管理 C++、Python、Protobuf 等非 Rust 代码。
- 单仓库支持弱:monorepo 场景下,跨 crate 的依赖处理不够灵活。
- 缓存粒度粗:没有分布式构建缓存(虽然有 sccache,但不是内建的)。
二、Bazel:为大型多语言项目而生
Bazel 是 Google 开源的构建系统,支持几十种语言,核心卖点是多语言、增量编译、分布式缓存。
一个典型的 Bazel Rust 项目结构如下:
# WORKSPACE —— 定义外部依赖和工作空间
workspace(name = "my_project")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# 导入 Rust 规则
http_archive(
name = "rules_rust",
sha256 = "...",
urls = ["https://github.com/bazelbuild/rules_rust/releases/download/..."],
)
load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies")
rules_rust_dependencies()
# 导入 crates.io 依赖(Bazel 也可以拉 Rust 的包)
load("@rules_rust//crate_universe:defs.bzl", "crates_repository")
crates_repository(
name = "crates_index",
cargo_lockfile = "//:Cargo.lock", # 复用 Cargo.lock
manifests = ["//:Cargo.toml"],
)
# BUILD.bazel —— 定义构建目标(每个目录一个 BUILD 文件)
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
# 定义 Rust 库
rust_library(
name = "my_lib",
srcs = ["src/lib.rs"],
deps = [
"@crates_index//:tokio", # 引用外部 crate
"@crates_index//:serde",
],
visibility = ["//visibility:public"], # 控制可见性
)
# 定义 Rust 二进制
rust_binary(
name = "my_service",
srcs = ["src/main.rs"],
deps = [
":my_lib", # 引用同项目的库
"//proto:greeter_rust_proto", # 引用 Protobuf 生成的 Rust 代码
"@crates_index//:tonic",
],
)
Bazel 的真正威力在多语言项目中体现:
# proto/BUILD.bazel —— 一个目录同时管理多种语言的生成代码
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_rust//proto:defs.bzl", "rust_proto_library")
# Protobuf 定义(语言无关)
proto_library(
name = "greeter_proto",
srcs = ["greeter.proto"],
visibility = ["//visibility:public"],
)
# 自动生成 Rust 代码
rust_proto_library(
name = "greeter_rust_proto",
deps = [":greeter_proto"],
)
# 自动生成 Python 代码
load("@rules_python//python:defs.bzl", "py_proto_library")
py_proto_library(
name = "greeter_py_proto",
deps = [":greeter_proto"],
)
三、核心取舍对比
我把两种构建系统在大型项目中的关键差异总结如下:
| 维度 | Cargo | Bazel | 大项目影响 |
|---|---|---|---|
| 学习曲线 | 低(30 分钟入门) | 高(需要学 Starlark、BUILD 语法) | 团队培训成本 |
| 配置复杂度 | Cargo.toml 一个文件 | WORKSPACE + 每个目录 BUILD.bazel | 维护负担 |
| 增量编译 | 自动(crate 粒度) | 精确(文件级别,更细粒度) | CI 时间 |
| 分布式缓存 | 需要 sccache | 内建 Remote Cache + Remote Execution | 编译速度 |
| 多语言支持 | 仅 Rust | Rust/C++/Python/Java/Go/Proto… | 跨语言项目必需 |
| 外部依赖 | crates.io (版本好管理) | 需要手动声明或通过 crate_universe | 依赖管理复杂度 |
| IDE 支持 | rust-analyzer 完美 | 需要生成 Cargo.toml 代理 | 开发体验 |
| CI 集成 | 简单(cargo test) | 复杂(bazel 配置 + 缓存服务) | CI 维护成本 |
四、什么时候该切换到 Bazel?
我的判断标准很简单,满足以下任意两条就应该考虑 Bazel:
- 项目超过 3 种编程语言:如果你在写 Rust 服务 + C++ FFI + Python 脚本 + Protobuf 定义,Cargo 管不了这些。
- 团队超过 10 个开发者:Bazel 的可见性控制(
visibility)、构建目标粒度和缓存共享对于大团队更有价值。 - CI 编译时间超过 10 分钟:Bazel 的分布式缓存(Remote Cache)可以让第二次及以后的全量构建变成几乎即时的。
- 单仓库(monorepo)中有多个独立服务:Bazel 的精确依赖分析能确保"只重新编译真正需要的东西"。
如果你只是一个 Rust 项目,没有其他语言,团队不超过 5 个人,别折腾 Bazel——Cargo 完全够用,换 Bazel 反而会增加维护负担。
实际项目中,如果你已经用 Cargo 开发了一段时间想迁移到 Bazel,可以借助 rules_rust 的 crate_universe 直接读取现有的 Cargo.toml 和 Cargo.lock,减少迁移成本:
# 直接读取 Cargo.toml 中的依赖,生成 Bazel 的 BUILD 文件
crates_repository(
name = "crates_index",
cargo_lockfile = "//:Cargo.lock",
manifests = ["//:Cargo.toml", "//sub_crate:Cargo.toml"],
)
实际迁移时还踩过一个坑:crate_universe 生成的 BUILD 文件不包含 build.rs 的编译产物,导致依赖 OpenSSL 的 crate 在 Bazel 下直接编译失败,需要手动补 cargo_build_script 规则。
五、总结
- Cargo——Rust 专属、简单直观、生态完善。纯 Rust 项目首选。
- Bazel——多语言、增量精确、分布式缓存。大团队多语言项目的最佳选择。
- 迁移成本——如果未来可能上 Bazel,建议一开始就把
Cargo.toml和目录结构调整好,减少后续迁移的工作量。 - 混合使用——也可以在 Bazel 项目中让 Bazel 管理外部依赖,内部 Rust 代码继续用
cargo build开发(通过 Bazel 的 genrule 桥接)。
作为程序员,接触 Bazel 的过程确实比 Cargo 痛苦很多——Starlark 语法、WORKSPACE 配置、BUILD 文件规则……每一个概念都值得专门写一篇文章。但理解了它的设计理念后,不得不承认:在大型多语言项目中,Bazel 的复杂度是有道理的,因为它解决的问题本身就是复杂的。

35

被折叠的 条评论
为什么被折叠?



