Multi-Module Project: How/Where to apply plugins?

Hello.
I am not fully sure whether this is the right forum to ask. Depends on whether I am doing things right, then this forum might be wrong, otherwise (in case I am doing something wrong) this will be the right forum. :slight_smile:

I have multi-module projects like this:

rootproject
- lib-project
  build.gradle
- webapp-project
  build.gradle
settings.gradle

As I understand it is not required (or even not good practice) to have build.gradle in root project?

Now; When plugin is needed in both subprojects is it ok then to apply plugins to the subprojects directly or should it be declared in the root project first (maybe with apply:false).

Actual case: artifactory plugin.
Omitting build.gradle in root project I added to subprojects

./webapp/build.gradle
plugins {
    id "war"
    id "com.jfrog.artifactory" version "5.1.10"
}

./lib/build.gradle
plugins {
    id "java"
    id "com.jfrog.artifactory" version "5.1.10"
}

With this, build is working.

But as soon as I add another plugin (let’s say id "com.liferay.source.formatter" version "5.2.58") to only one of the subprojects the build (refresh, etc…) processes fail with

An exception occurred applying plugin request [id: 'com.jfrog.artifactory', version: '5.1.10']
> Failed to apply plugin class 'org.jfrog.gradle.plugin.artifactory.ArtifactoryPlugin'.
   > Cannot add extension with name 'artifactory', as there is an extension already registered with that name.

Adding id "com.liferay.source.formatter" version "5.2.58" to both subprojects lets the build succeed.

Adding build.gradle to root project with

plugins {
    id "com.jfrog.artifactory" version "5.1.10" apply false
}

and eventually removing version from subprojects also seems to solve the problem.

What is the quintessence of that? That build.gradle in root is basically required in case plugins are needed on all subprojects? Or is this an artifactory plugin issue?
As recommendations say to better use buildSrc/convention plugins rather than subprojects {...} or allprojects {...}in root build.gradle what are the usual practices here?

Background of this is, that I have a published convention-plugin project which also predefines artifactory stuff. In the projects I have e.g. lib-conventions.gradle and web-conventions.gradle both based on a common-conventions.gradle file.
The latter provides the jfrog.artifactory stuff. With these plugins I basically run into the same problems concerning artifactory because for sure I virtually want to apply lib-conventions to lib-project and web-conventions to web-project for example (and via common-conventions both of them apply artifactory plugin). And using my plugins this way runs into the same error when applying e.g. id "com.liferay.source.formatter" version "5.2.58" to one of the subprojects.
This was the initial reason why I tested it with the small reproducer above without convention plugins.

One could argue and say, adding it to subprojects leads to multiple locations where version is set. But I could then maybe set a variable in gradle.properties or even better using a libs.versions.toml file then I could simply alias(libs.plugins.jfrog.arti) on the subprojects that need it (this is basically what I am doing with my plugins and the included version-catalog) - but this unsurprisingly issues the same error.

Besides… also tested usage of id "com.jfrog.artifactory" version "5.1.10" together with one or two other random plugins to make sure that this is not caused by id "com.liferay.source.formatter" version "5.2.58". Always results in the error above.

Thx!

As I understand it is not required to have build.gradle in root project?

That’s correct, a project can exist with an absent build script, it will then just have the defaults, or things injected into the project configuration from outside (which then would be bad practice :slight_smile: )

or even not good practice

Not at all.
It would be extremely bad practice if you have one and in side use subprojects { ... } or allproject { ... } or project(...) { ... } or any other means of doing cross-project configuration.
But just having a root project build script is fine.
And also doing things in there can be fine, like doing aggregate reports from the other projects or similar.
It is also find to have the root project as normal project and not have subprojects at all.
All depends on the project at hand the current needs.

When plugin is needed in both subprojects is it ok then to apply plugins to the subprojects directly

For most plugins this is perfectly fine.
Though I would recommend using a version catalog to centralize version declarations.
And if you need more logic on multiple projects, you should consider to use convention plugins, for example implemented as precompiled script plugins in buildSrc or and included build.

or should it be declared in the root project first (maybe with apply:false).

This sometimes might be necessary to overcome class loader problems.
For example if you do like said above, the plugin ends up in two different class loaders and so the classes are effectively not the same.
This can sometimes make problems with some plugins or usages.
In those cases, it might be necessary to drag up the plugin to a common class loader, for example by declaring it with apply false in the root build script or by declaring it as runtimeOnly dependency in buildSrc.
But I would not do that proactively, but only when really necessary, otherwise you only add unnecessary clutter.

only one of the subprojects the build (refresh, etc…) processes fail with

In your case, the problem is indeed the Artifactory plugin, that is greatly misbehaving.
If you only apply it to a subproject, it applies itself also to the root project unasked which is very bad practice for a plugin.
So in your case applying the plugin to :lib auto-applies it to the root project.
Then (not meaning order here, order is not guaranteed) applying to :webapp also auto-applies it to the root project.
As long as the class paths of both build scripts are identical, because you apply the same plugins (i.e. before you add the new plugin or after you applied it to both project) the class loader is reused for both projects and so the classes of the Artifactory plugin are the same (unlike to what I said above, this is a special case).
So you apply the same plugin class a second time to the root project and that is fine with Gradle, as the second time will just be a no-op.

As soon as you make the class paths of the two build scripts different by only apply a plugin to one of them, you get different class loaders for their classpaths. That means the second application to the root project applies an effectively different plugin class, so it is indeed applied again.
It then searches for its own extensions by type and tries to register it, as it didn’t find it, as the classes are different ones and then fails as an extension with that name already exists.

By applying the new plugin to both build scripts you align the classpaths and thus the class loader is reused again and the double-application to the root project is prevented again.

By declaring the plugin in the root project build script with apply false you do not apply it, but add it to the classpath of the root project build script. The class loader for the root project is a parent class loader of the class loaders for the subprojects. And as in Java world a class loader has to ask its parent calss loader for an asked class before serving it itself if possible, this alos mitigates the problem, as then both projects use the Artifactory classses from the root project class loader and there is not discrepancy.

So yes, the Artifactory plugin is one of those cases, where you should indeed declare it on the root project.

But even more, as the Aritfactory plugin evilly applies itself to the root project anyway, you can right away omit the apply false and just apply it explicitly.

Or is this an artifactory plugin issue?

Exactly. :slight_smile:

Boah. Thx alot for taking the time to give such a thorough answer.

1 Like

Some other issue arises here, or is this only me?
But using convention plugins and version-catalogs with multi-module projects give me some hard times currently.

Publishing an artifact in a multi-module basically works but is showing some log like

> Task :extractModuleInfo
No publisher config found for project: myproject-root

And I also found out that there is no build info in artifactory.

Without plugins I applied artifactory to root build.gradle basically with

plugins {
    id 'com.jfrog.artifactory' version '5.1.10' apply true
}
artifactoryPublish.skip = true

artifactory { ... }

So to enable this i would either have to repeat artifactory/maven-publish stuff in root-project.
But somehow I think this would require to create some plugins with tight rules…
like

  • if you want to have artifactory build info published you will have to apply this plugin to root
  • for web-project additionally apply web-plugin to actual subprojects…

For now I wanted to create a small additional root-conventions.gradle which should enable root projects for artifactory build info publish.

But I guess because of

I can not simply do

./myproject-root/build.gradle
plugins {
    alias(libs.plugins.root.conventions) apply true
}

This gives me

Error resolving plugin [..]
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

What I did now is basically repeating all the plugins which are used in subprojects of the current root project with apply false

./myproject-root/build.gradle
plugins {
    alias(libs.plugins.root.conventions) apply true
    alias(libs.plugins.web.conventions) apply false
    alias(libs.plugins.lib.conventions) apply false
}

with root-conventions

plugins {
    id 'maven-publish'
    id 'com.jfrog.artifactory'
}

artifactoryPublish.skip = true
artifactory {
    contextUrl = artifactory_url
    publish {
        repository {
        ...
        }
        defaults {
            publications('mavenJava')
            publishPom = true
        }
    }
}

Now this publish log message concerning publisher config is gone and also build info is written to artifactory.

But not fully satisfied. Feels like a workaround. Definitely need to find a better solution. But I am afraid that the whole artifactory thingy might lead to some restrictions and require some workarounds do deal with multi-module projects.

I cannot tell you, I never used Artifactory, and from what I read about constant problems with it, would probably try to avoid it wherever possible. :slight_smile:

Where the “because the plugin is already on the classpath with an unknown version” I can hardly guess without seeing the actual build. But probably you have your convention plugins in buildSrc and then have some dependency defined in buildSrc that you also depend on in the build script and that makes problems, as buildSrc code and dependencies are just put to a higher class loader and overwrites anything else without conflict resolution and without having the version information which can easily lead to that message. Using an included build instead could maybe resolve that problem.

Thank you. I will look into this included builds, and maybe place some more questions in the artifactory plugin github.

Avoiding artifactory is unfortunately no option. At least it is not my decision. :slight_smile:

It seems that this is related to libs and alias '...' usage. Using plugins and id '...' does not give that “plugin already on classpath” error. See paragraph further down.

It was a buildSrc initially but I moved it to a separate gradle project to which I also introduced a libs.versions.toml file. So customplugins-project uses libraries/plugins from toml and also publishes it for other projects. If things work out the custom plugins should be extended with other stuff, enhanced version-catalog, additional plugins etc; basically looks like this now:

./build.gradle
  id 'groovy-gradle-plugin'
  id 'java-gradle-plugin'
  id 'maven-publish'
  id 'version-catalog'
  alias(libs.plugins.jfrog.artifactory)
  alias(libs.plugins.catalog.update)
  alias(libs.plugins.manes.versions)


  version = libs.versions.pluginAndCatalogVersion.get()

  dependencies {
    implementation libs.bundles.pluginlibs 
  }

  publishing {
    // version-catalog publishing here
  }

  artifactory {
    context_url = ...
    publish { { repository { ... } }
    defaults { publications('ALL_PUBLICATIONS')  //to also get marker artifacts
  }

  //for publishing catalog file
  catalog {
      versionCatalog { from(files('/gradle/libs.versions.toml'))}
 }

 versionCatalogUpdate { ... } //handling of catalog.update plugin


 ./settings.gradle
  dependencyResolutionManagement {
    repositories {
        gradlePluginPortal()
      }
  }
  
  rootProject.name="customPlugins"

./gradle/libs.versions.toml
  [versions]
  pluginAndCatalogVersion = "1.0" //this is also build/publish version of project
   ...
  [libs]
  bxmsvn = { ... }
  artifactory = { ... }
  lombok = { ... }
   ...
  [bundles]
   pluginlibs = [ bxmsvn, artifactory, lombok ]  // omitted quotes here
   ...
  [plugins]
   root-plugin = { id = "com....root-conventions", version.ref = "pluginAndCatalogVersion"}
   web-plugin = { id = "com....web-conventions", version.ref = "pluginAndCatalogVersion"}
   lib-plugin = { id = "com....lib-conventions", version.ref = "pluginAndCatalogVersion"}
   ...

  ./src/main/groovy/com.mycomp.common-conventions.gradle //common stuff for web and libs
  plugins {
      id 'maven-publish'
      id 'at.bxm.svntools'
      id 'io.freefair.lombok'
      id 'com.jfrog.artifactory'
  }
  
  lombok {
      version = libs.versions.lombok
  }
  
  tasks.withType(JavaCompile).configureEach {
      options.release = 8
  }
  
  tasks.withType(Jar).configureEach {
      manifest {
          attributes(
            //writing some attributes here
          )
      }
  }
  
  test {
      useJUnitPlatform()
  }
  
  artifactory {
      contextUrl = artifactory_url
      publish {
        ... //stuff to publish actual artifacts of type .war and .jar to artifactory
      }
  }

./src/main/groovy/com.mycomp.web-conventions.gradle
  plugins {
    id 'war'
    id 'com.mycomp.common-conventions'
  }
  
  publishing {
      publications {
          mavenJava(MavenPublication) {
              from components.web
          }
      }
  } 

./src/main/groovy/com.mycomp.lib-conventions.gradle
  plugins {
      id 'java'
      id 'com.mycomp.common-conventions'
  }
  
  publishing {
      publications {
          mavenJava(MavenPublication) {
              from components.java
          }
      }
  } 

//this one now I have added afterwards to enable root project build info publish.
./src/main/groovy/com.mycomp.root-conventions.gradle
  plugins {
      id 'maven-publish'
      id 'com.jfrog.artifactory'
  }
  
  artifactoryPublish.skip = true
  
  artifactory {
      contextUrl = artifactory_url
      //simplified publishing here as only build info should be deployed
  }

Publishing works and marker artifacts, plugins.jar and version-catalog is in repository with version “1.0” and can be used from other projects as well.

Target projects are multi-module projects using the plugins/version-catalog. Withn myproject-root this (after adding root-conventions)

./build.gradle
    plugins {
        alias(libs.plugins.root.conventions) apply true
        alias(libs.plugins.web.conventions) apply false  //this now because of "plugin already on classpath error" if only root.conventions are applied
        alias(libs.plugins.lib.conventions) apply false  //same here
    }


./settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
        //other repos here
    }
}
dependencyResolutionManagement {
    repositories {
      mavenCentral()
      //artifactory and other repos here
    }
    versionCatalogs {
         //me thought of being smart and instead of defining plugins in pluginManagement use libs also for custom plugins :)
        libs { 
            from("com.mycomp.conventions:version-catalog:latest.release")
        }
    }
}
rootProject.name = 'myproject-root'
include 'myproject-lib'             
include 'myproject-web'


./myproject-web/build.gradle
   plugins {
    alias(libs.plugins.web.conventions)
  } 

  dependencies {
    compileOnly group: ...  //TODO: use version-catalog libs and bundles    
    implementation project (':myproject-lib') 
 } 


./myproject-lib/build.gradle
   plugins {
    alias(libs.plugins.lib.conventions)
  }
  
 dependencies {
   //same here with dependencies;
 }                

Using only lib-plugin and web-plugin on subproject was working.
Enabling root build info publish requires additional artifactory/maven-publish within root-project build.gradle. I decided to move this to root-conventions.gradle. But introducing this in the root build.gradle resulted in the “already in classpath” error. My solution right now is adding all the other custom plugins that are used in the subprojects with apply false.

Additionally, what also seems to work is adding

    plugins {
        id 'com.mycomp.root-conventions' version '1.0'
        id 'com.mycomp.web-conventions' version '1.0'
        id 'com.mycomp.lib-conventions' version '1.0'
    }

to pluginManagement inmyproject-root/settings.gradle

Then every subproject would use id '...' (instead of alias '...') and root project is enough to set id 'com.mycomp.root-conventions'.
But then I need to make sure that the versions of the plugins and the version-catalog in dependencyResolutionManagement match and maybe set a variable in gradle.properties.

My initial idea here was to have specific plugins for specific types of project; given the fact that I have some projects structured as multi-module projects but there are other projects as well which basically are war-archives with dependencies to custom jars but those projects are still structured in single modules.
But now I find myself asking whether it makes more sense to create some master-plugin that should be used on root projects and configures everything in subprojects based on “type of project”?
So… set master-plugin on root project and subprojects only declare plugin id 'war' or id 'java'.

Wonder if this would be a feasible way. Is there something like task.withType on projects to recognize whether a project is of type war or java?
But then again this would be bit of a mess because I still would like to have the specific plugins for single modules as well.

It seems that this is related to libs and alias '...' usage. Using plugins and id '...' does not give that “plugin already on classpath” error.

Of course, when using alias you use the plugin including version from the version catalog.
When you use id without version, it is … well … without version.
You must not use a plugin with two different versions within the same scope, so if you add a plugin with version multiple times, it has to be with the same version.
If the version cannot be determined e.g. because it comes through buildSrc, then the check fails as Gradle does not know the version it has to compare.
When applying a plugin without version, it just applies the version that is already on the classpath and fails if no version is present already.

Then every subproject would use id '...' (instead of alias '...' ) and root project is enough to set id 'com.mycomp.root-conventions' .

As the convention plugins are all in the same project, you can also use alias for the root project plugin, which adds the jar to the classpath and thus also for subprojects, and then use id in the subprojects. No need to use pluginManagement { plugins { ... } } to define a version. I wonder why you get the “already on classpath” error actually, but that would need a deeper look into it. From a cursory look I’d expect that not to happen.

My initial idea here was to have specific plugins for specific types of project

Yes, that’s a very good idea and exactly how convention plugins are meant to be used.

But now I find myself asking whether it makes more sense to create some master-plugin that should be used on root projects and configures everything in subprojects based on “type of project”?

Definitely not.
That is exactly contradictory to the main purpose of convention plugins.

Wonder if this would be a feasible way. Is there something like task.withType on projects to recognize whether a project is of type war or java ?

pluginManager.withPlugin("war") { ... }
This is the way to react to plugins being applied and then configuring them.
But again, don’t do this as cross-project configuration, that is discouraged bad practice.

1 Like

Once again thank you for sharing your knowledge.

I definitely do not want to create a “master-plugin” because of said reasons. Feels bit like asking myself “why did you switch to conventions and not stick with allprojects { ... } and subproject { ... } in build.gradle?”.
I will stick with the current solution and apply the plugins to the root project with apply false.
As soon as I have time I will try to dig deeper. Maybe stacktrace tells more.

I wonder why you get the “already on classpath” error actually, but that would need a deeper look into it.

Same here. From my pov I am using the same version an all plugins and as currently only this version (1.5) is available in mavenLocal() I cannot think of another version.
This was the reason why I wanted to make the custom plugins available via version-catalog. So from my understanding there should only be one version of the plugins available.

It doesn’t say the plugin is there in different versions.
It says it is there with version X and additionally with an unknown version, so it cannot compare whether both are version X.

Yes. That’s right. Definitely need to have a deeper look what is causing this. Thx.

1 Like

For a quick test I tried to reproduce this with two simple projects.
One basically empty plugin project only with build.gradle

plugins {
    id 'groovy-gradle-plugin'
    id 'java-gradle-plugin'
    id 'maven-publish'
    id 'version-catalog'
}

group = 'my.test.conventions'
version = libs.versions.testConventionsVersion.get()

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(8)
    }
}

publishing {
    publications {
        versionCatalogTest(MavenPublication) {
            artifactId = 'version-catalog-test'
            from components.versionCatalog
        }
    }
}

catalog {
    versionCatalog {
        from(files('/gradle/libs.versions.toml'))
    }
}

and two empty plugin files in ./src/main/groovy.

The testproject also is empty and does not apply any plugins except the two convention plugins:
the web.conventions on the subproject and the root.conventions on the root project.

And I get the exactly same error

Error resolving plugin [id: 'my.test.conventions.web-conventions', version: '1.0']
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

So this is not working

./build.gradle
plugins {
    alias(libs.plugins.test.root.conventions)
}

./testweb/build.gradle
plugins {
    alias(libs.plugins.test.web.conventions)
}

but this is

./build.gradle
plugins {
    alias(libs.plugins.test.root.conventions)
    alias(libs.plugins.test.web.conventions) apply false
}

./testweb/build.gradle
plugins {
    alias(libs.plugins.test.web.conventions)
}
> Task :buildEnvironment

------------------------------------------------------------
Root project 'test-run'
------------------------------------------------------------

classpath
+--- my.test.conventions.root-conventions:my.test.conventions.root-conventions.gradle.plugin:1.0
|    \--- my.test.conventions:testConventions:1.0
\--- my.test.conventions.web-conventions:my.test.conventions.web-conventions.gradle.plugin:1.0
     \--- my.test.conventions:testConventions:1.0

(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 1s

This is not working either

./build.gradle
  plugins {
    id 'my.test.conventions.root-conventions' version libs.versions.testConventionsVersion
  }
./testweb/build.gradle
    plugins {
        id 'my.test.conventions.web-conventions' version libs.versions.testConventionsVersion
    }

Funny thing: it delivers the same error. But why? Versions are there in both cases still the error says
...already on classpath with an unknown version...
From my understanding this would basically be the same as using alias, because with this the version is also there from the version catalog in both build.gradle files.

And the marker artifacts in repo should give the same testConventions.jar version 1.0 for both root and web, shouldn’t they?

But this works:

./build.gradle
  plugins {
    id 'my.test.conventions.root-conventions' version libs.versions.testConventionsVersion
    //or use alias notation here also works
  }
./testweb/build.gradle
    plugins {
        id 'my.test.conventions.web-conventions'
        //but unsurprisingly this will fail in cases, when nothing is set in root build.gradle
        //in this case either id + version is needed, or alias instead
    }

It’s probably because the web-conventions is on the class loader of the root project which is a parent of the class loader of the testweb project, but without having requested the web-conventions plugin by id in the root project classpath. So Gradle does not know in which version it is present. Theoretically it could also be that the marker artifact for root-conventions has version 1, the marker artifact for web-conventions has version 2 and both point to the artifact with the code in version 3. There is no hard rule that these versions have to be aligned, it just is usually the case. So as the web-conventions were not requested and included by id, but are just there in the class loader, Gradle cannot know the version that the marker artifact would have had and thus the error comes.

Your final version is exactly waht I meant, just that you could use alias in the root project.

Your final version is exactly waht I meant, just that you could use alias in the root project.

Yes. This version looks the most appealing. I have implemented it now like this.

Thank you.

kr
Rob

1 Like

Hello Rob,

You seems found a solution to jfrog publication plugin.

I have the same issue. I have a multimodule project. All sub projects use the plugin id ‘com.jfrog.artifactory’.

When i execute the main project, i receive this error (Like you)

Failed to apply plugin class ‘org.jfrog.gradle.plugin.artifactory.ArtifactoryPlugin’.
Cannot add extension with name ‘artifactory’, as there is an extension already registered with that name.

Can you please explain to me the solution that you found.

Thank you,

Best regards,

MMD

Did you actually read the thread?
I think I pretty well described what the problem is and how to mitigate it. :wink:

Hello. I would say @Vampire pretty much nailed it down in his answer from October 9 - Multi-Module Project: How/Where to apply plugins? - #2 by Vampire.

But there are a lot of topics covered in this thread not only related to artifactory plugin.

So in short for artifactory plugin to overcome classpath issues configure it in the root build.gradle

plugins {
    id "com.jfrog.artifactory" version "5.1.10" apply false
}

and then only use the id in subprojects

plugins {
    id "com.jfrog.artifactory"
}

Thank you Rob for your response.

It works well. Thank you for your help,