Gradle cross-project configuration side effects

I was always using gradle allprojects and suprojects sections as a very handy way for multi-module projects configuration.

But, it appears, that using these shortcuts is officially a bad practice.

Quote from gradle forum thread:

  1. Consider switching from Groovy DSL to Kotlin DSL. This is by now the recommended default DSL, it immediately gives you type-safe build scripts, it gives you actually helpful error messages if you mess up the syntax, and you get amazingly better IDE support when using a proper IDE like IntelliJ IDEA.

  2. Do not use allprojects { ... }, subprojects { ... }, project(...) { ... } or similar. Those are bad practice and immediately introduce project coupling by doing cross-project configuration. This disturbs more sophisticated Gradle features and even prevents some upcoming features to work properly. Instead you should use convention plugins, for example in buildSrc or and included build, for example implemented as precompiled script plugins. Those you can then targetedly apply to the projects where their effect should be present, so that you for example only apply Java conventions to projects that are actually Java projects.

Disclaimer: I put this on top to point to official incorectness of such way of configuration. Still, I consider it as the most understandable and easy to use.

Below you'll find one of hard to debug problems appeared after configuring common staff in the root project with groovy DSL (which, eventually, lead me to the above insight).

The problem

My problem appeared with pom configuration (using my pom plugin) inside multi-module project: as all modules share the same core pom information (developer, license, scm etc.), it would be logical to apply it in allprojects section:

plugins {
    id 'java-platform'
    id 'ru.vyarus.pom'
}

allprojects {
    maven.pom {
          developers {
              developer {
                  id = 'johnd'
                  name = 'John Doe'
                  email = 'johnd@somemail.com'
              }
         }
    }
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'ru.vyarus.pom'
}

Root project is a BOM, but I omit related configurations to clearly show the problem.

This should put developer section in all poms, including root BOM (allprojects). But java plugin must be applied only for sub-modules because root project does not have sources. Looks logical.

But actual behaviour would be: BOM (root pom) would contain many duplicated developer tags and sub-modules would not contain any!

What happend: When allprojects is applied for sub-module, ru.vyarus.pom plugin is not applied yet and gradle goes to the root project, trying to find referenced extension (maven), and successfully finds it (so root pom being configured multiple times with the same chunk).

If we try to move subprojects section above the allprojects - it would work as planned. But it might be much harder to spot in the real build file.

Note that simply moving pom plugin activation (apply plugin: 'ru.vyarus.pom') into allprojects section would not work, becuase plugin is activated only after java (or java-platform) plugin activation.

Debugging

Mentioned gradle forum topic provides a nice way to debug such things:

println("ROOT: ${System.identityHashCode(maven)}")

allprojects {
    println("$name: ${System.identityHashCode(maven)}")
    maven.pom {
        ...
    }
}

Here we print identity of the maven extension object, which would be equal if root project extension is used in sub-modules.

I didn't use it in my case (becuase pom plugin supports debug mode, showing all xml modifications), but need to mention it as a very nice alternative.

Solution

Suppose we can't move subprojects, then the simplest solution would be in delaying this configuration (afterEvaluate usage was highly not recommended in thread):

allprojects {
    afterEvaluate {
        maven.pom {
            ...
        }
    }
}

But, it might not be enoght if extension values are used in afterEvaluate in plugin, which is a common practice (and so your afterEvaluate block would execute after extension values were used, and would be ignored).

The best way would be to wait for required (or related) plugin activation:

allprojects {
    plugins.withId('java') {
        maven.pom {
            ...
        }
    }
}

(or pluginManager.withPlugin("java-base") as suggested in thread, but this version is shorter).

Side note: when groovy closure was used in pom plugin for xml configuration, it was possible to resolve closure declaration project and detect such mis-use automatically. But, unfortunately, it is impossible with pure Action or direct model object access.