Continuous integration for a Bazel Android project

Developer Advocate

Bazel (pronounced like the tasty herb: “bay-zell”) is an universal build tool developed by Google. Some notable companies like Twitter and projects like the Android Open Source project have migrated to Bazel. In this tutorial, you will learn how to build a Bazel Android project and set it up for continuous integration with CircleCI. We will wrap up by automatically running tests and producing a binary APK file.
In addition to the written guide there is a working sample project. The sample project is also available to view on CircleCI.
About the sample project
The sample project for this tutorial is a minimal Android app written in Kotlin with a Bazel build configuration. The project app has build targets for both app binary - //src/main:app
, as well as unit tests with - //src/test
.
Prerequisites
To complete this tutorial, you should have some experience with modern Android development, Kotlin, Gradle, and Git. You do not need any experience with Bazel.
Setting up a project for Bazel
To get started, you will need to go to GitHub, clone the sample project, and review the setup.
The sample project is a modified version of the example project in Bazel repository. Under the src/main
directory, it has a MainActivity
and a Greeter
file. It also contains the test
and androidTest
subdirectories under src
containing unit tests and Roboelectric tests respectively.
Note: The sample project uses Java for the src
files. Refer to the Bazel Kotlin Rules GitHub repository if you want to use Bazel with Kotlin code.
Using modules, builds, and rules
You need two files to get started with Bazel: MODULE.bazel
and BUILD
. The MODULE.bazel
file should be in the top level directory from where all other resources are referenced. It is a manifest, declaring its name, version, list of direct dependencies, and other information.
Here’s a typical MODULE.bazel
file for an Android project:
module(
name = "basicapp",
)
bazel_dep(name = "rules_java", version = "8.12.0")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(
name = "rules_android",
version = "0.6.4",
)
# Maven dependencies for testing
bazel_dep(name = "rules_jvm_external", version = "6.6")
This code snippet defines:
- The
module()
function, which declares the module name ie.basicapp
. bazel_dep()
, which declares dependencies on other Bazel modules with specific versions. This approach is more declarative and version-aware compared to the olderWORKSPACE
file approach.
Fetching Maven dependencies
In Android and JVM projects, you usually fetch dependencies from a Maven repository. The two most common repositories for open source dependencies are either Maven Central or JCenter. For Android-specific dependencies there is also Google’s own Maven repository. For Maven dependencies, you can use the extensions provided by the rules_jvm_external module:
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
artifacts = [
"junit:junit:4.13.2",
"org.assertj:assertj-core:3.24.2",
],
repositories = [
"https://repo1.maven.org/maven2",
],
)
use_repo(maven, "maven")
The artifacts argument contains all the dependencies and their versions, and repositories specifies where they come from.
The dependencies are downloaded for the whole module, and not included in the app yet. You will find them included a bit later in this tutorial.
Setting up Android SDK and tools
For Android development, you need to configure the Android SDK and related tools:
remote_android_extensions = use_extension(
"@rules_android//bzlmod_extensions:android_extensions.bzl",
"remote_android_tools_extensions")
use_repo(remote_android_extensions, "android_gmaven_r8", "android_tools")
android_sdk_repository_extension = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension")
android_sdk_repository_extension.configure()
use_repo(android_sdk_repository_extension, "androidsdk")
register_toolchains("@androidsdk//:sdk-toolchain", "@androidsdk//:all")
This configuration automatically detects your Android SDK installation using the ANDROID_HOME
environment variable, making it work seamlessly in both local development and CI environments. Later in the tutorial, you will learn how to set the environment variable and build the app locally.
What is a Bazel package?
Bazel apps are called targets, and they are located inside Bazel packages. A Bazel package is:
- Any directory that has a
BUILD
file. - Its subdirectories, unless a subdirectory contains its own
BUILD
file. In that case that particular subdirectory becomes its own package.
Packages in Bazel are addressed from within the workspace with a double slash //
and their directory structure. Your application has:
- Source package under
//src/main
with aBUILD
file for the main app. - Unit test package under
//src/test
. - Instrumentation test package under
//src/androidTest
.
Using targets in Bazel applications
The BUILD
file contains the load
method calls described earlier, as well as android_binary
calls. android_binary
is a target in the Bazel application. The test
directory has a java_test
target for unit testing and androidTesting
has the android_library
target for instrumentation testing.
Targets can be anything that takes input, and produces an output of the build. In our case that can be source code, or another target. Each Bazel application can contain multiple targets.
The Android Binary outputs your .apk
file, and test
does the test. You can find documentation for both in the Bazel docs. For each Android target, you must include the Android Manifest file.
load("@rules_android//rules:rules.bzl", "android_binary")
android_binary(
name = "app",
manifest = "AndroidManifest.xml",
deps = ["//src/main/java/com/example/bazel:greeter_activity"],
)
Building the project using Bazel commands
To build, use bazel build [target]
. The [target]
is the fully qualified Bazel target in your workspace. For the example app in this tutorial, the target is: //src/main:app
, so the command is:
bazel build //src/main:app
The first build may take some time, but Bazel will cache most dependencies and interim artifacts, so future builds will be faster.
Installing the sample application
All final build artifacts are stored in bazel-bin/app/src/main/app.apk
. To install the app, you can use the adb install
command:
adb install bazel-bin/src/main/app.apk
This command will install the app on the connected device. You can launch the app to review it.
Setting up a Bazel project with CircleCI
CircleCI has a number of Android Docker images that ship with everything you need to build Android applications. That is, almost everything. Bazel is not installed by default so that is your first step.
The android/android_docker
Docker images are based on Debian Linux, so you can use the Ubuntu installation instructions from the Bazel documentation. There are two steps:
- Install the Bazel apt repositories
- install Bazel itself with
apt install
A single setup-bazel
CircleCI command does the work:
commands:
setup-bazel:
description: |
Setup the Bazel build system used for building Android projects
parameters:
bazel-version:
type: string
default: "bazel"
steps:
- run:
name: Add Bazel Apt repository
command: |
sudo apt install curl gnupg
curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg
sudo mv bazel.gpg /etc/apt/trusted.gpg.d/
echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
- run:
name: Install Bazel from Apt
command: |
sudo apt update && sudo apt install << parameters.bazel-version >>
This snippet is mostly copy/pasteable and reusable. You might just want to pin a specific version of Bazel for even more deterministic builds. I’ll show you why in the next steps, and in the final sample project.
Testing and building Bazel targets
To test and build Bazel targets, you need bazel test
and bazel build
commands respectively, passing the qualified package and name for the correct target. In the case of our example these are //app/src:unit_tests
for the tests, and //app/src:app
for the application binary.
In the example we have them built right after the setup-bazel
step.
jobs:
build:
parameters:
bazel-version:
description: "Pinned Bazel version"
default: "bazel-8.2.1"
type: string
executor:
name: android/android_docker
tag: 2025.03.1
steps:
- checkout
- android/accept_licenses
- setup-bazel:
bazel-version: <<parameters.bazel-version>>
- run:
name: Run build
command: << parameters.bazel-version >> build //src/main:app
Storing test and build artifacts
Bazel for Android stores all test output in bazel-testlogs
and all binary output in the bazel-bin
directory in the project.
The outputs will take the same package structure as Bazel targets (src/main
for this project). CircleCI stores every useful piece of output when you add these stanzas:
- store_artifacts:
path: ~/project/bazel-bin/src/main/app.apk
- store_artifacts:
path: ~/project/bazel-bin/src/main/app_unsigned.apk
You’re storing the app_unsigned.apk
because you will need to sign it yourself, if you want to produce a release build to distribute it. You can read more about signing manually on the Android developers portal.
Installing and using a specific Bazel version for more deterministic builds
When installing Bazel using apt install bazel
you are installing the latest stable version. Always using the latest and greatest may be fine on a local machine, but in a CI/CD context you likely want more determinism in your builds.
By modifying your apt install bazel
line to use a specific version you ensure using the latest version consistently: apt install bazel-8.2.1
. You will need to make sure to use that specific version in all subsequent calls. For example, bazel-8.2.1 build ...
.
One way to use a specific version of Bazel is by using CircleCI reusable parameters in your config.yml
. The sample project uses parameters inside the build
job:
jobs:
unit-test:
parameters:
bazel-version:
description: "Pinned Bazel version"
default: "bazel-8.2.1"
type: string
executor:
name: android/android_docker
tag: 2025.03.1
steps:
- checkout
- android/accept_licenses
- setup-bazel:
bazel-version: <<parameters.bazel-version>>
- run:
name: Run tests
command: << parameters.bazel-version >> test //src/test:all
Running the workflow on CircleCI
On the CircleCI dashboard, click Projects. Search for the GitHub repo name and click Set Up Project for your project.
You will be prompted to add a new configuration file manually or use an existing one. You have already pushed the required configuration file to the codebase, so you can select the Fastest option. Enter the name of the branch hosting your configuration file and click Set Up Project to continue.
Completing the setup will trigger the pipeline and after a few minutes the build should succeed.
You can click the unit-tests
job to review details of the test execution.
Click the build
job to view details of the build job.
Note that the first execution of the pipeline might take longer to complete depending on the complexity of the Android project.
Conclusion and next steps
I hope this tutorial has given you an idea of how to get a Bazel Android application running and building in your CI/CD pipeline. Next steps could be expanding the pipeline even further with automatic deployment to a testing service, or even directly distributing the app on an app store.