12 August 2014

Recently, I have been working on a few Gradle plugins that use the Gradle JavaExec Task to execute a Java main class from within the plugin. This is pretty straightforward and not at all interesting. However, in testing the plugin, I realized that I did not want to expose the required dependencies needed to run the main class outside of the plugin. If I did, I would require each project that applies my plugin to list the dependencies in its dependencies block. This is obviously a leaky abstraction, so I decided to see if I could programmatically set the classpath of the Gradle JavaExec Task to us use the plugin’s dependencies:

task generate(type: JavaExec) {
    main = 'com.example.Generator'
    args = ['arg1', 'arg2']
    classpath = buildscript.configurations.classpath
}

In the example above, the task’s classpath is set to the classpath created by the buildscript DSL, which includes my plugin dependency and all of its transitive dependencies (unless you disable transitive resolution when you define the plugin dependency). This worked great until I used my plugin as part of a multi-module project, where the plugin dependency is declared and applied in the root project, but the task is executed on the sub-project. What happened is that the above example only loaded the build script classpath for the current project (the sub-project), which did not have the plugin dependency on its classpath (apparently, the build script classpath is not additive/transitive). I addressed this by writing a little recursive function to traverse up the project tree and add the dependencies from each build script:

task generate(type: JavaExec) {
    main = 'com.example.Generator'
    args = ['arg1', 'arg2']
    classpath = files(getClasspath(project))
}

private List<File> getClasspath(project, classpath=[]) {
    if(project == null || project == project.rootProject) {
        classpath
    } else {
        classpath.addAll(project.buildscript.configurations.classpath.getFiles())
        getClasspath(project.rootProject, classpath)
    }
}

The above example starts with the current project, adds its build script dependencies to the classpath and then recursively looks at the project’s root project. This continues until the project does not have a root project or the root project is the project itself. The result is then converted to a FileCollection and the classpath is set, ensuring that the Gradle JavaExec Task has the dependencies provided by the custom plugin.

comments powered by Disqus