Bazel is a build system (analogous to make, ant, etc.) that promises better dependency analysis, faster builds and better reproducibility. One of the goals of Bazel is to make it easy to be extended to support multiple languages; Currently extensions exist for most of the popular programming languages.

Bazel is a fork of Blaze, Google’s internal build system, so it has been battle tested on very large codebases.

Installation

For up-to-date instructions, visit the official Bazel documentation

To install in Ubuntu based systems, we need to first register the software source:

1
2
3
4
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

And then install it:

1
sudo apt update && sudo apt install bazel

First Binary

Let’s start by creating a folder where we’ll run some examples:

1
2
mkdir bazel-example
cd bazel-example

This folder is going to be our workspace. In Bazel a workspace is a folder that contains the source code that we want to build. To officially make this directory a workspace we need to create a file:

1
touch WORKSPACE

This file tells Bazel that this is the root of the project.

The primary unit of code organization in Bazel is called a package. A package is defined by a folder inside a workspace that contains a BUILD file:

1
2
3
mkdir example-package
cd example-package
touch BUILD

Before we fill our BUILD file, let’s write some source code. Create a file:

1
touch ExamplePackage.java

And add this content:

1
2
3
4
5
6
7
package example;

public class ExamplePackage {
  public static void main(String args[]) {
    System.out.println("Hello world");
  }
}

The code above is a very simple “Hello world” program. Now, let’s tell Bazel how to build our application. Add this content to the BUILD file:

1
2
3
4
5
6
7
java_binary(
    name = 'example_package',
    srcs = [
      'ExamplePackage.java'
    ],
    main_class = 'example.ExamplePackage',
)

We use the java_binary rule. All rules in Bazel require a name. This name is called a target name in Bazel, and it’s used to create unique way to refer to this specific binary. We chose example_package as a name, but it could have been anything. The srcs argument is used to specify the source files we want to compile. Finally, we need to tell Bazel which package contains the main function.

We can now build our binary:

1
bazel build //example-package:example_package

And run it:

1
bazel run //example-package:example_package

You might be wondering about the //example-package:example_package syntax. This is called a label. A label is used to uniquely identify a target. The // at the beginning tells Bazel to go to the root of this workspace (Where the WORKSPACE file is). After that, we have the path to the package (Where the BUILD file is) we want to build. Finally, we have the target name.

The build command does what we expect; It builds our binary. Bazel will create a few directories in the root of our workspace as a result. The one we care about the most now is bazel-bin. This is where the files generated by building our target are stored. We used bazel run to execute the binary, but we could have also executed the binary directly from this directory:

1
./bazel-bin/example-package/example_package

Local dependencies

Let’s look at a few ways we can deal with dependencies that live in the same workspace. Let’s start by creating a new file inside example-package:

1
touch Greeter.java

And add this content:

1
2
3
4
5
6
7
package example;

public class Greeter {
  public static void greet(final String name) {
    System.out.println("Hello " + name);
  }
}

We will also modify ExamplePackage.java to use this new file:

1
2
3
4
5
6
7
package example;

public class ExamplePackage {
  public static void main(String args[]) {
    Greeter.greet("world");
  }
}

We don’t need an import statement because Greeter is in the same package as ExamplePackage.

Before we can build we need to also update the BUILD file, so Bazel knows about the new file:

1
2
3
4
5
java_binary(
    name = 'example_package',
    srcs = glob(['*.java']),
    main_class = 'example.ExamplePackage',
)

Note that we used glob to select all java files in the current folder. We can now build and run our binary:

1
bazel build //example-package:example_package && bazel run //example-package:example_package

We successfully ran a binary that consists of a single package with multiple files.

Let’s now move Greeter to a different package. In java, usually we have one package per folder, so let’s create a folder, and move our Greeter there:

1
2
mkdir util
mv Greeter.java util/

We also need to change the package name in Greeter.java:

1
2
3
4
5
6
7
package util;

public class Greeter {
  public static void greet(final String name) {
    System.out.println("Hello " + name);
  }
}

Import this package from ExamplePackage.java:

1
2
3
4
5
6
7
8
9
package example;

import util.Greeter;

public class ExamplePackage {
  public static void main(String args[]) {
    Greeter.greet("world");
  }
}

And modify the glob in the BUILD file so it includes this new folder:

1
2
3
4
5
java_binary(
    name = 'example_package',
    srcs = glob(['**/*.java']),
    main_class = 'example.ExamplePackage',
)

We can run the binary again and we will get the same result:

1
bazel build //example-package:example_package && bazel run //example-package:example_package

Although this works; Bazel recommends to split projects into different build targets. One way we can do this is by modifying the BUILD file again. This time, adding a java_library rule:

1
2
3
4
5
6
7
8
9
10
11
java_binary(
    name = 'example_package',
    srcs = ['ExamplePackage.java'],
    main_class = 'example.ExamplePackage',
    deps = [':util'],
)

java_library(
    name = 'util',
    srcs = glob(['util/*.java']),
)

We made 2 important changes. First, we added a java_library rule. This rule is very similar to java_binary, but it doesn’t require a main_class parameter. The other important change is the addition of the deps parameter to example_package target. Since the label we depend on is in the same package, we can omit the path and use only the target name (:util). Same command can be used to run the binary:

1
bazel build //example-package:example_package && bazel run //example-package:example_package

The command builds the java_binary, and all the dependencies that have changed since the last build.

We can also build only the library if that’s what we want:

1
bazel build //example-package:util

Working with external dependencies

Now that we know the basics of Bazel, let’s look at how we can access dependencies that are not in our workspace. We’ll start with dependencies in the same filesystem. Let’s create the directory structure we need for this exercise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Root directory for this example
mkdir external-dependencies
cd external-dependencies

# Workspace for our greeter library
mkdir greeter
touch greeter/BUILD
touch greeter/WORKSPACE
touch greeter/Greeter.java

# Workspace for our binary
mkdir example
touch example/BUILD
touch example/WORKSPACE
touch example/Example.java

external-dependencies/greeter/Greeter.java:

1
2
3
4
5
6
7
package greeter;

public class Greeter {
  public static void greet(final String name) {
    System.out.println("Hello " + name);
  }
}

external-dependencies/greeter/BUILD:

1
2
3
4
java_library(
    name = 'greeter',
    srcs = ['Greeter.java']
)

external-dependencies/example/Example.java:

1
2
3
4
5
6
7
8
9
package example;

import greeter.Greeter;

public class Example {
  public static void main(String args[]) {
    Greeter.greet("world");
  }
}

So far, everything we have done should feel familiar. It’s now when we have to learn more about the WORKSPACE file.

So far our WORKSPACE file has been empty. To configure external sources we will need to add repository rules (also called workspace rules) to external-dependencies/example/WORKSPACE:

1
2
3
4
local_repository(
  name = 'greeter',
  path = '../greeter'
)

With the repository configured, we can add our external dependency to external-dependencies/example/BUILD:

1
2
3
4
5
6
java_binary(
    name = 'example',
    srcs = ['Example.java'],
    main_class = 'example.Example',
    deps = ['@greeter//:greeter'],
)

Note the value for deps. It starts with an @, which means we are going to look for an external repository. Then, we have the name of the repository greeter. Followed by the target //:greeter.

If we try to build this, we will get an error. Bazel helps separate internal implementation from the public api by only allowing external repositories to depend on rules that have been marked as available to the public. This means we need to mark the java_library in external-dependencies/greeter/BUILD as public:

1
2
3
4
5
java_library(
    name = 'greeter',
    srcs = ['Greeter.java'],
    visibility = ["//visibility:public"],
)

We can now build and run our example binary (from within the example workspace):

1
bazel build //:example && bazel run //:example

Third party dependencies

So far I’ve gotten away without any third party dependencies because my examples have been trivial, but most real projects depend on many third party libraries. In this section we’ll learn how to consume these libraries.

Bazel supports multiple programming languages, and each programming language deals with external packages in different ways. I’m just going to show how to do it in Java.

Dealing with third party packages in Bazel is somewhat complicated compared to other build systems I have used. We have to start by downloading the Maven repository rule by adding this to our WORKSPACE file:

1
2
3
4
5
6
7
8
9
10
11
load('@bazel_tools//tools/build_defs/repo:http.bzl', 'http_archive')

RULES_JVM_EXTERNAL_TAG = '4.1'
RULES_JVM_EXTERNAL_SHA = 'f36441aa876c4f6427bfb2d1f2d723b48e9d930b62662bf723ddfb8fc80f0140'

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,
)

This code tells Bazel to install a new repository rule found at https://github.com/bazelbuild/rules_jvm_external/archive/4.1.zip and name it rules_jvm_external. This rule knows how to retrieve dependencies from Maven.

The next step is to tell Bazel which dependencies we want. We can do it by adding this in the same file:

1
2
3
4
5
6
7
8
9
10
load('@rules_jvm_external//:defs.bzl', 'maven_install')

maven_install(
  artifacts = [
    'org.junit.jupiter:junit-jupiter-api:5.7.1',
  ],
  repositories = [
    'https://repo1.maven.org/maven2',
  ],
)

This time we are loading the maven_install rule from the repository we just installed. With this rule we can list which artifacts we want to download from which Maven repositories.

Once we have our artifacts installed, we can use them. Let’s create a BUILD file:

1
2
3
4
5
6
7
8
java_binary(
  name = 'example',
  srcs = ['Example.java'],
  main_class = 'example.Example',
  deps = [
    '@maven//:org_junit_jupiter_junit_jupiter_api'
  ],
)

Note that we use @maven to specify a dependency on a Maven artifact. The name of the dependency is the name of the artifact replacing dots, colons and dashes (., :, -) with underscores (_): org_junit_jupiter_junit_jupiter_api.

Finally, we can use our dependency in Example.java:

1
2
3
4
5
6
7
8
9
package example;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String args[]) {
    assertEquals(1, 2, "They are not equal");
  }
}

Conclusion

Bazel makes it very easy to split an app into libraries and modules that can be compiled separately and reused where necessary.

The problem comes when we need to make use of third party dependencies. The documentation doesn’t do a good job at explaining what each step is doing, so it’s very hard to get the initial setup working if you don’t know what you are doing. Once the setup is there, it should be easy to add more dependencies.

[ automation  dependency_management  java  productivity  ]
Dependency injection with Dagger and Bazel
Monetizing a Jekyll blog with Adsense
Introduction to Simple Workflow Service (SWF)
Introduction to CloudFormation
AWS CodePipeline