This article describes how to add a CLI module to an existing Android Studio project that generates an executable .jar archive. The primary audience are software engineers working on Android.
When developing for Android – or other mobile platforms – the change-to-effect latency can be often quite high due to the involved compilation and cross-device communication. The regular unit tests get around this by executing in a VM on the host machine. However, they can be inflexible when it comes to incorporating larger (binary) assets to test against. Also, they do not lend themself to facilitate long fuzzing session or interactive usage.
For this reason I like to generate CLI from my Android Studio projects that provide a wrapper around the core logic. This is especially true for library projects. Having an executable with all dependencies included then allow e.g. to copy it to a dedicated cluster to fuzz the underlying implementation.
Since I have not found a comprehensive tutorial or how-to for creating an executable .jar file inside an Android Studio project, I decided to quickly document the required steps. For a quick start, I have also published a sample project on GitHub: https://github.com/lambdapioneer/android-with-cli-jar.
Requirements
A core requirement is, of course, that the module that we want to wrap does not depend on any Android specific APIS.
This should be true for most interesting algorithms – if not, this undertaking might provide motivation to add a sensible abstraction and to extract the core into its own module.
Let’s assume it’s called lib
.
Writing the gradle.build files
We first make our the library module lib
use the java-library
plugin so that it can output a .jar archive. Afterwards, the plugins sections of lib/build.gradle
should look like this:
plugins {
id 'java-library'
id 'kotlin'
}
We then create a new cli
module.
It’s cli/build.gradle
should have the plugins of a standard Java/Kotlin executable:
plugins {
id 'java'
id 'application'
}
Next we provide the main entry-point and our dependencies:
mainClassName = 'org.example.MyMainClass'
dependencies {
implementation project(':lib')
// any third-party dependencies to build a proper CLI or UI
}
Finally, we add a jar
task that will generate our .jar
archive.
We also want it to collect all of its dependencies and include it.
These “far jar” files make sharing across machines much easier.
jar {
// required to make sure that :lib generates a .jar we can include
dependsOn {
':lib:jar'
}
manifest {
attributes 'Main-Class': mainClassName
}
// this bundles the dependencies with the .jar (also known as FatJar)
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
// otherwise it complains about duplicated files (e.g. `META-INF/versions/9/module-info.class`)
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
Running
All the Android parts of the project still build and work as usual.
For the new cli
target, we can generate the fat jar file using the new task:
$ ./gradlew :cli:jar
And then we execute it like so:
$ java -jar ./cli/build/libs/cli.jar <arg1> <arg2> <...>
I have uploaded a working sample project here: https://github.com/lambdapioneer/android-with-cli-jar. It also comes with GitHub Actions to show how the CLI can be used for CI.
Credits: cover photo by Laura Adai on Unsplash.