Bazel Book

Anatomy of a BUILD File

A BUILD.bazel file is easiest to read as a small declaration with a predictable shape: imports at the top, package-wide defaults if the package needs them, then a flat list of target declarations. The important catch is that this top-to-bottom shape is not an execution script. A BUILD file tells Bazel what targets exist in the package and how they relate, so Bazel can derive the build graph later.1,2

load("@rules_java//java:defs.bzl", "java_binary", "java_library")

package(default_visibility = ["//app:__subpackages__"])

java_library(
    name = "core",
    srcs = ["Core.java"],
)

java_binary(
    name = "cli",
    srcs = ["Main.java"],
    main_class = "app.Main",
    deps = [":core"],
)

Read that example in two passes. First, notice the anatomy: load() header, optional package(), then target declarations. Second, notice the meaning of deps = [":core"]: it records that :cli depends on :core; it does not mean "build :core now, then move to the next line". A real file with the shorter load() -> rules shape is app/BUILD.bazel. The visual below combines both reads: file anatomy first, declarative meaning second.

reads top to bottom
BUILD.bazel package declaration
LOADS
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
PACKAGE
optional
package(default_visibility = ["//app:__subpackages__"])
TARGETS
java_library(
name = "core",
srcs = ["Core.java"],
)
java_binary(
name = "cli",
srcs = ["Main.java"],
main_class = "app.Main",
deps = [":core"],
)
LOADS
Imports Starlark symbols
from .bzl files
load("@rules_java//...")
PACKAGE
Sets package defaults
optional when needed
default_visibility = [...]
TARGETS
Declares package targets
with rules or macros
java_binary(name = "cli")
COMMON MISREAD
Treat lines like ordered steps
first this, then that
1
Line 1 builds :core
2
Line 2 builds :cli
3
Expect actions immediately
Treats declaration order as execution
DECLARATIVE MODEL
Read as a dependency graph
:cli depends on :core — that is graph data
order in file ≠ build order
deps
:cli
:core
edge in the graph, not a scheduled step
LOAD BUILD DERIVE GRAPH ACTIONS IF NEEDED
Only the last phase may run work; the BUILD file only feeds the earlier ones.

First Fix The Mental Model

A BUILD file looks enough like Python that beginners often read it as a script. That is the wrong mental model2,3. When Bazel evaluates a call such as java_library(...), it does not immediately compile code. The call creates a target declaration in the package: a target with a rule kind, a name, and attributes that other targets can refer to2,4.

That distinction is why the word target matters. The rule is the type (java_library); the target is one concrete instance created by calling that rule with name = "core" and the rest of the attributes4. The same goes for deps: deps = [":core"] does not schedule an imperative step. It declares a graph edge that Bazel uses later when it analyzes and executes the build4,5.

During loading, Bazel reads those declarations and derives the target graph; only later phases turn that graph into concrete actions and decide which ones actually need to run5. In simple BUILD files, many declarations can therefore be reordered without changing behavior2. What matters is not "which line ran first", but which targets and attribute values exist by the time package evaluation finishes. Once Bazel has that description, it can analyze dependencies, cache results, parallelize independent work, and avoid rebuilding targets whose inputs have not changed5.

The Header: load() Then package()

Many BUILD files begin with one or more load() statements, which import symbols from .bzl files into the current file3,6. The first argument is a label pointing at the .bzl file, so the label syntax from 0.2.1 Label Anatomy already matters before you define a single target6. load() belongs at top level, and the style guide treats all load() calls as the first structural block in the file1,6.

If the package needs defaults, the next slot is package()1,7. That call does not declare a target. It declares metadata for the whole package, which is why it belongs in the file header rather than mixed into the body below7. In Level 0, the default you will most often notice is default_visibility; the detailed semantics are covered in 0.2.6 package() Function.

The Body Is Target Declarations

Below the header, a BUILD file is mostly a series of top-level rule or macro calls. That body reads more like a manifest than a shell script. You are not telling Bazel "first compile this, then run that". You are declaring which targets exist in the package, and with what attribute values, in a form Bazel can analyze later2,4.

That is also why BUILD files stay intentionally boring. The detailed rule kinds and attribute roles come next in 0.3.2 Rules and 0.3.3 Attributes & Semantic Roles. At this level, the important thing is to recognize that each call contributes one more declaration to the package, not one more step to execute.

Still Starlark, But Deliberately Smaller

BUILD files use Starlark, but not the full language surface you might expect from Python-like syntax2,3. They cannot contain function definitions, top-level for or if statements, *args / **kwargs, or arbitrary I/O2. The restriction is intentional: Bazel wants BUILD-file evaluation to stay hermetic and predictable, and the reduced language makes parallel loading possible without hidden side effects2,3,4.

Declarative does not mean "no syntax at all". BUILD files can still use simple variables, list comprehensions, and if expressions in the limited places Bazel allows2. Those constructs are fine when they support target declarations rather than turning the file into a miniature program.

That design choice also explains why BUILD files tend to stay flat. If the file starts looking like a small application, it is probably carrying logic that belongs in 0.3.5 Load Statements or, later, in custom Starlark abstractions rather than in the BUILD file itself.

key takeaway

A BUILD file has both a recognizable anatomy and a declarative meaning: load() statements first, optional package() defaults second, target declarations after that, with attributes such as deps describing graph relationships rather than ordered build steps. When you open an unfamiliar BUILD file, first find those zones, then ask what targets the package declares and how they connect. The follow-up topics in 0.3.2 Rules, 0.3.3 Attributes & Semantic Roles, and 0.3.5 Load Statements explain what each zone can contain and why Bazel keeps them so constrained.

Check your understanding

1.What is the correct way to think about a BUILD file?

2.What is the standard structure of a BUILD file?

3.Why can't BUILD files contain function definitions or top-level for loops?

Answer all questions to check

Footnotes

  1. BUILD Style Guide — recommended file structure, DAMP-over-DRY guidance, and tool-oriented formatting conventions 1 2 3 4

  2. BUILD files — BUILD files as sequential Starlark programs, rule calls creating targets, order semantics, allowed syntax, and language restrictions 1 2 3 4 5 6 7 8 9

  3. Bazel Training 101 (Part 12): Manually using rules in BUILD files — BUILD vs .bzl Starlark subsets, load() as the common opening pattern, and package statement placement in practice 1 2 3 4

  4. Sponsored Session: Writing Bazel Rules - Instructor: Jay Conrod — rule-vs-target terminology, the graph-building model, and why BUILD files are more restricted than general Starlark 1 2 3 4 5

  5. Writing Bazel rules: moving logic to execution — loading/analysis/execution phase split and why explicit declarations enable later action planning, caching, and incremental work 1 2 3

  6. BUILD filesload() syntax, top-level restriction, label-based .bzl lookup, and aliasing 1 2 3

  7. BUILD filespackage() as package-wide metadata, at-most-once rule, and placement right after load() statements 1 2

  8. Sharing Variables — BUILD files are intended to stay simple and declarative, and duplication is often preferable to hidden abstraction 1 2

  9. BUILD Style Guide — Buildifier standardization and guidance against making BUILD files too clever 1 2 3