TutorialsLast Updated Jul 16, 20257 min read

Continuous integration for a Bazel Android project

Zan Markan

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 older WORKSPACE 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 a BUILD 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.

Bazel sample Android app demo

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:

  1. Install the Bazel apt repositories
  2. 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.

Setup project on CircleCI

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.

Configure project on CircleCI

Completing the setup will trigger the pipeline and after a few minutes the build should succeed.

Successful build on CircleCI

You can click the unit-tests job to review details of the test execution.

Successful unit test details on CircleCI

Click the build job to view details of the build job.

Successful build details on CircleCI

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.