Using Gradle to Repackage & Embed Incompatible Dependencies

by Matt Cholick

One of the things I enjoy about developing on the JVM is the impressive number of open source libraries that are available. When I'm trying to solve a common problem, chances are that someone else has already written and open sourced a solution. Some of the core libraries that sit at the root of many open source projects, though, can be problematic when their APIs change across versions.

Today I was trying to get a particular markdown processor to work with Dropwizard. The two libraries, though, depend on incompatible versions of ASM. I could have chosen a different markdown library, but I was feeling stubborn and sought a solution that would force them to work together. After quite a lot of Googling, I finally arrived at this Gradle build file (I've excluded several non-relevant sections):

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

configurations {
    patch
    [compile, testCompile]*.exclude module: 'jersey-server'
}

dependencies {
    compile('com.yammer.dropwizard:dropwizard-core:0.6.2')
    compile('com.sun.jersey:jersey-client:1.17.1')
    compile files('lib/jersey-server-1.17.1.patched.jar')

    compile('org.codehaus.groovy:groovy-all:2.1.3')
    compile('com.google.inject:guice:4.0-beta')
    compile('org.pegdown:pegdown:1.4.1')

    patch('com.sun.jersey:jersey-server:1.17.1')
    patch('asm:asm:3.1')
    patch('com.googlecode.jarjar:jarjar:1.3')
}

project.ext.set("shouldBuildPatch", { !new File('lib/jersey-server-1.17.1.patched.jar').exists() })

task downloadPatchLibs(type: Copy) {
    into('lib')
    from(configurations.patch)
    exclude('jarjar*')
    duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
}
downloadPatchLibs.doFirst {
    if(!shouldBuildPatch()) { throw new StopExecutionException() }
}

task applyPatch(dependsOn: 'downloadPatchLibs') << {
    if (shouldBuildPatch()) {
        project.ant {
            taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", classpath: configurations.patch.asPath
            jarjar(jarfile: 'lib/jersey-server-1.17.1.patched.jar', filesetmanifest: "merge") {
                zipfileset(src: 'lib/jersey-server-1.17.1.jar')
                zipfileset(src: 'lib/asm-3.1.jar')
                rule pattern: "org.objectweb.asm.**", result: "org.objectweb.asm3.@1"
            }
        }
    }
}

task cleanupDownloadPatchLibs(type: Delete, dependsOn: 'applyPatch') {
    delete 'lib/jersey-server-1.17.1.jar'
    delete 'lib/asm-3.1.jar'
}
compileGroovy.dependsOn(cleanupDownloadPatchLibs)

task cleanPatch(type: Delete) {
    delete 'lib'
}
clean.dependsOn(cleanPatch)

The solution relies on a neat little utility, Jar Jar Links. This tool renames compiled classes and thus can remove version collisions.

In the configurations section of the build file, I've declared a new configuration: patch. This is where the problematic dependencies are going to exist. From the other configurations, I've excluded jersey-server, which is the source of the old asm version.

In the dependencies section, the compile and testCompile configurations are mostly declared like normal. The only non-standard declaration is compile files('lib/jersey-server-1.17.1.patched.jar'). This is the customized version of the jar I build in subsequent tasks. The patch dependencies include jersey-server, the old version of asm it needs, and the jarjar tool itself.

The downloadPatchLibs task copies the patch configuration's dependencies to a lib directory for processing. I could have targeted a more out of the way directory, but this makes it easy to include as an IDE dependency. The doFirst setting for downloadPatchLibs will prevent the task from executing if I've already built the jar.

Task applyPatch will actually execute jarjar. It takes the raw asm and jersey-server dependencies, renames the package org.objectweb.asm to org.objectweb.asm3, and combines both the jars' modified classes into a single jersey-server-1.17.1.patched.jar.

The task cleanupDownloadPatchLibs deletes the unmodified dependencies. I added this so that I could include the whole lib directory in my IDE.

The last task definition is cleanPatch, which will delete the entire lib directory.

The final two pieces to make this work are: compileGroovy is assigned a dependency on cleanupDownloadPatchLibs to ensure the patched jersey-server is built and clean depends on cleanPath so files are removed with a clean.