Bazel Workspace and BUILD Files: A Practical Linux C++ Guide

Official Bazel logo for Bazel build system guides

A Bazel project becomes much easier to understand once you can separate five ideas: the repository, workspace, package, target, and label. These terms describe where code lives, how build rules are grouped, and how one component refers to another.

This practical Linux C++ guide builds a small project from the root directory upward. It uses the current MODULE.bazel workflow rather than starting with the legacy WORKSPACE file.

The project structure

Start with a repository containing an application and a reusable library:

demo/
├── MODULE.bazel
├── app/
│ ├── BUILD
│ └── main.cpp
└── lib/
├── BUILD
├── greeter.cpp
└── greeter.h

The root marker tells Bazel where the main repository begins. Each directory containing a BUILD or BUILD.bazel file defines a Bazel package. In this example, app and lib are separate packages.

What MODULE.bazel does

MODULE.bazel identifies the repository as a Bazel module and is the main location for declaring external module dependencies. A minimal file can contain only the module declaration:

module(
name = "demo",
version = "0.1.0",
)

The module name is not the same thing as a C++ namespace or an executable name. It identifies the Bazel module when other modules depend on it. Keeping this file small is reasonable until the project actually needs external dependencies or toolchain configuration.

What a BUILD file defines

A BUILD file contains rules. Each rule creates a target that Bazel can analyze, build, test, or use as a dependency. For the library package, define one cc_library target:

cc_library(
name = "greeter",
srcs = ["greeter.cpp"],
hdrs = ["greeter.h"],
visibility = ["//app:__pkg__"],
)

The srcs attribute lists implementation files compiled for the target. The hdrs attribute lists public headers that direct dependents may include. The visibility setting allows targets in the app package to depend on this library.

Create the executable target

The application package defines a binary and declares its direct dependency:

cc_binary(
name = "demo-app",
srcs = ["main.cpp"],
deps = ["//lib:greeter"],
)

The dependency label //lib:greeter has two parts. //lib is the package path from the repository root, and greeter is the target name declared in lib/BUILD.

How Bazel labels work

Labels are stable names for targets and files. Learning their forms removes much of the mystery from Bazel commands:

  • //app:demo-app refers to the executable target in the app package.
  • //lib:greeter refers to the library target in the lib package.
  • :greeter is a relative label used from inside the same package.
  • //app/... selects targets recursively below the app package.
  • //... selects targets recursively throughout the main repository.

The colon separates the package path from the target name. When the target name matches the final package directory, Bazel often permits a shorter form, but explicit labels are clearer while learning.

Build and run the application

From the repository root, build the executable by label:

bazel build //app:demo-app
./bazel-bin/app/demo-app

You can also let Bazel run the binary:

bazel run //app:demo-app

Bazel places convenient output links such as bazel-bin in the repository root. Treat them as generated output rather than project source, and do not commit them to Git.

Packages create boundaries

A package includes files in its directory and subdirectories, except where another BUILD file starts a subpackage. Adding a new BUILD file therefore changes package boundaries.

This matters when labels suddenly stop resolving after a directory is converted into a package. A file that previously belonged to its parent package now belongs to the new package and must be addressed with a different label.

Use visibility intentionally

Visibility controls which packages may depend on a target. Keeping internal libraries private prevents unrelated code from creating accidental dependencies. For a deliberately public target, use:

visibility = ["//visibility:public"]

Do not make every target public merely to silence an error. Decide whether the dependency is part of the intended architecture. A narrow package-level permission is often safer than repository-wide access.

Add a C++ test target

Tests are ordinary targets. A small library test can live beside the library rule:

cc_test(
name = "greeter_test",
srcs = ["greeter_test.cpp"],
deps = [":greeter"],
)

Run one test or every test in the repository:

bazel test //lib:greeter_test
bazel test //...

Inspect the dependency graph

When a dependency is unclear, ask Bazel instead of reading every BUILD file manually:

bazel query 'deps(//app:demo-app)'
bazel query 'rdeps(//..., //lib:greeter)'

The first command lists the transitive dependencies of the application. The second finds targets that depend on the library. Querying the graph is especially useful before changing visibility or splitting a package.

Common structure mistakes

  • Placing one enormous BUILD file at the repository root.
  • Depending on implementation files instead of library targets.
  • Using broad public visibility for every internal component.
  • Adding a subpackage without updating labels that reference its files.
  • Listing indirect dependencies while omitting the direct dependency actually used by the target.
  • Committing bazel-bin, bazel-out, or other generated links.

A maintainable starting point

Keep packages aligned with meaningful components rather than creating a package for every folder. Give each library a focused API, declare direct dependencies in deps, and limit visibility to intended consumers. As the repository grows, this structure gives Bazel enough information to build and test only the affected graph.

Conclusion

MODULE.bazel marks and configures the module, while BUILD files divide source code into packages and targets. Labels connect those targets, deps defines the graph, and visibility protects architectural boundaries. Once those roles are clear, Bazel commands become precise descriptions of what you want to build rather than cryptic strings.

Continue with the official Bazel C++ tutorial, build reference, and C/C++ rule reference.

Leave a Reply