[GRADLE-2317] Groovy AST transformations that refer to GroovyTestCase fail with a NoClassDefFoundError Created: 18/May/12 Updated: 04/Jan/13 Resolved: 25/May/12 |
|
Status: | Resolved |
Project: | Gradle |
Affects Version/s: | 1.0-rc-3 |
Fix Version/s: | 1.1-rc-1 |
Type: | Bug | ||
Reporter: | Gradle Forums | Assignee: | Peter Niederwieser |
Resolution: | Fixed | Votes: | 0 |
Description |
See ```groovy ``` worked just fine for me, but I'd prefer not to use a workaround. I'm on windows 7 x64 and CentOS 5 and 6 x64. |
Comments |
Comment by Gradle Forums [ 18/May/12 ] |
Do you have a stack trace for the failure? You'll need to run with the --full-stacktrace command-line option to get this, as the regular --stacktrace option doesn't include the information we need. |
Comment by Gradle Forums [ 18/May/12 ] |
The failing line in our code is a dependency that references GroovyTestCase. junit is confirmed to be pulled in by the dependencies command, not to mention the fact that using the workaround does it just fine.
BUILD FAILED Total time: 10.579 secs |
Comment by Gradle Forums [ 18/May/12 ] |
Looks like it might be a different problem with the same symptom. Do you know where `test.testing.transformations.TestTransformation` comes from? Do you have the code, or can you tell me what it's doing on line 69? |
Comment by Gradle Forums [ 18/May/12 ] |
Sorry, I should have been clearer on that. The ```test.testing.transformations.TestTransformation``` is an AST transformation (modeled after grails TestForTransformation almost exactly), and it references the GroovyTestCase class as I mentioned above (not so clearly): public static final ClassNode GROOVY_TEST_CASE_CLASS = new ClassNode(GroovyTestCase.class); |
Comment by Peter Niederwieser [ 19/May/12 ] |
This problem occurs when a transform statically references a class (from now on called "evil") residing in the Groovy Jar that in turn references a class residing outside the Groovy Jar. GroovyTestCase is (by far) the most prominent example for such a class, but there are quite a few others. (One other example is GroovyServlet.) Running such a transform does work (in most cases) with the groovyc command line compiler and the Ant groovyc task, but does not currently work with Gradle or GMaven. The reason why the Ant groovyc task with default settings (includeAntRuntime=false and fork=false) can run such a transform is that it adds all compile dependencies to its own class loader. (Looking at the source code, I get the impression that this isn't intentional.) With fork=true, the situation is similar: both the compiler and the compile dependencies end up on the application class loader, hence everything works. With includeAntRuntime=false and fork=false, a separate class loader is created, and compilation fails. (Even worse, with includeAntRuntime=false the groovyc Ant task can't compile any transform. This explains why Gradle's Ant-based groovyc integration (which hardcodes includeAntRuntime=false and defaults to fork=true) fails to compile any transform if fork=false, a fact that has always baffled me.) The groovyc command line compiler works in a similar fashion: it loads the Groovy Jar, dependent Jars (like ASM), and all compile dependencies with one and the same class loader. Because the command line compiler doesn't use groovy-all, it looks as if it will run into troubles if the compile class path contains an incompatible version of a library like ASM or commons-cli. (However I haven't verified this.) The fundamental issue that makes life hard for Gradle (and everyone else who wants to separate class loading of the compiler from that of compile dependencies) is that the Groovy compiler isn't separated from the Groovy runtime and library, neither on a package nor on a Jar level. Potential solutions from a Gradle perspective: 1. Require transform authors not to statically reference "evil" Groovy classes from transforms. Usually this is easy to achieve. In this particular case, one can use ClassHelper.make("groovy.util.GroovyTestCase") instead of new ClassNode(groovy.util.GroovyTestCase). 2. Require transform users to put dependencies of "evil" classes on the groovy configuration. In this particular case, adding JUnit will solve the problem. 3. Feed ApiGroovyCompiler with a (hard-coded) list of classes that, although they reside in the Groovy Jar, should nevertheless be loaded with the transform class loader (rather than the class loader that loads the compiler classes). Essentially this is a pragmatic (and programmatic) separation of the Groovy compiler from the Groovy library, maintained in the Gradle codebase. 4. Do it like (Ant) groovyc and put all compile dependencies on the same class loader as the compiler itself. Solution 1. has the drawback that the transform author has to be aware of the problem, and has to change his code although it works with (Ant) groovyc. Furthermore, it isn't applicable to existing transforms that have this problem. Solution 2. puts the burden on users. It also pollutes the class path from which the compiler is loaded, albeit not as much as 4. Solution 4. sounds like a big hack, but might work out in practice as long as the groovy configuration contains groovy-all, which inlines all mandatory dependencies in shaded form. Still, the fact that this solution makes it risky to use groovy instead of groovy-all is a breaking change. It will also require bigger changes to ApiGroovyCompiler, mainly because we will have to make sure that a new compiler daemon isn't started every time the compile class path changes. This leaves solution 3., which on paper looks like the winner to me. I think it would just need a small enhancement to FilteringClassLoader that allows to exclude individual classes. It is quite possible that excluding just GroovyTestCase will completely solve the problem for all practical purposes. On a side note, Groovy 2.0 will finally split up the Groovy codebase into several Jars. For example, there will be a groovy-test and a groovy-sql Jar. This will likely solve the problem for Gradle builds that are careful enough to only put the compiler-required Jars on the groovy configuration. But again, this is something one has to know and adapt to, and it might be inconvenient to use. My guess is that a single all-in-one Groovy Jar will continue to exist. |
Comment by Peter Niederwieser [ 19/May/12 ] |
I have a fix based on 3. that works fine for GroovyTestCase. |
Comment by Adam Murdoch [ 20/May/12 ] |
Option 3. looks like the best option to me. I think you should check your fix in (to master branch). |