Running Groovy Closure in a Configuration


(Justin Ryan) #1

In Ant, it’s easy to run a specific blurb of Groovy code in a custom classpath, e.g.

<groovy>
  <classpath>
    <pathelement path="guava-11.0.1.jar"/>
  </classpath>
  println Joiner.on(',').join(['A','B'])
</groovy>

I’m glossing over the details of the classpath here, but the point is that I can inline domain specific Groovy code directly in my build file, with it’s own classpath. How do I do the same in Gradle? It’s trivial to create a configuration, and get it resolved. But how would I run a specific closure in a class loader based on this configuration? I could write a plugin or build up an AntBuilder, but it seems like this is something that could be done natively on a Configuration. I’d like to be able to something like this in Gradle:

configuration {
    deployment
}
dependencies {
    deployment 'netflix:odin:1+' // Orchestration library
}
task(deploy) << {
    deployment.execute {
        new Odin('us-west-1').deployAmi('ami-12345678')
    }
}

I could also pollute the buildscript scope with these dependencies, but then it removes my ability to isolate and customize the classpath for a specific task. Another use case is for testing with different versions, which isn’t possible when using buildscript:

configuration {
    V1, V2
}
dependencies {
    V1 'netflix:client:1+' // Version 1 of client api
    V2 'netflix:client:2+' // Version 2 of client api
}
  def clientCall = new Closure() {
    import app.client.*;
    def client = new Client();
    client.call();
}
task(legacyClient) << {
    V1.execute clientCall
}
task(modernClient) << {
    V2.execute clientCall
}

I can see if this was in place, most of my reasons for using buildSrc would go away. Since I could compile my code into one configuration, then use that code right away.


(Luke Daley) #2

Doing this as code would be pretty tricky. We’d have to use some pretty complicated compile transforms and detecting what to transform would be tricky. However, executing the code as a string of script is reasonably straightforward. Here’s a quick (working) sketch of how a plugin for this might look:

import org.codehaus.groovy.ast.*
import org.codehaus.groovy.classgen.*
import org.codehaus.groovy.control.*
import org.codehaus.groovy.control.customizers.*
import groovy.transform.InheritConstructors
  class InlineScriptPlugin implements Plugin<Project> {
 void apply(Project project) {
  project.extensions.create("inlineScript", InlineScriptEngine, project)
 }
}
  abstract class DelegatingScript extends groovy.lang.Script {
  private final Project project
    DelegatingScript(Project project) {
 this.project = project
  }
     def getProperty(String property) {
    project."$property"
    }
    void setProperty(String property, newValue) {
 project."$property" = newValue
  }
    def invokeMethod(String name, args) {
 project."$name"(*args)
  }
  }
  class InlineScriptEngine {
  final Project project
    InlineScriptEngine(Project project) {
  this.project = project
 }
    def run(Iterable<File> classpath, String script) {
  def compilerConfiguration = new CompilerConfiguration()
  compilerConfiguration.setScriptBaseClass(DelegatingScript.name)
  def scriptClassLoader = new GroovyClassLoader(this.class.classLoader, compilerConfiguration)
  compilerConfiguration.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.CONVERSION) {
       public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {
         classNode.addAnnotation(new AnnotationNode(new ClassNode(InheritConstructors.class)));
       }
     });
  classpath.each { scriptClassLoader.addClasspath(it.absolutePath) }
  scriptClassLoader.parseClass(script, "InlineScript").newInstance(project).run()
 }
}
  apply plugin: InlineScriptPlugin
  repositories {
 mavenCentral()
}
  configurations {
  guava
}
  dependencies {
 guava 'com.google.guava:guava:11.0'
}
  task joinStuff << {
 def joined = inlineScript.run(configurations.guava, """
  import com.google.common.base.Joiner
  Joiner.on(',').join(['A', 'B', name])
 """)
    assert joined == "A,B,${project.name}"
}