[Issue 001] Gradle project properties

Abstract: Figuring out the scope of a project property in a Gradle build can be confusing at times. In this very first issue we look at different options for defining project properties and reading their values.

Welcome to the first issue of my newsletter! The goal of this newsletter is to share tips and experiences gathered while working on both commercial and open source projects across the years. Let's get started!

Java has no shortage of all kind of projects, in the build space almost everyone has encountered Apache Ant or Apache Maven in way or another. Some years ago a new upstart broke into the build tool scene, Gradle, chances are you've encountered a couple of times already. I've been a user of the Gradle build tools since the early days, I must say it's been fun and quite rewarding to see the evolution of the tool across the years. In a very basic and simplistic way, Gradle builds on top of the ideas brought by Ant and Maven while delivering more bang for your buck so to speak. One of the early decisions of the development team was to create a build DSL using a real programming language (Apache Groovy) rather than following its predecessors in leveraging XML. This decision has paid dividends as it's quite easy to extend a build process with custom code on the go when the need arises. But there are some issues that appear from time to time, some of the tied to the usage of the Groovy language due to the way scopes work in Groovy scripts. Let's have a look at a common issue I've found across different teams: handling of project properties.

Project properties are used to configure and alter the behavior of a particular build run. Without getting in deep details let's say that each build is a series of operations applied to an instance of Project. This type defines a small set of static properties such as group and version, commonly used to define artifact coordinates for publication to an artifact repository. A Project can also have dynamic properties which can be defined in 4 ways:

  • a file named gradle.properties, local to the project.
  • a file named gradle.properties, available to all projects at ~/.gradle/ (%USER_HOME%/.gradle on Windows).
  • on the command line using the -P flag.
  • using the ext block inside the build file.

It's worth noting that static and dynamic properties can be defined by other build elements such as plugins, conventions, and extensions, however we won't cover those elements today. Assume you've got a Gradle build file (typically named build.gradle) like the following one

build.gradle

println foo

You should get build error if you attempt to run the build in its current state, as there's no definition of the foo property available to the Project, not yet. We can fix this by creating a file named gradle.properties with the following contents

gradle.properties

foo = local

Invoking the build should no longer cause an error, instead the word "local" should be printed out to the console. Congratulations! you just defined a dynamic property on the Project using an external source. Personally I prefer to define almost all dynamic properties that provide a static literal value in this way as the gradle.properties files are usually small and can only contain text. The next option for defining a dynamic property is to use the globally available gradle.properties file. Be advised that any properties defined on this file are available to all projects associated with a particular user. Also, properties set on this file are considered to be private (user specific) thus you can use this file to store sensitive data such as API keys and passwords. Go ahead and edit this file with the following content

~/.gradle/gradle.properties

foo = global

Running the build once more results in the word "global" printed out. This means global settings have precedence over local ones. This is a good thing, as you may define default values at the local level but override them at the global one, per user. It's a good thing to discuss with your team which settings can be shared across all members and build locations (think Continuous Integration servers). Going one step further we can define a variable on the go using the command line, for example by running the same build as before by invoking

$ gradle -Pfoo=command

Now the print out should ready "command" instead of "global". This tells us that properties defined at the command line have higher precedence over global ones. This allows you to tweak a property value for a single run without changing the build sources, neat! But we're not done yet, there's one more option to be explored. Edit the build file again and make it look like the following

build.gradle

ext.foo = 'build'
println foo

Executing the build once more should result in "build" being printed out to the console. What happens if you attempt combining this new build with a command line flag? Give it a try, invoke the previous command where the foo property was defined on the spot using -P. Did you get the expected result? This is where some people are left with their head scratching. Half would expect the build to print out "command" while others would guess "build" to be the correct outcome. What's going on here? It looks like all external definitions are taken into account first then the build file's go in. It kind of makes sense given that the build tool must follow a certain evaluation order, with the build script itself being last.

And now that we bring back the topic of Groovy scripts, one could write a build file like this one

build.gradle

def bar = 'value'
println bar

What you see in this file is not a project property but a script variable. You can tell the difference because the "property" has a type definition. Yes, def is a type in Groovy, it denotes the dynamic type. You may as well use String, int, or any other Java/Groovy type for that matter. The distinction between a project property versus a script variable is very important, as you can get unexpected results when reading their values in different contexts. Let's modify the build file one more time

build.gradle

ext.foo = 'build'
def bar = 'value'
def cls_printFoo = { println foo }
def cls_printBar = { println bar }
def mtd_printFoo() { println foo }
def mtd_printBar() { println bar }

task printIt {
    doLast {
        println foo
        println bar
        cls_printFoo()
        cls_printBar()
        mtd_printFoo()
        mtd_printBar()
    }
}

This file contains a project property definition (foo) and a script variable (bar). It also defines two closures -- in Groovy closures are akin to Java8's lambda expressions or JavaScript's functions, with some differences of course. These closures print out the property and the variable respectively. There's also a pair of method definitions that perform the same operation as their closure counterparts; notice the slight change in syntax used to define them. Now, running the build by specifying the printIt task results in an output similar to

:printIt
build
value
build
value
build
:printIt FAILED

FAILURE: Build failed with an exception.

* Where:
Build file '/tmp/foo/build.gradle' line: 6

* What went wrong:
Execution failed for task ':printIt'.
> Could not get unknown property 'bar' for root project 'foo' of type org.gradle.api.Project.

Uh oh, something went wrong. It looks like the mtd_printBar cannot read the value, the exception message gives a hint at what happened. It appears the method tried to read project property named bar but we know that it's actually a script variable. To understand what's happening one has to be aware of the differences between closure and method invocations in Groovy, most specifically how the closure delegate works. Each closure in Groovy has a read/write property named delegate, which can be used to alter the behavior of the same closure instance across multiple invocations. By default a closure's delegate is set to match another read-only property of the closure, the owner. This is the instance that owns the closure, in our case it's the build script itself. Next, the Gradle build tools sets the closure delegates to match the Project instance. When the cls_printFoo closure is executed it attempts to resolve the reference to foo, and finds it in its delegate, the Project, thus it succeeds. When the cls_printBar closure is executed it attempts to resolve the reference to bar, failing to find it in the Project properties it turns out to the script and finds it as a variable. Now, when the mtd_printFoo is called it's actually invoked on the Project itself, thus the reference to foo is correctly found. But that's not the case when the last method is invoked. You see, a method defined on a script is the same as any other method defined anywhere else, that is, methods don't understand the concept of owner and delegate, thus it can't reach out into the script's variables.

That concludes this issue. Thank you for reading. Any feedback is appreciated.

See you next time.

Andres

ˆ Back To Top