Extend plugin with other plugins (plugin system for a plugin)

I’m interested to know what would be the best approach for a plugin system to a plugin.
I have a base plugin and I want to have additional plugins that extend its functionality.
What I came up with so far is an interface that plugins should implement in addition to Gradle’s own Plugin interface. Then in the base plugin I get all the plugins and check which one is of the interface type.
The interface is published as a separate maven artifact.
It works well, but I want to add a check in the extended plugin that verifies that the base plugin has been applied, otherwise it throws an error.

Usually the approach would be to apply the base plugin in your extended plugin, because applying the same plugin more than once is simply a NOP.

I should have explained myself better. The base plugin is the runtime of the actual plugin. It must be applied to get the basic functionality of the plugin. In addition to that, users can apply extra plugins that connect with the base plugin via an interface to receive events. The extra plugins do not depend on the base plugin. Instead there’s a common api artifact, which isn’t a Gradle plugin that, that holds this interface where both the base plugin and the extra plugins depend on.

So there’s no case of applying the same plugin more than once.

Hi Nimrod,

I still don’t understand what the use-case is. I agree with Schalk that the extension plugins should apply the base plugin. I even thing they should register on the base plugin via an API method exposed by the base plugin, probably via an Extension added by the base plugin.

Could you describe your use-case a little bit more?

Cheers,
Stefan

Hi Stefan,

I’m developing a plugin that tracks build metrics. The base plugin is the “runtime” which by default tracks to local database. Users can apply extension plugins that implement the API interface (shipped as a separate artifact), which is called by the runtime to track metrics on analytics services. For example, there’s one plugin for Google Analytics and another one for Mixpanel, etc.

It looks something like this:

plugins {
  id "runtime"
  id "google-analytics"
  id "mixpanel"
  // other services can go here...
}

the API interface is a simple listener:

interface MetricsListener {
  fun onTrackMetric(metrics: Metrics)
}

then each extension plugin implements the above like so:

class MixpanelPlugin : Plugin<Project>, MetricsListener {
//impl here
}

the runtime plugin does the following to find all extension plugins:

class RuntimePlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val listeners = mutableListOf<BuildMetricsListener>()
        log.info { "Registering all build metrics listeners" }
        project.plugins.all {
            if (it is MetricsListener) {
                listeners += it
            }
        }
        //... at some point loops listeners and call onTrackMetric()
   }
}

As you can see having the extension plugins apply the “runtime” (base) plugin isn’t a possibility since there can be many extension plugins and the point is that there should be only one “runtime”.

That’s at least what I came up with, but I’d like to hear if there’s a common practice to achieve a similar goal.

That is the point from above: applying the base plugin multiple times doesn’t cause there to be multiple runtimes, since only the first application does something. The subsequent applications are ignored by Gradle.

Note that the above call

project.plugins.all {
            if (it is MetricsListener) {
                listeners += it
            }
        }

is live. That means that the closure adding to the listeners list will even be called when a plugin is applied after your base plugin is applied. And your base plugin would need to react to that as well.

That is the point from above: applying the base plugin multiple times doesn’t cause there to be multiple runtime s

Ok, I didn’t know that.

About the live closure: Yes, I noticed that and it is actually causing problems. I’d like to start the runtime only after I get the full list of listeners. What’s the recommended way to do that?

Regarding your previous message:

I even thing they should register on the base plugin via an API method exposed by the base plugin, probably via an Extension added by the base plugin.

Could you please provide an example?

Thanks a lot!

About the live closure: Yes, I noticed that and it is actually causing problems. I’d like to start the runtime only after I get the full list of listeners. What’s the recommended way to do that?

Your plugin should listen to a lifecycle callback. Something like afterEvaluate should be fine. See the user manual.

Regarding your previous message:

I even thing they should register on the base plugin via an API method exposed by the base plugin, probably via an Extension added by the base plugin.

Could you please provide an example?

This is for example what the ear plugin does: EarPlugin.

We also have some examples in the user manual.

Each of your extra plugins would obtain the extension and then register a listener to it.
Something like

    project.getExtensions().getByType(MyBasePluginExtensions.class).addListener(theListener);