jenkins-pipeline-shared-libraries-gradle-plugin
jenkins-pipeline-shared-libraries-gradle-plugin copied to clipboard
Solution to the @Grab issue
I have figured out a solution to the classloader issue with @Grab
. See my answer for this StackOverflow question.
To summarize, because the GrapeIvy.groovy
implementation of Grape does not allow classloaders other than groovy.lang.GroovyClassLoader
or org.codehaus.groovy.tools.RootLoader
, if blows up with a big exception:
Caused by: java.lang.RuntimeException: No suitable ClassLoader found for grab
at groovy.grape.GrapeIvy.chooseClassLoader(GrapeIvy.groovy:180)
at groovy.grape.GrapeIvy.grab(GrapeIvy.groovy:247)
at groovy.grape.Grape.grab(Grape.java:167)
However, I found that it was possible by using the metaclass to override the GrapeIvy.chooseClassLoader
with an implementation that allows any classloader:
GrapeIvy.metaClass.chooseClassLoader = { Map args ->
def loader = args.classLoader
if (loader?.class == null) {
loader = (args.refObject?.class
?: ReflectionUtils.getCallingClass(args.calleeDepth?:1)
)?.classLoader
while (loader && loader?.class == null) {
loader = loader.parent
}
if (loader?.class == null) {
throw new RuntimeException("No suitable ClassLoader found for grab")
}
}
return loader
}
This just needs to run somewhere before the script gets loaded somehow and the @Grab
gets executed.
If you are using Junit (or JenkinsPipelineUnit), you probably would want to put this code in a method annotated with @BeforeClass
.
If you are using jenkins-spock like I am (instead of JenkinsPipelineUnit), you will need to create a custom Spock global extension (you can't simply use setupSpec
because the super class' setupSpec
gets executed first and it scans the classpath and loads the scripts, executing the @Grab
)
So in my project, added a file test/unit/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension
that has a single line, the fully qualified name of the custom global extension class:
org.cvp.extension.GroovyGrapeExtension
Then I have a class called GroovyGrapeExtension
that extends org.spockframework.runtime.extension.AbstractGlobalExtension
and implements the start()
method:
package org.cvp.extension
import groovy.grape.GrapeIvy
import org.codehaus.groovy.reflection.ReflectionUtils
import org.spockframework.runtime.extension.AbstractGlobalExtension
/**
* This is necessary because by default, {@link GrapeIvy} will not allow @Grab to load classes using anything but the
* classloader implementations {@link groovy.lang.GroovyClassLoader} or {@link org.codehaus.groovy.tools.RootLoader}.
* However, the classloader used by Gradle when running unit tests is different, so @Grab will fail with an error
* unless it is overridden. To make this more difficult, the offending method
* {@link GrapeIvy#isValidTargetClassLoaderClass} is private, and due to a bug in Groovy
* (https://issues.apache.org/jira/browse/GROOVY-7368), private methods cannot be overridden using the meta class.
*
* I would have overridden this behavior by adding a setupSpec() method to {@link org.cvp.pipeline.MessageUtilsSpec}
* however {@link com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification#setupSpec} gets called first
* and that is where the script is loaded into the classpath (and the @Grab executed).
*
* This extension needed to be written because the extension gets executed before any setupSpec() methods are run.
* This extension is also global, so it gets run before the initialization of every Spock instance.
*/
class GroovyGrapeExtension extends AbstractGlobalExtension {
/**
* The {@link org.spockframework.runtime.extension.IGlobalExtension#start} gets executed at the startup of Spock,
* before any setupSpec() functions are executed.
*/
@Override
void start() {
GrapeIvy.metaClass.chooseClassLoader = { Map args ->
def loader = args.classLoader
if (loader?.class == null) {
loader = (args.refObject?.class
?: ReflectionUtils.getCallingClass(args.calleeDepth?:1)
)?.classLoader
while (loader && loader?.class == null) {
loader = loader.parent
}
if (loader?.class == null) {
throw new RuntimeException("No suitable ClassLoader found for grab")
}
}
return loader
}
}
}
So far it is working in my project, and I haven't really had time to explore if something like this could be integrated into the Gradle plugin itself so people don't need to add the boilerplate code to their repository.