Bazel never fails to impress, and its support for Rust demonstrates its versatility and commitment to modern development. Two distinct dependency management modes—Cargo—based and pure Bazel—allow developers to tailor workflows to their projects’ needs. This adaptability is particularly valuable for integrating Rust applications into monorepos or scaling complex systems.
I decided to explore how Bazel supports Rust, including managing dependencies, migrating from
Harnessing Cargo-Based Dependency Management
Bazel’s ability to integrate with Cargo, Rust’s native package manager, is a standout feature. This approach preserves compatibility with the Rust ecosystem while allowing projects to benefit from Bazel’s powerful build features. By using rules_rust, a Bazel module can seamlessly import dependencies defined in
It was pretty easy to feed dependency information from Cargo to Bazel. The only tricky part is Cargo workspaces. If you use a Cargo workspace like I do, you should list all
crate.from_cargo(
name = "crate_index",
cargo_lockfile = "//:Cargo.lock",
manifests = [
"//:Cargo.toml",
"//:echo/Cargo.toml",
"//:tests/Cargo.toml",
],
)
use_repo(crate, "crate_index")
This setup allows Bazel to parse and manage dependencies from multiple
Integrating with Cargo in this way provides the best of both worlds. Developers can continue using Rust’s native tooling for development while leveraging Bazel for its scalability and advanced dependency management. Although this dual-system approach adds some complexity, the flexibility it offers is invaluable for scaling projects and ensuring compatibility with the broader Rust ecosystem.
Migrating from Cargo.toml to BUILD.bazel
The next step after making a proper
name = "echo"
version = "0.1.0"
edition = "2021"
[[bin]]
path = "src/main.rs"
name = "echo"
[dependencies]
async-trait = "0.1.83"
maelstrom-node = "0.1.6"
In Bazel, each
rust_binary(
name = "echo",
srcs = ["src/main.rs"],
proc_macro_deps = [
"@crate_index//:async-trait",
],
deps = [
"@crate_index//:maelstrom-node",
],
)
This shift ensures that Bazel can handle the entire build lifecycle, from compiling dependencies to linking final binaries. While
Simplifying Integration Testing with Bazel
Integration testing is another area where Bazel simplifies workflows compared to Cargo. In Rust’s native system, it’s not straightforward to ensure that an application is built before a test requiring that application is executed. With Bazel, this process becomes effortless.
Consider a
rust_library(
name = "utils",
srcs = [
"utils/lib.rs",
"utils/paths.rs",
"utils/runner.rs",
],
)
rust_test(
name = "test_echo",
size = "small",
srcs = ["echo/test_echo.rs"],
data = [
":maelstrom/lib/maelstrom.jar",
],
deps = [
":utils",
"//echo",
],
)
This configuration defines a
It took some Googling to figure out how to access external resources like
PathBuf::from(env::var_os("RUNFILES_DIR").unwrap()).join("_main")
}
pub fn maelstrom_dir() -> PathBuf {
bazel_runfiles_dir()
.join("tests")
.join("maelstrom")
.join("lib")
}
Bazel’s approach eliminates the need for custom scripts or workarounds, streamlining the test lifecycle and ensuring reliable, reproducible results. This simplicity becomes increasingly important as projects grow, where manual processes can hinder productivity and introduce inconsistencies.