0

I'm trying to understand deeply how gradle is working.

Let's take the case of a basic dependencies {} declaration in a build.gradle script. dependencies is a method on Project object.

DSL Project object = org.gradle.api.Project interface:

void dependencies​(Closure configureClosure)
  Configures the dependencies for this project.
  This method executes the given closure against the DependencyHandler for this project. The 
  DependencyHandler is passed to the closure as the closure's delegate.

So: method receives as parameter a configuration closure and establish that closure's delegate object is DependencyHandler class, than execute the closure against it's delegate object.

Having this example:

dependencies {
//    configurationName dependencyNotation
    implementation 'commons-lang:commons-lang:2.6'
}

This is translated in a call of method add of DependencyHandler class:

  Dependency    add​(String configurationName, Object dependencyNotation)    Adds a dependency to the given configuration.

And now the question:

How exactly is done the translation of the line

 implementation 'commons-lang:commons-lang:2.6'

into a class method call (e.g. DependencyHandler.add()) and who is responsable ?

My impression is that there is a missing explanation in the documentation, something like: default method on this delegate object DependencyHandler is add(...), so each configClosure's line, if matching notation configurationName dependencyNotation, will be translate into delegate's object default method.

But this is just a possible interpretation.

Ideea is: I give a closure with multiple lines, this is executed agains a delegation object which happens to have methods add(), and magically for each line this method is called ... how is this happening, based on what mechanism ? Is the Project.dependencies() method doing this, is the delegate object itself, or some other groovy specific mechanisms, etc.

Thank you.

Dacian
  • 678
  • 6
  • 12

1 Answers1

2

If you want to get deeper insight on how Gradle (or its DSL) work under the hood, you can always check the actual source code. However, since this is not required to understand how to write build scripts, it is not included in the documentation.

Regarding your specific example, I have to admit that I do not exactly know how it is done, but I have a guess. If someone else has better insights, feel free to prove me wrong.

While Gradle indeed uses AST transformations to extend the regular Groovy syntax in some cases (e.g. the task definition syntax), I think they just rely on dynamic methods for dependency definitions.

Groovy is a dynamic language. This includes a method called methodMissing that may be defined by any class and will be called whenever a missing method is called on an object of that class:

class Example {
    def methodMissing(String name, args) {
        println name
    }
}

def example = new Example()
example.method1()

You can find a more detailed example in Mr. Hakis blog.

Since Groovy allows omitting parentheses for method calls with arguments, your example implementation 'commons-lang:commons-lang:2.6' is basically nothing else but calling the method implementation with the dependency notation string as its argument.

Now Gradle could catch these calls via methodMissing and then call DependencyHandler.add() if the configuration actually exists. This allows you to dynamically add configurations in your build script:

configurations {
    myConfig
}

dependencies {
    myConfig 'commons-lang:commons-lang:2.6'
}
Lukas Körfer
  • 13,515
  • 7
  • 46
  • 62
  • 1
    You are right: [DefaultDependencyHandler.groovy](https://github.com/kevinyang0906/gradle/blob/master/subprojects/core/src/main/groovy/org/gradle/api/internal/artifacts/dsl/dependencies/DefaultDependencyHandler.groovy) Thanks – Dacian Jun 29 '21 at 11:40