Learning Gradle
A sleeping build time bug arises
I haven’t written in a while. Sorry. It’s hot here and I’ve been spending extra time working on the adventure game . I sat down to start writing some of the unit tests for the game. I wanted to do one simple one and test to see if everything built and tested correctly. I always do things in code incrementally; a little at a time and test. Sometimes, it seems pedantic, but it pays off. Though it was not expected to fail, JUnit pushed a failure during testing that hadn’t been there when no tests were available.
So, I’m going to explain the situation. It may take two posts so it doesn’t get too large, but here goes.
There are three common build systems for Java programs; Ant, Gradle, and Maven. These are both configuration and build software systems ( or maybe environments? ). They take your Java code and compile them to produce usable programs. I have a difficult time describing this because they do so many things. They integrate third party Java plugins, such as ironpdf that I use. They can produce a Java jar file for easy running on many systems. Jar files are my default way of doing it. They can even move files around, call report writers and unit test frameworks.
I used gradle at first because it was built into Intellij Idea, the IDE I used at the time. I have also used Maven, but didn’t like the XML configuration idea. I have not used Ant. I am back to using gradle because it has a very simple way of configuring to produce a jar file.
So, there I sat with a working Java adventure program that only broke because I added a unit test. After a bit, I discovered the cause was the following gradle task I used to place my final jar file in the project root. I have a play tester now, my Sweetie, and needed to make it easy for her to download the program. When I removed it from gradle tasks, everything worked again.
tasks.register<Copy>( "copyJarToRoot" ){
dependsOn( tasks.named( "jar" ) )
from( layout.buildDirectory.dir( "libs" ) )
include( "adventure.jar" )
into( layout.projectDirectory )
}
Don’t worry if it seems different. For one thing, I use kotlin gradle instead of groovy gradle. I know kotlin better and so it seemed a better method. This is supposed to just get adventure.jar from the build/libs folder and place it into my root folder. Or, so I thought. It was set to run as the last thing after the build finished.
I will admit that I have not spent time learning how a build system works. I had built a small one of my own using bash, but it really just compiled things into a jar. I treated my build environment like a magic bag of tricks. So, I just always asked the Interwebs or now, Gemini how to do things. This was what Gemini said would copy my jar file to the root folder. But, it has a bug.
Fixing the bug
I was unaware of how gradle operated. Gradle uses my kotlin configuration file, build.gradle.kts and compiles it to kotlin, then runs that output to build and manage my code. It has three steps, Initialization, Configuration, and Execution. In initialization, gradle wants to know which projects make up this build. Configuration executes build.gradle.kts to create and configure the tasks ( things like clean, build, deploy, etc ) then create the task graph for the build. A task graph is a way of representing tasks to be done, in what order, and what depends on what?
It took quite a bit of work between myself and chatGPT to find all the problems. Not only did my ignorance about my magic bag of tricks lead to this bug, but I had several other minor bugs as well.
The final diagnosis was that this line: into( layout.projectDirectory ) was not only copying my adventure.jar but it was copying the entire projectDirectory as well. This led to a fault when the unit test wanted to run. Because my Copy task had hold of the entire project, the test task in gradle could not get access to the files it needed to operate; namely my JaCoCo test reports couldn’t access what they needed.
Because I run gradle as a command line tool, not as part of an IDE, I don’t have immediate access to test and coverage reports. chatGPT helped me in the past to install JaCoCo so that I can get the same test and converage reports that one would find in a larger IDE. Since they couldn’t access their files, running tests broke the build.
After an hour or so of work between myself and chatGPT trying to find the problems, this was the final solution for the Copy task:
tasks.named<Jar>("jar"){
manifest {
attributes["Main-Class"] = "com.tardisgallifrey.adventure.Main"
}
finalizedBy( "copyJarToRoot" )
}
abstract class CopyJarToRoot : DefaultTask( ) {
@get:InputFile
abstract val jarFile: RegularFileProperty
@get:OutputFile
abstract val destination: RegularFileProperty
@TaskAction
fun copyJar( ){
jarFile.get( ).asFile.copyTo(
destination.get( ).asFile,
overwrite = true
)
}
}
tasks.register<CopyJarToRoot>( "copyJarToRoot" ){
jarFile.set( tasks.named<Jar>( "jar" ).flatMap { it.archiveFile } )
destination.set( layout.projectDirectory.file( "adventure.jar" ) )
}
Don’t worry if this isn’t understandable. It wasn’t to me either; however, chatGPT explained it to me. We needed to decouple anything my copyJarToRoot task was doing with the project folders and concentrate on moving just one file. Creating an abstract kotlin class, using gradle notification tags and simplifying ( sort of ) the task to a file copy function, we allowed tests to continue even if this wasn’t complete. Because, we uncoupled our copy task from the project structure.
The proof was in the pudding. Gradle now compiles, runs unit tests, and puts my jar file in the root folder just like it is supposed to do.
That’s enough for now. Yes, I left a lot unexplained. I’m going to write another post a little deeper on how gradle with kotlin works. But, other tasks are pressing. Happy coding!