Gradle tip #2: understanding syntax
In the Part 1 we talked about tasks and different stages of the build lifecycle. But after I published it I realized that before we jump into Gradle specifics it is very important to understand what we are dealing with - understand its syntax and stop being scared when we see complex
build.gradle
scripts. With this article I will try to fill this missing gap.Syntax
Gradle build scripts are written in Groovy, so before we start analyzing them, I want to touch (briefly) some key Groovy concepts. Groovy syntax is somewhat similar to Java, so hopefully you won't have much problems understanding it.
If you feel comfortable with Groovy - feel free to skip this section.
There is one important Groovy aspect you need to understand in order to understand Gradle scripts - Closure.
Closures
Closure is a key concept which we need to grasp to better understand Gradle. Closure is a standalone block of code which can take arguments, return values and be assigned to a variable. It is some sort of a mix between
Callable
interface, Future
, function pointer, you name it..
Essentially this is a block of code which is executed when you call it, not when you create it. Let's see a simple Closure example:
def myClosure = { println 'Hello world!' }
//execute our closure
myClosure()
#output: Hello world!
Or here is a closure which accepts a parameter:
def myClosure = {String str -> println str }
//execute our closure
myClosure('Hello world!')
#output: Hello world!
Or if closure accepts only 1 parameter, it can be referenced as
it
:def myClosure = {println it }
//execute our closure
myClosure('Hello world!')
#output: Hello world!
Or if closure accepts multiple input parameters:
def myClosure = {String str, int num -> println "$str : $num" }
//execute our closure
myClosure('my string', 21)
#output: my string : 21
By the way, argument types are optional, so example above can be simplified to:
def myClosure = {str, num -> println "$str : $num" }
//execute our closure
myClosure('my string', 21)
#output: my string : 21
One cool feature is that closure can reference variables from the current context (read class). By default, current context - is the class within this closure was created:
def myVar = 'Hello World!'
def myClosure = {println myVar}
myClosure()
#output: Hello world!
Another cool feature is that current context for the closure can be changed by calling
Closure#setDelegate()
. This feature will become very important later:def myClosure = {println myVar} //I'm referencing myVar from MyClass class
MyClass m = new MyClass()
myClosure.setDelegate(m)
myClosure()
class MyClass {
def myVar = 'Hello from MyClass!'
}
#output: Hello from MyClass!
As you can see, at the moment when we created closure,
myVar
variable doesn't exist. And this is perfectly fine - it should be present in the closure context at the point when we execute this closure.
In this case I modified current context for the closure right before I executed it, so
myVar
is available.Pass closure as an argument
The real benefit of having closures - is an ability to pass closure to different methods which helps us to decouple execution logic.
In previous section we already used this feature when passed closure to another class instance. Now we will go through different ways to call method which accepts closure:
- method accepts 1 parameter - closure
myMethod(myClosure)
- if method accepts only 1 parameter - parentheses can be omitted
myMethod myClosure
- I can create in-line closure
myMethod {println 'Hello World'}
- method accepts 2 parameters
myMethod(arg1, myClosure)
- or the same as '4', but closure is in-line
myMethod(arg1, { println 'Hello World' })
- if last parameter is closure - it can be moved out of parentheses
myMethod(arg1) { println 'Hello World' }
At this point I really have to point your attention to example #3 and #6. Doesn't it remind you something from gradle scripts? ;)
Gradle
Now we know mechanics, but how it is related to actual Gradle scripts? Let's take simple Gradle script as an example and try to understand it:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}
Look at that! Knowing Groovy syntax we can somewhat understand what is happening here!
- there is (somewhere) a
buildscript
method which accepts closure:def buildscript(Closure closure)
- there is (somewhere) a
allprojects
method which accepts closure:def allprojects(Closure closure)
...and so on.
This is cool, but this information alone is not particularly helpful... What does "somewhere" mean? We need to know exactly where this method is declared.
And the answer is - Project
Project
This is a key for understanding Gradle scripts:
All top level statements within build script are delegated toProject
instance
This means that Project - is the starting point for all my searches.
This being said - let's try to find
buildscript
method.
If we search for
buildscript
- we will find buildscript {}
script block. But wait.. What the hell is script block??? According to documentation:A script block is a method call which takes a closure as a parameter
Ok! We found it! That's exactly what happens when we call
buildscript { ... }
- we execute method buildscript
which accepts Closure.
If we keep reading
ScriptHandler from buildscript. It means that execution scope for the closure we pass as an input parameter will be changed to ScriptHandler. In our case we passed closure which executes
buildscript
documentation - it says: Delegates to:ScriptHandler from buildscript. It means that execution scope for the closure we pass as an input parameter will be changed to ScriptHandler. In our case we passed closure which executes
repositories(Closure)
and dependencies(Closure)
methods. Since closure is delegated to ScriptHandler
, let's try to search for dependencies
method within ScriptHandler
class.
And here it is - void dependencies(Closure configureClosure), which according to documentation, configures dependencies for the script. Here we are seeing another terminology: Executes the given closure against the DependencyHandler. Which means exactly the same as "delegates to [something]" - this closure will be executed in scope of another class (in our case - DependencyHandler)
"delegates to [something]" and "configures [something]" - 2 statements which mean exactly the same - closure will be execute against specified class.
Gradle extensively uses this delegation strategy, so it is really important to understand terminology here.
For the sake of completeness, let's see what is happening when we execute closure
{classpath 'com.android.tools.build:gradle:1.2.3'}
within DependencyHandler
context. According to documentation this class configures dependencies for given configuration and the syntax should be:<configurationName> <dependencyNotation1>
So with our closure we are configuring configuration with name
classpath
to use com.android.tools.build:gradle:1.2.3
as a dependency.Script blocks
By default, there is a set of pre-defined script blocks within
Project
, but Gradle plugins are allowed to add new script blocks!
It means that if you are seeing something like
something { ... }
at the top level of your build script and you couldn't find neither script block or method which accepts closure in the documentation - most likely some plugin which you applied added this script block.
android
Script block
Let's take a look at the default Android
app/build.gradle
build script:apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.trickyandroid.testapp"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
As we can see, it seems like there should be
android
method which accepts Closure as a parameter. But if we try to search for such method in Project
documentation - we won't find any. And the reason for that is simple - there is no such method :)
If you look closely to the build script - you can see that before we execute
android
method - we apply com.android.application
plugin! And that's the answer! Android application plugin extends Project
object with android
script block (which is simply a method which accepts Closure and delegates it to AppExtension
class1).
But where can I find Android plugin documentation? And the answer is - you can download documentation from the official Android Tools website (or here is a direct link to documentation).
If we open
AppExtension
documentation - we will find all the methods and attributes from our build script:compileSdkVersion 22
. if we search forcompileSdkVersion
we will find property. In this case we assign"22"
to propertycompileSdkVersion
- the same story with
buildToolsVersion
defaultConfig
- is a script block which delegates execution toProductFlavor
class- .....and so on
So now we have really powerful ability to understand the syntax of Gradle build scripts and search for documentation.
Exercise
With this powerful ability (oh, that's sounds awesome), let's go ahead and try reconfigure something :)
In
AppExtension
I found script block testOptions
which delegates Closure to TestOptions
class. Going to TestOptions
class we can see that there are 2 properties: reportDir
and resultsDir
. According to documentation, reportDir
is responsible for test report location. Let's change it!android {
......
testOptions {
reportDir "$rootDir/test_reports"
}
}
Here I used
rootDir
property from Project
class which points to the root project directory.
So now if I execute
./gradlew connectedCheck
, my test report will go into [rootProject]/test_reports
directory.
Please don't do this in your real project - all build artifacts should go into
build
dir, so you don't pollute your project structure.
Happy gradling!
P.S. Thanks a lot @Mark Vieira for proof-reading this article!
- It is worth mentioning that "com.android.library" plugin delegates closure to "LibraryExtension" class instead of "AppExtension" ↩
No comments:
Post a Comment