0.3.1 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. This top-to-bottom shape declares what targets exist in the package and how they relate. It is not an execution script.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.
Rule Calls Declare Targets
A BUILD file looks enough like Python that beginners often read it as a script2,3. When Bazel evaluates a call such as java_library(...), it creates a target declaration in the package with a rule kind, a name, and attributes that other targets can refer to2,4. It does not immediately compile code.
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. Similarly, deps = [":core"] declares a graph edge that Bazel uses later when it analyzes and executes the build. It does not schedule an imperative step4,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. The targets and attribute values that exist when package evaluation finishes determine the package description. Bazel can then 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.
The official examples repository makes that progression concrete: its staged C++ tutorial starts with one target, then introduces a library and the explicit deps edge that connects a binary to it.8 The directory is tutorial evidence for this declarative shape, not a claim that every real repository should use the same package layout.
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.
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 one starts
looking like a small application, move reusable logic into a .bzl file and
bring its symbols in with 0.3.5 Load Statements. Later, custom Starlark
abstractions provide a structured home for more substantial logic.
Boring BUILD Files Are Easier To Maintain
Explicit, repetitive-looking BUILD files are preferable to clever abstractions9,10. Duplication is often acceptable because both people and tools such as Buildifier, dependency updaters, and BUILD-file generators maintain these files9,10.
BUILD files therefore favor DAMP over DRY, keep a simple structural order, and avoid patterns that make target definitions harder to read or rewrite automatically1,10.
The build-syntax-error snippet makes the parser boundary reproducible. Its unbalanced filegroup fails before target analysis begins.
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 · 2 questions
1.Match each structural zone of a BUILD file to its purpose:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.True or false: BUILD file evaluation model.
Choose True or False for each sentence
Footnotes
-
BUILD Style Guide — recommended file structure, DAMP-over-DRY guidance, and tool-oriented formatting conventions ↩1 ↩2 ↩3 ↩4
-
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
-
Bazel Training 101 (Part 12): Manually using rules in BUILD files — BUILD vs
.bzlStarlark subsets,load()as the common opening pattern, and package statement placement in practice ↩1 ↩2 ↩3 ↩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
-
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
-
BUILD files —
load()syntax, top-level restriction, label-based.bzllookup, and aliasing ↩1 ↩2 ↩3 -
BUILD files —
package()as package-wide metadata, at-most-once rule, and placement right afterload()statements ↩1 ↩2 -
Bazel examples repository map —
cpp-tutorial/grows a runnable BUILD graph from a binary into separate library and consumer targets. ↩ -
Sharing Variables — BUILD files are intended to stay simple and declarative, and duplication is often preferable to hidden abstraction ↩1 ↩2
-
BUILD Style Guide — Buildifier standardization and guidance against making BUILD files too clever ↩1 ↩2 ↩3