Multi-project build can't resolve custom plugin

Hi,

What I’m trying to do:

  • I have a custom Maven repository containing Plugins, for example a Sonarqube Plugin which I want to add in my Gradle project
  • The project should consist of multiple subprojects where I can share common build logic via buildSrc as well as share logic between subprojects

Thus I did setup the following project structure:

.
├── api
│   ├── build.gradle
├── buildSrc
│   ├── build.gradle
│   └── src
│       └── main
│           └── groovy
│               └── java-common-conventions.gradle
├── gradle
└── settings.gradle

The content of aforementioned files is as following:

settings.gradle

pluginManagement {
    repositories {
        maven {
            name = "plugin-releases"
            url = "https://my-custom-repo-url/artifactory/plugins-release"
        }
    }
}

rootProject.name = 'gradle-multiproject'

include("api")

buildSrc/build.gradle

plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}

buildSrc/src/main/groovy/java-common-conventions.gradle

plugins {
    id 'java-library'
    id 'org.sonarqube'
}

dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

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

tasks.named('test', Test) {
    useJUnitPlatform()
}

// Just a test to make sure the plugin is loaded correctly and can be configured
sonarqube {
   description = "Global sonarqube configuration"
   property 'sonar.sourceEncoding', 'UTF-8'
}

api/build.gradle

plugins {
    id 'java-common-conventions'
}

Error message:

An exception occurred applying plugin request [id: ‘java-common-conventions’]
Failed to apply plugin ‘java-common-conventions’.
org.gradle.api.plugins.UnknownPluginException: Plugin with id ‘org.sonarqube’ not found.

If I’m browsing the repository URL, I can find the plugin, but I think it couldn’t be resolved.
Former projects used buildscript and classpath as following:

buildscript {
    group = group
    version = version
    description = description
    apply from: "${repositoryBaseUrl}/main.gradle"

    dependencies {
        classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1'
    }

    project.repositories {
        mavenLocal()
        maven {
            name 'lib-snapshots'
            url 'https://my-custom-repo-url/artifactory/libs-snapshot'
        }
    }
}

I think using buildscript this way doesn’t work in newer versions of Gradle and Java and is even discouraged, otherwise I am not sure how to tell Gradle the exact plugin ID or even if that’s the real issue here.

The maven POM looks like this:

<?xml version="1.0" encoding="US-ASCII"?>
<metadata>
  <groupId>org.sonarqube</groupId>
  <artifactId>org.sonarqube.gradle.plugin</artifactId>
  <version>3.1</version>
  <versioning>
    <latest>3.1</latest>
    <release>3.1</release>
    <versions>
      <version>3.1</version>
    </versions>
    <lastUpdated>20250515071239</lastUpdated>
  </versioning>
</metadata>

I’m sure I’m confusing a few things, so any advice would be highly appreciated!

Hi again,

just as an update, I somehow got it working, my files now look like the following:

settings.gradle

pluginManagement {
    repositories {
        maven {
            name = "plugin-releases"
            url = "https://my-custom-repo-url/artifactory/plugins-release"
        }
    }
}

rootProject.name = 'gradle-multiproject'

include("api")

buildSrc/build.gradle

plugins {
    id 'groovy-gradle-plugin'
}

repositories {
        maven {
            name = "plugin-releases"
            url = "https://my-custom-repo-url/artifactory/plugins-release"
        }
    gradlePluginPortal()
}

dependencies {
    implementation("org.sonarqube:org.sonarqube.gradle.plugin:3.3")
}

buildSrc/src/main/groovy/java-common-conventions.gradle


plugins {
    id 'java-library'
    id 'org.sonarqube'
}

repositories {
        maven {
            name = "plugin-releases"
            url = "https://my-custom-repo-url/artifactory/plugins-release"
        }
}

dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.17'
    implementation 'org.slf4j:slf4j-simple:2.0.17'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

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

tasks.named('test', Test) {
    useJUnitPlatform()
}

sonarqube {
    properties {
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.projectKey", "${projectGroup}:${serviceKey}-${projectName}"
        property "sonar.projectName", "${serviceKey}-${projectName}"
        property "sonar.host.url", "https://sonar.dev.local"
    }
}

gradle.properties

projectName=gradle-multiproject
description=A neat description!
projectGroup=org.example
gitProjectPath=gradle-multiproject
serviceKey=test

Now when I run ./gradlew sonarqube, it will run the task as expected. But I’m still uncertain why I actually have to declare the repositories all over again. I actually thought that setting it up in settings.gradle would enable subprojects to also use the somewhat globally configured repositories, but if I’m going to remove the explicit repository declaration in build.gradle of subprojects, it won’t be fetching subproject-relevant dependencies e.g. slf4j anymore.

Also, in Intellij it will not recognize the sonarqube task in buildSrc/build.gradle, but in any other subproject e.g. api/build.gradle, any idea here (although it’s IDE specific..)?

Again, thanks for your time!

1 Like

I somehow got it working

What you were missing and now added is the dependency on the SQ plugin in your buildSrc/build.gradle which was missing in your original files.
The plugin-releases in the java-common-conventions.gradle should be unnecessary, unless you need that repository for dependencies in the actual project.

I actually thought that setting it up in settings.gradle would enable subprojects to also use the somewhat globally configured repositories

In your settings script you only configured plugin repositories which are for resolving plugins and their dependencies.

slf4j is not a plugin or plugin dependency but a dependency of your actual project.
For that you can either declare dependencyManagement { repositories { ... } } in your settings script that will be used for all projects, or have it in your convention plugin.

Btw. your convention plugin should contain a dot . somewhere in its id (filename). Plugin ids without namespace should stay reserved for built-in plugins provided by Gradle itself.

Also, in Intellij it will not recognize the sonarqube task in buildSrc/build.gradle, but in any other subproject

I strongly recommend switching to Kotlin DSL. By now it is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.

Thank you very much for the clarification and hints, now it works and I can both use my custom repositories in subprojects as well as plugins and dependencies that are pre-compiled by buildSrc/conventions-plugin.

What’s confusing and not working now is that in :api-subproject, I can’t import classes that I declared in buildSrc/src/main/groovy|java.I can use slf4j but not junit (both should be compiled at the same time in buildSrc).

Stacktrace for the import issue from classes in buildSrc:

Execution failed for task ':api:compileJava'.
> Could not resolve all files for configuration ':api:compileClasspath'.
   > Could not find :buildSrc:.
     Required by:
         project :api

What I tried already was to add buildSrc in api/build.gradle:

plugins {
    id 'my-org.java-common-conventions'
}


dependencies {
    implementation(
            ':buildSrc'
    )
}

Of course this does not work, why should it? o_O
In buildSrc you develop build logic to be used in your build scripts like convention plugins.
It is a separate stand-alone build that “happens” to run before your main build and its results added to your build script classpaths by adding it to a parent classloader.
You do not need to do anything to use the convention plugin in your build scripts.

The dependency you declared is a production dependency on a project called buildSrc but you do not have (and cannot have) a project called buildSrc in your build.

Applying the convention plugin already does add the slf4j dependency as that is what you programmed in your convention plugin.

Thank you very much for the explanation, I think I got it now.

I moved the test classes out of buildSrc and use java-test-fixtures to share these between subprojects.

If I remove the repository {…} config in buildSrc/build.gradle, the plugins can’t be fetched anymore. I thought that will be done in settings.gradle via pluginManagement, no?

No.

If you would use an included build, then it would use both.
The one in the build logic build script to build it,
the on in the consuming builds settings script to resolve it.
With buildSrc the result including the transitive dependencies is put to a parent class loader so the declared plugin repositories are irrelevant for that.