Wednesday, February 27, 2013

Gradle: Configuration vs Execution

People new to Gradle get trapped very early in the difference between the configuration and execution phases of the Gradle build lifecycle.  Understanding that difference is critical to success in using Gradle as your tool of automation.

Gradle has three build phases: initialization, configuration, and execution.  Initialization is pretty straight-forward, but coming from a more declarative build tool background (like Ant), differentiating the latter two can cause headaches.  The key is to understand the difference between configuring properties and adding actions.

Let's take a simple example:

 task copyFiles (type: Copy) {  
      from "srcDir"  
      into "destDir"  
 }  

Pretty straightforward, right?  I'm using an existing task type--Copy--to copy some files.  When I run this in Gradle, I see the following:

 C:\temp\myProject>gradle  
 :help  
 Welcome to Gradle 1.3.  
 To run a build, run gradle <task> ...  
 To see a list of available tasks, run gradle tasks  
 To see a list of command-line options, run gradle --help  
 BUILD SUCCESSFUL  
 Total time: 2.582 secs  

Which means, of course, that nothing actually executed.  In order to actually run the task, I'd need to explicitly ask Gradle to run it.

 C:\temp\myProject>gradle copyFiles  
 :copyFiles  
 BUILD SUCCESSFUL  
 Total time: 2.04 secs  

So what's really going on here?  Let's add a print statement.

 task copyFiles (type: Copy) {  
      println "copying files"  
      from "srcDir"  
      into "destDir"  
 }  

And the output:

 C:\temp\myProject>gradle copyFiles  
 copying files  
 :copyFiles
 BUILD SUCCESSFUL  
 Total time: 2.736 secs  

Notice something strange?  Our println statement is happening before Gradle executes the task.  What if I run Gradle without any tasks?

 C:\temp\myProject>gradle  
 copying files  
 :help  
 ...
 BUILD SUCCESSFUL  
 Total time: 1.791 secs  

Again, the output still prints, even though we didn't actually execute the task.  Why?

The answer is that all task configurations get executed on every Gradle build, no matter whether the tasks actually execute or not.  Think about it this way: all code inside the main body of a task is setup.  So when we invoke the methods "from" and "into" on the Copy task, Gradle is not actually copying the files ... yet.  It's simply instructing the task: "when it's your turn to execute, here's what I want you to do."

Because our println statement is in the configuration section of the task, it always runs, whether or not the task actually executes.

So the question now becomes, how to we write code that only executes when the task executes?  This is handled by what Gradle calls actions.  Actions are simply chunks of code attached to a task that run--in succession--when the task executes.  Actually performing the copy operation is the Copy task's default action.  We can add actions to any task by invoking doLast().

 task copyFiles (type: Copy) {  
      from "srcDir"  
      into "destDir"  
      doLast {  
           println "copying files"  
      }  
 }  

Now when we run the task, we get the expected behavior.  Our println happens during the execution phase.

 C:\temp\myProject>gradle copyFiles  
 :copyFiles  
 copying files  
 BUILD SUCCESSFUL  
 Total time: 1.932 secs  

Likewise if we omit the task, we no longer see the text.

 C:\temp\gradle2>gradle  
 :help  
 ...  
 BUILD SUCCESSFUL  
 Total time: 1.932 secs  

The method doLast() (and its counterpart doFirst()) simply manipulate the stack of actions attached to a task.  In order to make task definitions a little simpler, Gradle introduces a little syntactic sugar for doLast():

 task newTask << {  
      print "You will only see this in the execution phase"  
 }  

Notice, this is not the same thing as this:

 task newTask  {  
      print "You will see this in the configuration phase"  
 }  

So remember these little rules:
  • Task configuration runs every build (during the configuration phase)
  • Task actions run only when a task is actually run (during the execution phase)
  • Code in the main body of a task declaration is configuration code
  • Add actions to a task using doFirst(), doLast() and the << shortcut
One concluding note: beginning with version 1.4, the Gradle team has begun experimenting with "configuration on demand".  This feature is just like it sounds; Gradle will try to determine which tasks will actually be executed, and only configure those tasks.  This is to mitigate an excessively long configuration phase for a small number of actual executed tasks.