A Groovy Build Script Primer
Ideally, a Groovy build script looks mostly like configuration: setting some properties of the project, configuring dependencies, declaring tasks, and so on. That configuration is based on Groovy language constructs. This primer aims to explain what those constructs are and — most importantly — how they relate to Gradle’s API documentation.
The Project
object
As Groovy is an object-oriented language based on Java, its properties and methods apply to objects. In some cases, the object is implicit — particularly at the top level of a build script, i.e. not nested inside a {}
block.
Consider this fragment of build script, which contains an unqualified property and block:
version = '1.0.0.GA'
configurations {
...
}
Both version
and configurations {}
are part of org.gradle.api.Project.
This example reflects how every Groovy build script is backed by an implicit instance of Project
. If you see an unqualified element and you don’t know where it’s defined, always check the Project
API documentation to see if that’s where it’s coming from.
Properties
<obj>.<name> // Get a property value
<obj>.<name> = <value> // Set a property to a new value
"$<name>" // Embed a property value in a string
"${<obj>.<name>}" // Same as previous (embedded value)
version = '1.0.1'
myCopyTask.description = 'Copies some files'
file("$buildDir/classes")
println "Destination: ${myCopyTask.destinationDir}"
A property represents some state of an object. The presence of an =
sign is a clear indicator that you’re looking at a property. Otherwise, a qualified name — it begins with <obj>.
— without any other decoration is also a property.
If the name is unqualified, then it may be one of the following:
-
A task instance with that name.
-
A property on Project.
-
An extra property defined elsewhere in the project.
-
A property of an implicit object within a block.
-
A local variable defined earlier in the build script.
Note that plugins can add their own properties to the Project
object. The API documentation lists all the properties added by core plugins. If you’re struggling to find where a property comes from, check the documentation for the plugins that the build uses.
💡
|
When referencing a project property in your build script that is added by a non-core plugin, consider prefixing it with project. — it’s clear then that the property belongs to the project object.
|
Properties in the API documentation
The Groovy DSL reference shows properties as they are used in your build scripts, but the Javadocs only display methods. That’s because properties are implemented as methods behind the scenes:
-
A property can be read if there is a method named
get<PropertyName>
with zero arguments that returns the same type as the property. -
A property can be modified if there is a method named
set<PropertyName>
with one argument that has the same type as the property and a return type ofvoid
.
Note that property names usually start with a lower-case letter, but that letter is upper case in the method names. So the getter method getProjectVersion()
corresponds to the property projectVersion
. This convention does not apply when the name begins with at least two upper-case letters, in which case there is not change in case. For example, getRAM()
corresponds to the property RAM
.
project.getVersion()
project.version
project.setVersion('1.0.1')
project.version = '1.0.1'
Methods
<obj>.<name>() // Method call with no arguments
<obj>.<name>(<arg>, <arg>) // Method call with multiple arguments
<obj>.<name> <arg>, <arg> // Method call with multiple args (no parentheses)
myCopyTask.include '**/*.xml', '**/*.properties'
ext.resourceSpec = copySpec() // `copySpec()` comes from `Project`
file('src/main/java')
println 'Hello, World!'
A method represents some behavior of an object, although Gradle often uses methods to configure the state of objects as well. Methods are identifiable by their arguments or empty parentheses. Note that parentheses are sometimes required, such as when a method has zero arguments, so you may find it simplest to always use parentheses.
✨
|
Gradle has a convention whereby if a method has the same name as a collection-based property, then the method appends its values to that collection. |
Blocks
Blocks are also methods, just with specific types for the last argument.
<obj>.<name> {
...
}
<obj>.<name>(<arg>, <arg>) {
...
}
configurations {
assets
}
sourceSets {
main {
java {
srcDirs = ['src']
}
}
}
project(':util') {
apply plugin: 'java-library'
}
Blocks are a mechanism for configuring multiple aspects of a build element in one go. They also provide a way to nest configuration, leading to a form of structured data.
There are two important aspects of blocks that you should understand:
-
They are implemented as methods with specific signatures.
-
They can change the target ("delegate") of unqualified methods and properties.
Both are based on Groovy language features and we explain them in the following sections.
Block method signatures
You can easily identify a method as the implementation behind a block by its signature, or more specifically, its argument types. If a method corresponds to a block:
-
It must have at least one argument.
-
The last argument must be of type
groovy.lang.Closure
or org.gradle.api.Action.
For example, Project.copy(Action) matches these requirements, so you can use the syntax:
copy {
into "$buildDir/tmp"
from 'custom-resources'
}
That leads to the question of how into()
and from()
work. They’re clearly methods, but where would you find them in the API documentation? The answer comes from understanding object delegation.
Delegation
The section on properties lists where unqualified properties might be found. One common place is on the Project
object. But there is an alternative source for those unqualified properties and methods inside a block: the block’s delegate object.
To help explain this concept, consider the last example from the previous section:
copy {
into "$buildDir/tmp"
from 'custom-resources'
}
All the methods and properties in this example are unqualified. You can easily find copy()
and buildDir
in the Project
API documentation, but what about into()
and from()
? These are resolved against the delegate of the copy {}
block. What is the type of that delegate? You’ll need to check the API documentation for that.
There are two ways to determine the delegate type, depending on the signature of the block method:
-
For
Action
arguments, look at the type’s parameter.In the example above, the method signature is
copy(Action<? super CopySpec>)
and it’s the bit inside the angle brackets that tells you the delegate type — CopySpec in this case. -
For
Closure
arguments, the documentation will explicitly say in the description what type is being configured or what type the delegate it (different terminology for the same thing).
Hence you can find both into() and from() on CopySpec
. You might even notice that both of those methods have variants that take an Action
as their last argument, which means you can use block syntax with them.
All new Gradle APIs declare an Action
argument type rather than Closure
, which makes it very easy to pick out the delegate type. Even older APIs have an Action
variant in addition to the old Closure
one.
Local variables
def <name> = <value> // Untyped variable
<type> <name> = <value> // Typed variable
def i = 1
String errorMsg = 'Failed, because reasons'
Local variables are a Groovy construct — unlike extra properties — that can be used to share values within a build script.
⚠
|
Avoid using local variables in the root of the project, i.e. as pseudo project properties. They cannot be read outside of the build script and Gradle has no knowledge of them. Within a narrower context — such as configuring a task — local variables can occasionally be helpful. |