Bazel Build System Guide — Complete Polyglot Build Automation
In this tutorial, you'll learn about Bazel Build System Guide. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Bazel is a build and test system developed by Google that supports multiple languages in a single Repository with hermetic, reproducible builds, incremental Caching, and parallel execution across distributed workers.
What You'll Learn
You'll learn how to write BUILD files with Bazel rules, configure workspaces and external dependencies, create custom rules with Skylark (Starlark), set up remote Caching, and build multi-language monorepos.
Why Bazel Matters
Bazel solves the Monorepo build problem — hundreds of thousands of targets in multiple languages built with absolute reproducibility. Its hermetic build model guarantees that the same source always produces the same output, eliminating environmental inconsistencies.
Real-World Use
The Doda Browser rendering engine uses Bazel to build WebAssembly modules written in Rust, C++ layout engines, and JavaScript bundles — all within a single Repository with shared Caching across developer machines and CI agents.
Prerequisites
- Familiarity with Java, C++, or Go
- Understanding of build concepts (compilation, linking, testing)
- Bazel 7.x installed
Step 1: Workspace and BUILD File Setup
A Bazel workspace is a directory containing a WORKSPACE file and one or more BUILD files that define targets.
# WORKSPACE — empty for a basic setup, or with external dependency declarations
workspace(name = "myproject")
# BUILD — in the root directory
load("@rules_cc//cc:defs.bzl", "cc_binary")
cc_binary(
name = "hello",
srcs = ["hello.cc"],
deps = [
"//lib:greeter",
],
)
# lib/BUILD
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "greeter",
srcs = ["greeter.cc"],
hdrs = ["greeter.h"],
visibility = ["//main:__pkg__"],
)
Expected output: Running bazel build //:hello compiles the greeter library first, then links the hello binary. Subsequent builds use cached outputs.
Step 2: Multi-Language Builds
Bazel supports multiple languages in the same Repository. This example combines Go and Java targets.
# BUILD — Go binary
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
go_binary(
name = "server",
srcs = ["server.go"],
deps = [
"//proto:api_go_grpc",
],
)
# BUILD — Java library
load("@rules_java//java:defs.bzl", "java_library")
java_library(
name = "client",
srcs = glob(["src/main/java/**/*.java"]),
deps = [
"@maven//:com_google_protobuf_protobuf_java",
"//proto:api_java_proto",
],
)
# BUILD — protobuf definitions compiled to both Go and Java
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_java//java:defs.bzl", "java_proto_library")
proto_library(
name = "api_proto",
srcs = ["api.proto"],
)
go_proto_library(
name = "api_go_grpc",
importpath = "myproject/proto/api",
proto = ":api_proto",
)
java_proto_library(
name = "api_java_proto",
deps = [":api_proto"],
)
Expected behavior: Running bazel build //... builds all targets across all languages. Bazel parallelizes independent targets and reuses cached results from previous builds.
Step 3: External Dependencies
Bazel fetches external dependencies through Repository rules defined in the WORKSPACE file.
# WORKSPACE with external dependencies
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Google Test for C++ tests
http_archive(
name = "googletest",
url = "https://github.com/google/googletest/archive/release-1.12.1.tar.gz",
sha256 = "81964fe578e9bd7c94dfdb09c8e4d6e6759e19967e397dbea48d2c10e4d4372f",
strip_prefix = "googletest-release-1.12.1",
)
# Maven dependencies for Java
load("@rules_jvm_external//:defs.bzl", "maven_install")
maven_install(
artifacts = [
"com.google.code.gson:gson:2.10.1",
"org.junit.jupiter:junit-jupiter-api:5.10.0",
],
repositories = [
"https://repo1.maven.org/maven2",
],
)
Expected behavior: On first build, Bazel downloads and caches all external dependencies. Subsequent builds use the cached versions unless the checksum in WORKSPACE changes.
Step 4: Remote Caching and Execution
Bazel can share build outputs across machines using a remote cache or execute actions on remote workers.
# .bazelrc configuration for remote caching
build --remote_cache=grpc://cache.example.com:9092
build --remote_upload_local_results=true
# Use Google Cloud Storage as a remote cache
build --remote_cache=https://storage.googleapis.com/my-bucket/cache
build --google_credentials=/path/to/creds.json
# Remote execution
build --remote_executor=grpc://build.example.com:9090
build --remote_instance_name=projects/my-project/instances/default
Expected behavior: After populating the remote cache, running bazel build on a clean CI machine downloads cached outputs instead of rebuilding. Remote execution distributes compilation across a cluster of workers.
Architecture
flowchart LR
subgraph "Workspace"
WS[WORKSPACE]
BUILD[BUILD Files]
SRC[Source Code]
end
subgraph "Bazel Engine"
EVAL[Target Graph]
DEP[Action DAG]
CACHE[Local Cache]
end
subgraph "Remote"
RCACHE[Remote Cache]
REXEC[Remote Execution]
end
subgraph "Outputs"
BIN[Binaries]
TEST[Test Results]
end
WS --> EVAL
BUILD --> EVAL
SRC --> DEP
EVAL --> DEP
DEP --> CACHE
CACHE --> RCACHE
DEP --> REXEC
RCACHE --> BIN
REXEC --> BIN
DEP --> TEST
Common Errors
1. no such package @googletest//
The external dependency was not fetched correctly. Run bazel sync to re-fetch all external repositories, or verify the http_archive URL and checksum.
2. ERROR: /BUILD:1:10: name 'cc_binary' not defined
The Starlark rule is not loaded. Use load("@rules_cc//cc:defs.bzl", "cc_binary") at the top of the BUILD file. Rule sets must be explicitly imported.
3. Action failed because input is not declared
Bazel's sandboxing prevents access to undeclared files. All inputs must be declared as dependencies or data attributes. This ensures hermetic builds.
4. Cycle in dependency graph Target A depends on B, and B depends on A. Bazel detects cycles at analysis time and errors before building. Break the cycle by restructuring the dependency.
5. Remote cache returned HTTP 403
Credentials for the remote cache are missing or expired. Verify --google_credentials path or configure authentication for the cache server.
Practice Questions
1. What makes a Bazel build hermetic? Hermetic builds declare every input — source files, tools, and dependencies — explicitly in BUILD files. Sandboxing prevents accidental access to undeclared files. The same inputs always produce the same outputs.
2. How does Bazel handle multi-language builds? Each language has a set of Starlark rules (rules_go, rules_cc, rules_java). Targets in different languages can depend on each other through generated artifacts like protobuf stubs.
3. What is the difference between bazel build and bazel run?
bazel build compiles targets and produces output files. bazel run builds (if needed) and then executes the resulting binary, handling stdin/stdout for the user.
4. Challenge: Custom Starlark rule
Write a Starlark rule that reads a YAML file and generates a C++ header file with the YAML keys as constants. Use the ctx.actions.write action to produce the output.
5. Challenge: Multi-platform build
Configure Bazel to build the same Go binary for linux/amd64, linux/arm64, and darwin/amd64 platforms using --platforms flags and a custom toolchain configuration.
FAQ
Next Steps
- Compare Bazel with Maven for Java-only projects to understand trade-offs
- Learn how Docker images fit into Bazel build pipelines
- Explore the Go rules for building Go services with Bazel
- Review the Monorepo build tools guide for alternatives like Nx and Turborepo
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro