Building native Java CLIs with GraalVM, Picocli, and Gradle

Mitch Seymour
Mar 8 · 6 min read


The Java ecosystem is evolving in exciting ways. Historically, Java developers who wanted to build standalone CLI programs would have to leave the Java ecosystem and instead use a language like Golang, which can compile source code into light-weight, standalone binaries that are easy to use and distribute.

However, a new ahead-of-time compilation technology called GraalVM gives Java developers the ability to compile Java code into machine code. Building standalone executables (native images) with GraalVM not only allows us to build modern CLI apps with Java, but also improves the portability of our code (binary files are easy to distribute). There are also some performance benefits, as well:

The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM ¹

Furthermore, as GraalVM has matured, so too have Java CLI frameworks. One in particular stands out for its ease of use and large feature list. Picocli makes it easy to build feature-rich CLI applications by simply adding annotations to your Java classes, and includes features like tab completion, command hierarchies, colorized text, interactive password support, and more.

All of this is promising, and if you’re like me, you’ll want to quickly jump in and start building standalone CLIs with these technologies. However, the tutorials and documentation I’ve found on this subject are a little dense (and often Maven-based). Therefore, I wanted to create something that is more in the spirit of a quick start tutorial which is specifically aimed at Gradle users who are interested in building standalone Java apps with GraalVM and picocli.

The primary goal of this article is to help you get a native Java-based CLI running as quickly as possible. Once you’ve got something running and have familiarized yourself with the development workflow, you can then explore the intricacies of each technology and add more complexity and features where it’s needed. So without further ado, let’s get started by initializing our CLI project with Gradle.

Project Setup with Gradle

The first thing we’ll do is initialize a new project with Gradle (instructions for installing Gradle can be found here). We can do this very easily using the gradle init command. The —-type flag is used to specify what kind of project we’d like to build, and should be kept as java-application . The rest of the flags can be set to whatever values work best for your project (note: the examples in this article use the groovy DSL).

$ mkdir mycli && cd mycli$ gradle init \
--type java-application \
--dsl groovy \
--test-framework junit-jupiter \
--project-name mycli \
--package com.mitchseymour.mycli

This above command will create a sample application, with a full project structure and build file.

$ tree ..
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └──
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── mitchseymour
│ │ └── mycli
│ │ └──
│ └── resources
└── test
├── java
│ └── com
│ └── mitchseymour
│ └── mycli
│ └──
└── resources

The sample application can be found in src/java/<yourpackage>/ The content of this file is shown below.

You can verify that everything works by running the hello world app that Gradle just created for you.

$ ./gradlew run

Hello world.

Now that we’ve bootstrapped our project with Gradle, let’s learn how to use picocli to turn our application into a fully-functional CLI.

Adding picocli

The next step is to wire up picocli. This involves adding the picocli dependencies to our build file, and also a compiler argument to ensure the picocli annotations (which we will add in the next step) are processed. The following code block shows which lines to add to your build file (build.gradle).

Now that we’ve pulled in picocli as a project dependency, we’re ready to start building our CLI app. As mentioned earlier, picocli uses annotations to build a CLI from your application code. This means we need to make the following changes to our App.javafile.

  • Add a @Command annotation with some information about our CLI. Include the mixinStandardHelpOptionsconfig to add a default —-help and —-version flag to our CLI.
  • Modify theApp class so that it implements Callable<Integer>. picocli needs a predictable way of executing our command and returning an exit code, and Callable is the interface we will implement to accomplish this.
  • Add a main method to invoke our CLI using the picocli CommandLine class.

With the above requirements in mind, we can update as shown below:

Now, we can verify that our CLI is working by re-running our application:

$ ./gradlew run

Hello CLI :)

As mentioned before, picocli automatically adds the —-help and —-version flags to our CLI for us. Furthermore, we’ll likely add our own flags as we start building our application. So how do we run our CLI with a set of flags from Gradle? The solution is to pass our CLI flags using./gradlew run --args. The following code shows how to invoke our new CLI with the —-help flag:

$ ./gradlew run --args="--help"

Usage: mycli [-hV]
Says hello
-h, --help Show this help message and exit.
-V, --version Print version information and exit.

So far, our application isn’t feeling too much like an actual CLI. Ideally, we’d be able to invoke our application code using:


instead of:

./gradlew run --args="--help"

This requires us to build our CLI as a native image and run the binary directly, instead of building a JAR and running it with Gradle. The next section will demonstrate how to accomplish this with GraalVM.

Building a native image

In order to build our CLI app as a native image, we need to add the palantir/gradle-graal plugin to our build file. This plugin will install GraalVM, and invoke the necessary commands for compiling our Java code into a standalone executable.

In order for the native image compilation to work, you must configure the following properties:

  • mainClass: The main class (entry point) of the CLI application
  • outputName: The name of the binary to build

If you don’t set both of these properties, you’ll get a compilation error. The following code block demonstrates how to configure these properties in your build file (adjust the values appropriately).

Finally, you can build the native image using the following command. Note: this will take several seconds (on my machine, it took 18 seconds).

./gradlew nativeImage

The first time this runs, the graal plugin will check for an installed version of GraalVM. By default, it checks ~/.gradle/caches/com.palantir.graal/. You can change this using the com.palantir.graal.cache.dir property, as shown below:

$ ./gradlew nativeImage -P com.palantir.graal.cache.dir=/tmp

If the expected version of GraalVM can’t be found in the cache directory, the graal plugin will download it for you.

Once you’ve executed the nativeImage task, the resulting binary will be placed in ./build/graal. The command below shows how to run our new native image:

$ cd ./build/graal# no args example
$ ./mycli
# output
Hello CLI :)
# args example
$ ./mycli --help
# output
Usage: mycli [-hV]
Says hello
-h, --help Show this help message and exit.
-V, --version Print version information and exit.


You’ve now learned how to easily build standalone CLIs with Java, GraalVM, and Gradle. For quick feedback loops, the ./gradlew run --args command will quickly build and run your CLI with the provided argument list. Once you are ready to distribute your CLI (or if you want to test the standalone binary), you can use the slower but more powerful ./gradlew nativeImage command to compile your Java code into a native image.

In a follow up article, we will explore some of the features of picocli, and demonstrate how to distribute your CLI via homebrew. Until then, take a look at the picocli docs and see if you can add some cool features to your new CLI project :)

Thanks for reading.

Mitch Seymour

Written by

Software engineer @ Mailchimp

More From Medium

Also tagged Graalvm

Related reads

Related reads

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade