An overview of Gradle's build.gradle.kts
Background and beginning
If you read my previous post, you know that I recently spent a good deal of time debugging a Java build of a program. Not a bug in the Java code, but a bug in gradle, my build environment. Specifically, in my gradle build file: build.gradle.kts.
After going through that problem, I spent time, finally, not pursuing more programming, but learning how gradle’s build operated instead. So, this is a summary of that learning. Sorry, it’s going to be a bit long. But, first, I need to take a minute to go over background things so that we are all on the same page. If you read my post about writing and compiling a Hello World! program in C and in Java , you will know what a basic Java program looks like. You will also see that, in theory, you can compile a Java class with javac MyMain.java into a program.
Well, not so fast. javac produces a MyMain.class file. You have to run this with java MyMain, assuming that Java knows where to find your class file. Also, if your Java program requires multiple classes, which nearly all usable ones do, calling this into being and correctly compiling your project into a usable program gets into nightmare territory.
Enter the Java build systems ( or environments ): Ant, Gradle, and Maven. There are likely others, but these are the common ones. I use gradle, so we are going to talk about gradle. More specifically, I use gradle on the command line with kotlin so that is what I am going to talk about.
On either linux or Windows, one begins with the command gradle init inside your completely empty project folder. Gradle is going to ask you questions about your project setup. The following is an output of setting up a new gradle project. Though there are other options, I think this is the best one to use. It is very basic and doesn’t create much boilerplate. I will explain in a minute why I choose kotlin instead of groovy for my build.gradle file.
$ gradle init
Select type of build to generate:
1: Application
2: Library
3: Gradle plugin
4: Basic (build structure only)
Enter selection (default: Application) [1..4] 4
Project name (default: test):
Select build script DSL:
1: Kotlin
2: Groovy
Enter selection (default: Kotlin) [1..2] 1
Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] no
> Task :init
Learn more about Gradle by exploring our Samples at https://docs.gradle.org/9.5.1/samples
BUILD SUCCESSFUL in 13s
1 actionable task: 1 executed
This is the only time you will call gradle. When gradle builds your initial project, it includes two files for you: gradlew and gradlew.bat. These stand for gradle wrapper. They are your wrapper around gradle and from now on with this project, you will call gradlew with a task of some type like build, compile, clean, or other task.
One would think that option 1: Application would be correct. Possibly. But, Application is very full of boilerplate that I often don’t need and haven’t tested. 4:Basic puts only the bare minimum of what I need in the project. No boilerplate source files. build.gradle.kts is empty. No folder structure. I have to put those in myself. It’s not difficult when you see how I do it.
I chose kotlin over groovy for my build.gradle file because I know some kotlin. Now, that I know more about how gradle works, I am even more glad I chose kotlin. Some of that will be visible as we go deeper. Yes, it may help you to know some kotlin before diving off the deep end.
For just your basic folder structure this is all you need if your project folder is named test:
test/src/main/java/com/example/myAppName/Main.java
At the top of your class files, use package com.example.myAppName; – assuming your domain is “example.com”. If not, use your own. If you don’t have one, just make up what you want. This is a folder structure, not a web address.
There is more, but this isn’t about all that and I need to move on.
build.gradle.kts and gradle
If we are going to learn how to handle our build.gradle.kts class, we first need a basic one. The following is a minimal, functional file we can start from:
plugins {
id("application")
}
repositories {
mavenCentral()
}
application {
mainClass.set("com.example.xxx.Main")
}
tasks.test {
useJUnitPlatform()
}
tasks.run.configure{
standardInput = System.`in`
}
tasks.named<Jar>("jar"){
manifest {
attributes["Main-Class"] = "com.example.xxx.Main"
}
}
dependencies {
implementation("org.apache.logging.log4j:log4j-core:2.25.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.junit.platform:junit-platform-launcher:1.13.4")
}
I apologize that I am going to still keep some things as magic beans instead of real knowledge, but I am still learning myself. However, the critical part of what we are going to talk about are gradle’s tasks and the task graph. But, each of these has a part to play. I’ll go through them briefly.
plugins– Yes, even your own project is a plugin to gradle. So,id( "application" )is our basic plugin.application– Establishes the main package level of our Main class. Gradle wants to know where is the Main class. It’s not always called Main.repositories– Gradle wants to know where to find any packages it may need to make your project work.dependencies– Gradle wants to know what packages your project depends upon. In the example, I havelog4jandjunit. These are called by package name and version level.tasks– We are going to hang out here a while. Gradle wants to know what tasks are needed and how they work.
Tasks and task graph
The task graph is one of the most important parts of gradle. This is where the action takes place. In a simplified way, the basic task graph looks a bit like this:
Project
|
+-- tasks
|
+-- jar
+-- test
+-- build
+-- copyJarToRoot
+-- ...
Gradle compiles build.gradle.kts and uses that to build the task graph. When you see tasks.named<Jar>( "jar" ), you are referencing a gradle property tasks that is of the kotlin class type TaskContainer. You are also calling the named method of tasks that is looking for a name. This name must be of type Jar and must be of the name ( "jar" ). This jar is the same as when you type ./gradlew jar on the command line.
If we are writing a new task, then we are also going to use tasks.register to tell gradle that is exists.
Every task in gradle needs to know three things: what are the inputs (files, folders, etc.), what must I produce (files, folders, etc.), and what are the actions I must use (methods, functions). We definitely can use kotlin functions with gradle API annotations (@InputFile, @OutputFile, @TaskAction) to create custom tasks.
Gradle’s API also gives us other methods to order things the way we need them. These are methods like:
dependsOn( )
finalizedBy( )
mustRunAfter( )
The end result of running your build.gradle.kts with gradlew is a model of what your final project should look like. It is not merely:
1. Do A
2. Do B
3. Do C
4. Do D
But instead, it is more like:
B requires A
D is a finalizer for C
E must run after F
G consumes the output of H
So,this is not a master class in Gradle. It is just a beginning overview. From what I have learned so far, I am better able to read the documentation from Gradle. I am now able to understand what the Gradle User Manual has to say about things I am looking for. There is a host of things that Gradle and its API can do to help you create a build file for your projects.
When I got to a usable build.gradle.kts file, I keep it at the top level of my java development folder. When I create a new project in the java folder and run gradle init, my next step is to copy my stored build.gradle.kts file down to my project. Then I create the proper folder structure as I described above. If I am going to run unit tests ( and you should run unit tests ), I will also build a test folder structure of project/src/main/tests as well.
I hope this helps you understand some of what Gradle does and why it works, or not. It barely scratches the surface, but it should help you go further into the documentation for what you need. Happy coding and building!