C++ and Bazel
- Description: Bazel for C++ and Python — Bzlmod and legacy WORKSPACE, BUILD files,
cc_*andpy_*rules, visibility, external deps, configurations, remote caching, Starlark macros and rules - My Notion Note ID: K2A-B2-6
- Created: 2020-01-17
- Updated: 2026-04-30
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Design Properties
- 2. Workspace Setup
- 3. BUILD Files
- 4. Common C++ Rules
- 5. Beyond C++: Python and Other Languages
- 6. Labels and Visibility
- 7. External Dependencies
- 8. Configurations and Platforms
- 9. Common Commands
- 10. Remote Caching
- 11. Macros and Rules (Starlark)
- 12. References
1. Design Properties
-
Open-source build system from Google. 4 distinguishing properties:
-
Hermetic — every input declared (sources, headers, compilers, deps). Same inputs → same outputs.
-
Multi-language — C++, Java, Python, Go, JS, etc. share one action graph.
-
Incremental + parallel — action-level caching, minimal rebuilds.
-
Remote-cache + remote-execution friendly — hermeticity makes shared caches across machines safe.
-
Trade-off: every
#include+ dependency must be in aBUILDtarget.
2. Workspace Setup
- Bazel project = workspace. Identified by root marker:
MODULE.bazel(modern, Bzlmod) orWORKSPACE(legacy).
2.1 Bzlmod (Modern)
- Current dependency-management system. Default since Bazel 7.
MODULE.bazel:
module(name = "myapp", version = "0.1.0")
bazel_dep(name = "rules_cc", version = "0.0.17")
bazel_dep(name = "googletest", version = "1.15.2")
bazel_dep(name = "abseil-cpp", version = "20240722.0")
bazel_dep(name = "protobuf", version = "27.5")
- Modules from Bazel Central Registry.
bazel mod tidy— updates lockfiles.bazel mod graph— resolved tree.
2.2 WORKSPACE (Legacy)
- Older projects pin deps imperatively in
WORKSPACE(orWORKSPACE.bazel). - Declares workspace name + invokes repository rules fetching external code into local repos accessed via
@name//....
# WORKSPACE
workspace(name = "myapp")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository")
# 1) Fetch a tarball over HTTP and extract it as an external repo
http_archive(
name = "com_google_googletest",
urls = ["https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz"],
strip_prefix = "googletest-1.15.2",
sha256 = "7b42b4d6ed48810c5362c265a17faebe90dc2373c885e5216439d37927f02926",
)
# 2) Pin a git checkout (use a commit, not a moving branch, for hermeticity)
git_repository(
name = "abseil",
remote = "https://github.com/abseil/abseil-cpp.git",
commit = "abc123def456...",
shallow_since = "2024-08-01",
)
# 3) Same idea, but the upstream has no BUILD files -- you supply one
new_git_repository(
name = "lib_with_no_bazel",
remote = "https://github.com/example/lib.git",
commit = "...",
build_file = "//third_party:lib_with_no_bazel.BUILD",
)
# 4) Point at a sibling checkout on disk -- handy for local development
local_repository(
name = "shared_protos",
path = "../shared_protos",
)
# Many ecosystems require a two-step setup: fetch the rule repo, then call
# its setup macro to register toolchains and pull transitive deps.
load("@rules_cc//cc:repositories.bzl", "rules_cc_dependencies")
rules_cc_dependencies()
-
Once registered, refer with
@<name>//path:target— e.g.@com_google_googletest//:gtest_main. -
Why Bzlmod replaces this:
- WORKSPACE has no native version resolution. 2 deps pulling
abseilat different commits → firstgit_repositorywins silently. - "Wrong header" / ABI mismatches painful to diagnose.
- Bzlmod's modules carry version metadata; resolver picks one consistent version per module.
- WORKSPACE has no native version resolution. 2 deps pulling
-
New projects → Bzlmod. Existing → migrate via
bazel mod tidyafter--enable_bzlmod. -
WORKSPACEdeprecated, removed in future Bazel. Bazel 7+ → must opt in with--enable_workspace.
3. BUILD Files
- Each directory with buildable targets has
BUILD(orBUILD.bazel). - Targets in
path/to/pkg/BUILDreferenced by labels like//path/to/pkg:target_name.
myapp/
├── MODULE.bazel
├── BUILD
└── src/
├── BUILD
├── greeter.h
├── greeter.cc
└── main.cc
4. Common C++ Rules
load(...)rule fromrules_ccat top of eachBUILD. Bazel previously built-in; recent versions require load.
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
4.1 cc_library
cc_library(
name = "greeter",
srcs = ["greeter.cc"],
hdrs = ["greeter.h"],
deps = ["@abseil-cpp//absl/strings"],
copts = ["-Wall", "-Wextra"],
visibility = ["//visibility:public"],
)
srcs—.cc/.cpp; built into the library.hdrs— public headers; consumers may#include.deps— othercc_librarytargets needed at link time. Public headers become visible.copts,linkopts,defines— per-target flags.includes— dirs added to consumers' include path (use sparingly; preferstrip_include_prefix/include_prefix).srcsvshdrsmatters: onlyhdrsfiles visible to dependents.
4.2 cc_binary
cc_binary(
name = "myapp",
srcs = ["main.cc"],
deps = [":greeter"],
)
bazel run //src:myapp -- arg1 arg2builds + runs.
4.3 cc_test
cc_test(
name = "greeter_test",
srcs = ["greeter_test.cc"],
deps = [
":greeter",
"@googletest//:gtest_main",
],
)
- Pass/fail by exit code.
- Run:
bazel test //src:greeter_testorbazel test //src/...for all undersrc/. - Add
size = "small"/"medium"/"large"for resource hints.
5. Beyond C++: Python and Other Languages
- Same workspace, any language Bazel has rules for — Python, Go, Java, Rust, JS.
- Workflow mirrors C++: load ruleset, call rules from
BUILD, build/test withbazel build/bazel test.
Python uses rules_python. Add to MODULE.bazel:
bazel_dep(name = "rules_python", version = "0.40.0")
# Optional: pull a specific Python interpreter and pin it for reproducibility
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(python_version = "3.12")
In BUILD:
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
py_library(
name = "greeter",
srcs = ["greeter.py"],
visibility = ["//visibility:public"],
)
py_binary(
name = "myapp",
srcs = ["main.py"],
main = "main.py",
deps = [":greeter"],
)
py_test(
name = "greeter_test",
srcs = ["greeter_test.py"],
deps = [":greeter"],
)
- Run Python test same as C++:
bazel test //src:greeter_test
bazel test //src:greeter_test --test_output=streamed # see prints in real time
- pip deps:
rules_pythonpipextension readsrequirements_lock.txt, exposes each pkg as@pip_deps//<pkg>:
# MODULE.bazel
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(hub_name = "pip_deps", python_version = "3.12",
requirements_lock = "//:requirements_lock.txt")
use_repo(pip, "pip_deps")
# BUILD
py_test(
name = "uses_numpy_test",
srcs = ["uses_numpy_test.py"],
deps = ["@pip_deps//numpy", "@pip_deps//pytest"],
)
- Same Bazel value for Python as C++: hermetic, cached, parallel, shareable via remote cache.
- Monorepo can have C++ binary + Python integration test exercising it, sharing one
bazel test //.... - Other languages:
rules_go,rules_rust,rules_jvm_external+ built-injava_*, etc.
6. Labels and Visibility
- Label = global name for a target.
| Form | Meaning |
|---|---|
//src:greeter |
Target greeter in package src |
//src |
Shorthand for //src:src |
:greeter |
Same package as the current BUILD |
@abseil-cpp//absl/strings |
Target strings in module abseil-cpp |
- Visibility controls who can depend on a target. Default = package-private. Lift explicitly:
cc_library(
name = "internal_helpers",
srcs = ["helpers.cc"],
hdrs = ["helpers.h"],
visibility = [
"//src:__pkg__", # only the //src package
"//src/utils:__subpackages__", # //src/utils and below
"//visibility:public", # everywhere
],
)
__pkg__(just this package),__subpackages__(this + below), explicit package lists — common patterns.//visibility:public= deliberate widening — use only on stable public APIs.
7. External Dependencies
- Bzlmod:
bazel_depinMODULE.bazel, reference public targets via@module_name//path:target. - Bazel Central Registry has most popular C++ libs (Abseil, Protobuf, gRPC, Boost, Eigen, googletest, ...).
- Non-registry:
module_extension+http_archive(modern), orhttp_archivedirectly inWORKSPACE(legacy).
# Override a module to point at a local checkout (useful for development)
local_path_override(
module_name = "abseil-cpp",
path = "../abseil-cpp",
)
# Override to a specific git revision
git_override(
module_name = "googletest",
remote = "https://github.com/google/googletest.git",
commit = "abc123...",
)
8. Configurations and Platforms
--config=NAMEselects a named bundle of flags from.bazelrc:
# .bazelrc
build:opt --compilation_mode=opt --copt=-O3 --copt=-DNDEBUG
build:dbg --compilation_mode=dbg --copt=-O0 --copt=-g
build:asan --copt=-fsanitize=address --linkopt=-fsanitize=address
build:tsan --copt=-fsanitize=thread --linkopt=-fsanitize=thread
# Per-platform toolchain
build:linux_arm64 --platforms=//build/platforms:linux_arm64
bazel build --config=opt //...
bazel build --config=asan //src:greeter_test
bazel build --config=linux_arm64 //...
- Platforms (
@platforms//os:linux,@platforms//cpu:aarch64, ...) drive toolchain selection +select()-based conditional sources/deps:
cc_library(
name = "platform_specific",
srcs = ["common.cc"] + select({
"@platforms//os:linux": ["linux.cc"],
"@platforms//os:macos": ["macos.cc"],
"//conditions:default": ["other.cc"],
}),
)
9. Common Commands
# Build everything in the workspace
bazel build //...
# Build one target
bazel build //src:myapp
# Run a binary target
bazel run //src:myapp -- arg1 arg2
# Run all tests under a directory
bazel test //src/...
# Query: who depends on this target?
bazel query "rdeps(//..., //src:greeter)"
# Query: what does this target depend on (transitively)?
bazel query "deps(//src:myapp)"
# Show the action graph for a target (debugging)
bazel aquery //src:myapp
# Discard everything not actively in use
bazel clean
# Wipe everything including external repos and cached downloads
bazel clean --expunge
# Show effective flags
bazel info
bazel build //...over large repo: slow first run, very fast incremental, faster still cross-machine with remote caching.
10. Remote Caching
- Shared cache across machines — if anyone in cluster built a particular
(inputs → outputs)action before, all others get it free.
# In .bazelrc or on the command line
build --remote_cache=grpcs://my-cache.example.com:443
build --remote_instance_name=myproject
# Read-only cache (don't upload anything; just consume)
build --remote_upload_local_results=false
-
Common backends:
bazel-remote(open-source self-hosted), Google Cloud RBE, BuildBuddy, EngFlow. -
Any service implementing Remote Execution API works.
-
Remote execution (
--remote_executor=...) — runs actions on remote workers. More setup (toolchain defs match worker env); order-of-magnitude speedup on cold builds.
11. Macros and Rules (Starlark)
- Extension language = Starlark — Python subset, deterministic by design (no I/O, no recursion, no side-effect while-loops).
Macros — functions emitting other rule calls. Wrap common patterns:
# tools/proto.bzl
# In Bazel 8+, proto_library / cc_proto_library moved out of Bazel
# core into the protobuf repo; load them explicitly.
load("@protobuf//bazel:proto_library.bzl", "proto_library")
load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library")
def cc_proto_lib(name, srcs, deps = []):
"""Wraps proto_library + cc_proto_library + a stable target name."""
proto_library(
name = name + "_proto",
srcs = srcs,
deps = deps,
)
cc_proto_library(
name = name,
deps = [":" + name + "_proto"],
)
# BUILD
load("//tools:proto.bzl", "cc_proto_lib")
cc_proto_lib(name = "user", srcs = ["user.proto"])
Rules — full custom build steps. Define inputs, outputs, action(s) producing them.
- Use when macro isn't enough — custom code generator, one-off tool integration.
# tools/codegen.bzl
def _codegen_impl(ctx):
out = ctx.actions.declare_file(ctx.attr.name + ".cc")
ctx.actions.run(
executable = ctx.executable._gen,
arguments = [ctx.file.src.path, out.path],
inputs = [ctx.file.src],
outputs = [out],
mnemonic = "Codegen",
)
return [DefaultInfo(files = depset([out]))]
codegen = rule(
implementation = _codegen_impl,
attrs = {
"src": attr.label(allow_single_file = True, mandatory = True),
"_gen": attr.label(executable = True, cfg = "exec",
default = "//tools:codegen"),
},
)
- Custom rules → providers: typed data structures returned to dependents (e.g.,
CcInfofor C++ headers/libs). - Most projects get far with just macros + built-in rules.
12. References
- Bazel main site — install, getting started, conceptual overview.
- C/C++ Rules —
cc_library,cc_binary,cc_testreference. - Bzlmod docs — modules, registries, lockfiles.
- Bazel Central Registry — published modules.
- Starlark Language Spec — language used in
BUILDand.bzlfiles. - Macros vs Rules — when to use which.
- Remote Execution API — protocol behind remote cache + execution.