Introducing pluginx-resolver: Auto Resolve and Inject Gradle Plugin Classpath with Custom Repositories

Introducing pluginx-resolver: Auto Resolve and Inject Gradle Plugin Classpath with Custom Repositories

Hi Gradle community! :waving_hand:

I’m excited to share a new open-source Gradle plugin I created called pluginx-resolver. It aims to simplify how you manage plugin dependencies and repositories in Gradle builds, especially when using private or custom Maven repositories.


The Problem

Gradle’s plugin system resolves plugins through the Plugin Portal by default. However, many enterprises or projects host plugins in private Maven repositories. In such cases, users usually need to:

  • Manually declare plugin classpath dependencies inside buildscript.classpath
  • Configure private Maven repositories separately in multiple places (pluginManagement.repositories, buildscript.repositories)
  • Keep plugin coordinates and classpath dependencies in sync, which can be error-prone

Example of duplicated config:

pluginManagement {
    repositories {
        gradlePluginPortal()
        maven { url = uri("https://my.company.repo/releases") }
    }
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "com.company.plugin") {
                useModule("com.company:internal-plugin:${requested.version}")
            }
        }
    }
}

buildscript {
    dependencies {
        classpath("com.company:internal-plugin:1.2.3")
    }
}

The Solution: pluginx-resolver

pluginx-resolver solves this by:

  • Automatically resolving plugin artifacts declared in pluginManagement.resolutionStrategy
  • Fetching plugin POM metadata and extracting their dependencies
  • Injecting required classpath dependencies into the build without manual duplication
  • Reading Maven repository definitions (including credentials) from gradle.properties and injecting them into pluginManagement.repositories automatically (optional, enabled by default)

How to Use

Add to your settings.gradle.kts:

plugins {
    id("io.github.neallon.pluginx.resolver") version "1.1.0"
}

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "com.example.plugin") {
                useModule("com.example:my-plugin:${requested.version}")
            }
        }
    }
}

pluginxResolver {
    autoInjectRepositories = false // Optional; default is true
}

Define your private repositories in gradle.properties:

hfx.myRepo.repo.url=https://my.company.repo/maven
hfx.myRepo.user=admin
hfx.myRepo.password=supersecret

Benefits

  • Eliminate manual duplication of plugin classpath dependencies
  • Centralize repository configuration via gradle.properties
  • Support private Maven repos with credentials
  • Compatible with Kotlin & Groovy DSLs
  • Works seamlessly with Gradle 7.5+

Resources


Feedback & Contribution

The plugin is open source and actively maintained. I welcome bug reports, feature requests, and contributions.

If you deal with enterprise or complex Gradle setups, give it a try and share your experience!


Thanks for reading!
Neallon
GitHub: neallon

In such cases, users usually need to

How do you come to these points?
Because I can agree to none of them.
And neither to the “Example of duplicated config”.
If you have a config like that, it more means that the plugin was not published properly.

What I have is exactly one time declaring the repository and applying one plugin like this:

pluginManagement {
    repositories {
        maven("https://nexus.company.com/repository/maven")
    }
}

plugins {
    id("com.company.settings") version "1.4.0"
}

and that’s it.
The settings plugin is properly resolved and applied and configures the repository for the dependencies.

No buildscript block is necessary anywhere,
no resolutionStrategy is necessary anywhere,
the repository is not configured multiple times in the consumer project,
nothing needs to be kept in sync manually.


Please don’t get me wrong, I’m just wondering what the rational is for the plugin and what the features you listed should help.
I see no additional value from what you described.
On the other hand, using your plugin makes using custom settings plugins way worse as it cannot be resolved from this centrally defined repository and cannot have type-safe accessors used.

I’m sure your plugin tries to solve some use-case, I just do not see which one as just from this post I only see functionality that already works out-of-the-box or - in case of the repo from properties - degrades functionality.

Hi

Thank you very much for your feedback — I truly appreciate your time and your insights.

— Plugin Purpose & Resolution Logic —

The motivation behind pluginx-resolver stems from practical limitations encountered in real-world enterprise projects, especially involving:

  • Internally published plugins without marker artifacts
  • Private plugin repositories without Portal compatibility
  • Scenarios where only raw plugin coordinates (group/artifact/version) are available
  • Legacy Gradle environments where publishing best practices were not yet in place

Here’s a quick summary of what the plugin does:

  1. Hooks into pluginManagement.resolutionStrategy.eachPlugin.
  2. Attempts to infer the expected Maven coordinates:
  • Typically, group = pluginId.substringBeforeLast(‘.’)
  • artifact = pluginId
  1. For each configured pluginManagement.repositories, it:
  • Constructs the plugin’s expected .pom URL
  • Parses the POM to extract dependencies
  1. It then injects those dependencies into buildscript.classpath, enabling the plugin to be applied.

This mechanism enables users to write:

plugins {
id(“com.company.legacy-plugin”) version “1.0.0”
}

…without needing to manually declare buildscript blocks or apply(…).

— Where the Plugin Helps — and Where It Shouldn’t Be Used —

I fully agree with your assessment: in properly structured builds with compliant plugin publication, pluginx-resolver is unnecessary, and we should not recommend it for those cases.

Instead, this plugin is intended for non-standard scenarios where Gradle’s plugin resolution fails — often due to:

  • Misconfigured or legacy publishing pipelines
  • Repositories that do not publish marker artifacts
  • Build scripts needing quick onboarding to plugin DSL

The current documentation does not make this distinction explicit enough — and based on your feedback, I will update the README to clearly outline:

  • The specific use cases where pluginx-resolver adds value
  • The caveats (such as breaking type-safe accessors)
  • A strong recommendation to prefer standard Gradle mechanisms whenever possible

Thanks again for your guidance.

Best regards,

Neallon

practical limitations encountered in real-world enterprise projects

I do real-world enterprise projects and build logic too, that’s why I wonder. :slight_smile:

Internally published plugins without marker artifacts

The question is why you should do that?
And whether it would not be way better to simply fix the publishing of those plugins than to invent a new plugin that works around that publishing bug and thus is probably only useful to very few situations where that publishing bug is present and for some reason cannot be fixed.

Private plugin repositories without Portal compatibility

Not sure what you mean with “Portal compatibility”, the plugin portal is just a usual Maven repository with an additional web frontend for searching plugins. We for example have one internal Nexus server that mirrors the approved Gradle plugins from plugin portal and also contains our own company plugins.

Scenarios where only raw plugin coordinates (group/artifact/version) are available

Again, that is a publishing bug.
Plugins should always also publish the marker artifact and if they don’t do, should be fixed.

Legacy Gradle environments where publishing best practices were not yet in place

I’d still expect it to be way easier to simply publish the missing marker artifacts of the old releases instead of using another plugin that tries to mitigate the problem somehow and in a way that you for example cannot use type-safe accessors in settings plugins and so on.

  • Typically, group = pluginId.substringBeforeLast(‘.’)
  • artifact = pluginId

That’s actually generally very untypical.
If it is the case for your internal plugins and there this logic helps, great.
But for the average plugin this is quite unlikely.
Just some examples.
In the project I happen to have opened right now there are 14 external plugins used and only two of them (the gradlex ones) follow the pattern you called “typical”:

"com.autonomousapps.dependency-analysis" => "com.autonomousapps:dependency-analysis-gradle-plugin"
"com.github.ben-manes.versions" => "com.github.ben-manes:gradle-versions-plugin"
"com.github.jk1.dependency-license-report" => "com.github.jk1:gradle-license-report"
"com.google.osdetector" => "gradle.plugin.com.google.gradle:osdetector-gradle-plugin"
"com.klinec.gradle.javacard" => "gradle.plugin.com.klinec:gradle-javacard"
"de.fayard.refreshVersions" => "de.fayard.refreshVersions:refreshVersions"
"net.researchgate.release" => "net.researchgate:gradle-release"
"org.gradle.toolchains.foojay-resolver-convention" => "org.gradle.toolchains:foojay-resolver"
"org.gradlex.extra-java-module-info" => "org.gradlex:extra-java-module-info"
"org.gradlex.jvm-dependency-conflict-resolution" => "org.gradlex:jvm-dependency-conflict-resolution"
"org.jetbrains.gradle.plugin.idea-ext" => "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext"
"org.jetbrains.kotlin.jvm" => "org.jetbrains.kotlin:kotlin-gradle-plugin"
"org.owasp.dependencycheck" => "org.owasp:dependency-check-gradle"
"org.sonarqube" => "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin"

And none of your internal plugins follow it, because we have one artifact with multiple plugins that is called like company-gradle-plugins with multiple plugins with varying IDs inside.

Parses the POM to extract dependencies
It then injects those dependencies into buildscript.classpath, enabling the plugin to be applied.

But why?
If the plugin marker artifact is properly published it will just work.
If it is not published and you have the resolutionStrategy.eachPlugin that draws the missing link from plugin id to plugin artifact, it also will just work.
There should be no need to manually parse any POMs or to manually add any transitive dependencies anywhere.

Furthermore, parsing only POMs is inherently broken anyway, because plugins can for example also release different feature variants of the plugin that are automatically used according to the used Gradle version, or according to the Java version executing Gradle right now. All there feature variants are encoded in the Gradle Module Metadata and the POM is ignored completely if there is a Gradle Module Metadata. So by only handling POMs you also potentially change what is applied completely.

This mechanism enables users to write:

plugins {
id(“com.company.legacy-plugin”) version “1.0.0”
}

…without needing to manually declare buildscript blocks or apply(…).

Even if not published properly, you should always be able to use the plugins { ... } block and never need to use a buildscript block or a legacy apply(...).

Easiest if talking about plugins under your control is simply publishing the missing marker artifacts.
They are only consisting of a POM file with the code artifact as dependency so even manually creating and publishing them would be trivial.

And even if not, just having a resolutionStrategy.eachPlugin that maps from ID to code artifact would already be enough. That is all the marker artifact is doing and all that is necessary. Everything else will just work automatically.

So if the whole problem is missing marker artifacts that you don’t want to or cannot simply publish, then still all you need is the resolutionStrategy.eachPlugin that does the missing ID => code artifact mapping, and if all your internal plugins are following the mentioned naming strategy, this could of course be done in a settings plugin as long as no settings plugins are affected, but that should even then be all that is necessary, no manual POM parsing, no manual transitive dependency handling, no manual buidlscript blocks, no legacy apply(...), …

Also again, your approach would not enable the plugins { ... } syntax for settings plugins, as your settings plugin would do the according configuration too late to be effective for settings plugins.

I fully agree with your assessment: in properly structured builds with compliant plugin publication, pluginx-resolver is unnecessary, and we should not recommend it for those cases.

Again, please don’t get me wrong, I really don’t want to degrade your work, I just want to save you the trouble of implementing and maintaining a plugin that is not necessary.

But even with broken plugin publication, and with refusing to simply fix the situation by publishing the marker artifact, all that is necessary is the resolutionStrategy.eachPlugin line doing the ID → code artifact mapping and everything else will just work as if the marker artifact would be present and that even for settings plugins.

In your usage example you also still have the manual resolutionStrategy.eachPlugin call, so there should be actually nothing that your plugin does to improve the situation from what I understood still.

Thank you for taking the time to write such a detailed response — I truly appreciate the concrete examples and the reasoning you’ve provided. I agree with many of your points, especially that:

  • In an ideal setup, all plugins should be published with proper marker artifacts and Gradle Module Metadata.
  • The cleanest and most future-proof solution is to fix the publishing process rather than work around it.
  • resolutionStrategy.eachPlugin is sufficient in many cases and avoids the need for any POM parsing.

I also acknowledge that the coordinate inference pattern I mentioned is not generally representative across the plugin ecosystem. Your examples clearly demonstrate how diverse plugin publishing patterns can be, and I agree that hardcoded inference is not a robust universal approach.


Why the plugin exists at all

The primary reason pluginx-resolver was created is not to replace Gradle’s built-in resolution logic, but to serve as a short-term bridge in situations where:

  • The plugin is produced by a team or vendor outside my control.
  • The publishing process cannot be changed quickly (e.g., due to contractual restrictions or long release cycles).
  • The target environment includes older Gradle versions or CI/CD pipelines where resolutionStrategy.eachPlugin was not yet in place or is not easily maintainable across dozens of projects.
  • Developers have no direct write access to the corporate Nexus or Artifactory to manually add missing marker artifacts.

In other words, it was meant as an “emergency adapter” to allow teams to continue using the plugin DSL while migration or publishing fixes are in progress — not as a recommendation for standard practice.


Points you raised that I plan to address

Your feedback about POM-only parsing is spot-on — it ignores Gradle Module Metadata and can produce incorrect results when variants differ by Gradle or Java version. I will:

  • Add GMM parsing support before falling back to POMs.
  • Make coordinate mapping fully configurable so it does not rely on a fixed “group = pluginId.substringBeforeLast(‘.’)” assumption.
  • Update the documentation to clearly state that:
    • Proper publication with marker artifacts is always preferred.
    • This plugin is a temporary workaround, not a long-term replacement for Gradle’s resolution logic.
    • It should be avoided in environments where type-safe accessors or settings plugin resolution are required.

Next steps

If you are open to it, I’d be happy to share the revised design and documentation for review before the next release, to make sure it aligns better with Gradle’s direction and best practices.

Thanks again for the detailed technical perspective — it will directly improve how this tool is positioned and implemented.

Best regards,
Neallon

resolutionStrategy.eachPlugin is sufficient in many cases and avoids the need for any POM parsing

I’m really curious, in which cases do you think it is not sufficient but you need manual metadata parsing?

older Gradle versions

Older then Gradle 3.5? Those builds should definitely be upgraded. :smiley:

to make sure it aligns better with Gradle’s direction

I’m just a Gradle user like you, helping other users, I’m not affiliated to Gradle and cannot speak for them.
And I will probably not have time for a review, especially as it seems like the plugin will never be useful for me from what I understood. I just had read your post here. :slight_smile: