6

I'm having a hard time understanding how Gradle's Groovy DSL works.

Unfortunately Gradle is the main use-case for Groovy that I come across in my day to day work, and I've noticed that for many devs, their exposure to Groovy is strictly through Gradle. And that a majority of Gradle users have a very limited grasp of Groovy as a consequence.

In my limited understanding of Groovy, the following sintax tokenA tokenB { tokenC } where all tokens are not language keywords, tokenA would be a method that we are calling with arguments tokenB and the final argument is a closure. I would like to think I'm correct, but I know I'm wrong because there probably needs to be a comma after tokenB for that analysis to be correct.

I am by no means, as you can already tell, a Groovy dev, and I think using Gradle without learning the basics of Groovy is a bad thing to do, because it limits me from fully exploiting its capabilities. But my only viable option is to learn though examples without learning the theory unfortunately.

I did check out some similar questions like this one but no answers where clear or complete enough for me.

TL;DR

  1. How are the tokens task myTask { doLast {} } interpreted in Groovy?
  2. Does Gradle use a standard Groovy interpreter?
  3. How is myTask interpreted as an identifier when there is task and not def or a type behind it?
  4. If later in the file I added myTask { dependsOn myOtherTask } how does that get interpreted?
Anthony
  • 644
  • 7
  • 23

2 Answers2

11

I believe its all groovy and nothing special to gradle. Here's the groovy concepts you need to know.

  1. If the last argument of a method is a closure, you can put the closure after the closing bracket for the method arguments.
class MyClass {
   void doStuff(String name, Closure c) {
      c.call()
   } 
} 

def o = new MyClass() 
o.doStuff('x') {
   println "hello" 
} 
  1. You can implement method missing on your object. If someone tries to call a method that doesn't exist you can do stuff
class MyClass {
    def methodMissing(String name, args) {
        println "You invoked ${name}(${args})" 
    }
} 
def o = new MyClass() {
   o.thisMethodDoesNotExist('foo')
}
  1. You can set the delegate on a closure
class MyBean {
   void include(String pattern) {...} 
   void exclude(String pattern) {...} 
} 
class MyClass {
   private MyBean myBean = new MyBean() 
   void doStuff(Closure c) {
      c.setDelegate(myBean)
      c.call()
   } 
} 

def o = new MyClass() 
o.doStuff {
   include 'foo' 
   exclude 'bar' 
} 

These 3 groovy features pretty much explain the "magic" behaviour going on in a gradle script that have java developers scratching their heads.

So, let's break down your snippet

task myTask(type:Foo) { 
   doLast {...} 
}

Let's add some brackets and also add the implicit project references. Let's also extract the closure into a variable

Closure c = { 
   doLast {...} 
}
project.task(project.myTask([type: Foo.class], c)) 

The project.myTask(...) method doesn't exist and the behavior is ultimately implemented via methodMissing functionality. Gradle will set the delegate on the closure to the task instance. So any methods in the closure will delegate to the newly created task.

Ultimately, here's what's logically called

Action<? extends Task> action = { task ->
   task.doLast {...} 
}
project.tasks.create('myTask', Foo.class, action)

See TaskContainer.create(String, Class, Action)

lance-java
  • 25,497
  • 4
  • 59
  • 101
  • Thank you for the very useful answer. I wasn't aware of delegates, and that can explain a lot of the magic. I still have one question though. What is the word `task` considered in `task myTask { ... }`, is it a type like `int` or `MyClass`, is it a method that is looked up in a delegate? How is it that we can name the task without quotes? How can we have such a syntax `task `? Is such a construct standard Groovy? I can't figure out how to come up with such a DSL using only closures and delegates. Maybe the missing link is in the question I link to in my question? – Anthony Jul 09 '19 at 10:23
  • 1
    Please re-read my breakdown. Its a method call on the project object. `project.task(project.myTask({ ... }))` – lance-java Jul 09 '19 at 12:55
2

(disclaimer, I am not a groovy developer)

When running a build (eg. gradle clean), the contents of the build.gradle are evaluated against a Project object (created by the Gradle runner); see the Javadoc at API-Gradle Project; also read the entire summary as it contains a lot information. In that page they clarify that:

A project has 5 method 'scopes', which it searches for methods: The Project object itself ... The build file ... extensions added to the project by the plugins ... The tasks of the project .. a method is added for each task, using the name of the task as the method name ...

task myTask { } should be equivalent to project.task('myTask'). It creates a new task called "myTask" and adds the task to the current project (see the Javadoc). Then, a property is added to the project object, so that it can be accessed as project.myTask. doLast {..} invokes the doLast method on that task object; see the Javadoc at Task-doLast


So for some of your points:

  1. project.task('myTask').doLast(..) (maybe with more about closures in here)
  2. it does (try building from github); but there is additional processing; the build.gradle file is "injected" in a Project instance before running the build. Plus a lot of other steps
  3. project.task('myTask')
  4. project.myTask.dependsOn(project.myOtherTask) (probably with an additional closure, or Action instance involved) . This is due to tasks being added to the project as properties.

Also note that explicit statements, like project.myTask... are valid and they can be used in the build.gradle script; but the are verbose so rarely used.

Daniele
  • 2,672
  • 1
  • 14
  • 20