New Java Project With Bazel
Jun 19, 2020
This article is a walk-through on starting a new Java project with Bazel. An overview of the steps we’ll go through:
- Install Bazel.
- Setup the project tree.
- Configure IntelliJ.
Today is Juneteenth and what better way to celebrate than making a donation.
Install Bazel
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
or 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 .gitignore
file:
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 importshttp_archive
from@bazel_tools//tools/build_defs/repo:http.bzl
. The import can be interpreted asbazel_tools
workspace//
always denotes the root of a given workspace.tools/build_defs/repo
is the path. Finallyhttp.bzl
is 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_tools
is 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 caserules_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 themaven_install
command from our newly downloadedrules_jvm_external
workspace.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 themaven_install_json
property 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 ourWORKSPACE
. This allows updates of the pinnedmaven_install_json
file 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_library
is 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_binary
is 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
Configure IntelliJ
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 2019.3.3
to 2019.3.5
inclusive.
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.