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
A full example can be found at: https://github.com/soonick/ncona-code-samples/tree/master/introduction-to-bazel/first-binary
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
A full example can be found at: https://github.com/soonick/ncona-code-samples/tree/master/introduction-to-bazel/local-dependencies
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
A full example can be found at: https://github.com/soonick/ncona-code-samples/tree/master/introduction-to-bazel/external-dependencies
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");
}
}
A full example can be found at: https://github.com/soonick/ncona-code-samples/tree/master/introduction-to-bazel/third-party-dependencies
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
]