Skip to content
C++
Build System
Updated 2026-07-14T00:00:00.000Z

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.

bash
# 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:app

Bazel 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 reads .bazelversion in your repo root and downloads the exact Bazel binary automatically. You never manually manage Bazel versions.

bash
# 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/bazel

After install, bazel is provided by bazelisk. To pin the version:

bash
echo "7.4.1" > .bazelversion
bazel version   # downloads and caches the pinned version on first run

Direct 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.
  • WORKSPACE or WORKSPACE.bazel — legacy workspace file. Still works; gradually being replaced by Bzlmod.
python
# 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.

cpp
myproject/
├── MODULE.bazel
├── .bazelversion
├── .bazelrc
├── src/
│   ├── BUILD.bazel      ← package: //src
│   ├── main.cpp
│   ├── mylib.cpp
│   └── mylib.h
└── tests/
    ├── BUILD.bazel      ← package: //tests
    └── mylib_test.cpp

Targets

A target is a named, buildable unit declared inside a BUILD.bazel file. Targets are addressed by their label:

cpp
//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 //tests

Shorthand: //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

python
# 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"],
)
python
# 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

AttributePurpose
srcs.cpp / .cc files compiled as part of this library
hdrsPublic headers; added to the include path of dependents
depsOther targets this library depends on
coptsExtra compiler flags for this target only
linkoptsExtra linker flags (prefer deps when possible)
visibilityWho may depend on this target
includesExtra include paths (use sparingly; prefer hdrs)
defines-D preprocessor definitions

cc_binary attributes

Same as cc_library plus:

AttributePurpose
srcsSource files and main()
mallocOverride the malloc implementation
stampEmbed build info (VCS revision, timestamp)

Building and Testing

bash
# 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=all

Clean

bash
# Remove the output directory for the current workspace
bazel clean

# Remove the entire Bazel cache (output base) — use sparingly
bazel clean --expunge

bazel 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:

bash
bazel build //... --announce_rc  # shows resolved flags
bazel info --show_make_env       # shows CC, CXX, etc.

Per-target compiler flags

python
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.

python
# 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")
bash
# 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.

ini
# .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=true

Activate a config:

bash
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.

python
# 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:

python
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:

bash
bazel mod tidy

Inspect what version was selected:

bash
bazel mod graph
bazel mod show_repo googletest   # print the resolved repository rule

Legacy WORKSPACE (still common)

python
# 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

ini
# .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_credentials

Upload vs read-only

By default, Bazel reads from but does not write to a remote cache (safe for untrusted contributors). CI machines should upload:

ini
# .bazelrc
build:ci --remote_upload_local_results=true

Managed remote cache providers

ProviderNotes
BuildBuddyFree tier, gRPC cache + remote execution + UI
EngFlowEnterprise, supports RBE
NativeLinkOpen-source, self-hosted
Google Cloud StorageSimple 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.

bash
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

bash
# 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.

bash
bazel cquery 'deps(//src:app)' --config=asan

bazel aquery — action graph query

aquery shows the actual compiler/linker invocations Bazel would run.

bash
# 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=jsonproto

Monorepo Layout Example

cpp
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
python
# 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

FactorBazelCMake
Project sizeScales to millions of filesPractical up to ~100k files
LanguagesPolyglot: C++, Java, Python, Go, Rust, ProtoPrimarily C/C++
HermeticityHermetic by designDepends on developer discipline
Remote cachingFirst-class, built-inVia ccache; no action-level cache
Remote executionSupported (RBE)Not supported
Learning curveSteep (new concepts, Starlark)Moderate (CMake quirks)
Open-source library supportGrowing (BCR); gaps still existExcellent (vcpkg, Conan, find_package)
Windows supportGood (improving)Excellent
IDE integrationCLion, VS Code (limited); compile_commands.json via rules_ccFirst-class everywhere
Existing ecosystemStrong in large tech orgsDe 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:

python
# MODULE.bazel
bazel_dep(name = "hedron_compile_commands", version = "0.0.0.20240703")
bash
# Generate compile_commands.json
bazel run @hedron_compile_commands//:refresh_all

# Or for a specific target
bazel run @hedron_compile_commands//:refresh -- //src:app

This lets clangd, clang-tidy, and most C++ IDE plugins work correctly with a Bazel-built project.


Common Patterns

Header-only library

python
cc_library(
    name = "my_header_only",
    hdrs = ["my_header_only.h"],
    # No srcs — header only
    visibility = ["//visibility:public"],
)

Glob (use sparingly)

python
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

python
# 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)

python
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_test examples above
  • Build Optimization — broader build performance strategies
Edit on GitHubUpdated 2026-07-14T00:00:00.000Z