Loading...

February 29, 2016

Gradle Goodness: Lazy Task Properties

When we create our own custom tasks we might need to support lazy evaluation of task properties. A Gradle build has three phases: initialisation, configuration and execution. Task properties can be set during the configuration phase, for example when a task is configured in a build file. But our task can also evaluate the value for a task property at execution time. To support evaluation during execution time we must write a lazy getter method for the property. Also we must define the task property with def or type Object. Because of the Object type we can use different types to set a value. For example a Groovy Closure or Callable interface implementation can be used to execute later than during the configuration phase of our Gradle build. Inside the getter method we invoke the Closure or Callable to get the real value of the task property.

Let's see this with a example task. We create a simple class with two task properties: user and outputFile. The user property must return a String object and the outputFile property a File object. For the outputFile property we write a getOutputFile method. We delegate to the Project.file method, which already accepts different argument types for lazy evaluation. We also write an implementation for getUser method where we run a Closure or Callable object if that is used to set the property value.

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

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.util.concurrent.Callable

class Hello extends DefaultTask {

    // Setter method will accept Object type.
    def outputFile

    File getOutputFile() {
        // The Project.file method already
        // accepts several argument types:
        // - CharSequence
        // - File
        // - URI or URL
        // - Closure
        // - Callable
        project.file outputFile
    }

    // Setter method will accept Object type.
    def user

    String getUser() {
        // If the value is set with a Closure
        // or Callable then we calculate
        // the value. Closure implements Callable.
        if (user instanceof Callable) {
            user.call()
        // For other types we return the
        // result of the toString method.
        } else {
            user.toString()
        }
    }

    @TaskAction
    void sayHello() {
        getOutputFile().text = "Hello ${getUser()}"
    }
}

We already learned in a previous post that setting a property value can also be done using a setter method that is created by Gradle. In the following specification we use different methods and types to assign values to the user and outputFile properties:

package com.mrhaki.gradle

import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import spock.lang.Specification
import spock.lang.Subject

import java.util.concurrent.Callable

class HelloTaskSpec extends Specification {

    @Subject
    private Hello task

    private Project project = ProjectBuilder.builder().build()

    def setup() {
        task = project.task('helloSample', type: Hello)
    }

    def "set user property during configuration phase"() {
        when:
        project.ext.username = 'sample'

        and:
        // Property value known at configuration time.
        task.user project.property('username')

        then:
        task.user == 'sample'
    }

    def "user property not set when value not known during configuration phase"() {
        when:
        // Property value not known at configuration time.
        // Exception will be thrown because username property
        // is not set yet.
        task.user project.property('username')
        
        and:
        project.ext.username = 'sample'

        then:
        thrown(MissingPropertyException)
    }

    def "set user property with lazy evaluation GString during configuration phase"() {
        when:
        // Property value not known at configuration time.
        // Using GString lazy evaluation.
        task.user "${ -> project.property('username')}"

        and:
        project.ext.username = 'lazyGString'

        then:
        task.user == 'lazyGString'
    }

    def "set user property during execution phase using Closure"() {
        when:
        // Property value known at execution time, but not
        // during configuration phase.
        task.user { project.property('username') }

        and:
        // Set value for user property assignment.
        project.ext.username = 'closureValue'

        then:
        task.user == 'closureValue'
    }

    def "set user property during execution phase using Callable"() {
        when:
        // Property value known at execution time, but not
        // during configuration phase.
        task.user = new Callable<String>() {
            @Override
            String call() throws Exception {
                project.property('username')
            }
        }

        and:
        // Set value for user property assignment.
        project.ext.username = 'callableValue'

        then:
        task.user == 'callableValue'
    }
    
    def "delegate getOutputFile to project.file() and support all types for project.file()"() {
        given:
        task.user 'mrhaki'
        
        task.outputFile { project.property('helloOutput') }
        
        and:
        project.ext.helloOutput = 'hello.txt'

        when:
        task.sayHello()
        
        then:
        task.outputFile.text == 'Hello mrhaki'
    }
    
}

Written with Gradle 2.11.