Hello Android developers,
Today we are going to go through an interesting topic, we will learn what it is and how to use Bazel, a build and test tool similar to Make, Maven, and Gradle.
Many great companies and OSS projects are already using Bazel to build their software. Would you like to try it out for your Android app? In this blog post you will learn how to do it.
What is Bazel?
The bazel.build site says: Bazel is an open-source build and test tool. It uses a human-readable, high-level build language. It supports projects in multiple languages and build outputs for multiple platforms. Bazel supports large codebases across multiple repositories, and large number of users.
Benefits of using Bazel
These are some of the benefits that Bazel provides (from the website):
High-level build language: Bazel uses Starlack that is a dialect of Python. It was designed for Bazel and is intended for use as a configuration language. Bazel operates on the concepts of libraries, binaries, scripts and data sets taking out from you the complexity of writing individual calls to tools such as compilers and linkers.
Bazel is fast and reliable: Bazel caches all previously compiled work and tracks changes to both file content and build commands. This way, Bazel knows what is needed to rebuild avoiding spending time and resources building parts of the code that didn’t change since last time.
Bazel is multi-platform: It can be used on GNU/Linux, macOS and Windows.
Bazel scales: Bazel maintains agility while handling builds with 100K+ source files. It works as well with multiple repositories.
Bazel is extensible: Many languages are supported, and you can extend Bazel to support any other language or framework.
Important concepts worth knowing
If we are new to Bazel there are some things that will sound strange to us and could be difficult to understand. Along the article we also will use some Bazel specific terminology so it’s a good idea to go through some of the most important ones:
- Workspace: is a directory where Bazel looks for build inputs and BUILD files, and where it stores build outputs. Each Workspace has a text file named WORKSPACE which may be empty or may contain references to external dependencies required to build the outputs.
- BUILD files: specifies what software outputs can be built from the source. We can have one file where all configurations live or several, splitting the configuration through them.
- Packages: a package is a collection of related files and a specification of how they can be used to produce output artifacts. Every package contains a BUILD file.
- Rules: are used and set to support popular languages and packages. We can find rules for many purposes such as building Android apps, using Docker containers, or to simply access Maven repositories.
- Targets: a package is a container of targets, which are defined in the package’s BUILD file.
- Action graph: represents the build artifacts, the relationships between them and the build actions that Bazel will perform.
Building projects using Bazel
Bazel can be used to build code written in different programming languages or frameworks, following a common set of steps:
- Set up Bazel.
- Setup a project workspace.
- Write a BUILD file (or several depending on how you want to distribute the configuration).
- Run Bazel.
Bazel can also be used to run tests and query the build to trace dependencies in code, but in this article, we will just focus on building code.
Building an Android app using Bazel
We already have seen what Bazel is, its benefits, key components and terminology that we should know, and the common steps to build projects. Now we are going to see how to build an Android app.
In order to explain the process and to provide you an example that you can follow and use, we have created an Android sample app implemented using Kotlin and that shows specific Surface Duo resource qualifiers information. We will show here its code and will comment on it.
This is the app structure:
Figure 1. Sample app project structure.
As you see in the above figure, this project follows the conventional project structure of any Android app, where we have our source files, the resource files, and the manifest. But as we see, we don’t have the typical Gradle files that are common on most Android apps, instead, we have two files: BUILD and WORKSPACE. Those, as we have seen before, are Bazel files.
WORKSPACE
Let’s assume that we have followed the first step mentioned before about how to build a project with Bazel, that is installing and setting up Bazel. Now we will set up the project workspace. For the workspace we are using the project’s root level, so we will place our WORKSPACE file in there.
Following our sample app, let’s see step by step what we need to configure to access and set up the external dependencies that we need to build the output for our app:
-
Set up the Android SDK and define the specific build tools we want to use:
android_sdk_repository( name = "androidsdk", build_tools_version = "30.0.3")
-
Define some rules we need in order to access the different resources that we will need, for instance Maven dependencies:
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") RULES_JVM_EXTERNAL_TAG = "4.2" RULES_JVM_EXTERNAL_SHA = "cd1a77b7b02e8e008439ca76fd34f5b07aecb8c752961f9640dea15e9e5ba1ca" http_archive( name = "rules_jvm_external", strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, sha256 = RULES_JVM_EXTERNAL_SHA, url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, ) load("@rules_jvm_external//:defs.bzl", "maven_install")
-
Now that we have set the rule to access Maven dependency feeds, we can define the specific feed that we will use and the dependencies we want to get from there:
maven_install( artifacts = [ "androidx.appcompat:appcompat:1.0.2", "com.google.android.material:material:1.0.0", ], repositories = [ "https://maven.google.com", ], fetch_sources = True, )
-
The last step we need to define is to set up the rules and toolchains that will help us to build Kotlin code, including the version of the Kotlin compiler that we will use:
#---- Kotlin version ---# rules_kotlin_version = "legacy-1.4.0-rc4" rules_kotlin_sha = "9cc0e4031bcb7e8508fd9569a81e7042bbf380604a0157f796d06d511cff2769" http_archive( name = "io_bazel_rules_kotlin", sha256 = rules_kotlin_sha, urls = ["https://github.com/bazelbuild/rules_kotlin/releases/download/%s/rules_kotlin_release.tgz" % rules_kotlin_version], ) #---- compiler version ---- # kotlin_version = "1.6.20-RC" kotlin_release_sha = "2f78ced6b983db49ea1cbcbe41c18bff19ced596861f6bd8af01311d71b6d81d" rules_kotlin_compiler_release = { "urls": [ "https://github.com/JetBrains/kotlin/releases/download/v{v}/kotlin-compiler-{v}.zip".format(v = kotlin_version), ], "sha256": kotlin_release_sha, } load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories") kotlin_repositories(compiler_release = rules_kotlin_compiler_release) #--- Kotlin toolchains ---- # load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_register_toolchains") kt_register_toolchains()
And this is the basic setup and rules that we need to build our Kotlin Android app. You can see the complete WORKSPACE file from the sample code.
BUILD
Now let’s look at the other Bazel file that we need to setup, the BUILD file. Remember that this file is used to specify what software outputs can be built from the source. In our case, we need:
- Build the Android resource files.
- Build the Kotlin code.
- Create the Android binary with everything packed in there.
Let’s see what we need to add:
-
We must define the rules we need to get the dependencies needed to build our resources and to build the Kotlin code:
load("@rules_jvm_external//:defs.bzl", "artifact") load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
-
Define the package used and the manifest file name so we can reuse them later:
PACKAGE = "com.cesarvaliente.qualifiers" MANIFEST = "AndroidManifest.xml"
-
Build the resources:
android_library( name = "lib_res", custom_package = PACKAGE, manifest = MANIFEST, resource_files = glob(["res/**"]), enable_data_binding = False, deps = [ artifact("com.google.android.material:material"), ], )
-
Build the Kotlin source code:
kt_android_library( name = "lib_kt", srcs = glob(["java/**/*.kt"]), deps = [ ":lib_res", artifact("androidx.appcompat:appcompat"), ] )
-
As the last step, create the binary using the previously created outputs:
android_binary( name = "app", manifest = MANIFEST, custom_package = PACKAGE, manifest_values = { "minSdkVersion": "21", "versionCode" : "1", "versionName" : "1", "targetSdkVersion": "29", }, deps = [ ":lib_res", ":lib_kt", ], )
And that’s all. We already have everything that we need to build our Kotlin Android app using Bazel. You also can see the complete BUILD file in our sample app.
Build and deploy the app
Now we can start the build process and install the binary created in a device or emulator. Let’s do it:
-
In a terminal, go inside the project’s folder. For instance, in our sample app, it would be
/foo/bar/qualifiers-bazel-sample
. -
In a terminal, inside the previously accessed folder, type
bazel build //src/main:app
. This will start the build process. We have defined the package used where the BUILD file is located and the application name that we set in that file. -
Once we have the binary, it is time to install it in a device or emulator, simply type now:
bazel mobile-install //src/main:app
And that’s all! You should see the sample app running on your device or emulator! We invite you to follow these steps and build a Bazel configuration for your own app.
Resource qualifiers using Bazel
If you are using resource qualifiers that match screen configuration, like the ones we use on Surface Duo (e.g. values-sw732dp-2754x1832
on Surface Duo 2); when building your app using Bazel, you may find an error during the build process if the last part of the qualifier, that defines the screen size (e.g. 2754×1832), uses the values inverted: the lower value as a first parameter and the higher value as the second (e.g. 1832×2754).
But why would you use these inverted values? In the example that we have mentioned just above, we have used the resource qualifier that matches the Surface Duo dimensions when the app is spanned across displays, meaning that the total width is 2754 and the total height 1832. But if for example, we need to use resource qualifiers that match the single screen configuration we should use values-sw537dp-1344×1832 (being 1344 the width of just one display). If you have that resource qualifier configuration, when building your app with Bazel, it will trigger error: invalid configuration 'sw537dp-1344x1832'
.
When Bazel processes Android resources using aapt2, the resources where the left value is smaller than the right value are considered malformed resources. To fix that, we must change the position of the values and instead values-sw537dp-1344x1832
, now it would become values-sw537dp-1832x1344
. Later, when the resources are used in the app, they will be correctly used in the correct configuration.
In the sample app we have been using throughout the whole article you will also find these specific resource qualifiers so you can also test that.
Summary
As we have seen, in this article we have learned what Bazel is, the benefits that Bazel can provide to your build system, how to use it and a special scenario that we should be aware of when using resource qualifiers with Bazel.
Resources and feedback
The code for the qualifiers-bazel-sample is available on GitHub.
If you have any questions, or would like to tell us about your apps, use the feedback forum or message us on Twitter @surfaceduodev.
Finally, please join us for our dual-screen developer livestream tomorrow (Friday 10th June) at 13h00 Central European Time, and replayed at 11am (Pacific time) – mark it in your calendar and check out the archives on YouTube.
0 comments