Over the past year, I’ve spent a lot of time hacking in the Gradle ecosysgtem which, for better or worse, has earned me a reputation of knowing Gradle-y things within Lookout. Recently, my colleague Ron approached me with a Gradle problem: using the shadow plugin (a great plugin for building fat jars), he was having trouble excluding some dependencies from the produced jar artifact. I figured I would emulate Mr. Haki’s Gradle Goodness series and post one of my own.
The Problem
The Shadow plugin will, by default, include runtime dependencies inside of the
produced artifact. The way that it handles this is by unzipping (effectively)
the .jar
dependencies into the shadow jar’s file tree.
In Ron’s case, he’s building an Apache Storm topology which must compile against the “storm-core” dependency, but it must not include that dependency in the resulting artifact. Otherwise the deployment of the topology explodes with all kinds of classpath conflicts, as Storm already has the “storm-core” code loaded at runtime.
His build.gradle
originally looked something like this:
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '1.2.1'
}
shadowJar {
manifest {
attributes 'Implementation-Title': 'Storm Health Check',
'Implementation-Version': project.version,
'Main-Class': 'com.github.lookout.storm.healthcheck.HealthCheckTopology'
}
}
dependencies {
compile group: 'org.apache.storm', name: 'storm-core', version: '0.9.4'
testCompile group: 'junit', name: 'junit', version: '4.11'
}
Attempt #1
Overly confident, as per usual, I say “well there’s your
problem”
and change the dependencies { }
closure to:
dependencies {
runtime group: 'org.apache.storm', name: 'storm-core', version: '0.9.4'
testCompile group: 'junit', name: 'junit', version: '4.11'
}
This, of course, will fail to compile the Java code included in the project since it needs the transitive dependencies of “storm-core” to compile properly.
Duh.
Attempt #2
Reading through the docs again for the Shadow plugin, I noticed this bit about excluding dependencies and figured I would give that a try:
shadowJar {
manifest {
attributes 'Implementation-Title': 'Storm Health Check',
'Implementation-Version': project.version,
'Main-Class': 'com.github.lookout.storm.healthcheck.HealthCheckTopology'
}
dependencies {
exclude(dependency('org.apache.storm:storm-core:0.9.4'))
}
}
Unfortnuately this functionality provided by the Shadow plugin on excludes the top-level dependency, and doesn’t do anything different with transitive dependencies.
So that doesn’t work for us, trying again!
Attempt #3
I’ve worked with the Shadow plugin a number of times before, and I’m aware of
the defaults that are applied to the default shadowJar { }
task, so my next
attempt was to simply avoid that task
configurations {
shadow
}
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
task stormTopology(type: ShadowJar) {
manifest {
attributes 'Implementation-Title': 'Storm Health Check',
'Implementation-Version': project.version,
'Main-Class': 'com.github.lookout.storm.healthcheck.HealthCheckTopology'
}
configurations = [project.configurations.shadow]
from(project.sourceSets.main.output)
}
This introduces a new configuration called “shadow” which is assigned to the new “stormTopology” task. So this technically works.
It’s got some downsides:
- Introduction of a new task
- Introduction of a new, empty, configuration
- Additional code to get our
ShadowJar
task to be configured properly
This didn’t feel right to me, so I tried one more time to make it cleaner
Attempt #3 1/2
While re-reviewing the code for the default shadowJar { }
task, I noticed
this
line
which configures the task to use the “runtime” configuration by default. The
Gradle Java
plugin defines a
number of configurations, “compile” being one of them and another
notable one being “runtime.” The latter is intended for runtime dependencies
for the application being built. The Java plugin also makes “runtime” extend
from “compile,” turning it into a superset of dependency information.
This means that if you define a “compile” dependency, it will be present in the “runtime” configuration automatically.
Recognizing this as part of a potential solution, I refactored the build.gradle one more time:
plugins {
id 'com.github.johnrengelman.shadow' version '1.2.1'
}
shadowJar {
manifest {
attributes 'Implementation-Title': 'Storm Health Check',
'Implementation-Version': project.version,
'Main-Class': 'com.github.lookout.storm.healthcheck.HealthCheckTopology'
}
}
dependencies {
compile group: 'org.apache.storm', name: 'storm-core', version: '0.9.4'
testCompile group: 'junit', name: 'junit', version: '4.11'
}
configurations {
/* We don't want the storm-core dependency in our shadowJar */
runtime.exclude module: 'storm-core'
}
Note the last configurations { }
closure. This is using Gradle’s built in
dependency management semantics to purge storm-core
and its transitive
dependencies from the “runtime” configuration.
This is short, functional and doesn’t require any special configuration, yay!