How to separate gradle plugin dependencies and project dependencies

Hello,

I’m a developer on a plugin that generates documentation from the source code. How do I manage my plugin dependencies and the dependencies on the project? And how do I prevent collisions?

I’m currently in the situation where a project is depending on a certain dependency, but my plugin is depending on a different version of that dependency. When needing the dependency in my plugin it’s using the project’s dependency and not the plugin dependency (Resulting in a NoSuchMethodError due to the version mismatch). Manually finding the class in the classloader results in a LinkageError mentioning a duplicate class.

I haven’t found any resources regarding this in the gradle manual on how to deal with this. Could anyone point me in the right direction?

This has been discussed before here. There’s a design doc discussing the feature here but as yet there’s no best-practice solution that I’m aware of.

As a workaround, you could create your own configuration in the plugin and create a custom classloader from the jars in the configuration. This custom classloader could then load the conflicting class(es). If using this approach, be careful about the parent classloader so it doesn’t inherit from the buildscript classloader. Classloaders in gradle plugins discussed in @Schalk_Cronje’s idiomatic plugin authoring

2 Likes

Thank you for your comprehensive overview! It’s a pity that the plugins aren’t properly isolated.

All in good time… I have faith that the gradle team will come up with a good solution
In the mean time… you could solve this using a reusable base class which delegates to a plugin implementation

eg:

public abstract class AbstractIsolatedPlugin implements Plugin<Project> {
    private String delegateClassName;
    private String configurationName;
    private Object[] dependencies;

    public AbstractIsolatedPlugin(String delegateClassName, String configurationName, Object[] dependencies) {
        this.delegateClassName = delegateClassName;
        this.configurationName = configurationName;
        this.dependencies = dependencies;
    }

    public void apply(Project project) {
        Configuration configuration = project.getConfigurations().create(configurationName);
        DependencyHandler depHandler = project.getDependencies();
        for (Object dependency : dependencies) {
           configuration.add(depHandler.create(dependency));
        }
        Set<File> jars = configuration.resolve();
        URL[] urls = getUrlsFromFiles(jars); // TODO: implement
        Classloader parentLoader = Project.class.getClassLoader();
        Classloader classLoader = new URLClassloader(urls, parentLoader);
        Plugin<Project> delegate = classLoader.loadClass(delegateClassName).newInstance();
        delegate.apply(project);
    }    
}

public class MyPluginDelegate implements Plugin<Project> {
    public void apply(Project project) {
        // plugin logic goes here
    }
}

public class MyPlugin extends AbstractIsolatedPlugin {
    private static final Object[] ISOLATED_DEPENDENCIES = { "com.foo:bar:1.1", "com.foo:baz:1.1" };
    public MyPlugin() {
       super("com.foo.MyPluginDelegate", "myplugin", ISOLATED_DEPENDENCIES);
    }
}

When building your plugin, you’ll want to declare the “isolated” dependencies as compileOnly so that they don’t get published by the plugin (and end up bleeding onto the buildscript classpath)

1 Like

Cool. Thanks for taking the time to write that out!