Bazel Build System Introduction for Java

You can find the source code of tutorial in bazel-101 branch.

What will you learn?

  • Introduction to bazel build system
  • How to build and run Java package?
  • How to add maven dependency to bazel build files?
  • How to add protobuf compiler to bazel build?

Introduction

Bazel is imperative build system that can build packages for Java, C++, Python, Ruby, Go, etc … The two main advantages of bazel,

  1. One build tool can build packages for variety of languages and easier for platform teams to build packages across variety of languages. Consider learning many different build systems - Pip, bundle, maven, etc…
  2. Bazel build system can cache already built packages in a remote or local environment and can reuse it without compiling be it for binary, library, or tests.

The main difference between bazel and other build/dependency management systems is imperative vs declarative approach.

Consider a Java package sample with the following structure with one file Sample.java

$ls_custom

.
BUILD
src/main/java/com/example/Sample.java
WORKSPACE

Every project contains one WORKSPACE file and contains one or many BUILD files. One BUILD file for a package. In the example project, BUILD

WORKSPACE: A directory containing a WORKSPACE file and source code for the software you want to build. Labels that start with // are relative to the workspace directory.

WORKSPACE FILE: Defines a directory to be a workspace. The file can be empty, although it usually contains external repository declarations to fetch additional dependencies from the network or local filesystem.

A BUILD file is the main configuration file that tells Bazel what software outputs to build, what their dependencies are, and how to build them. Bazel takes a BUILD file as input and uses the file to create a graph of dependencies and to derive the actions that must be completed to build intermediate and final software outputs. A BUILD file marks a directory and any sub-directories not containing a BUILD file as a package, and can contain targets created by rules. The file can also be named BUILD.bazel.

The Sample.java file looks like

package com.example;

public class Sample{
    public static void main(String[] args) {
        String label = "Krace";
        System.out.println(String.format("Hello: %s", label));

    }
}

Build the target

Now let’s build the java binary and execute it. The Sample.java file has no external dependency.

Assuming the bazel is installed, let’s write imperative code to build the binary package(BUILD file).

java_binary(
    name = "Sample",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

In BUILD file mention, it’s a Java binary by invoking java_binary function, name the package as Sample and source files as srcs=glob(["src/main/java/com/example/*.java"]). All the java files inside src/main/java/com/example directory is part of the package Sample.

Now build the package using bazel build <target> syntax.

$bazel build Sample
INFO: Analyzed target //:Sample (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:Sample up-to-date:
  bazel-bin/Sample.jar
  bazel-bin/Sample
INFO: Elapsed time: 0.046s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

The command built the package without any error and the target is generated in bazel-bin directory.

Run the target

Now run the target using bazel run <target> or ./bazel-bin/Sample

 ./bazel-bin/Sample
Hello: Krace

When you invoke bazel run <target>, bazel build the package and executes it(uses the cache, if there is no change).

$ bazel run Sample
INFO: Analyzed target //:Sample (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:Sample up-to-date:
  bazel-bin/Sample.jar
  bazel-bin/Sample
INFO: Elapsed time: 0.046s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Hello: Krace

Similar to java_binary, notable functions are java_library, java_test.

Add a dependency from the maven repository

Bazel has rules and definition for how to download and build the packages that are distributed to the repositories like maven or zip files.

To download file from maven repository, bazel needs to some information about the repository and it’s structure.

In WORKSPACE file, you can details about the maven bazel rules and what to packages are required.

Let’s add okhttp3 from maven as dependency to Sample Project.

Update WORKSPACE file

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",
    sha256 = RULES_JVM_EXTERNAL_SHA,
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps")

rules_jvm_external_deps()

load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup")

rules_jvm_external_setup()

load("@rules_jvm_external//:defs.bzl", "maven_install")

maven_install(
    artifacts = [
        # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
        "com.squareup.okhttp3:okhttp:jar:4.10.0",
    ],
    repositories = [
        "https://repo1.maven.org/maven2",
    ],
)

That’s a lot of copy-paste code!

  1. First the bazel workspace loads http build file located in bazeltools repo.
  2. Set some rules for JVM, external dependencies and external setup.
  3. Then workspace loads maven_install function.
  4. maven_install function specifies the dependency and repository location for installation.

Update the BUILD file

Now the workspace knows what to load for the project, now let’s update the build file.

Most of the heavy lifting happens in the WORKSPACE file. In build file, mention the dependency to load using deps parameter to the function java_binary.

java_binary(
    name = "Sample",
    srcs = glob(["src/main/java/com/example/*.java"]),
    deps = [
        "@maven//:com_squareup_okhttp3_okhttp",
    ],
)

@maven indicates the dependency is a maven install. And in the name, . in artifcat becomes _.

bazel build Sample
INFO: Analyzed target //:Sample (41 packages loaded, 753 targets configured).
INFO: Found 1 target...
Target //:Sample up-to-date:
  bazel-bin/Sample.jar
  bazel-bin/Sample
INFO: Elapsed time: 5.997s, Critical Path: 2.27s
INFO: 20 processes: 7 internal, 10 linux-sandbox, 3 worker.
INFO: Build completed successfully, 20 total actions

Add protobuf as dependency

Similar to maven rules, it’s possible to download any dependency from the internet and add it as dependency.

Protobuf is a binary data serialization format for communicating with services. Since it’s a binary format, the proto buffer compiler generates the java class to encode and decode.

Let’s add a proto buf definition to the project and use it.

Create a new directory protos in example directory and add label.proto.

A simple Label with list of names. Extra option configuration is to generate java class from proto definition.

syntax = "proto3";
package example;

option java_multiple_files = true;
option java_package = "com.example.protos";
option java_outer_classname = "LabelProtos";

message Label {
  repeated string names = 1;
}

Add following lines to WORKSPACE file

# proto
# rules_cc defines rules for generating C++ code from Protocol Buffers.
http_archive(
    name = "rules_cc",
    sha256 = "35f2fb4ea0b3e61ad64a369de284e4fbbdcdba71836a5555abb5e194cf119509",
    strip_prefix = "rules_cc-624b5d59dfb45672d4239422fa1e3de1822ee110",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_cc/archive/624b5d59dfb45672d4239422fa1e3de1822ee110.tar.gz",
        "https://github.com/bazelbuild/rules_cc/archive/624b5d59dfb45672d4239422fa1e3de1822ee110.tar.gz",
    ],
)

http_archive(
    name = "rules_java",
    sha256 = "ccf00372878d141f7d5568cedc4c42ad4811ba367ea3e26bc7c43445bbc52895",
    strip_prefix = "rules_java-d7bf804c8731edd232cb061cb2a9fe003a85d8ee",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_java/archive/d7bf804c8731edd232cb061cb2a9fe003a85d8ee.tar.gz",
        "https://github.com/bazelbuild/rules_java/archive/d7bf804c8731edd232cb061cb2a9fe003a85d8ee.tar.gz",
    ],
)

# rules_proto defines abstract rules for building Protocol Buffers.
http_archive(
    name = "rules_proto",
    sha256 = "2490dca4f249b8a9a3ab07bd1ba6eca085aaf8e45a734af92aad0c42d9dc7aaf",
    strip_prefix = "rules_proto-218ffa7dfa5408492dc86c01ee637614f8695c45",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/218ffa7dfa5408492dc86c01ee637614f8695c45.tar.gz",
        "https://github.com/bazelbuild/rules_proto/archive/218ffa7dfa5408492dc86c01ee637614f8695c45.tar.gz",
    ],
)

load("@rules_cc//cc:repositories.bzl", "rules_cc_dependencies")

rules_cc_dependencies()

load("@rules_java//java:repositories.bzl", "rules_java_dependencies", "rules_java_toolchains")

rules_java_dependencies()

rules_java_toolchains()

load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")

rules_proto_dependencies()

rules_proto_toolchains()

So many rules and setup for proto conversion and java specific instructions!

protobuf BUILD instructions and example

Now add the proto build instructions in BUILD file

  1. Load the bazel definition for proto library and java proto library.

load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_java//java:defs.bzl", "java_proto_library")
  1. Convert proto definition and generate java code

proto_library(
    name = "label_proto",
    srcs = ["src/main/java/com/example/protos/label.proto"],
)

java_proto_library(
    name = "label_java_proto",
    deps = [":label_proto"],
)
  1. Update the deps in java_binary function call to include the generate java code.
deps = [
        ":label_java_proto",
        "@maven//:com_squareup_okhttp3_okhttp",
    ],
  1. Modify the Sample.java code to use generated Java class
package com.example;
import com.example.protos.Label;
import java.util.ArrayList;

public class Sample{
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<String>();
        names.add("Adult!");
        names.add("Programmer");

        Label.Builder builder = Label.newBuilder();
        builder.addAllNames(names);
        Label label = builder.build();
        System.out.println(String.format("Hello: %s", label));

    }
}
  1. Run the target.
bazel run Sample
INFO: Analyzed target //:Sample (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:Sample up-to-date:
  bazel-bin/Sample.jar
  bazel-bin/Sample
INFO: Elapsed time: 0.501s, Critical Path: 0.45s
INFO: 5 processes: 1 internal, 2 linux-sandbox, 2 worker.
INFO: Build completed successfully, 5 total actions
INFO: Build completed successfully, 5 total actions
Hello: names: "Adult!"
names: "Programmer"

Common beginner mistakes

  1. Using wrong function in BUILD and WORKSPACE.
  2. Not loading relevant load functions or rules.
  3. Missing out dependency in deps.

Conclusion

There are a lot of more important concepts like visibility, local dependency that’s skipped. Another tutorial for another day.

Bazel is definitely confusing and powerful build system that can make you hate building the package. In my opinion, learning bazel is like learning new programming language with step-learning curves.

References

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Powered by Buttondown.