Bazel Java Starter Pack
Jun 19, 2020
This article is a quick walk-through on getting up and running with Bazel on a new Java project from local dev to IDE. An overview of the steps we’ll go through:
- Install Bazel.
- Setup the project tree.
- Configure IntelliJ.
Installing Bazel is pretty straightforward. Download the latest installer for your OS, execute with the
--user flag to install into your home directory, and adjust your path as advised by the installer.
Setup the Project Tree
Aside from your source a new Bazel project requires a minimum of 2 things a root
WORKSPACE file and
BUILD.bazel files. We’ll build a simple
HelloWorld console app and the core of the folder structure will look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 helloworld +- .gitignore +- WORKSPACE +- BUILD.bazel +- helloworld +- src +- main +- java +- ca/junctionbox/helloworld +- BUILD.bazel +- Printer.java +- Main.java
First up is the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Ignore backup files. *~ # Ignore Vim swap files. .*.sw? # Ignore files generated by IDEs. /.classpath /.factorypath /.idea/ /.ijwb/ /.project /.settings /.vscode/ /bazel.iml # Ignore all bazel-* symlinks. There is no full list since this can change # based on the name of the directory bazel is cloned into. /bazel-* # Ignore outputs generated during Bazel bootstrapping. /output/ # User-specific .bazelrc user.bazelrc
The above should keep your commits fairly devoid of unnecessary cruft. Adjust to your needs. Next up is the
WORKSPACE file. This indicates to Bazel the root of your repository and is the entry-point for what people familiar with maven would refer to as plugins.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 workspace(name="helloworld") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") RULES_JVM_EXTERNAL_TAG = "3.2" RULES_JVM_EXTERNAL_SHA = "82262ff4223c5fda6fb7ff8bd63db8131b51b413d26eb49e3131037e79e324af" 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") # this is only required if you use the long form `maven.artifact()` load("@rules_jvm_external//:specs.bzl", "maven") # maven_install will resolve all transitive dependencies. maven_install( # If you update a dependency below execute this command: # bazel run @unpinned_maven//:pin artifacts = [ # compact string form "junit:junit:4.13", # long form dependency definition allows for exclusion of # transitive dependencies. maven.artifact("org.hamcrest", "hamcrest", "2.2"), ], repositories = [ "https://repo1.maven.org/maven2", ], # after pinning your dependencies you can uncomment this # bazel run @maven//:pin maven_install_json = "@helloworld//:maven_install.json", ) # used to provide the @unpinned_maven//:pin load("@maven//:defs.bzl", "pinned_maven_install") pinned_maven_install()
The above is a lot to unpack but let’s walk through it:
workspace(name="helloworld")defines the workspace name and is most often used when composing multiple namespaces together.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")load statement that imports
@bazel_tools//tools/build_defs/repo:http.bzl. The import can be interpreted as
//always denotes the root of a given workspace.
tools/build_defs/repois the path. Finally
http.bzlis the target with load this is a file however this pattern of path specification is common in bazel and in some cases it can be a rule. More on that later. Note:
bazel_toolsis a built-in workspace included with Bazel.
- Next is
http_archive. This is a call the function loaded in the previous load command which retrieves an archive and makes it available in a workspace as defined by the name, in this case
rules_jvm_external. It is advised to use the SHA to ensure reproducible builds and increase security.
load("@rules_jvm_external//:defs.bzl", "maven_install")this load command loads the
maven_installcommand from our newly downloaded
load("@rules_jvm_external//:specs.bzl", "maven")this is an optional load and only required if you need to exclude particular transitive dependencies from one or more of your imports.
maven_install()this is where you’ll specify your direct dependencies. The plugin will take care of resolving the transitive dependencies from there. I strongly recommend using the
maven_install_jsonproperty as it pins you dependency graph providing 2 core benefits: reproducibility and speed. The first I’ll assume needs no explaination the second is worthy of a brief description. With most dependency resolution tools such as maven a clean checkout will need to walk the graph of dependencies which can result in recursive queries for each dependency encountered direct or transitive. The json file in effect resolves that graph once and any clean checkout only need download a list of dependencies which can be done with maximum concurrency.
pinned_maven_install()is the final function call in our
WORKSPACE. This allows updates of the pinned
maven_install_jsonfile without having to comment and then uncomment the parameter.
Phew that was a lot to ingest. Hopefully you’re still with me. Up next is the
helloworld/BUILD.bazel file. You can organise your project similar to maven by putting this file in the root of your sub-module. This is easy to get started however over the life of your project if it gets large you’ll lose some of the speed benefits that Bazel can achieve by having more fine grained build targets at the package level. A simple
BUILD.bazel file looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 java_library ( name = 'lib', srcs = glob(['*.java']), ) java_binary( name = 'helloworld', main_class = 'ca.junctionbox.helloworld.Main', runtime_deps = [ ':lib', ], )
In the above build file there are two Java rules:
java_libraryis a java rule to build shared bundles of libraries. I’ve used a single rule specification here but you can split it up as appropriate for your project.
java_binaryis a java rule to build an executable JAR file.
There are a number of other parameters you can specify for each rule depending on your requirements which can include resources, direct dependencies, exports, visibility, etc. I’ll make note on visibility now, by default all rules are “private” which means rules outside of the current scope/build file cannot reference the rules we’ve just created without some adjustments to the visibility. I’ll cover some examples of visibility in a follow-up post. The root
BUILD.bazel can be an empty file.
At this point you’ll be able to build your project assuming the code was already in place. A handful of commands you’ll find useful are:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # build all targets in the workspace bazel build //... # update the maven_install dependencies bazel run @unpinned_maven//:pin # list all of the maven dependency rules bazel query @maven//:all --output=build # clean the build output (this should be rare) bazel clean //... # build an uberjar bazel build //helloworld/src/main/java/ca/junctionbox/helloworld:helloworld_deploy.jar # run the java_binary target bazel run //helloworld/src/main/java/ca/junctionbox/helloworld:helloworld # run the java_binary target passing arguments into the app bazel run //helloworld/src/main/java/ca/junctionbox/helloworld:helloworld -- arg0 arg1
Let’s continue with getting your IDE configured. The best IDE to use with Bazel is arguably IntelliJ. The plugin doesn’t offer a lot in terms of functionality. Its primary focus is providing project Import and Run/Debug Configurations. If you’re running the latest version of IntelliJ you’ll need to downgrade to a version the plugin is compatible with. Currently that is the
2019.3.x series. You can see the compatible versions as specified on the plugin versions page. The second column indicates the compatibility range. As an example for Release
2020.06.01.1.0 you can use IntelliJ
If you use multiple JDK’s as I do ensure your login profiles JAVA_HOME matches the version you want to build with in IntelliJ. Any of the above commands can be added as build configurations to IntelliJ to ease execution.
That’s it your up and running with a minimal Java setup for IntelliJ. In a follow-up article I’ll demonstrate how to add resources, tests, and CI using Github Actions.