Multiple invocations of "allprojects {}"

Hello,
I just encountered a problem at work, and I would like to get some insight what could be the reason.
The project itself is already very complicated, so unfortunately I cannot produce any example code showing the problem.

It is a multiproject, and it applies several other “big” plugins, for example the Spring dependency management plugin, and the JFrog artifactory plugin, as well as a bunch of other proprietary plugins, which themselves have transitive plugin dependencies.

The specific problem that occurred was that the artifactoryUpload task of the JFrog plugin failed to actually upload anything after a cleanup was made that combined two allprojects invocations into a single one.
Originally (the working case) the first allprojects Closure would only apply all the plugins, and the second invocation would then configure (mostly) the Artifactory plugin and and the dependency management plugin.

Questions:

  1. Is it expected to make any difference if you use one or multiple invocations of allprojects {}?
    My understanding is that it should not, regardless of project complexity.

  2. Does it generally make a difference in which order plugins are applied?
    My understanding is that this would depend on what the plugins themselves are doing, e.g. first plugin adds a source set, second plugin configures all source sets, in which case the order would matter…

  3. In the docs it states that plugin application was idempotent. Is this always true, regardless of
    other plugins being applied in between, and the order in which this happens?

Thanks for shedding some light on this.
Thomas

This depends on how the plugin is written, it’s certainly possible to write plugins where order is important. Usually, Gradle provides an API such that you can code your plugin to work the same regardless of execution order.

Let’s consider project.tasks which is an instance of TaskCollection, as with most of the collections in the Gradle model this extends DomainObjectCollection which has “live” methods including matching(Closure) and all(Closure). These will “Executes the given closure against all objects in this collection, and any objects subsequently added to this collection”

But, TaskCollection also extends Collection which has non-live methods such as findAll(Closure) and each(Closure). These Closures will only execute against the current objects in the collection and won’t apply to objects subsequently added to the collection.

The first code block below is written in a style where order is not important, it will print

“hello task1”
“hello task2”

task task1 {...}

tasks.matching { 
   it.name.startsWith('task') 
}.all { 
   println "hello $it.name" 
}

task task2 {...}

The second code block below is written in a style where order is important, it will print

“hello task1”

task task1 {...}

tasks.findAll { 
   it.name.startsWith('task') 
}.each { 
   println "hello $it.name" 
}

task task2 {...}

Plugin authors should always aim to use the “live” methods as shown in the first code block

Yes, plugin application is always idempotent, but that doesn’t say anything about order. It simply means that:

apply plugin: 'java'

is identical to:

apply plugin: 'java'
apply plugin: 'java'
apply plugin: 'java'
apply plugin: 'java'

The plugin will only be applied once, and therefore it’s not possible to apply another plugin in between.

Thanks for your replies! This is already helpful. Could you maybe say a word about how the specific problem regarding allprojects can occur?
I tried reading the Gradle source code starting from the Project#allprojects method, but I did not get very far…
Could you think of a way to reproduce this problem?
What kind of code would I need to have inside a plugin, to make a difference if it gets applied and configured in the same allprojects invocation together with other plugins being applied, or in a different one, that is following right after the first one, with nothing in between?
(I strongly suspect a problem inside the JFrog plugin, as that one is quite a behemoth, and it was also the only thing that apparently didn’t work.)

Basically, the (git) diff from reverting the breaking cleanup looked like this:

// all plugins were applied at this point
+ }
+ allprojects {
// configuration of plugins comes next

Thanks again for your insights!

Let’s consider these two snippets

Snippet1

allprojects {
   task task1 {
      doLast { println "$project.name:task1" }
   }
}
allprojects {
   task task2 {
      doLast { println "$project.name:task2" }        
   }
   allprojects*.tasks.findAll {it.name == 'task1'}.each {
      task2.dependsOn it
   } 
}

Snippet2

allprojects {
   task task1 {
      doLast { println "$project.name:task1" }
   }
   task task2 {
      doLast { println "$project.name:task2" }        
   }
   allprojects*.tasks.findAll {it.name == 'task1'}.each {
      task2.dependsOn it
   } 
}

Since this is using findAll and each it will act differently. Snippet1 will have the desired behaviour, Snippet2 will not work as expected (for a multi-module build)

To fix this, you’d use the “live” methods matching and all. Then you could get the desired behaviour with a single allprojects closure. Eg:

allprojects {
   task task1 {
      doLast { println "$project.name:task1" }
   }
   task task2 {
      doLast { println "$project.name:task2" }        
   }
   allprojects*.tasks.matching {it.name == 'task1'}.all {
      task2.dependsOn it
   } 
}
1 Like