Gradle mixes your dependencies with itself

2014-01-19 in gradle
A fractal of bad implementation

Update 2017-03-04: Gradle no longer has these problems.

One of the most important features of the Maven family of build tools, of which Gradle is a member, is the ability to automatically download dependencies and configure the Java classpath. The latter often is the more important part of the feature — unlike virtually every other module-based environment, the JVM is bafflingly incapable of locating dependencies itself (eg, from a standardised location as in C).

Gradle’s implementation, however, is rather problematic. Virtually every task undertaken by Gradle — including simply invoking the javac compiler — involves spawning a child Gradle process, whose classpath includes that of the user code in question. This is perfectly fine as long as the user code’s dependencies are disjoint from Gradle’s; had gradle been written without any non-JRE dependencies, it wouldn’t be an issue at all.

But Gradle isn’t at all light-weight — its memory footprint dwarfs that of Windows XP — and it has almost as many dependencies as one might suspect.

Exhibit A — Groovy++

Depending on who you ask, “Groovy++” is an extension, invasive augmentation, or eldritch abomination of Groovy 1.8x. Regardless, its purpose is to add static typing and inference into Groovy, while preserving as many Groovyisms as possible. It also adds some nice compile-time checks, such as whether variable names are actually defined. And in many cases, instead of finding out a typo in your program via it crashing at run-time, the Groovy++ compiler crashes at compile-time. Usually it vomits enough of its state when crashing to figure out where the typo is.

All in all, programming with Groovy++ is somewhat less unpleasant than programming in Groovy. So let’s try adding it as a dependency to our base project. It ships its own version of Groovy, so we’ll also remove the explicit dependency on Groovy. We also need to use the special groovy declaration so that Gradle knows to use the alternate compiler. Our build.gradle now looks like this:

build.gradle

 1 apply plugin: "groovy";
 2 apply plugin: "java";
 3 apply plugin: "maven";
 4 apply plugin: "application";
 5 
 6 mainClassName = "gl.lin.Main";
 7 
 8 task wrapper(type: Wrapper) {
 9   gradleVersion = "1.6";
10 }
11 
12 repositories {
13   mavenCentral();
14 }
15 
16 dependencies {
17   groovy group: 'org.mbte.groovypp', name: 'groovypp', version: '0.9.0_1.7.10';
18 }

Unfortunately, it doesn’t run as is, since the dependency doesn’t exist in Maven Central, and I’m not about to publish the credentials to the repository I’m getting it from. If you could get your hands on Groovy++, as soon as you add even one .groovy file to the project, Gradle produces this wonderful output.

 1 $ ./gradlew run
 2 
 3 The groovy configuration has been deprecated and is scheduled to be removed in G
 4 radle 2.0. Typically, usages of 'groovy' can simply be replaced with 'compile'.
 5 In some cases, it may be necessary to additionally configure the 'groovyClasspat
 6 h' property of GroovyCompile and Groovydoc tasks.
 7 :compileJava UP-TO-DATE
 8 :compileGroovy FAILED
 9 
10 FAILURE: Build failed with an exception.
11 
12 * What went wrong:
13 Execution failed for task ':compileGroovy'.
14 > loader constraint violation: loader (instance of groovy/lang/GroovyClassLoader
15 ) previously initiated loading for a different type with name "org/objectweb/asm
16 /MethodVisitor"
17 
18 * Try:
19 Run with --stacktrace option to get the stack trace. Run with --info or --debug
20 option to get more log output.
21 
22 BUILD FAILED

As it turns out, there is exactly one Gradle version that can run with Groovy++ (at least this version of Groovy++): 1.0-rc-2. Any newer version and Groovy++ crashes as seen here. Any older version and Gradle crashes.

The main reason behind this issue seems to be the modified/mutilated version of Groovy shipped with Groovy++. Since Gradle is written in Groovy and expects to run in the same JVM as some user code, it causes one version of the Groovy class loader to attempt to load classes from another Groovy version.

Unfortunately, this means that, to use Groovy++ with Gradle, one is forced to use a version of Gradle that even the authors didn’t consider fully stable. It ultimately ends up not being worth it.

Exhibit B — BeanShell

BeanShell is a dynamic scripting language that, unlike Groovy, is virtually completely backwards-compatible with a specific version of Java. Unfortunately, that version is 1.4, so it doesn’t support nicities like generics. Originally apparently on track to become the “standard” Java scripting language, the entire world suddenly forgot about it shortly after Java 1.5 was released or so it seems, after which it fell into obsolescence.

Why did I want to add an obsolete scripting language to a service? We wanted a Java-like scripting language for a debugging/inspection console, and BeanShell isn’t as horrible as Groovy, and it’s feather-weight. The reasoning is similar to choosing a Model T over a Pinto strapped to a Komet.

In any case, depending BeanShell still isn’t as weird as depending on Groovy and BeanShell, which Gradle apparently does. Going back to the base project, let’s go back to normal Groovy and add in a dependency on BeanShell.

build.gradle

 1 apply plugin: "groovy";
 2 apply plugin: "java";
 3 apply plugin: "maven";
 4 apply plugin: "application";
 5 
 6 mainClassName = "gl.lin.Main";
 7 
 8 task wrapper(type: Wrapper) {
 9   gradleVersion = "1.6";
10 }
11 
12 repositories {
13   mavenCentral();
14 }
15 
16 dependencies {
17   compile 'org.codehaus.groovy:groovy-all:1.8.7';
18   compile "org.beanshell:beanshell:2.0b4";
19 }

In order to test compilation, we’ll also have the Java compiler tell us whether the class actually exists.

Main.java

 1 package gl.lin;
 2 
 3 /* Just here to test whether the class can be found in the classpath that
 4  * Gradle gives us.
 5  */
 6 import bsh.Interpreter;
 7 
 8 public class Main {
 9   public static void main(String args[]) {
10   }
11 }

Trying to build this gives us

1 $ ./gradlew build
2 
3 :compileJava UP-TO-DATE
4 :compileGroovysrc/main/groovy/gl/lin/Main.java:6: package bshdoes not exist
5 import bsh.Interpreter;
6           ^
7 1 error

My guess is that Gradle thinks that the BeanShell dependency is its own, and strips it from the classpath. It does dowload the jar, and manual inspection reveals that the jar really does have the class in question. Gradle just runs without it on the classpath.

And if you instead use the “bsh” jar name, it works.

  compile "org.beanshell:bsh:2.0b4";

In Conclusion

Gradle is the only build system I have ever used where the build system and the code being built need to have inter-compatible dependencies. In particular, the fact that Gradle isn’t even insulated against differences in the versions of the Groovy runtime it uses and that needed by the client often causes problems for Groovy-based projects — exactly the type of project Gradle was really designed to work with.

The worst part, though, is that it’s all so needless. Why another process just to wrap javac? Why does it need to have the same classpath that it’s passing to javac? While Gradle does need to have some code live with the user code for running tests, that could just be a shim depending only on the JRE. Why did they not think this would be a problem?

Then again, the authors seem to feel pretty positively about Groovy, so maybe it’s better that that question remain unanswered.