Search

Dark theme | Light theme
Showing posts with label GradleGoodness:Tasks. Show all posts
Showing posts with label GradleGoodness:Tasks. Show all posts

March 8, 2024

Gradle Goodness: Organizing Tasks Using The Task Container

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:

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.

February 25, 2024

Gradle Goodness: Using System Properties Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of a Java system property. In our build script we can use the providers property of type ProviderFactory and the method systemProperty(String). This method returns a Provider<String> instance that can be used to get the value of a system property in a lazy way. The method systemProperty can also be used with a Provider<String> argument.

In the following example we register a task that prints the value of the Java system property user.name to the console. We use lazy configuration to make sure the value of the system property is only fetched when the task is executed.

tasks {
    register<PrintSystemProperty>("printSystemProperty") {
        // We can use providers.systemProperty(String)
        // to get the value of an Java system property
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the system property "user.name" is fetched.
        systemProperty = providers.systemProperty("user.name")
    }
}

// Simple task to print the value of a Java system property.
abstract class PrintSystemProperty : DefaultTask() {
    @get:Input
    abstract val systemProperty: Property<String> // Use lazy property.

    @TaskAction
    fun printSystemPropertyValue() {
        // Only here we actually will get the value
        // for the system property.
        logger.quiet(systemProperty.get())
    }
}
$ ./gradlew printSystemProperty

> Task :printSystemProperty
mrhaki

BUILD SUCCESSFUL in 685ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

Gradle Goodness: Using Environment Variables Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of an environment variable. In our build script we can use the providers property of type ProviderFactory and the method environmentVariable(String). This method returns a Provider<String> instance that can be used to get the value of an environment variable in a lazy way.

In the following example we register a task that prints the value of the environment variable USER. We use lazy configuration to make sure the value of the environment variable is only fetched when the task is executed.

tasks {
    register<PrintEnvironmentVariable>("printEnvironmentVariable") {
        // We can use providers.environmentVariable(String)
        // to get the value of an environment variable
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the environment variable "USER" is fetched.
        environmentVariable = providers.environmentVariable("USER")
    }
}

// Simple task to print the value of an environment variable.
abstract class PrintEnvironmentVariable : DefaultTask() {
    @get:Input
    abstract val environmentVariable: Property<String> // Use lazy property.

    @TaskAction
    fun printEnvironmentVariable() {
        // Only here we actually will get the value
        // for the environment variable.
        logger.quiet(environmentVariable.get())
    }
}

When we execute the tasks we see the value of the environment variable USER:

$ ./gradlew printEnvironmentVariable

> Task :printEnvironmentVariable
mrhaki

BUILD SUCCESSFUL in 599ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

March 16, 2021

Gradle Goodness: Create Properties File With WriteProperties Task

If we need to create a Java properties file in our build we could create a custom task and use the Properties class to store a file with properties. Instead of writing our custom task we can use the task WriteProperties that is already part of Gradle. Using this task Gradle will not add a timestamp in the comment to the properties file. Also the properties are sorted by name, so we know the properties are always in the same order in the output file. Finally, a fixed line separator is used, which is \n by default, but can be set via the task property lineSeparator.

To define the properties that need to be in the output file we can use the property method for a single property, or we can use properties method with a Map argument to set multiple properties.

In the following build file we define a new task projectProps of task type WriteProperties and we use task output as dependency for the processResources task. This way our new task will be executed, if there are changes, when the processResources task is executed:

// File: build.gradle.kts
plugins {
    java
}

version = "1.0.0"

tasks {
    val projectProps by registering(WriteProperties::class) {
        description = "Write project properties in a file."

        // Set output file to build/project.properties
        outputFile = file("${buildDir}/project.properties")
        // Default encoding is ISO-8559-1, here we change it.
        encoding = "UTF-8"
        // Optionally we can specify the header comment.
        comment = "Version and name of project"

        // Define property.
        property("project.version", project.version)

        // Define properties using a Map.
        properties(mapOf("project.name" to project.name))
    }

    processResources {
        // Depend on output of the task to create properties,
        // so the properties file will be part of the Java resources.
        from(projectProps)
    }
}

When we invoke the classes task we can see that our new task is executed:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps
> Task :processResources
> Task :classes

BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date

$ cat build/project.properties
#Version and name of project
project.name=write-properties-sample
project.version=1.0.2

Without any changes we see our task was up-to-date and doesn’t have to run:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE

BUILD SUCCESSFUL in 596ms
3 actionable tasks: 3 up-to-date

Written with Gradle 6.8.3

October 5, 2020

Gradle Goodness: Replace Files In Archives

Sometimes we might need to replace one or more files in an existing archive file. The archive file could be a zip, jar, war or other archive. Without Gradle we would unpack the archive file, copy our new file into the destination directory of the unpacked archive and archive the directory again. To achieve this with Gradle we can simply create a single task of type Zip. To get the content of the original archive we can use the project.zipTree method. We leave out the file we want to replace and define the new file as replacement. As extra safeguard we can let the tsak fail if duplicate files are in the archive, because of our replacement.

The following code shows an example of a task to replace a README file in an archive sample.zip using Groovy and Kotlin DSL. First the Groovy DSL:

// Register new task replaceZip of type org.gradle.api.tasks.bundling.Zip.
tasks.register("replaceZip", Zip) {
    archiveBaseName = "new-sample"
    destinationDirectory = file("${buildDir}/archives")

    // Include the content of the original archive.
    from(zipTree("${buildDir}/archives/sample.zip")) {
        // But leave out the file we want to replace.
        exclude("README")
    }

    // Add files with same name to replace.
    from("src/new-archive") {
        include("README")
    }

    // As archives allow duplicate file names we want to fail 
    // the build when that happens, because we want to replace
    // an existing file.
    duplicatesStrategy = "FAIL"
}

And the same task but now with Kotlin DSL:

// Register new task replaceZip of type org.gradle.api.tasks.bundling.Zip.
tasks.register<Zip>("replaceZip") {
    archiveBaseName.set("new-sample")
    destinationDirectory.set(project.layout.buildDirectory.dir("archives"))

    // Include the content of the original archive.
    from(project.zipTree("$buildDir/archives/sample.zip")) {
        // But leave out the file we want to replace.
        exclude("README")
    }

    // Add files with same name to replace.
    from(file("src/new-archive")) {
        include("README")
    }

    // As archives allow duplicate file names we want to fail 
    // the build when that happens, because we want to replace
    // an existing file.
    duplicatesStrategy = DuplicatesStrategy.FAIL
}

Written with Gradle 6.6.1.

February 4, 2019

Gradle Goodness: Only Show All Tasks In A Group

To get an overview of all Gradle tasks in our project we need to run the tasks task. Since Gradle 5.1 we can use the --group option followed by a group name. Gradle will then show all tasks belonging to the group and not the other tasks in the project.

Suppose we have a Gradle Java project and want to show the tasks that belong to the build group:

$ gradle tasks --group build
> Task :tasks

------------------------------------------------------------
Tasks runnable from root project - Sample
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
bootBuildInfo - Generates a META-INF/build-info.properties file.
bootJar - Assembles an executable jar archive containing the main classes and their dependencies.
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.
generateGitProperties - Generate a git.properties file.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>

Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.1.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

Written with Gradle 5.1.1.

November 7, 2018

Gradle Goodness: Rerun Incremental Tasks At Specific Intervals

One of the most important features in Gradle is the support for incremental tasks. Incremental tasks have input and output properties that can be checked by Gradle. When the values of the properties haven't changed then the task can be marked as up to date by Gradle and it is not executed. This makes a build much faster. Input and output properties can be files, directories or plain object values. We can set a task input property with a date or date/time value to define when a task is up to date for a specific period. As long as the value of the input property hasn't changed (and of course also the other input and output property values) Gradle will not rerun task and mark it as up to date. This is useful for example if a long running task (e.g. large integration test suite) only needs to run once a day or another period.

In the following example Gradle build file we define a new task Broadcast that will get content from a remote URL and save it in a file. In our case we want to save the latest messages from SDKMAN!. If you don't know SKDMAN! you should check it out!. The Broadcast task has an incremental task output property, which is the output file of the task:

// File: build.gradle

task downloadBroadcastLatest(type: Broadcast) {
    outputFile = file("${buildDir}/broadcast.latest.txt")
}

class Broadcast extends DefaultTask {

    // URL with latest announcements of SDKMAN!
    private static final String API = "https://api.sdkman.io/2/broadcast/latest"

    @OutputFile
    File outputFile

    @TaskAction
    void downloadLatest() {
        // Download text from URL and save in File.
        logger.lifecycle("Downloading latest broadcast message from SDKMAN!.")
        outputFile.text = API.toURL().text
    }

}

We can run the task downloadBroadcastLatest and the contents of the URL is saved in the output file. When we run the task a second time the task action is executed again and the contents of the URL is fetched again and saved in the output file.

$ gradle downloadBroadcastLatest --console plain
> Task :broadcastLatest
Downloading latest broadcast message from SDKMAN!.

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
$ gradle downloadBroadcastLatest --console plain
> Task :broadcastLatest
Downloading latest broadcast message from SDKMAN!.

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
$

Suppose we don't want to access the remote URL for each task invocation. One time every hour is enough to get the latest messages from SKDMAN!. Let's add a new incremental task input property with the value of the current hour to the task downloadBroadcastLatest.

// File: build.gradle
...
task downloadBroadcastLatest(type: Broadcast) {
    // Add incremental input property, with value that changes only
    // every hour. Gradle will mark the task as up-to-date for
    // every invocation as long as the hour value hasn't changed.
    // For example to set the value so that the task is only
    // execute once a day we could use java.time.LocalDate.now().
    inputs.property 'check_once_per_hour', java.time.LocalDateTime.now().hour

    outputFile = file("${buildDir}/broadcast.latest.txt")
}
...
$ gradle downloadBroadcastLatest --console plain
> Task :broadcastLatest
Downloading latest broadcast message from SDKMAN!.

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
$ gradle downloadBroadcastLatest --console plain
> Task :broadcastLatest UP-TO-DATE

BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date
$

Another option in our example is to add the incremental task input property to the source of our Broadcast task. We can do that, because we have written the task class ourselves. If we cannot change the source of a task the previous example is the way to add an incremental task input property to an existing task. The following code sample adds an input property to our task definition:

// File: build.gradle
...
class Broadcast extends DefaultTask {

    // URL with latest announcements of SDKMAN!
    private static final String API = "https://api.sdkman.io/2/broadcast/latest"

    @Input
    int checkOncePerHour = java.time.LocalDateTime.now().hour

    @OutputFile
    File outputFile

    @TaskAction
    void downloadLatest() {
        // Download text from URL and save in File.
        logger.lifecycle("Downloading latest broadcast message from SDKMAN!.")
        outputFile.text = API.toURL().text
    }

}
...

Finally it is important that to make this work a task has to have at least an increment task output property. If an existing task doesn't have one, we can add a outputs.upToDateWhen { true } to a task configuration so Gradle recognises the task as being incremental with output and the output is always up to date. In the following example we create a new task Show without an incremental task output property. In the task showBroadcastLatest we define that the task has an always up to date output:

// File: build.gradle
...
task showBroadcastLatest(type: Show) {
    inputs.property 'check_once_a_day', java.time.LocalDate.now()

    // The original task definition has no increment task
    // output property, so we add one ourselves.
    outputs.upToDateWhen { true }

    inputFile = broadcastLatest.outputFile
}

class Show extends DefaultTask {

    @InputFile
    File inputFile

    @TaskAction
    void showContents() {
        println inputFile.text
    }

}
...

Written with Gradle 4.10.2.

June 5, 2018

Gradle Goodness: Enable Task Based On Offline Command Line Argument

One of the command line options of Gradle is --offline. With this option we run Gradle in offline mode to indicate we are not connected to network resources like the internet. This could be useful for example if we have defined dependencies in our build script that come from a remote repository, but we cannot access the remote repository, and we still want to run the build. Gradle will use the locally cached dependencies, without checking the remote repository. New dependencies, not already cached, cannot be downloaded of course, so in that scenario we still need a network connection.

We can check in our build script if the --offline command line argument is used. We can use this to disable tasks that depend on network resources so the build will not fail. To see if we invoked our build with the --offline option we can access the property gradle.startParameter.offline. The value is true if the command line argument --offline is used and false if the command line argument is not used.

In the following example build file we use the task type VfsCopy from the VFS Gradle Plugin to define a new task download. The task will download the file index.html from the site http://www.mrhaki.com. We enable the task if the --offline command line argument is not used. If the argument is used the task is disabled.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.ysb33r.gradle:vfs-gradle-plugin:1.0'
        classpath 'commons-httpclient:commons-httpclient:3.1'
    }
}

task download(type: org.ysb33r.gradle.vfs.tasks.VfsCopy) {
    description = 'Downloads index.html from http://www.mrhaki.com'
    group = 'Remote'

    // Only enable task when we don't use
    // the --offline command line argument.
    enabled = !gradle.startParameter.offline

    from 'http://www.mrhaki.com/index.html'
    into project.file("${buildDir}/downloads")
}

Let's run the download task with and without the --offline option:

$ gradle download --offline --console=plain
> Task :download SKIPPED

BUILD SUCCESSFUL in 0s
$ gradle download --console=plain
> Task :download

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

Written with Gradle 4.8.

May 10, 2018

Gradle Goodness: Command Line Options For Custom Tasks

Gradle added an incubation feature to Gradle 4.6 to add command line options for custom tasks. This means we can run a task using Gradle and add command line options to pass information to the task. Without this feature we would have to use project properties passed via the -P or --project-property. The good thing about the new feature is that the Gradle help task displays extra information about the command line options supported by a custom task.

To add a command line option we simply use the @Option annotation on the setter method of a task property. We must make sure the argument for the setter method is either a boolean, Boolean, String, enum, List<String> or List<enum>. The @Option annotation requires an option argument with the name of the option as it must be entered by the user. Optionally we can add a description property with a description about the option. It is good to add the description, because the help task of Gradle displays this information and helps the user of our custom task.

Let's start with a custom task that opens a file in the default application associated for the type of file. The task has own property file of type File. Remember we cannot create an command line option for all property types. To add a command line option we must overload the setFile method to accept a String value. This setter method is used to expose the file property as option.

// File: buildSrc/src/main/groovy/mrhaki/gradle/OpenFile.groovy
package mrhaki.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option

import groovy.transform.CompileStatic
import java.awt.Desktop

/**
 * Open a file or URI with the associated application.
 */
@CompileStatic
class OpenFile extends DefaultTask {

    /**
     * File to open.
     */
    @Input
    File file

    /**
     * Set description and group for task.
     */
    OpenFile() {
        description = 'Opens file with the associated application.'
        group = 'Help'
    }

    /**
     * Overload the setter method to support a String parameter. Now
     * we can add the @Option annotation to expose our file property
     * as command line option.
     *
     * @param path The object to resolve as a {@link File} for {@link #file} property.
     */
    @Option(option = 'file', description = 'Set the filename of the file to be opened.')
    void setFile(final String path) {
        this.file = project.file(path)
    }

    /**
     * Check if {@link Desktop} is supported. If not throw exception with message.
     * Otherwise open file with application associated to file format on the
     * runtime platform.
     *
     * @throws GradleException If {@link Desktop} is not supported.
     */
    @TaskAction
    void openFile() {
        if (Desktop.isDesktopSupported()) {
            Desktop.desktop.browse(new URI("file://${file.absolutePath}"))
        } else {
            throw new GradleException('Native desktop not supported on this platform. Cannot open file.')
        }
    }

}

We use the task in the following build file. We create a task openReadme and set the file property value in the build script. We also create the task open, but don't set the file property. We will use the command line option file for this task:

task open(type: mrhaki.gradle.OpenFile)

task openReadme(type: mrhaki.gradle.OpenFile) {
    file = project.file('README')
}

To run the open task and set the file command line option we use a double-dash as prefix:

$ gradle open --file=README

We can more information about the supported options for our task by invoking the help task. We define the task name with the option task:

$ gradle help --task=open

> Task :help
Detailed task information for open

Path
     :open

Type
     OpenFile (mrhaki.gradle.OpenFile)

Options
     --file     Set the filename of the file to be opened.

Description
     Opens file with the associated application.

Group
     Help

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

To define a set of valid values for an option we must add a method to our class that returns a list of valid values annotated with the @OptionValues annotation. We must set the task property name as argument for the annotation. Then when we invoke Gradle's help task we see a list of valid values. Validation of the property value is not done with the annotation, it is only for informational purposes. So validation must be added to the task code by ourselves.

We rewrite our OpenFile task and add the requirement that only files in the project directory can be opened and the filename must be in upper case. The method availableFiles return all files in the project directory with their name in upper case. The annotation @OptionValues is added to the method. In the task action method openFile we check if the file is valid to be opened

package mrhaki.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.api.tasks.options.OptionValues

import groovy.transform.CompileStatic
import java.awt.Desktop

/**
 * Open a file or URI with the associated application.
 */
@CompileStatic
class OpenFile extends DefaultTask {

    /**
     * File to open.
     */
    @Input
    File file

    /**
     * Set description and group for task.
     */
    OpenFile() {
        description = 'Opens file with the associated application.'
        group = 'Help'
    }

    /**
     * Check if {@link Desktop} is supported. If not throw exception with message.
     * Otherwise open file with application associated to file format on the
     * runtime platform.
     *
     * @throws GradleException If {@link Desktop} is not supported or
     *                         if filename not all uppercase in project dir.
     */
    @TaskAction
    void openFile() {
        if (!fileAllUpperCaseInProjectDir) {
            throw new GradleException('Only all uppercase filenames in project directory are supported.')
        }

        if (Desktop.isDesktopSupported()) {
            Desktop.desktop.browse(new URI("file://${file.absolutePath}"))
        } else {
            throw new GradleException('Native desktop not supported on this platform. Cannot open file.')
        }
    }

    private boolean isFileAllUpperCaseInProjectDir() {
        getAllUpperCase()(file.name) && project.projectDir == file.parentFile
    }

    /**
     * Overload the setter method to support a String parameter. Now
     * we can add the @Option annotation to expose our file property
     * as command line option.
     *
     * @param path The object to resolve as a {@link File} for {@link #file} property.
     */
    @Option(option = 'file', description = 'Set the filename of the file to be opened.')
    void setFile(final String path) {
        this.file = project.file(path)
    }

    /**
     * Show all files with filename in all uppercase in the project directory.
     *
     * @return All uppercase filenames.
     */
    @OptionValues('file')
    List<String> availableFiles() {
        project.projectDir.listFiles()*.name.findAll(allUpperCase)
    }

    private Closure getAllUpperCase() {
        { String word -> word == word.toUpperCase() }
    }
}

Let's run the help task and see that available values are shown this time:

$ gradle help --task=open

> Task :help
Detailed task information for open

Path
     :open

Type
     OpenFile (mrhaki.gradle.OpenFile)

Options
     --file     Set the filename of the file to be opened.
                Available values are:
                     README

Description
     Opens file with the associated application.

Group
     Help

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

Written with Gradle 4.7.

February 28, 2017

Gradle Goodness: Skip Task When Input Empty Using @SkipWhenEmpty Annotation

Gradle has excellent incremental build support. This means that Gradle can determine if a task needs to be executed based on the input and output of that task. If for example nothing changed in one of the input and output files, then the task can be skipped. We can add incremental build support for our custom tasks by defining the input and output of the task. We can also define that a task can be skipped when a collection of files or a directory that is the input of the task are empty or not exists. Gradle offers the @SkipWhenEmpty annotation we can apply on the input of our task.

In the next example we have a task DisplayTask that prints the contents of files in a directory. We want to skip the task when the directory is empty.

task display(type:DisplayTask) {
    contentDir = file('src/content')
}

class DisplayTask extends DefaultTask {

    @SkipWhenEmpty
    @InputDirectory
    File contentDir

    DisplayTask() {
        description = 'Show contents of files'
    }

    @TaskAction
    void printMessages() {
        contentDir.eachFile { file ->
            println file.text
        }
    }

}

When we run the task without any files in the input directory we see in the output NO-SOURCE for our task. If we wouldn't have added the @SkipWhenEmpty annotation the build would have failed.

$ gradle display
:display NO-SOURCE

BUILD SUCCESSFUL

Total time: 0.866 secs

Let's add a file in the directory src/content and re-run the task:

$ gradle display
:display
Gradle rocks!


BUILD SUCCESSFUL

Total time: 0.866 secs

Written with Gradle 3.4.

February 1, 2017

Gradle Goodness: Check Operating System In Build Scripts

Sometimes we want to check which operating system is used in our build script. For example we have tasks that need to run if the operating system is Windows and not for other operating systems. Gradle has an internal class org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem, but we should not use this class in our build scripts. The class is used internally by Gradle and can change without warning. If we would depend on this class and it changes we break our build scripts. But we can use a class from Ant that is already in Gradle's class path: org.apache.tools.ant.taskdefs.condition.Os. The class has several methods and constants to check the operating system name, version and architecture. The values are based on the Java system properties os.name, os.version and os.arch.

In the following example build script we use import static to include the Os class, so we can directly invoke the methods and refer to the constants in the Os class. We add some tasks that have a condition check with onlyIf so the task only runs when the condition in the closure is true. The task osInfo simply shows values from the Os class:

// File: build.gradle
import static org.apache.tools.ant.taskdefs.condition.Os.*

task os {
    description 'Run all conditional os tasks'
}

// Create 3 tasks that simply print
// the task name that is executed
// if the build scripts runs on the
// recognized operating system.
[FAMILY_WINDOWS, FAMILY_UNIX, FAMILY_MAC].each { osName ->

    // Create task.
    tasks.create(osName) {
        description "Run when OS is ${osName}"

        // Add condition to check operating system.
        onlyIf { isFamily(osName) }

        doLast {
            println "Execute task '${it.name}'"
        }
    }

    // Add task as dependency for the os task.
    os.dependsOn osName
}


task osInfo {
    description 'Show information about the operating system'
    doLast {
        println "Family:       ${OS_NAME}"
        println "Version:      ${OS_VERSION}"
        println "Architecture: ${OS_ARCH}"
    }
}

Let's run the os and osInfo tasks on MacOS:

$ gradle os osInfo
mac
Execute task 'mac'
:unix
Execute task 'unix'
:windows SKIPPED
:os
:osInfo
Family:       mac os x
Version:      10.12.3
Architecture: x86_64

BUILD SUCCESSFUL

Total time: 0.697 secs

Written with Gradle 3.3.

December 20, 2016

Gradle Goodness: Run Task Ignoring Up-to-date Checks

Gradle builds are fast because Gradle supports incremental tasks. This means Gradle can determine if input or output of task has changed, before running the task. If nothing has changed a task is marked a up-to-date and the task is not executed, otherwise the task is executed. If we want execute a task even if it is up-to-date we must use the command line option --rerun-tasks.

In the following example we run the assemble task for a simple Java project, and we see all tasks are executed. When we invoke the assemble task again we see the tasks are all up-to-date:

$ gradle assemble

:compileJava
:processResources
:classes
:jar
:assemble

BUILD SUCCESSFUL

Total time: 1.765 secs
$ gradle assemble

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.715 secs
$

To run all tasks without an up-to-date check we use the option --rerun-tasks:

$ gradle --rerun-tasks assemble
:compileJava
:processResources
:classes
:jar
:assemble

BUILD SUCCESSFUL

Total time: 1.037 secs
$

Written with Gradle 3.2.1.

November 15, 2016

Gradle Goodness: Replacing << Operator For Tasks

Gradle 3.2 deprecates the << operator to add actions to a task. The << operator maps to the leftShift method of a task. This operator confuses a lot people that are new to Gradle. Because without the operator we are configuring a task instead of adding actions. I can tell from experience the mistake is easily made. If we use the << in our build script with Gradle 3.2 we get a warning on the console. The warning message already mentions a solution: use the doLast method to add actions.

In the following example build script we define the task deprecatedSample using the << operator. The other task newSample uses the doLast method to add an action:

// Since Gradle 3.2 the << (leftShift) operator
// is deprecated. The operator can confuse
// people, because without the operator
// we would configure the deprecatedSample task,
// instead of adding the action statement:
// println 'Sample task'.
task deprecatedSample << {
    println 'Sample task'
}

// To have no confusion we should use
// the doLast method of a task to add
// the action statement:
// println 'Sample task'.
task newSample {
    doLast {
        println 'Sample task'
    }
}

When we run the deprecatedSample task we see in the output the warning that the leftShift method has been deprecated:

$ gradle deprecatedSample
The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.
        at build_dq65b0mbv52w2ikhya3h9ru8d.run(/Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/leftShift/build.gradle:7)
:deprecatedSample
Sample task

BUILD SUCCESSFUL

Total time: 0.793 secs
$

We still have time to fix our build scripts, because in Gradle 5 the leftShift method will be removed.

Written with Gradle 3.2.

November 14, 2016

Gradle Goodness: Show Hidden Model Objects

We use the model task to see which objects are available in the Gradle model space. The model space is managed by Rule based model configuration. Objects can be defined as hidden by the object author(s). By default a hidden object is not shown in the model report. We must use the task option --showHidden to show also the hidden objects in the model report.

$ gradle -q model --showHidden

------------------------------------------------------------
Root project
------------------------------------------------------------

+ buildDir
      | Type:           java.io.File
      | Value:          /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build
      | Creator:        Project.<init>.buildDir()
+ extensionContainer
      | Type:           org.gradle.api.plugins.ExtensionContainer
      | Creator:        Project.<init>.extensionContainer()
+ fileOperations
      | Type:           org.gradle.api.internal.file.FileOperations
      | Creator:        DefaultProject.BasicServicesRules#fileOperations(ServiceRegistry)
+ instantiator
      | Type:           org.gradle.internal.reflect.Instantiator
      | Creator:        DefaultProject.BasicServicesRules#instantiator(ServiceRegistry)
+ nodeInitializerRegistry
      | Type:           org.gradle.model.internal.core.NodeInitializerRegistry
      | Creator:        DefaultProject.BasicServicesRules#nodeInitializerRegistry(ModelSchemaStore, StructBindingsStore)
+ projectIdentifier
      | Type:           org.gradle.api.internal.project.ProjectIdentifier
      | Value:          root project 'versionrule'
      | Creator:        Project.<init>.projectIdentifier()
+ proxyFactory
      | Type:           org.gradle.model.internal.manage.instance.ManagedProxyFactory
      | Creator:        DefaultProject.BasicServicesRules#proxyFactory(ServiceRegistry)
+ schemaStore
      | Type:           org.gradle.model.internal.manage.schema.ModelSchemaStore
      | Creator:        DefaultProject.BasicServicesRules#schemaStore(ServiceRegistry)
+ serviceRegistry
      | Type:           org.gradle.internal.service.ServiceRegistry
      | Value:          ProjectScopeServices
      | Creator:        Project.<init>.serviceRegistry()
+ sourceDirectorySetFactory
      | Type:           org.gradle.api.internal.file.SourceDirectorySetFactory
      | Creator:        DefaultProject.BasicServicesRules#sourceDirectorySetFactory(ServiceRegistry)
+ structBindingsStore
      | Type:           org.gradle.model.internal.manage.binding.StructBindingsStore
      | Creator:        DefaultProject.BasicServicesRules#structBindingsStore(ServiceRegistry)
+ taskFactory
      | Type:           org.gradle.api.internal.project.taskfactory.ITaskFactory
      | Creator:        DefaultProject.BasicServicesRules#taskFactory(ServiceRegistry)
+ tasks
      | Type:           org.gradle.model.ModelMap<org.gradle.api.Task>
      | Creator:        Project.<init>.tasks()
      | Rules:
         ⤷ VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile)
    + buildEnvironment
          | Type:       org.gradle.api.tasks.diagnostics.BuildEnvironmentReportTask
          | Value:      task ':buildEnvironment'
          | Creator:    tasks.addPlaceholderAction(buildEnvironment)
          | Rules:
             ⤷ copyToTaskContainer
    + components
          | Type:       org.gradle.api.reporting.components.ComponentReport
          | Value:      task ':components'
          | Creator:    tasks.addPlaceholderAction(components)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencies
          | Type:       org.gradle.api.tasks.diagnostics.DependencyReportTask
          | Value:      task ':dependencies'
          | Creator:    tasks.addPlaceholderAction(dependencies)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencyInsight
          | Type:       org.gradle.api.tasks.diagnostics.DependencyInsightReportTask
          | Value:      task ':dependencyInsight'
          | Creator:    tasks.addPlaceholderAction(dependencyInsight)
          | Rules:
             ⤷ HelpTasksPlugin.Rules#addDefaultDependenciesReportConfiguration(DependencyInsightReportTask, ServiceRegistry)
             ⤷ copyToTaskContainer
    + dependentComponents
          | Type:       org.gradle.api.reporting.dependents.DependentComponentsReport
          | Value:      task ':dependentComponents'
          | Creator:    tasks.addPlaceholderAction(dependentComponents)
          | Rules:
             ⤷ copyToTaskContainer
    + generateVersionFile
          | Type:       mrhaki.gradle.VersionFileTask
          | Value:      task ':generateVersionFile'
          | Creator:    VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile) > create(generateVersionFile)
          | Rules:
             ⤷ copyToTaskContainer
    + help
          | Type:       org.gradle.configuration.Help
          | Value:      task ':help'
          | Creator:    tasks.addPlaceholderAction(help)
          | Rules:
             ⤷ copyToTaskContainer
    + init
          | Type:       org.gradle.buildinit.tasks.InitBuild
          | Value:      task ':init'
          | Creator:    tasks.addPlaceholderAction(init)
          | Rules:
             ⤷ copyToTaskContainer
    + model
          | Type:       org.gradle.api.reporting.model.ModelReport
          | Value:      task ':model'
          | Creator:    tasks.addPlaceholderAction(model)
          | Rules:
             ⤷ copyToTaskContainer
    + projects
          | Type:       org.gradle.api.tasks.diagnostics.ProjectReportTask
          | Value:      task ':projects'
          | Creator:    tasks.addPlaceholderAction(projects)
          | Rules:
             ⤷ copyToTaskContainer
    + properties
          | Type:       org.gradle.api.tasks.diagnostics.PropertyReportTask
          | Value:      task ':properties'
          | Creator:    tasks.addPlaceholderAction(properties)
          | Rules:
             ⤷ copyToTaskContainer
    + tasks
          | Type:       org.gradle.api.tasks.diagnostics.TaskReportTask
          | Value:      task ':tasks'
          | Creator:    tasks.addPlaceholderAction(tasks)
          | Rules:
             ⤷ copyToTaskContainer
    + wrapper
          | Type:       org.gradle.api.tasks.wrapper.Wrapper
          | Value:      task ':wrapper'
          | Creator:    tasks.addPlaceholderAction(wrapper)
          | Rules:
             ⤷ copyToTaskContainer
+ typeConverter
      | Type:           org.gradle.internal.typeconversion.TypeConverter
      | Creator:        DefaultProject.BasicServicesRules#typeConverter(ServiceRegistry)
+ versionFile
      | Type:           mrhaki.gradle.VersionFile
      | Creator:        VersionFileTaskRules#versionFile(VersionFile)
      | Rules:
         ⤷ versionFile { ... } @ build.gradle line 8, column 5
    + outputFile
          | Type:       java.io.File
          | Value:      /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build/version.file
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
    + version
          | Type:       java.lang.String
          | Value:      1.0.1.RELEASE
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
$

Written with Gradle 3.2.

Gradle Goodness: Get Model Report In Short Format

The Gradle model task shows the objects in the model space of Gradle. The output shows the object hierarchy. By default a full report is shown, with a lot of information. We can customize the output format with the --format task argument. The default value is full, but we can also use the value short. With the value short a lot less information is shown.

Let's see the output of the model task for a sample project:

$ gradle -q model
------------------------------------------------------------
Root project
------------------------------------------------------------

+ tasks
      | Type:           org.gradle.model.ModelMap<org.gradle.api.Task>
      | Creator:        Project.<init>.tasks()
      | Rules:
         ⤷ VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile)
    + buildEnvironment
          | Type:       org.gradle.api.tasks.diagnostics.BuildEnvironmentReportTask
          | Value:      task ':buildEnvironment'
          | Creator:    tasks.addPlaceholderAction(buildEnvironment)
          | Rules:
             ⤷ copyToTaskContainer
    + components
          | Type:       org.gradle.api.reporting.components.ComponentReport
          | Value:      task ':components'
          | Creator:    tasks.addPlaceholderAction(components)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencies
          | Type:       org.gradle.api.tasks.diagnostics.DependencyReportTask
          | Value:      task ':dependencies'
          | Creator:    tasks.addPlaceholderAction(dependencies)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencyInsight
          | Type:       org.gradle.api.tasks.diagnostics.DependencyInsightReportTask
          | Value:      task ':dependencyInsight'
          | Creator:    tasks.addPlaceholderAction(dependencyInsight)
          | Rules:
             ⤷ HelpTasksPlugin.Rules#addDefaultDependenciesReportConfiguration(DependencyInsightReportTask, ServiceRegistry)
             ⤷ copyToTaskContainer
    + dependentComponents
          | Type:       org.gradle.api.reporting.dependents.DependentComponentsReport
          | Value:      task ':dependentComponents'
          | Creator:    tasks.addPlaceholderAction(dependentComponents)
          | Rules:
             ⤷ copyToTaskContainer
    + generateVersionFile
          | Type:       mrhaki.gradle.VersionFileTask
          | Value:      task ':generateVersionFile'
          | Creator:    VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile) > create(generateVersionFile)
          | Rules:
             ⤷ copyToTaskContainer
    + help
          | Type:       org.gradle.configuration.Help
          | Value:      task ':help'
          | Creator:    tasks.addPlaceholderAction(help)
          | Rules:
             ⤷ copyToTaskContainer
    + init
          | Type:       org.gradle.buildinit.tasks.InitBuild
          | Value:      task ':init'
          | Creator:    tasks.addPlaceholderAction(init)
          | Rules:
             ⤷ copyToTaskContainer
    + model
          | Type:       org.gradle.api.reporting.model.ModelReport
          | Value:      task ':model'
          | Creator:    tasks.addPlaceholderAction(model)
          | Rules:
             ⤷ copyToTaskContainer
    + projects
          | Type:       org.gradle.api.tasks.diagnostics.ProjectReportTask
          | Value:      task ':projects'
          | Creator:    tasks.addPlaceholderAction(projects)
          | Rules:
             ⤷ copyToTaskContainer
    + properties
          | Type:       org.gradle.api.tasks.diagnostics.PropertyReportTask
          | Value:      task ':properties'
          | Creator:    tasks.addPlaceholderAction(properties)
          | Rules:
             ⤷ copyToTaskContainer
    + tasks
          | Type:       org.gradle.api.tasks.diagnostics.TaskReportTask
          | Value:      task ':tasks'
          | Creator:    tasks.addPlaceholderAction(tasks)
          | Rules:
             ⤷ copyToTaskContainer
    + wrapper
          | Type:       org.gradle.api.tasks.wrapper.Wrapper
          | Value:      task ':wrapper'
          | Creator:    tasks.addPlaceholderAction(wrapper)
          | Rules:
             ⤷ copyToTaskContainer
+ versionFile
      | Type:           mrhaki.gradle.VersionFile
      | Creator:        VersionFileTaskRules#versionFile(VersionFile)
      | Rules:
         ⤷ versionFile { ... } @ build.gradle line 8, column 5
    + outputFile
          | Type:       java.io.File
          | Value:      /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build/version.file
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
    + version
          | Type:       java.lang.String
          | Value:      1.0.1.RELEASE
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
$

Now we use the short format:

$ gradle -q model --format=short
------------------------------------------------------------
Root project
------------------------------------------------------------

+ tasks
    | buildEnvironment = task ':buildEnvironment'
    | components = task ':components'
    | dependencies = task ':dependencies'
    | dependencyInsight = task ':dependencyInsight'
    | dependentComponents = task ':dependentComponents'
    | generateVersionFile = task ':generateVersionFile'
    | help = task ':help'
    | init = task ':init'
    | model = task ':model'
    | projects = task ':projects'
    | properties = task ':properties'
    | tasks = task ':tasks'
    | wrapper = task ':wrapper'
+ versionFile
    | outputFile = /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build/version.file
    | version = 1.0.1.RELEASE
$

Written with Gradle 3.2.

Gradle Goodness: Adding Task With Rule Based Model Configuration

Gradle has an incubating feature Rule based model configuration. This is a new way to configure Gradle projects where Gradle has more control of the configuration and the dependencies between configuration objects. This allows Gradle to resolve configuration values before they are used, because Gradle knows there is a dependency. With this new model we don't need any lazy evaluation "tricks" we had to use. For example there was an internal convention mapping mechanism for tasks to assign values to a task configuration after the task was already created. Also the project.afterEvalute is a mechanism to have late binding for task properties. With the new rule based model Gradle can do without these options, we can rely on Gradle resolving all dependent configuration values when we create a task.

In Gradle we already know about the "project space" where the Project object is the root of the object graph. For example repositories are part of the project space. Gradle can get some useful information from the project space, but it is mostly a graph of objects that Gradle only partially can reason about. Then we have the "model space". This is part of a project and we can use it in our build script with the model configuration block. The model space is separate from the project space and contains objects that are managed by Gradle. Gradle knows dependencies between the objects and how to create and change them. This helps Gradle to optimise build logic. To help Gradle we must define rules to work with objects in the model space. Each rule is like a recipe for Gradle on how to work with the model. Gradle can build a graph of models and know about dependencies between models. This way Gradle guarantees that model objects are completely configured before being used. For example if a rule needs a VersionFile model configuration object then Gradle makes sure that the VersionFile is created and all properties are set. So we don't need any lazy or late binding anymore, because the properties will be set (Gradle makes sure) when we want to use them. The rules are defined a class that extends RuleSource. Such a class is stateless and only contains methods to work with the model objects. Gradle has some specific annotations that can be used on methods to indicate what a method should do.

In our example we have a Gradle custom task VersionFileTask. The task has some properties which we want to set via the model space using a model configuration block. We want to add this task to the list of tasks in our project by using a apply plugin: statement.

Let's first look at the source of the custom Gradle task:

// File: buildSrc/src/main/groovy/mrhaki/gradle/VersionFileTask.groovy
package mrhaki.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

/**
 * Simple task to save the value for the
 * {@link #version} property in a file.
 * The file is set with the {@link #outputFile}
 * property.
 */
class VersionFileTask extends DefaultTask {

    /**
     * Value for version to be saved.
     */
    @Input
    String version

    /**
     * Output file to store version value in.
     */
    @OutputFile
    File outputFile

    /**
     * Actual task actions to save the value
     * for {@link #version} in {@link #outputFile}.
     */
    @TaskAction
    void generateVersionFile() {
        outputFile.parentFile.mkdirs()
        outputFile.text = version
    }

}

Nothing special here. Now it is time to enter the model space of Gradle. First we create a object with properties that is used to configure our VersionFileTask task. Here we must use the annotation @Managed so Gradle knows this object will be managed in the object space:

// File: buildSrc/src/main/groovy/mrhaki/gradle/VersionFile.groovy
package mrhaki.gradle

import org.gradle.model.Managed

/**
 * Gradle is responsible for creating an implementation
 * for this interface. We use @Managed to let Gradle know.
 * We need to provide the get and set
 * methods following the Java Beans standards for properties.
 * 
 * In the model space Gradle provides an implementation and
 * knows how to create an instance of that implementation
 * and how to invoke the get and set methods to mutate the state.
 */
@Managed
interface VersionFile {
    String getVersion() 
    void setVersion(final String version) 

    File getOutputFile() 
    void setOutputFile(final File outputFile) 
}

Next we create a class with the rules for the model space VersionFileTaskRules. We can use this class like a plugin in our project using the statement apply plugin: mrhaki.gradle.VersionFileTaskRules. We need two rules to instruct Gradle about our model objects. First we need to make sure an instance of the managed VersionFile interface is created. We do this with the createVersionFile method. We need another method ( createVersionFileTask) to change the list of tasks (Gradle calls this mutate in the model space terminology) using an instance of VersionFile. Gradle knows about the connection between the two methods via the VersionFile object, so it makes sure VersionFile is created before the method createVersionFileTask is invoked:

// File: buildSrc/src/main/groovy/mrhaki/gradle/VersionFileTaskRules.groovy
package mrhaki.gradle

import org.gradle.api.Task
import org.gradle.model.Model
import org.gradle.model.ModelMap
import org.gradle.model.Mutate
import org.gradle.model.RuleSource

/**
 * Class contains several methods to tell Gradle
 * how to create a {@link VersionFile} instance
 * and how to mutate the list of tasks by creating
 * the {@link VersionFileTask} task.
 */
class VersionFileTaskRules extends RuleSource {

    /**
     * Method to tell Gradle that we need an instance
     * of {@link VersionFile} in the model space. The name of the method
     * is also used as in the model space to configure
     * the object. Another name can be used as an argument for the
     * {@code @Model} annotation.
     *
     * @param versionFile The type {@link VersionFile} has a {@code @Managed}
     *                    annotation, so Gradle can provide an implementation.
     */
    @Model
    void versionFile(final VersionFile versionFile) {}

    /**
     * Method to create the {@link VersionFileTask} task and add to list
     * of tasks. The first arguments is the type we want to mutate, the
     * other argument is an input argument used to mutate the list of tasks.
     * 
     * With the {@code versionFile} argument we can pass information to this method
     * that is needed to create the {@link VersionFileTask}. A user can use
     * the {@code model} configuration block in a build file to set values for 
     * the {@link VersionFile} instance.
     * 
     * Gradle will make sure the input argument is created and all properties
     * are set before it is used in this method. So no more {@link afterEvaluate}
     * or convention mappings are needed. Gradle makes sure all input arguments
     * are resolved before they are used.
     * 
     * @param tasks Tasks we want to add a new one to
     * @param versionFile Resolved instance used to configure new task
     */
    @Mutate
    void createVersionFileTask(final ModelMap<Task> tasks, final VersionFile versionFile) {
        tasks.create('generateVersionFile', VersionFileTask) { task ->
            task.version = versionFile.version
            task.outputFile = versionFile.outputFile
        }
    }
    
}

To use the rules we create a simple build.gradle file:

apply plugin: mrhaki.gradle.VersionFileTaskRules

To see the model space managed by Gradle we can invoke the model task. The output shows the current model of our project.

$ gradle model
...
------------------------------------------------------------
Root project
------------------------------------------------------------

+ tasks
      | Type:           org.gradle.model.ModelMap<org.gradle.api.Task>
      | Creator:        Project.<init>.tasks()
      | Rules:
         ⤷ VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile)
...
    + generateVersionFile
          | Type:       mrhaki.gradle.VersionFileTask
          | Value:      task ':generateVersionFile'
          | Creator:    VersionFileTaskRules#createVersionFileTask(ModelMap<Task>, VersionFile) > create(generateVersionFile)
          | Rules:
             ⤷ copyToTaskContainer
...
+ versionFile
      | Type:           mrhaki.gradle.VersionFile
      | Creator:        VersionFileTaskRules#createVersionFile(VersionFile)
    + outputFile
          | Type:       java.io.File
          | Value:      null
          | Creator:    VersionFileTaskRules#createVersionFile(VersionFile)
    + version
          | Type:       java.lang.String
          | Value:      null
          | Creator:    VersionFileTaskRules#createVersionFile(VersionFile)
...
$

We see the model type mrhaki.gradle.VersionFile is created with the method versionFile and it's properties outputFile and version. Also the model shows that the method generateVersionFile creates the task VersionFileTask.

We set values for the VersionFile properties in our build file:

apply plugin: mrhaki.gradle.VersionFileTaskRules

// Configure model space.
model {
    
    // Configure VersionFile instance created 
    // by method versionFile() from VersionFileTaskRules.
    versionFile {
    
        // Set value for version property of VersionFile.
        version = project.version

        // Set value for outputFile property of VersionFile.
        outputFile = project.file("${buildDir}/version.file")
    }   
}

version = '1.0.1.RELEASE'

We run the model task again and this time we see that the properties version and outputFile are set:

$ gradle model
...
------------------------------------------------------------
Root project
------------------------------------------------------------
...
+ versionFile
      | Type:           mrhaki.gradle.VersionFile
      | Creator:        VersionFileTaskRules#versionFile(VersionFile)
      | Rules:
         ⤷ versionFile { ... } @ build.gradle line 8, column 5
    + outputFile
          | Type:       java.io.File
          | Value:      /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/versionrule/build/version.file
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
    + version
          | Type:       java.lang.String
          | Value:      1.0.1.RELEASE
          | Creator:    VersionFileTaskRules#versionFile(VersionFile)
...
$

Finally we run the task generateVersionFile and check the result:

$ gradle generateVersionFile
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:generateVersionFile

BUILD SUCCESSFUL

Total time: 0.864 secs
$ more build/version.file
1.0.1.RELEASE
$

Please remember at the time of writing the Rule based model configuration is still incubating. In future versions things may change.

Written with Gradle 3.2

September 30, 2016

Gradle Goodness: Add But Do Not Apply Plugin Using Plugins Block

Sometimes we want to include the classes from a plugin, like tasks, in our build class path without actually applying the plugin. Or we want to add the classes to the root project and actually apply the plugin in subprojects. We can achieve this with a buildScript block and add the plugin dependency to the classpath configuration. But we can also do this with the newer plugins configuration block. Inside the plugins block we define the id and the version of the plugin, and since Gradle 3.0 we can also use the apply method. We have to set the value false to include the plugin to the class path, but not apply it to the project.

In the following example we add the Asciidoctor plugin to our build file, but we only want to use the AsciidoctorTask task from this plugin.

plugins {
    // Add Asciidoctor plugin, but do not apply it.
    id 'org.asciidoctor.convert' version '1.5.3' apply false
}

configurations {
    convert
}

repositories {
    jcenter()
}

dependencies {
    convert 'org.asciidoctor:asciidoctorj:1.5.4'
}

// Use of Asciidoctor task from the Asciidoctor plugin.
task convert(type: org.asciidoctor.gradle.AsciidoctorTask) {
    classpath = configurations.convert
}

Another use case can be that we have a multi-project build. We want to configure all plugins in a plugins configuration block in the root build file. And based on certain conditions we want to actually apply the plugin to a subproject. In the following example we add the Asciidoctor plugin and apply it to all subprojects where the name ends -doc:

plugins {
    id 'org.asciidoctor.convert' version '1.5.3' apply false
}

subprojects {
    if (name.endsWith('-doc')) {
        apply plugin: 'org.asciidoctor.convert'
    }
}

Written with Gradle 3.1.

September 20, 2016

Gradle Goodness: Use Command Line Options With Custom Tasks

Suppose we have a custom task with some properties that can be configured. Normally we would add the configuration in the build script. But we can also use command line options to configure a task. So when we run the task from the command line we can provide a configuration value for the task on the command line. To see which command line options are available for a task we can use the Gradle built-in task help followed by the option --task and the task name. To indicate a property as command line option we use a @Option annotation. We can specify the name of the command line option, a short description and also the order that is used to display the options with the help task.

Let's create a sample custom task and use the @Option annotation. In the following build file we create a custom task GenerateVersionFile. This task generates a file with a default name of version.txt in the build/ directory. The file contains the project version value. We make the property that defines the output filename as a command line option. This way the name can be defined when we run Gradle (and still of course using the default configuration in a build file).

// Import Option annotation
import org.gradle.api.internal.tasks.options.Option

version = 'demo'

// Create a task of the custom task type GenerateVersionFile.
task generateVersionFile(type: GenerateVersionFile)

/**
 * Custom task to generate a version value in a file.
 */
class GenerateVersionFile extends DefaultTask {
    
    String version

    // Specify outputFile property as
    // command line option.
    // Use as --outputFile filename.
    @Option(option = "outputFile", 
            description = "File to store the project version in",
            order = 1)
    Object outputFile
    
    GenerateVersionFile() {
        // Set default value for outputFile as version.txt.
        outputFile = 'version.txt'
        
        // Description for the task.
        description = 'Generate a file with the project version'
    }

    @TaskAction
    void generate() {
        // Create directory for the output file if 
        // it doesn't exist.
        final File versionFileDestination = getOutputFile()
        project.mkdir(versionFileDestination.parentFile)
        
        // Save version in file.
        versionFileDestination.text = getVersion()
    }

    @Input
    String getVersion() {
        return project.version
    }
    
    @OutputFile
    File getOutputFile() {
        return new File(project.buildDir, outputFile)
    }
    
}

If we run the help task for the generateVersionFile task we can see that our command line option is shown in the list of available options:

$ gradle help --task generationVersionFile
:help
Detailed task information for generateVersionFile

Path
     :generateVersionFile

Type
     GenerateVersionFile (GenerateVersionFile)

Options
     --outputFile     File where the project version is stored

Description
     Generate a file with the project version

Group
     -

BUILD SUCCESSFUL

Total time: 2.933 secs
$

Now we invoke the generateVersionFile task with a value for the command line option:

$ gradle generateVersionFile --outputFile version.saved
:generateVersionFile

BUILD SUCCESSFUL

Total time: 0.826 secs
$ more build/version.saved
demo
$

Written with Gradle 3.1.

September 19, 2016

Gradle Goodness: Change Gradle Wrapper Script Name

With the Gradle Wrapper task we can specify the name of the generated script files. By default the names are gradlew and gradlew.bat. The Wrapper task has the property scriptFile. We can set a different value for this property to let Gradle generate the script files with a different name.

In the following example we use the value mvnw (they will be surprised the build is so fast... ;-)) as the value:

task gradleWrapper(type: Wrapper) {
    scriptFile = 'mvnw'
}

Let's run the gradleWrapper task:

$ gradle gradleWrapper
:gradleWrapper

BUILD SUCCESSFUL

Total time: 8.597 secs
$ ls mvnw*
mvnw     mvnw.bat
$

Written with Gradle 3.1.

March 3, 2016

Gradle Goodness: Adding Custom Extension To Tasks

We can add extensions to our project in Gradle to extend the build script with extra capabilities. Actually we can add extensions to any object in Gradle that implements the ExtensionAware interface. The Task interface for example extends the ExtensionAware interface so we can add custom extensions to Gradle tasks as well. Inside a task configuration we can then use that extension.

In the following build script we use a custom extension for JavaCompile tasks to configure the compiler -Xlint arguments. The extension is added via the plugin com.mrhaki.gradle.JavaCompilerLintPlugin. First we take a look ate the extension class. This is a POGO for configuring the compiler arguments with -Xlint options:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/JavaCompilerLintExtension.groovy
package com.mrhaki.gradle

/**
 * Extension class for use with JavaCompile tasks
 * to set -Xlint options.
 */
class JavaCompilerLintExtension {

    Boolean cast
    Boolean classfile
    Boolean deprecation
    Boolean depAnn
    Boolean divzero
    Boolean empty
    Boolean fallthrough
    Boolean finallyblocks
    Boolean options
    Boolean overrides
    Boolean path
    Boolean processing
    Boolean rawtypes
    Boolean serial
    Boolean staticref
    Boolean tryblocks
    Boolean unchecked
    Boolean varargs

    void enableAll() {
        cast = true
        classfile = true
        deprecation = true
        depAnn = true
        divzero = true
        empty = true
        fallthrough = true
        finallyblocks = true
        options = true
        overrides = true
        path = true
        processing = true
        rawtypes = true
        serial = true
        staticref = true
        tryblocks = true
        unchecked = true
        varargs = true
    }

    /**
     * Create list of compiler -Xlint arguments.
     * 
     * @return List of -Xlint compiler arguments.
     */
    List<String> asCompilerArgs() {
        final List<String> optionArgs = []
        optionArgs << optionArg('cast', cast)
        optionArgs << optionArg('classfile', classfile)
        optionArgs << optionArg('deprecation', deprecation)
        optionArgs << optionArg('dep-ann', depAnn)
        optionArgs << optionArg('divzero', divzero)
        optionArgs << optionArg('empty', empty)
        optionArgs << optionArg('fallthrough', fallthrough)
        optionArgs << optionArg('finally', finallyblocks)
        optionArgs << optionArg('options', options)
        optionArgs << optionArg('overrides', overrides)
        optionArgs << optionArg('path', path)
        optionArgs << optionArg('processing', processing)
        optionArgs << optionArg('rawtypes', rawtypes)
        optionArgs << optionArg('serial', serial)
        optionArgs << optionArg('static', staticref)
        optionArgs << optionArg('try', tryblocks)
        optionArgs << optionArg('unchecked', unchecked)
        optionArgs << optionArg('varargs', varargs)

        // filter null values.
        final List<String> compilerArgs = optionArgs.findAll() 
        return compilerArgs
    }

    /**
     * Create -Xlint compile option if option is set. 
     * 
     * @param name Name of the -Xlint compile option.
     * @param enable Set option argument as -Xlint:option if true, otherwise as -Xlint:-option if false.
     * @return Null if enable is null, otherwise a valid -Xlint compiler option.
     */
    private String optionArg(final String name, final Boolean enable) {
        if (enable != null) {
            final String option = enable ? name : "-$name"
            return "-Xlint:$option"
        }
        return null
    }
}

Next we have a plugin class that registers the extension with the name lint on all JavaCompile tasks in our project:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/JavaCompilerLintPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile

/**
 * Plugin for applying a custom extension to 
 * JavaCompile tasks for configuring the 
 * -Xlint options.
 */
class JavaCompilerLintPlugin implements Plugin<Project> {
    
    void apply(final Project project) {
        
        // For all JavaCompile tasks we add 
        // a custom extension with the name lint
        // for configuring -Xlint options.
        project.tasks.withType(JavaCompile) { task ->
            
            // Let Gradle create a new extension.
            // Users can configure a compile task 
            // from the Java plugin like:
            // compileJava {
            //   lint {
            //     cast = true
            //   }
            // }
            JavaCompilerLintExtension taskExtension =
                    task.extensions.create('lint', JavaCompilerLintExtension)

            // Use options set via the lint extension
            // and assign them to the options.compilerArgs
            // property.
            // We do this in doFirst because the options are not
            // set yet at the Gradle configuration phase.
            task.doFirst {
                options.compilerArgs = taskExtension.asCompilerArgs()
            }
        }
    }
    
}

We have everything ready, so let's use the plugin in our Java project:

// File: build.gradle
apply plugin: 'java'

apply plugin: com.mrhaki.gradle.JavaCompilerLintPlugin

repositories {
    jcenter()
}

dependencies {
    testCompile 'junit:junit:4.11'
}

compileJava {
    // Here we use the custom
    // task extension. The closure
    // delegates to JavaCompilerLintExtension. 
    lint {
        enableAll()
        empty = false
        depAnn = false
    }
}

compileTestJava {
    // All JavaCompile task have the 
    // lint extension.
    lint {
        cast = true
    }
}

Written with Gradle 2.11.