Understanding Gradle by Creating an Executable Kotlin JAR

Adrian Mark Perea
6 min readDec 18, 2017

--

Building applications, be it in any language, is a non-trivial task. Thankfully, modern IDEs allow developers to enclose the entire build process in a black box through build automation software like Ant, Maven, and, the focus of our discussion, Gradle. While it is possible for a developer to get far by just leaving the black box alone and just hit the magical Run button to create their project artifacts, I believe knowing how the black box works underneath the hood would prove to be a valuable skill to have. To demonstrate how this would work, the Kotlin programming language will be used. This short tutorial will show you how to:

  • Initialize a Gradle project
  • Add a main function written in Kotlin
  • Package the project into an executable jar

Let’s get started.

Initializing a Gradle Kotlin Project

Assuming you’ve already installed Gradle (if not, follow this tutorial), the first thing to do is to initialize a new Gradle project in an empty directory:

$ mkdir gradle-test
$ cd gradle-test
$ gradle init
. . .BUILD SUCCESSFUL in 2s
2 actionable tasks: 2 executed

Gradle generates the following files and directories:

│   build.gradle
│ gradlew
│ gradlew.bat
│ settings.gradle
├── .gradle
│ └── ...
└── gradle
└── wrapper
└── gradle-wrapper.jar
└── gradle-wrapper.properties

We will define our build tasks in the file build.gradle. You can ignore the other files for now.

Just a quick note on the Gradle Wrapper, gradlew, from the official Gradle documentation:

The wrapper is a small script and supporting jar and properties file that allows a user to execute Gradle tasks even if they don’t already have Gradle installed. Generating a wrapper also ensures that the user will use the same version of Gradle as the person who created the project.

In light of that, we will use the project-scoped gradlew script instead of the system-wide gradle script that you already have installed.

Before configuring our build tasks, let’s first create the required directory structure for a Kotlin application. By default, Gradle looks for your production source code in src/main/kotlin and your test code in /src/test/kotlin . Create those directories now and our basic source file main.kt :

│   build.gradle
│ gradlew
│ gradlew.bat
│ settings.gradle
├── .gradle
│ └── ...
├── gradle
│ └── wrapper
│ └── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└───src
├── main
│ └── kotlin
│ └── main.kt
└── test
└── kotlin

Writing a Simple Kotlin Program

Very much unlike its JVM predecessor Java, Kotlin allows top-level functions and doesn’t require your main function to reside in a class. It can be seen later however that to remain consistent with the JVM bytecode, the Kotlin compiler encloses top-level functions in each file into its own class.

Now, create the trivial but necessary Hello World program:

fun main(args: Array<String>) {
println("Hello, World!")
}

Editing our Build Tasks

Now, we must edit our build.gradle in order to tell Gradle how to build our Kotlin application. Open the file, and replace all contents with the following:

// (1)
buildscript {
// Serves as a global variable
ext.kotlin_version = '1.2.0'
// Defines where we get our dependencies
repositories {
mavenCentral()
}
dependencies {
// Retrieves the Kotlin gradle plugin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
// (2)
apply plugin: 'kotlin'
// (3)
repositories {
mavenCentral()
}
// (4)
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

Here’s a short description of each item:

  1. The buildscript block defines the dependencies for the entire build task. In this particular example, it obtains the Kotlin plugin which allows Gradle to build Kotlin applications.
  2. This allows us to use the Kotlin plugin retrieved from block (1).
  3. Just like in the buildscript block, this is where we get our dependencies. The difference is, this block is responsible for where to get dependencies for the application code, and not for the build task.
  4. This block allows us to use the Kotlin SDK in our application.

Now that we have our build.gradle configured, it’s time to build!

Building the Application

As was said earlier, we would use the project-scoped gradlew instead of the system-wide gradle. To be able to determine our build tasks, do:

./gradlew tasks> Task :tasks------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'gradle-test'.
components - Displays the components produced by root project 'gradle-test'. [incubating]
dependencies - Displays all dependencies declared in root project 'gradle-test'.
dependencyInsight - Displays the insight into a specific dependency in root project 'gradle-test'.
dependentComponents - Displays the dependent components of components in root project 'gradle-test'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'gradle-test'. [incubating]
projects - Displays the sub-projects of root project 'gradle-test'.
properties - Displays the properties of root project 'gradle-test'.
tasks - Displays the tasks runnable from root project 'gradle-test'.
Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.
Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.
To see all tasks and more detail, run gradlew tasks --allTo see more detail about a task, run gradlew help --task <task>BUILD SUCCESSFUL in 2m 24s
1 actionable task: 1 executed

Doing so may invoke Gradle to download the dependencies, but once all of it is done you should see the list of tasks presented above. That is a lot, but let’s just focus on what we need: the build task. This task lets Gradle generate .class files from source code and compiles our project into a .jar file.

Issue the build task:

$ ./gradlew build

You can see that Gradle generated new files (some files are not displayed for brevity):

├── build
│ ├── classes
│ │ └── kotlin
│ │ └── main
│ │ └── MainKt.class
│ │
│ ├── kotlin
│ ├── kotlin-build
│ ├── libs
│ │ └── gradle-test.jar

Let’s try to run our generated .jar file:

$ java -jar build/libs/gradle-test.jar
$ no main manifest attribute, in build/libs/gradle-test.jar

Well, that didn’t work. The problem is we didn’t specify the entry point of our application in our build task. We should tell Gradle the class which holds our main function. In our case, this is our generated MainKt.class. The Kotlin compiler creates classes based on the file names of our source code (in our case, main.kt) and adds the Kt at the end. We should tell Gradle to use this class as the main entry point of our application. Add the following lines to your build.gradle file:

jar {
manifest {
attributes 'Main-Class': 'MainKt'
}
}

Let’s try building our application again:

$ ./gradlew build...$ java -jar build/libs/gradle-test.jar
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm /internal/Intrinsics
at MainKt.main(main.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more

This problem occurs because Gradle assumes that all the dependencies needed by our application exists in the same classpath where our .jar resides. In order to make our application standalone, we must include all of its dependencies inside our application. The downside of doing so would be increasing the file size, but in this demo it should be no problem. To be able to do this, we must add the following lines to our build.gradle file:

jar {    manifest {
attributes 'Main-Class': 'MainKt'
}
// Add this
from {
configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
}

Now issue the build task one more time and execute our application:

$ ./gradlew build...$ java -jar build/libs/gradle-test.jar
Hello, World!

It works!

Running the Application from Gradle

Typing java -jar build/libs/gradle-test.jar can be quite tedious. Fortunately, there is a way to run our application from Gradle itself by using the application plugin. To do so, add the following lines to the top of your build.gradle:

apply plugin: ‘application’mainClassName = ‘MainKt’

Now we can simply do:

$ ./gradlew run...> Task :run
Hello, World!
BUILD SUCCESSFUL in 1s
2 actionable tasks: 1 executed, 1 up-to-date

Alright! It works!

Congratulations on creating your own Gradle project from scratch. I hope you managed to learn a thing or two about how Gradle builds your applications. Check out the following links to learn more:

The code for this project is available at: https://github.com/adrianmarkperea/gradle-kotlin-demo

Till next time :)

--

--

Adrian Mark Perea

Software Engineer. AI Enthusiast. Writer. Teacher. I talk about JavaScript and Artificial Intelligence. Check me out at: https://adrianperea.dev