Loading...

September 14, 2016

Gradle Goodness: Lazy Project Property Evaluation

Sometime we need to define a project property in our Gradle build file for which the value must be evaluated later than the assignment. We can do this in different ways in Gradle. For example for a String type property we can rely on Groovy's support for lazy String evaluation. If the property is of a different type we can use Closure to define the value. The Closure code is not executed during the configuration phase directly, but we can write code to invoke the Closure at the right moment and get the value. Finally we can use the afterEvaluate method of the Project class to define a lazy property. Let's look at the different options we have with some code samples.

First we look at a lazy String property. We illustrate this with an example of a multi-project build with the following layout:

.
├── api
├── app
├── build.gradle
├── lib
└── settings.gradle

In our settings.gradle file we define the projects we want to be part of the root project:

include 'api'
include 'lib'
include 'app'

Let's create a build file with a lazy property buildVersion:

subproject {

    version = '1.0.0'

    // Use lazy String evaluation using
    // the ${->...} syntax. If we don't
    // make it lazy the value of project.version
    // is always 1.0.0, because that is the only
    // known value at this point and the changed value
    // in the app project is not noticed.
    ext.buildVersion = "${project.name}-${-> project.version}"

    task displayBuildVersion {
        description = 'Display value of buildVersion property'
        doFirst {
            println buildVersion
        }
    }

}

project('app') {
    // In the project app we define
    // a different value for the version
    // property. Remember this property
    // is used in the definition of the
    // buildVersion property.
    version = '1.0.3'
}

If we run the displayBuildVersion task we get the following output:

$ gradle -q displayBuildVersion

api-1.0.0
app-1.0.3
lib-1.0.0
$

If we have a property of another type than String we can use another mechanism to support lazy properties. We use a Closure to define the value and then invoke the Closure to calculate the value at a later time. In the next build script we add a support method lazyProperty to check if the property is defined with a Closure. If so we execute the Closure to calculate the value. Any other type is return as-is.

subprojects {

    ext {
        // Project property with Integer value.
        // Value is overriden in the 'app' project.
        port = 80

        // Value is lazy and depends on port
        // property. Port property can be overriden
        // later in the configuration phase of Gradle.
        // Using a Closure we can make the value
        // calculation at a later point in time.
        calculatedPort = { port + 10 }
    }

    task displayCalculatedPort {
        description = 'Display value of calculatedPort property'
        doFirst {
            print "Calculated port for ${project.name} "
            println lazyProperty(project, 'calculatedPort')
            // Alternative without support method:
            // println project.property('calculatedPort')()
            // Or even shorter:
            // println calculatedPort()
        }
    }

}

project('app') {
    // Override port property.
    ext.port = 8000
}

/**
 * Check if property value is a Closure. If so the value
 * is calculated by running the Closure and returned. 
 * Otherwise the value is returned without calculation.
 *
 * @param project Project containing the property to calculate.
 * @param propertyName Name of the property to calculate value for.
 */
def lazyProperty(final Project project, final String propertyName) {
    // Get property.
    def propertyValue = project.property(propertyName)

    // Check for type of property to see if we can
    // run it as a Closure.
    if (propertyValue instanceof Closure) {
        // Invoke Closure to calculate the value.
        propertyValue()
    } else {
        // Return value as-is.
        propertyValue
    }
}

Let's run the displayBuildVersion task and look at the output:

$ gradle -q displayCalculatedPort

Calculated port for api 90
Calculated port for app 8010
Calculated port for lib 90
$

Finally we use the afterEvaluate method:

subprojects {

    // Project property with Integer value.
    // Value is overriden in the 'app' project.
    ext.port = 80

    afterEvaluate {
        // Create calculatedPort property with
        // value at the latest moment.
        ext.calculatedPort = port + 10
    }

    task displayCalculatedPort {
        description = 'Display value of calculatedPort property'
        doFirst {
            print "Calculated port for ${project.name} "
            println calculatedPort
        }
    }

}

project('app') {
    // Override port property.
    ext.port = 8000
}

When we run the displayBuildVersion task and look at the output we see expected results:

$ gradle -q displayCalculatedPort

Calculated port for api 90
Calculated port for app 8010
Calculated port for lib 90
$

In our simple examples we used the lazy project property only in one task. We could have simply evaluated the value in the task definition as it is the only place where it is used. But if a property is used by other tasks and code in our build script we can rely on the mentioned solutions in this blog post.

Written with Gradle 3.0.