On passing a closure to a gradle extension, why the owner of closure is not the main Projects object?

934 views Asked by At

I was looking at the closure scopes and found the output counter intuitive, this code was in build.gradle file

plugins {
    id 'java'
}

sourceSets {

    println 'source sets closure scopes this,owner, delegate'
    println this.toString() // output: root project 'multigradle'
    println owner.toString() // output: DynamicObject for SourceSet container
    println delegate.toString() // output: SourceSet container
    
}

Why the owner is not equal to this, does gradle clone the closure?

PS : For anyone who will read it in future 'multigradle' is my gradle project name.

1

There are 1 answers

1
Matias Bjarland On BEST ANSWER

TL;DR;

Essentially within the gradle source there is a method something like this:

 public Object sourceSets(Closure closure) {
   // do stuff like configuring the closure
   closure.call()
 }

so when you call:

sourceSets { 
  some code
}

(which incidentally is the same as calling sourceSets({ some code }), just with the parens removed which is ok in groovy)

"some code" is not executed immediately when the sourceSets method is called. Gradle can choose to execute it whenever they decide it's time. Specifically, they can (and do) configure things like owner and delegate before actually executing the closure.

Longer version

Turns out the sourceSets method in your build.gradle file is actually added by plugins such as the java/kotlin/groovy plugins.

As an example we can look at the java plugin and the DefaultJavaPluginConvention class which has the following code:

    private final SourceSetContainer sourceSets;

    @Override
    public Object sourceSets(Closure closure) {
        return sourceSets.configure(closure);
    }

this is the method that gets called when you type sourceSets { ... } in your build.gradle file. It gets handed the closure and proceeds to hand it off to the configure method of the source set container. Note that we have not executed the closure yet, we are just passing it around as a non-executed block of code.

If we dig a little, we find the configure method in the AbstractNamedDomainObjectContainer class:

    public AbstractNamedDomainObjectContainer<T> configure(Closure configureClosure) {
        ConfigureDelegate delegate = createConfigureDelegate(configureClosure);
        ConfigureUtil.configureSelf(configureClosure, this, delegate);
        return this;
    }

(SourceSetContainer is an interface and the implementing class inherits from AbstractNamedDomainObjectContainer...this is the right configure method)

Where the ConfigureUtil has the following code:

    public static <T> T configureSelf(@Nullable Closure configureClosure, T target, ConfigureDelegate closureDelegate) {
        if (configureClosure == null) {
            return target;
        }

        configureTarget(configureClosure, target, closureDelegate);
        return target;
    }

    private static <T> void configureTarget(Closure configureClosure, T target, ConfigureDelegate closureDelegate) {
        if (!(configureClosure instanceof GeneratedClosure)) {
            new ClosureBackedAction<T>(configureClosure, Closure.DELEGATE_FIRST, false).execute(target);
            return;
        }

        // Hackery to make closure execution faster, by short-circuiting the expensive property and method lookup on Closure
        Closure withNewOwner = configureClosure.rehydrate(target, closureDelegate, configureClosure.getThisObject());
        new ClosureBackedAction<T>(withNewOwner, Closure.OWNER_ONLY, false).execute(target);
    }

where the relevant part is the call to the groovy Closure rehydrate method which according to docs does the following:

Returns a copy of this closure for which the delegate, owner and thisObject are replaced with the supplied parameters. Use this when you want to rehydrate a closure which has been made serializable thanks to the dehydrate() method.

Only on the last line of the configureTarget method does gradle call execute on the action created to represent the closure. So the execution of the closure is done after the owner, the delegate and the this pointer have been configured according to gradle needs.