22 July 2014

Gradle currently supports two different way to reference other .gradle files from a build script:

  1. Apply a script from file via apply from: 'path/to/some/script.gradle'

  2. Apply a script from URL via apply from: 'http://some/url/script.gradle'

The first is great for one-off scripts in your project, but is not really helpful when you want to share Gradle scripts among projects. The second approach would work for sharing, but is really limited with regards to the supported functionality (e.g. you cannot specify HTTP headers to handle login/security concerns, etc). While you can obviously create a plugin that approximates the logic in the script that you want to share, its a pain to have to create a new project and push it to your artifact repository each time you want to share build logic. The solution? Create a single Gradle plugin that is capable of scanning the build script classpath at execution time to find and apply .gradle files to the current project. This approach uses the PathMatchingResourcePatternResolver class from the Spring Framework to locate each .gradle file. Once the files have been located, the files are read from the classpath and extracted to a temporary scripts folder under the project’s build directory and then applied to the current project via the file-based approach outlined above. Here is the entire plugin code:

class BuildScriptSupportPlugin implements Plugin<Project> {

    /**
     * Spring {@link PathMatchingResourcePatternResolver} used to find all Gradle scripts
     * on the classpath.
     */
    private PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader());

    @Override
    public void apply(Project project) {
        project.logger?.debug("Applying Build Script Support Plugin to ${project.name}...")

        // Make sure the output directory for the extracted scripts exists.
        File outputDirectory = new File(project.buildDir, 'scripts')
        outputDirectory.mkdirs()

        // Find all .gradle files in the "scripts" package on the class path.
        Resource[] resources = resourceResolver.findPathMatchingResources('classpath*:/scripts/*.gradle')

        // For each script, copy it from the class path to the output directory and then apply it to the project.
        resources.each { Resource resource ->
            try {
                File output = new File(outputDirectory, resource.getFilename())
                output.withWriter { writer ->
                    writer.write(new InputStreamReader(resource.getInputStream()).getText())
                }
                project.apply([from: "${outputDirectory}/${resource.getFilename()}"])
            } catch (e) {
                project.logger?.error("Unable to retrieve and apply bulid script '${resource.getFilename()}': ${e.getMessage()}")
            }
        }
    }
}

By convention, the plugin looks for the extra scripts in the scripts package on the classpath. In practice, I put the scripts in src/main/resources/scripts in the projects/libraries that I use to bundle up the shared scripts. To use the plugin and any libraries that may contain build scripts in your project, simply add them to your build.gradle script in the buildScript section:

apply plugin: 'build-script-support'

buildscript {
    repositories {
        mavenLocal()
        maven { url 'your local repo URL' }
    }
    dependencies {
        classpath('org.gradle.plugins:gradle-build-script-support-plugin:1.0.0-SNAPSHOT')
        classpath('com.example:custom-gradle-scripts:1.0.0-SNAPSHOT')
    }
}

Future enhancements to this plugin will/could include the following:

  • Include/Exclude patterns for script names when scanning

  • Script name disambiguation to avoid two scripts from two different JAR’s with the same name clobbering each other.

  • Ability to explicitly specify location of script on classpath to scan (to support situations where a script cannot be moved to the conventional place).

It should be noted that the code above was written and used with Gradle 1.12 and has not yet been tested with 2.0, though I don’t expect any issues.

comments powered by Disqus