tasks
The following code shows three Gradle tasks, and we will explain the differences between these three later.
task myTask { println "Hello, World!" } task myTask { doLast { println "Hello, World!" } } task myTask << { println "Hello, World!" }My goal is to create a task that prints out "Hello, World!" when it executes. When I first created the task, I guessed it should be written like this:
task myTask { println "Hello, World!" }Now, try to execute this myTask, enter gradle myTask on the command line, and print it as follows:
user$ gradle myTask Hello, World! :myTask UP-TO-DATE
This task looks like it works. It prints "Hello, World!".
However, it is not actually what we expected. Let's take a look at why. Enter gradle tasks on the command line to view all available tasks.
user$ gradle tasks Hello, World! :tasks ------------------------------------------------------------ All tasks runnable from root project ------------------------------------------------------------ Build Setup tasks ----------------- init - Initializes a new Gradle build. [incubating] ..........
Wait, why is "Hello, World!" printed out? I just wanted to see what tasks are available and didn't execute any custom tasks!
The reason is actually very simple. Gradle task has two main stages in its life cycle: configuration stage and execution stage.
Maybe my words are not very accurate, but this can really help me understand tasks.
Gradle must configure the task before executing it. Then the question is, how do I know which code in my task is executed during the configuration process and which code is run when the task is executed? The answer is that the code at the top level of the task is the configuration code, such as:
task myTask { def name = "Pavel" //<-- This line of code will execute println "Hello, World!"////<-- This line of code will also execute in the configuration stage}This is why when I execute gradle tasks, I print out "Hello, World!" - because the configuration code is executed. But this is not the effect I want, I want "Hello, World!" to print it out only when I explicitly call myTask. To achieve this effect, the easiest way is to use the Task#doLast() method.
task myTask { def text = 'Hello, World!' //configure my task doLast { println text //this is executed when my task is called } }Now, "Hello, World!" will only print out when I execute gradle myTask. Cool, now I know how to configure and make the task do the right thing. There is another question. In the initial example, what does the << symbol of the third task mean?
task myTask2 << { println "Hello, World!" }This is actually just a syntactic sugar version of doLast. It has the same effect as the following writing method:
task myTask { doLast { println 'Hello, World!' //this is executed when my task is called } }However, all codes in this way are executed and there is no configuration part of the code, so they are more suitable for those simple tasks that do not require configuration. Once your task needs to be configured, you still need to use the doLast version.
grammar
Gradle scripts are written in the Groovy language. Groovy's syntax is a bit like Java, I hope you can accept it.
If you are already familiar with Groovy, you can skip this part.
There is a very important concept in Groovy that you need to understand Closure (closure)
Closures
Closure is the key to our understanding of Gradle. Closure is a separate block of code that can receive parameters, return values, or be assigned to variables. It is similar to the Callable interface in Java and Future, and it is also like a function pointer, which is easy to understand. . .
The key is that this code will be executed when you call it, not when it is created. See an example of Closure:
def myClosure = { println 'Hello world!' }//execute our closingClosure()#output: Hello world!Here is a Closure that receives parameters:
def myClosure = {String str -> println str }//execute our closingClosure('Hello world!')#output: Hello world!If Closure only receives one parameter, you can use it to reference this parameter:
def myClosure = {println it }//execute our closingClosure('Hello world!')#output: Hello world!Closure that receives multiple parameters:
def myClosure = {String str, int num -> println "$str : $num" }//execute our renovationyClosure('my string', 21)#output: my string : 21In addition, the type of the parameter is optional, and the above example can be abbreviated as follows:
def myClosure = {str, num -> println "$str : $num" }//execute our closingClosure('my string', 21)#output: my string : 21What's cool is that variables from the current context can be used in Closure. By default, the current context is the class where closure was created:
def myVar = 'Hello World!'def myClosure = {println myVar}myClosure()#output: Hello world!Another cool point is that the context of closure can be changed, via Closure#setDelegate(). This feature is very useful:
def myClosure = {println myVar} //I'm referencing myVar from MyClass classMyClass m = new MyClass()myClosure.setDelegate(m)myClosure()class MyClass { def myVar = 'Hello from MyClass!'}#output: Hello from MyClass!As you can see, myVar does not exist when creating closure. There is no problem with this because when we execute closure, myVar exists in the context of closure. In this example. Because I changed its context to m before executing closure, myVar exists.
Pass closure as a parameter
The advantage of closure is that it can be passed to different methods, which can help us decouple the execution logic. In the previous example I have shown how to pass closure to an instance of a class. Below we will take a look at various methods that receive closure as parameters:
1. Only receive one parameter and the parameter is closure method: myMethod(myClosure)
2. If the method only receives one parameter, parentheses can be omitted: myMethod myClosure
3. You can use inline closure: myMethod {println 'Hello World'}
4. Method to receive two parameters: myMethod(arg1, myClosure)
5. Similar to 4, the singular closure is inline: myMethod(arg1, { println 'Hello World' })
6. If the last parameter is closure, it can be taken out from brackets: myMethod(arg1) { println 'Hello World' }
Here I just want to remind you whether the writing of 3 and 6 looks familiar?
Gradle example
Now that we have understood the basic syntax, how do we use it in Gradle scripts? Let’s take a look at the following example:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }}allprojects { repositories { jcenter() }} Once you know the syntax of Groovy, is it easy to understand the above example?
First is a buildscript method, which receives a closure:
def buildscript(Closure closure)
Next is the allprojects method, which also receives a closure parameter:
def allprojects(Closure closure)
The others are similar. . .
It seems much easier now, but I don’t understand one thing, that is, where are these methods defined? The answer is Project
Project
This is a key to understanding Gradle scripts.
The statement blocks on the top level of the build script will be delegated to Project instances, which shows that Project is exactly where I am looking for.
Searching for the buildscript method on the Project's document page will find the buildscript{} script block (script block). etc. What the hell is script block? According to the documentation:
script block is a method that only receives closure as a parameter. Continue to read the buildscript documentation. The document says that Delegates to: ScriptHandler from buildscript. That is, we pass the closure to the buildscript method, and the final execution context is the ScriptHandler. In the example above, our closure passed to buildscript calls the repositories(closure) and dependencies(closure) methods. Since closure has been entrusted to ScriptHandler, then we will look for dependencies methods in ScriptHandler.
Void dependencies (Closure configureClosure) was found. According to the documentation, dependencies are used to configure script dependencies. And dependencies were eventually entrusted to the DependencyHandler.
I saw how widely used Gradles is. Understanding the entrustment is very important.
Script blocks
By default, a lot of script blocks are predefined in Project, but the Gradle plugin allows us to define new script blocks ourselves!
This means that if you post some {…} on the top level of the build script, but you can't find this script block or method in Gradle's documentation, in most cases, this is some script blocks defined in the plugin.
android Script block
Let's take a look at the default Android app/build.gradle file:
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' } }}Task order
I noticed that most of the problems I encountered when using Gradle are related to the execution order of the task. Obviously if my build would work better if my task is executed at the right time. Let's take a closer look at how to change the execution order of tasks.
dependsOn
I think the most direct way to explain the way you rely on other tasks when executing your task is to use the dependsOn method.
For example, in the following scenario, task A already exists. We want to add a task B, and its execution must be after A is executed:
This is a very simple scenario, assuming that the definition of A and B is as follows:
task A << {println 'Hello from A'}task B << {println 'Hello from B'} Just simply call B.dependentsOn A and that's fine.
This means that as long as I execute task B, task A will execute first.
paveldudka$ gradle B:AHello from A:BHello from B
In addition, you can declare its dependencies in the task configuration area:
task A << {println 'Hello from A'}task B { dependsOn A doLast { println 'Hello from B' }} What if we want to insert our task into an existing task dependency?
The process is similar to what just now. Assume that the following task dependencies already exist:
task A << {println 'Hello from A'}task B << {println 'Hello from B'}task C << {println 'Hello from C'}B.dependentsOn AC.dependentsOn BJoin our new task
task B1 << {println 'Hello from B1'}B1.dependsOn BC.dependsOn B1Output:
paveldudka$ gradle C:AHello from A:BHello from B:B1Hello from B1:CHello from C
Note that dependsOn adds tasks to the dependent collection, so it is no problem to rely on multiple tasks.
task B1 << {println 'Hello from B1'}B1.dependsOn BB1.dependsOn QOutput:
paveldudka$ gradle B1:AHello from A:BHello from B:QHello from Q:B1Hello from B1
mustRunAfter
Now suppose I have another task, which depends on the other two tasks. Here I use a real scenario where I have two tasks, one unit tested task and one UI tested task. There is also a task that runs all tests, which depends on the previous two tasks.
task unit << {println 'Hello from unit tests'}task ui << {println 'Hello from UI tests'}task tests << {println 'Hello from all tests!'}tests.dependsOn unittests.dependsOn uiOutput:
paveldudka$ gradle tests:uiHello from UI tests:unitHello from unit tests:testsHello from all tests!
Although unitest and UI test will execute before the test task, the execution order of unit and ui cannot be guaranteed. Although it is executed in the order of the alphabet now, this depends on the implementation of Gradle, and you must not rely on this order in your code.
Since the UI test time is much longer than the unit test time, I want the unit test to be executed first. One solution is to make the ui task depend on the unit task.
task unit << {println 'Hello from unit tests'}task ui << {println 'Hello from UI tests'}task tests << {println 'Hello from all tests!'}tests.dependsOn unittests.dependsOn uiui.dependsOn unit // <-- I added this dependencyOutput:
paveldudka$ gradle tests:unitHello from unit tests:uiHello from UI tests:testsHello from all tests!
Now the unit test will be executed before the ui test.
But there is a very disgusting problem here. My ui test actually does not rely on unit test. I hope to be able to execute ui test separately, but here every time I execute ui test, I will execute unit test first.
MustRunAfter is needed here. MustRunAfter does not add dependencies, it just tells Gradle the priority of execution if two tasks exist at the same time. For example, we can specify ui.mustRunAfter unit here. In this way, if the ui task and unit task exist at the same time, Gradle will first execute unit test, and if only gradle ui is executed, the unit task will not be executed.
task unit << {println 'Hello from unit tests'}task ui << {println 'Hello from UI tests'}task tests << {println 'Hello from all tests!'}tests.dependsOn unittests.dependsOn uiui.mustRunAfter unitOutput:
paveldudka$ gradle tests:unitHello from unit tests:uiHello from UI tests:testsHello from all tests!
The dependency relationship is as follows:
MustRunAfter is currently an experimental feature in Gradle 2.4.
finalizedBy
Now we have two tasks, unit and ui, assuming that both tasks will output test reports, now I want to merge these two test reports into one:
task unit << {println 'Hello from unit tests'}task ui << {println 'Hello from UI tests'}task tests << {println 'Hello from all tests!'}task mergeReports << {println 'Merging test reports'}tests.dependsOn unittests.dependsOn uiui.mustRunAfter unitmergeReports.dependsOn testsNow if I want to get test reports for ui and unit, execute task mergeReports.
paveldudka$ gradle mergeReports:unitHello from unit tests:uiHello from UI tests:testsHello from all tests!:mergeReportsMerging test reports
This task works, but it looks so stupid. mergeReports doesn't feel particularly good from a user's perspective. I want to execute the tests task to get the test report without having to know the existence of mergeReports. Of course I can move the merge logic into the tests task, but I don’t want to make the tests task too bloated, so I will continue to put the merge logic into the mergeReports task.
finalizeBy is here to save the scene. As the name suggests, finalizeBy is the task to be executed after the task is executed. Modify our script as follows:
task unit << {println 'Hello from unit tests'}task ui << {println 'Hello from UI tests'}task tests << {println 'Hello from all tests!'}task mergeReports << {println 'Merging test reports'}tests.dependsOn unittests.dependsOn uiui.mustRunAfter unitmergeReports.dependsOn tests.finalizedBy mergeReportsNow execute the tests task and you can get the test report:
paveldudka$ gradle tests:unitHello from unit tests:uiHello from UI tests:testsHello from all tests!:mergeReportsMerging test reports