You are viewing a preview of this lesson. Sign in to start learning
Back to Hermetic Builds

BUILD Files and Targets

Learn how Bazel uses declarative BUILD files to define the complete dependency graph.

BUILD Files and Targets

Master BUILD files and targets with free flashcards and spaced repetition practice. This lesson covers target declarations, dependency relationships, and visibility rulesβ€”essential concepts for understanding Bazel's hermetic build system. You'll learn how Bazel organizes build logic through structured files that define what to build and how different parts of your codebase connect.

Welcome to BUILD Files and Targets πŸ’»

Welcome to the foundation of Bazel's build system! Think of BUILD files as the blueprints of your software projectβ€”they tell Bazel exactly what artifacts to create and how all the pieces fit together. Unlike traditional build systems where configuration can be scattered or implicit, Bazel requires explicit declarations of every build unit. This explicitness is what enables hermetic builds: by knowing every input and output upfront, Bazel can guarantee reproducibility across different machines and environments.

In this lesson, you'll discover how targets work as the atomic units of builds, how to declare dependencies correctly, and why visibility controls matter for large codebases. By the end, you'll be able to read and write BUILD files confidently, understanding the mental model that makes Bazel both powerful and predictable.

Core Concepts

What is a BUILD File? πŸ“„

A BUILD file (or BUILD.bazel) is a Python-like configuration file that lives in a directory and declares targetsβ€”the things Bazel can build. Each BUILD file defines a package, which is the directory containing that BUILD file and all its subdirectories (until another BUILD file is encountered).

Key characteristics of BUILD files:

  • Declarative syntax: You describe what to build, not how to build it
  • No conditional logic: No if-statements or loops (though you can use list comprehensions)
  • Hermetic by design: All inputs must be explicitly declared
  • Package boundary: Each BUILD file creates a new package namespace
Directory Structure:

my_project/
β”œβ”€β”€ BUILD              ← Package "//"
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ BUILD          ← Package "//src"
β”‚   β”œβ”€β”€ main.cc
β”‚   └── lib/
β”‚       β”œβ”€β”€ BUILD      ← Package "//src/lib"
β”‚       └── helper.cc
└── test/
    β”œβ”€β”€ BUILD          ← Package "//test"
    └── main_test.cc

πŸ’‘ Tip: Each directory that needs to participate in the build must have its own BUILD file, even if it just declares visibility or filegroup targets.

Understanding Targets 🎯

A target is the fundamental unit of a Bazel build. Every target has:

  1. Name: A unique identifier within its package
  2. Rule type: The kind of thing being built (e.g., cc_library, java_binary, py_test)
  3. Attributes: Configuration parameters like source files, dependencies, and compiler flags
  4. Label: A full address in the form //package:target_name

Target types:

Type Purpose Examples
Files Source files or generated artifacts //src:main.cc
Rules Instructions to produce outputs cc_library, java_binary
Package groups Collections for visibility control package_group

Label syntax:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  //path/to/package:target_name              β”‚
β”‚  ↑                 ↑                        β”‚
β”‚  Repository root   Target name              β”‚
β”‚                                             β”‚
β”‚  Shorthand forms:                           β”‚
β”‚  :target_name  β†’ Same package              β”‚
β”‚  //pkg         β†’ //pkg:pkg (implicit name) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Rule Declarations πŸ“

Rules are functions that create targets. Each rule type has specific attributes, but common ones include:

Universal attributes:

  • name: (required) Target identifier
  • visibility: Who can depend on this target
  • tags: Metadata for filtering and tooling
  • testonly: Whether target is only for testing

Common build attributes:

  • srcs: Source files needed for compilation
  • deps: Other targets this target depends on
  • data: Runtime data files
  • outs: Explicitly named output files

Here's the anatomy of a typical rule declaration:

cc_library(
    name = "http_client",        ← Target name
    srcs = ["client.cc"],        ← Compile-time sources
    hdrs = ["client.h"],         ← Public headers
    deps = [                      ← Build dependencies
        ":common",
        "//third_party:curl",
    ],
    visibility = ["//visibility:public"],  ← Access control
)

⚠️ Common mistake: Forgetting to declare all dependencies. Bazel will fail if you use a symbol that isn't explicitly listed in deps. This strictness prevents hidden dependencies that break hermetic builds.

Dependencies: The Dependency Graph πŸ”—

Bazel constructs a directed acyclic graph (DAG) of all targets and their dependencies. This graph is what enables:

  • Parallel builds: Independent targets build simultaneously
  • Incremental builds: Only affected targets rebuild when sources change
  • Remote caching: Outputs are content-addressed by their inputs
Dependency Graph Example:

    //app:main_binary
           β”‚
           ↓
    //app:app_lib
       β•±       β•²
      ↓         ↓
//lib:auth   //lib:http_client
      ↓         ↓
//lib:common  //third_party:curl
      ↓
//proto:api_proto

β†’ Edges show "depends on" relationship
β†’ Bazel builds from leaves to root
β†’ No cycles allowed!

Dependency types:

  1. Direct dependencies (deps): Required at compile time, linked into the output
  2. Runtime dependencies (data): Files needed when running the binary/test
  3. Transitive dependencies: Dependencies of your dependencies (Bazel handles automatically)

πŸ’‘ Mental model: Think of deps as "I need this to compile" and data as "I need this file to exist at runtime."

Visibility: Access Control πŸ”’

The visibility attribute controls which other packages can depend on a target. This is crucial for large codebases where you want to:

  • Enforce architectural boundaries
  • Prevent internal implementation details from leaking
  • Control API surfaces

Visibility values:

Value Meaning
["//visibility:public"] Anyone can depend on this target
["//visibility:private"] Only targets in this package (default)
["//some/pkg:__pkg__"] Only targets in //some/pkg
["//some/pkg:__subpackages__"] Package and all subpackages
["//:__subpackages__"] Everything in the workspace

Best practice: Start with private visibility and only expose what's needed. This is the "principle of least privilege" applied to build systems.

Glob Patterns and File Selection πŸ—‚οΈ

The glob() function finds files matching patterns within a package:

cc_library(
    name = "utils",
    srcs = glob(["*.cc"]),           # All .cc files
    hdrs = glob(
        ["include/**/*.h"],           # Recursive match
        exclude = ["include/internal/**"],  # But not these
    ),
)

Glob characteristics:

  • Evaluated at loading time, not build time
  • Cannot escape the package directory
  • Excludes BUILD files automatically
  • Results are sorted deterministically

⚠️ Warning: Avoid globs that might accidentally include generated files or match too broadly. Explicit file lists are more maintainable for small sets.

Labels and Target References 🏷️

Understanding label syntax is essential for expressing dependencies:

Syntax Resolves To Use Case
":target" Target in current package Same BUILD file
"//pkg:target" Absolute path from workspace root Any package
"//pkg" "//pkg:pkg" Implicit name shorthand
"@repo//pkg:target" Target in external repository Third-party dependencies

Label resolution rules:

  1. Labels starting with // are absolute (from workspace root)
  2. Labels starting with : are relative (current package)
  3. Labels starting with @ reference external workspaces

Examples with Explanations

Example 1: Simple Library and Binary πŸ“¦

Let's build a basic C++ application with a library:

## //src/lib/BUILD

cc_library(
    name = "greeting",
    srcs = ["greeting.cc"],
    hdrs = ["greeting.h"],
    visibility = ["//src:__subpackages__"],  # Visible to src/ tree
)

## //src/BUILD

cc_binary(
    name = "hello",
    srcs = ["main.cc"],
    deps = ["//src/lib:greeting"],  # Absolute label
)

What happens during the build:

StepActionInputsOutputs
1 Compile library greeting.cc, greeting.h libgreeting.a
2 Compile binary main.cc, greeting.h main.o
3 Link binary main.o, libgreeting.a hello (executable)

πŸ’‘ Why this works: The visibility allows //src:hello to depend on //src/lib:greeting. Without that visibility declaration, the build would fail with "target '//src/lib:greeting' is not visible from target '//src:hello'".

Example 2: Test with Data Dependencies πŸ§ͺ

## //test/BUILD

cc_test(
    name = "parser_test",
    srcs = ["parser_test.cc"],
    deps = [
        "//src/lib:parser",
        "@com_google_googletest//:gtest_main",
    ],
    data = [
        "testdata/valid_input.json",
        "testdata/invalid_input.json",
    ],
)

Understanding data vs deps:

deps:  Compile-time dependencies
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚  Source code needs  β”‚ β†’ Linked into test binary
       β”‚  headers/libraries  β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

data:  Runtime dependencies
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚  Files needed when  β”‚ β†’ Copied to test's runfiles
       β”‚  test executes      β”‚   directory
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When you run bazel test //test:parser_test, Bazel:

  1. Builds the test binary with parser and gtest_main linked in
  2. Creates a runfiles tree containing the JSON test data
  3. Sets environment variables so the test can find its data files
  4. Executes the test with access to those files

Example 3: Visibility and Package Groups πŸ‘₯

For complex access control, use package_group:

## //lib/BUILD

package_group(
    name = "internal_users",
    packages = [
        "//app/...",      # app package and all subpackages
        "//services/...",
        "//test/...",
    ],
)

cc_library(
    name = "internal_crypto",
    srcs = ["crypto.cc"],
    hdrs = ["crypto.h"],
    visibility = [":internal_users"],  # Reference the package_group
)

cc_library(
    name = "public_api",
    srcs = ["api.cc"],
    hdrs = ["api.h"],
    visibility = ["//visibility:public"],
    deps = [":internal_crypto"],  # OK - same package
)

Why this pattern matters:

  • internal_crypto is protected from external use
  • Multiple packages can share access through internal_users
  • public_api acts as a facade, using internal libraries but exposing a clean interface
  • Changes to internal_users are centralized in one place

🧠 Mental model: Think of package_group as a "friends list" for your targets. Only packages on the list can depend on targets using that visibility.

Example 4: Filegroups and Intermediate Collections πŸ“‘

## //docs/BUILD

filegroup(
    name = "markdown_docs",
    srcs = glob(["*.md"]),
    visibility = ["//website:__pkg__"],
)

filegroup(
    name = "images",
    srcs = glob(["images/**/*.png"]),
    visibility = ["//website:__pkg__"],
)

## //website/BUILD

genrule(
    name = "generate_html",
    srcs = [
        "//docs:markdown_docs",
        "//docs:images",
        "template.html",
    ],
    outs = ["site.tar.gz"],
    cmd = "$(location //tools:doc_generator) --input $(SRCS) --output $@",
    tools = ["//tools:doc_generator"],
)

What's happening:

  1. filegroup creates a collection target (no build action, just groups files)
  2. The generate_html rule references these filegroups
  3. $(SRCS) expands to all source files from the filegroups
  4. $(location) expands to the path of the tool binary

πŸ’‘ Use case: Filegroups are perfect for sharing sets of files between multiple rules without duplicating the glob expressions. They also serve as abstraction boundariesβ€”other packages don't need to know the internal file structure.

Common Mistakes to Avoid ⚠️

1. Undeclared Dependencies

❌ Wrong:

cc_library(
    name = "network",
    srcs = ["network.cc"],
    hdrs = ["network.h"],
    # Missing: deps = ["//lib:sockets"],
)

Even if network.cc includes sockets.h, Bazel won't know about the dependency. The build might succeed locally (if you've built //lib:sockets before) but fail on remote build machines or for other developers.

βœ… Correct: Always declare every dependency explicitly. Use bazel query 'deps(//your:target)' to verify the dependency graph.

2. Circular Dependencies

❌ Wrong:

## //lib/a/BUILD
cc_library(name = "a", deps = ["//lib/b"])

## //lib/b/BUILD  
cc_library(name = "b", deps = ["//lib/a"])

Bazel will reject this with "cycle in dependency graph".

βœ… Solution: Extract common code into a third library that both depend on, or refactor to make dependencies unidirectional.

3. Overly Broad Visibility

❌ Wrong:

cc_library(
    name = "internal_impl",
    visibility = ["//visibility:public"],  # Too permissive!
)

βœ… Better: Use the narrowest visibility possible. Default to private, then expand only when needed:

cc_library(
    name = "internal_impl",
    visibility = ["//lib:__subpackages__"],  # Just this tree
)

4. Absolute Paths in Sources

❌ Wrong:

cc_library(
    srcs = ["/home/user/project/src/file.cc"],  # Absolute path!
)

This breaks hermetic buildsβ€”the path only exists on your machine.

βœ… Correct: Use labels that are relative to the workspace:

cc_library(
    srcs = ["file.cc"],  # Relative to BUILD file location
)

5. Missing Visibility on Intermediate Targets

❌ Wrong:

## //lib/BUILD - forgot visibility
cc_library(name = "helper")

## //app/BUILD
cc_binary(
    name = "app",
    deps = ["//lib:helper"],  # Will fail!
)

βœ… Remember: Default visibility is private. If another package needs to depend on a target, explicitly grant visibility.

Key Takeaways πŸŽ“

πŸ“‹ Quick Reference Card

BUILD File Defines a package and its targets
Target Atomic build unit with a label
Label Format //package:target (absolute)
deps Compile-time dependencies
data Runtime file dependencies
visibility Access control (default: private)
glob() Pattern matching for file selection
filegroup Collection of files without build action

Core principles to remember:

  1. 🎯 Explicit over implicit: Every dependency must be declared
  2. πŸ”’ Least privilege visibility: Start private, expand only when needed
  3. πŸ“Š DAG structure: Dependencies form a directed acyclic graph
  4. 🏷️ Labels are addresses: They uniquely identify every target
  5. πŸ“ Declarative syntax: Describe what to build, not how

The BUILD file mental model:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  BUILD File = Blueprint for a Package         β”‚
β”‚                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ Target 1 (library)                      β”‚ β”‚
β”‚  β”‚   inputs: source files                  β”‚ β”‚
β”‚  β”‚   outputs: compiled artifacts           β”‚ β”‚
β”‚  β”‚   dependencies: other targets           β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ Target 2 (binary)                       β”‚ β”‚
β”‚  β”‚   depends on: Target 1                  β”‚ β”‚
β”‚  β”‚   visibility: who can use this          β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                               β”‚
β”‚  Result: Bazel builds the dependency graph   β”‚
β”‚  and executes actions in correct order       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Next steps in your Bazel journey:

  • Explore rule-specific attributes for your language
  • Learn about macros and custom rules
  • Understand remote caching and execution
  • Master query language for analyzing build graphs

πŸ“š Further Study

  1. Official Bazel Documentation - BUILD Concept: https://bazel.build/concepts/build-files
  2. Bazel BUILD Encyclopedia: https://bazel.build/reference/be/overview
  3. Google's Bazel Tutorial: https://bazel.build/start/cpp

You now have a solid foundation in BUILD files and targetsβ€”the building blocks of hermetic builds with Bazel! πŸš€