A Gradle build file describes what is needed to build our Java project. We apply one or more plugins, configure the plugins, declare dependencies and create and configure tasks. We have a lot of freedom to organize the build file as Gradle doesn’t really care. So to create maintainable Gradle build files we need to organize our build files and follow some conventions. In this post we focus on organizing the tasks and see if we can find a good way to do this.

It is good to have a single place where all the tasks are created and configured, instead of having all the logic scattered all over the build file. The TaskContainer is a good place to put all the tasks. To access the TaskContainer we can use the tasks property on the Project object. Within the scope of the tasks block we can create and configure tasks. Now we have a single place where all the tasks are created and configured. This makes it easier to find the tasks in our project as we have a single place to look for the tasks.

Within the scope of the TaskContainer we can use a convention to put the task creation methods at the top of the TaskContainer block. And the task configuration methods are after the task creation in the TaskContainer block. The tasks that are created at the top of the TaskContainer scope can be referenced by configuration code for tasks later in the TaskContainer scope.

The following diagram shows the build file structure and an example of the implementation:

gradle tasks

In the example Gradle build file for a Java project we organize the tasks in the TaskContainer using this convention:

plugins {
    java
}
...
tasks {
    // ----------------------------------------------
    // Task creation at the top of the container.
    // ----------------------------------------------

    // Register new task "uberJar".
    val uberJar by registering(Jar::class) {
        archiveClassifier = "uber"

        from(sourceSets.main.get().output)

        dependsOn(configurations.runtimeClasspath)
        from({
            configurations.runtimeClasspath.get()
                .filter { it.name.endsWith("jar") }
                .map { zipTree(it) }
        })
    }

    // ----------------------------------------------
    // Task configuration after task creation.
    // ----------------------------------------------

    // The output of the "uberJar" tasks is part of
    // the output of the "assemble" task.
    // We can refer to the "assemble" task directly
    // as it is added by the Java plugin.
    assemble {
        // We can refer to the task name that
        // we just created in our
        // tasks configuration block.
        dependsOn(uberJar)
    }

    // Configure tasks with type JavaCompile.
    withType<JavaCompile>().configureEach {
        options.compilerArgs.add("--enable-preview")
    }
}
...

Although Gradle doesn’t enforce us to use this convention it can be very helpful as build file authors to use it as it makes it easier to find the tasks in the project.

Written with Gradle 8.6.

shadow-left