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.