Bazel — Hermetic, Reproducible Builds at Scale
Bazel reference for C++ developers: BUILD files, cc_binary/cc_library rules, remote caching, hermetic builds, and monorepo workflows.
TL;DR
Bazel is a build system designed for correctness and scale. Every build is hermetic (sandboxed, no ambient system tools leak in) and reproducible (same inputs always produce the same outputs). Results are cached by content hash — locally and optionally on a shared remote cache — so you never rebuild what is already built.
# Install bazelisk (recommended)
npm install -g @bazel/bazelisk # or: go install github.com/bazelbuild/bazelisk@latest
# Pin the Bazel version for the repo
echo "7.4.1" > .bazelversion
# Hello world
bazel build //...
bazel run //src:appBazel shines in monorepos, polyglot projects (C++, Java, Python, Go, Rust in one tree), and CI pipelines that benefit from remote execution and caching. For small, C++-only projects that need to ship as a library, CMake or Meson is usually simpler.
Installation
bazelisk — the recommended wrapper
bazelisk reads .bazelversion in your repo root and downloads the exact Bazel binary automatically. You never manually manage Bazel versions.
# via npm
npm install -g @bazel/bazelisk
# via Go
go install github.com/bazelbuild/bazelisk@latest
# via Homebrew (macOS/Linux)
brew install bazelisk
# Direct binary (Linux x86_64)
curl -fsSL https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64 \
-o /usr/local/bin/bazel
chmod +x /usr/local/bin/bazelAfter install, bazel is provided by bazelisk. To pin the version:
echo "7.4.1" > .bazelversion
bazel version # downloads and caches the pinned version on first runDirect Bazel binary
Download from github.com/bazelbuild/bazel/releases and place on $PATH. This is less convenient — prefer bazelisk.
Core Concepts
Workspace
The workspace is the top-level directory containing your source tree. Bazel recognizes it by:
MODULE.bazel— Bzlmod (Bazel 6+, recommended). Declares the module name and external dependencies.WORKSPACEorWORKSPACE.bazel— legacy workspace file. Still works; gradually being replaced by Bzlmod.
# MODULE.bazel (Bzlmod — recommended for Bazel 6+)
module(
name = "myproject",
version = "0.1.0",
)
bazel_dep(name = "googletest", version = "1.14.0")
bazel_dep(name = "abseil-cpp", version = "20240116.0")Packages
A package is any directory in the workspace that contains a BUILD or BUILD.bazel file. The BUILD.bazel filename takes precedence when both exist.
myproject/
├── MODULE.bazel
├── .bazelversion
├── .bazelrc
├── src/
│ ├── BUILD.bazel ← package: //src
│ ├── main.cpp
│ ├── mylib.cpp
│ └── mylib.h
└── tests/
├── BUILD.bazel ← package: //tests
└── mylib_test.cppTargets
A target is a named, buildable unit declared inside a BUILD.bazel file. Targets are addressed by their label:
//path/to/package:target_name
//src:app ← binary named "app" in the //src package
//src:mylib ← library named "mylib" in the //src package
//tests:mylib_test ← test target in //testsShorthand: //src:src can be written //src (when target name matches directory name).
Hermeticity
Bazel sandboxes every action: only explicitly declared inputs are visible. If your cc_binary forgot to list a header in hdrs, the build fails — not silently picks it up from /usr/include. This is the core guarantee that makes remote caching correct.
BUILD File Basics
# src/BUILD.bazel
cc_binary(
name = "app",
srcs = ["main.cpp"],
deps = [":mylib"],
)
cc_library(
name = "mylib",
srcs = ["mylib.cpp"], # implementation files — private
hdrs = ["mylib.h"], # public headers exposed to dependents
visibility = ["//visibility:public"],
)# tests/BUILD.bazel
cc_test(
name = "mylib_test",
srcs = ["mylib_test.cpp"],
deps = [
"//src:mylib",
"@googletest//:gtest_main",
],
size = "small", # test timeout: small=60s, medium=300s, large=900s
)cc_library attributes
| Attribute | Purpose |
|---|---|
srcs | .cpp / .cc files compiled as part of this library |
hdrs | Public headers; added to the include path of dependents |
deps | Other targets this library depends on |
copts | Extra compiler flags for this target only |
linkopts | Extra linker flags (prefer deps when possible) |
visibility | Who may depend on this target |
includes | Extra include paths (use sparingly; prefer hdrs) |
defines | -D preprocessor definitions |
cc_binary attributes
Same as cc_library plus:
| Attribute | Purpose |
|---|---|
srcs | Source files and main() |
malloc | Override the malloc implementation |
stamp | Embed build info (VCS revision, timestamp) |
Building and Testing
# Build a specific target
bazel build //src:app
# Build everything in the workspace
bazel build //...
# Build everything under a subtree
bazel build //src/...
# Run a binary directly
bazel run //src:app
# Run with arguments (everything after -- is passed to the binary)
bazel run //src:app -- --port=8080 --verbose
# Run all tests
bazel test //...
# Run a specific test target
bazel test //tests:mylib_test
# Run tests matching a filter (GoogleTest --gtest_filter syntax)
bazel test //tests:mylib_test --test_filter="FooTest.*"
# Show test output on failure (default: suppressed)
bazel test //... --test_output=errors
# Show all test output
bazel test //... --test_output=allClean
# Remove the output directory for the current workspace
bazel clean
# Remove the entire Bazel cache (output base) — use sparingly
bazel clean --expungebazel clean is rarely needed. Because builds are hermetic and cached by content hash, stale artifacts are not a problem in normal usage.
C++ Toolchain Configuration
Default toolchain
Bazel auto-detects the system C++ compiler (GCC or Clang). Verify:
bazel build //... --announce_rc # shows resolved flags
bazel info --show_make_env # shows CC, CXX, etc.Per-target compiler flags
cc_library(
name = "mylib",
srcs = ["mylib.cpp"],
hdrs = ["mylib.h"],
copts = [
"-Wall",
"-Wextra",
"-Wno-unused-parameter",
],
)Global flags via .bazelrc
Prefer .bazelrc for flags that apply to all targets (see next section).
Custom toolchain
For cross-compilation or reproducible toolchains (e.g., a hermetic Clang downloaded by Bazel itself), define a cc_toolchain and cc_toolchain_suite. This is an advanced topic — see rules_cc and toolchains_llvm.
# MODULE.bazel — pull in a hermetic LLVM toolchain
bazel_dep(name = "toolchains_llvm", version = "1.0.0")
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
llvm.toolchain(llvm_version = "17.0.6")
use_repo(llvm, "llvm_toolchain")
register_toolchains("@llvm_toolchain//:all")# Use it
bazel build //... --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux.bazelrc — Persistent Flags
.bazelrc lives in the workspace root and is loaded automatically. Use it to set flags that would be tedious to type on every invocation.
# .bazelrc
# Always use C++20
build --cxxopt=-std=c++20
build --host_cxxopt=-std=c++20
# Treat warnings as errors in CI (override locally with --nokeep_going)
build --cxxopt=-Wall
build --cxxopt=-Wextra
# Named configs — activate with --config=<name>
build:asan --cxxopt=-fsanitize=address
build:asan --linkopt=-fsanitize=address
build:asan --cxxopt=-fno-omit-frame-pointer
build:tsan --cxxopt=-fsanitize=thread
build:tsan --linkopt=-fsanitize=thread
build:ubsan --cxxopt=-fsanitize=undefined
build:ubsan --linkopt=-fsanitize=undefined
build:release --compilation_mode=opt
build:debug --compilation_mode=dbg
# Remote cache (see Remote Caching section)
build:remote --remote_cache=grpc://cache.internal:9090
# CI — turn on in CI via --config=ci
build:ci --config=release
build:ci --config=remote
build:ci --remote_upload_local_results=trueActivate a config:
bazel build --config=asan //...
bazel build --config=release //src:app
bazel test --config=ci //...User-local overrides go in ~/.bazelrc (not committed to the repo).
External Dependencies (Bzlmod)
Bzlmod (enabled by default in Bazel 6+) replaces the old WORKSPACE http_archive pattern with a proper module system backed by the Bazel Central Registry.
# MODULE.bazel
module(
name = "myproject",
version = "0.1.0",
)
bazel_dep(name = "googletest", version = "1.14.0")
bazel_dep(name = "abseil-cpp", version = "20240116.0")
bazel_dep(name = "nlohmann_json", version = "3.11.3")
bazel_dep(name = "fmt", version = "10.2.1")
bazel_dep(name = "re2", version = "2024-02-01")Then use in BUILD.bazel:
cc_binary(
name = "app",
srcs = ["main.cpp"],
deps = [
"@abseil-cpp//absl/strings",
"@abseil-cpp//absl/container:flat_hash_map",
"@fmt//:fmt",
"@nlohmann_json//:json",
],
)Tidy the lockfile after editing MODULE.bazel:
bazel mod tidyInspect what version was selected:
bazel mod graph
bazel mod show_repo googletest # print the resolved repository ruleLegacy WORKSPACE (still common)
# WORKSPACE.bazel
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "googletest",
sha256 = "8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7",
strip_prefix = "googletest-1.14.0",
urls = ["https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz"],
)Remote Caching
Remote caching lets every developer and CI machine share a single build cache. An action whose inputs hash to a known key is never re-executed — the outputs are downloaded instead.
Enable remote caching
# .bazelrc
build --remote_cache=grpc://cache.internal:9090
# Google Cloud Storage bucket
build --remote_cache=https://storage.googleapis.com/my-bazel-cache
# Authentication for GCS
build --google_default_credentialsUpload vs read-only
By default, Bazel reads from but does not write to a remote cache (safe for untrusted contributors). CI machines should upload:
# .bazelrc
build:ci --remote_upload_local_results=trueManaged remote cache providers
| Provider | Notes |
|---|---|
| BuildBuddy | Free tier, gRPC cache + remote execution + UI |
| EngFlow | Enterprise, supports RBE |
| NativeLink | Open-source, self-hosted |
| Google Cloud Storage | Simple HTTP cache, no RBE |
Remote Build Execution (RBE)
Beyond caching: Bazel can distribute compilation actions to a cluster of workers, dramatically cutting wall-clock build time for large codebases. Configured via --remote_executor.
bazel build //... \
--remote_executor=grpc://rbe.buildbuddy.io \
--remote_cache=grpc://rbe.buildbuddy.io \
--remote_header=x-buildbuddy-api-key=<KEY>Query — Understanding the Build Graph
Bazel's query tools let you inspect the dependency graph without actually building.
bazel query
# All targets in the workspace
bazel query //...
# Direct and transitive dependencies of a target
bazel query 'deps(//src:app)'
# Only direct dependencies (depth 1)
bazel query 'deps(//src:app, 1)'
# Reverse dependencies — what depends on //src:mylib?
bazel query 'rdeps(//..., //src:mylib)'
# Find all cc_test targets
bazel query 'kind(cc_test, //...)'
# Find all targets that transitively depend on a specific library
bazel query 'allrdeps(//third_party/openssl:ssl)'
# Show the dependency path between two targets
bazel query 'somepath(//src:app, //third_party/zlib:zlib)'bazel cquery — configured query
cquery respects build flags (e.g., --config=asan), so it shows the actual targets selected after configuration.
bazel cquery 'deps(//src:app)' --config=asanbazel aquery — action graph query
aquery shows the actual compiler/linker invocations Bazel would run.
# Show all actions for a target
bazel aquery //src:app
# Show the exact compile command for a source file
bazel aquery 'mnemonic("CppCompile", //src:app)'
# Output as JSON for tooling
bazel aquery //src:app --output=jsonprotoMonorepo Layout Example
myorg/
├── MODULE.bazel
├── .bazelversion
├── .bazelrc
├── WORKSPACE.bazel # (optional, legacy compat)
│
├── common/
│ ├── BUILD.bazel
│ ├── strings/
│ │ ├── BUILD.bazel
│ │ ├── string_util.h
│ │ └── string_util.cpp
│ └── logging/
│ ├── BUILD.bazel
│ ├── logger.h
│ └── logger.cpp
│
├── services/
│ ├── auth/
│ │ ├── BUILD.bazel
│ │ ├── auth_service.cpp
│ │ └── auth_service.h
│ └── payment/
│ ├── BUILD.bazel
│ └── payment_service.cpp
│
└── tools/
└── codegen/
├── BUILD.bazel
└── main.py # Python tool alongside C++ — Bazel handles both# common/strings/BUILD.bazel
cc_library(
name = "string_util",
srcs = ["string_util.cpp"],
hdrs = ["string_util.h"],
visibility = ["//visibility:public"],
deps = ["@abseil-cpp//absl/strings"],
)
# services/auth/BUILD.bazel
cc_library(
name = "auth_service",
srcs = ["auth_service.cpp"],
hdrs = ["auth_service.h"],
visibility = ["//services/..."], # only visible within //services
deps = [
"//common/strings:string_util",
"//common/logging",
"@abseil-cpp//absl/status",
],
)Visibility enforcement is one of Bazel's most useful monorepo features: a library can declare it is only visible to a subset of packages, preventing unwanted coupling.
When to Use Bazel vs CMake
| Factor | Bazel | CMake |
|---|---|---|
| Project size | Scales to millions of files | Practical up to ~100k files |
| Languages | Polyglot: C++, Java, Python, Go, Rust, Proto | Primarily C/C++ |
| Hermeticity | Hermetic by design | Depends on developer discipline |
| Remote caching | First-class, built-in | Via ccache; no action-level cache |
| Remote execution | Supported (RBE) | Not supported |
| Learning curve | Steep (new concepts, Starlark) | Moderate (CMake quirks) |
| Open-source library support | Growing (BCR); gaps still exist | Excellent (vcpkg, Conan, find_package) |
| Windows support | Good (improving) | Excellent |
| IDE integration | CLion, VS Code (limited); compile_commands.json via rules_cc | First-class everywhere |
| Existing ecosystem | Strong in large tech orgs | De facto standard for C++ OSS |
Use Bazel when:
- You have a large monorepo (millions of LOC, multiple languages)
- CI build times matter and you want shared remote caching/execution
- You need strict dependency hygiene enforced by the build system
- Your org already uses Bazel for other languages
Use CMake when:
- You are writing an open-source C++ library (consumers expect CMake)
- Your team already knows CMake and the project is single-language
- You rely heavily on vcpkg or Conan for dependencies
- You need deep IDE integration out of the box
compile_commands.json for IDE / clangd
Bazel does not generate compile_commands.json natively, but rules_cc provides a target for it:
# MODULE.bazel
bazel_dep(name = "hedron_compile_commands", version = "0.0.0.20240703")# Generate compile_commands.json
bazel run @hedron_compile_commands//:refresh_all
# Or for a specific target
bazel run @hedron_compile_commands//:refresh -- //src:appThis lets clangd, clang-tidy, and most C++ IDE plugins work correctly with a Bazel-built project.
Common Patterns
Header-only library
cc_library(
name = "my_header_only",
hdrs = ["my_header_only.h"],
# No srcs — header only
visibility = ["//visibility:public"],
)Glob (use sparingly)
cc_library(
name = "mylib",
srcs = glob(["src/*.cpp"]),
hdrs = glob(["include/**/*.h"]),
)Globs are convenient but hide new files from Bazel's dependency tracking until the next build invocation. Prefer explicit file lists in performance-critical repos.
cc_proto_library — Protocol Buffers
# MODULE.bazel
bazel_dep(name = "protobuf", version = "26.0")
# BUILD.bazel
proto_library(
name = "my_proto",
srcs = ["my.proto"],
)
cc_proto_library(
name = "my_proto_cc",
deps = [":my_proto"],
)
cc_binary(
name = "app",
srcs = ["main.cpp"],
deps = [":my_proto_cc"],
)Conditional dependencies (select)
cc_library(
name = "platform_util",
srcs = select({
"@platforms//os:linux": ["platform_linux.cpp"],
"@platforms//os:macos": ["platform_macos.cpp"],
"@platforms//os:windows": ["platform_windows.cpp"],
}),
hdrs = ["platform_util.h"],
)See Also
- CMake — the most widely-used C++ build system; required for most open-source libraries
- Ninja — low-level build executor that Bazel (and CMake) can use as a backend
- ccache — compiler cache that works with CMake/Make; Bazel's built-in cache supersedes it within a Bazel build
- Meson — simpler alternative to CMake for mid-size projects
- GoogleTest — test framework used in the
cc_testexamples above - Build Optimization — broader build performance strategies