Working prototype of extending both Jar and ShadowJar task types

This commit is contained in:
Schalk W. Cronjé 2014-09-20 00:28:23 +01:00
parent be27986b01
commit 69d9e4f52c
6 changed files with 351 additions and 246 deletions

View File

@ -36,6 +36,8 @@ dependencies {
exclude module : 'groovy-all'
}
testRuntime 'com.github.jengelman.gradle.plugins:shadow:1.+'
// For the testWarbler tests I am locking the versions, instead of a open version, as it makes
// unit testing easier, This does not affect the final artifact.
// If you change values here, you need to update JRubyJarPluginSpec as well.

View File

@ -0,0 +1,65 @@
package com.github.jrubygradle.jar
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
import org.gradle.api.GradleException
import org.gradle.api.Task
/**
* @author Schalk W. Cronjé.
*
* @since 0.1.1
*/
@TupleConstructor
class BootstrapClassExtension {
static final String BOOTSTRAP_TEMPLATE_PATH = 'META-INF/gradle-plugins/bootstrap.java.template'
/** The task this extension instance is attached to.
*
*/
Task task
/** The location of the Ruby initialisation/bootstrap script.
* The default bootstrap class will look for a file in this relative location.
* It is the user's responsibility that this script is added to the Jar.
*
* @since 0.1.1
*/
String initScript = 'META-INF/init.rb'
/** This is the JRuby language compatibility mode.
* @since 0.1.1
*/
String compatMode = '1.9'
Object source
Object getSource() {
if(null == source) {
setSourceFromResource()
}
this.source
}
void setSourceFromResource() {
Enumeration<URL> enumResources
enumResources = this.class.classLoader.getResources( BOOTSTRAP_TEMPLATE_PATH)
if(!enumResources.hasMoreElements()) {
throw new GradleException ("Cannot find ${BOOTSTRAP_TEMPLATE_PATH} in classpath")
} else {
URI uri = enumResources.nextElement().toURI()
String location = uri.getSchemeSpecificPart().replace('!/'+BOOTSTRAP_TEMPLATE_PATH,'')
if(uri.scheme.startsWith('jar')) {
location=location.replace('jar:file:','')
source= task.project.zipTree(location)
} else if(uri.scheme.startsWith('file')) {
source= location.replace('file:','')
} else {
throw new GradleException("Cannot extract ${uri}")
}
}
}
}

View File

@ -3,9 +3,6 @@ package com.github.jrubygradle.jar
import groovy.transform.PackageScope
import org.gradle.api.Incubating
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.bundling.Jar
import com.github.jrubygradle.GemUtils
@ -15,7 +12,8 @@ import com.github.jrubygradle.GemUtils
*/
class JRubyJarConfigurator {
static final String DEFAULT_MAIN_CLASS = 'com.lookout.jruby.JarMain'
static final String DEFAULT_BOOTSTRAP_CLASS = 'com.github.jrubygradle.jar.bootstrap.JarMain'
static final String SHADOW_JAR_TASK_CLASS = 'com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar'
// This is used by JRubyJarPlugin to configure Jar classes
@PackageScope
@ -29,15 +27,8 @@ class JRubyJarConfigurator {
@PackageScope
static void afterEvaluateAction( Project project ) {
project.tasks.withType(Jar) { t ->
if (t.manifest.attributes.containsKey('Main-Class')) {
if (t.manifest.attributes.'Main-Class' == JRubyJarConfigurator.DEFAULT_MAIN_CLASS) {
t.with {
from({ project.configurations.jrubyEmbeds.collect { project.zipTree(it) } }) {
include '**'
exclude '**/WarMain.class'
}
}
}
if(t.class.superclass.name == SHADOW_JAR_TASK_CLASS && t.name=='shadowJar') {
t.configurations.add(project.configurations.getByName('jrubyJar'))
}
}
}
@ -63,15 +54,10 @@ class JRubyJarConfigurator {
*/
@Incubating
void mainClass(final String className) {
maybeAddExtraManifest()
archive.with {
manifest {
attributes 'Main-Class': className
}
metaInf {
from { this.getDependencies() }
into 'lib'
}
}
}
@ -102,74 +88,20 @@ class JRubyJarConfigurator {
*/
@Incubating
void defaultMainClass() {
mainClass(DEFAULT_MAIN_CLASS)
mainClass(DEFAULT_BOOTSTRAP_CLASS)
}
/** Adds a configuration to the list of dependencies that will be packed when creating an executable jar
* This method is ignored if {@code mainClass} is not set.
*
* @param name Name of configuration
*/
@Incubating
void configuration(String name) {
this.configuration(archive.project.configurations.getByName(name))
}
/** Adds a configuration to the list of dependencies that will be packed when creating an executable jar
* This method is ignored if {@code mainClass} is not set.
*
* @param name Configuration
*/
@Incubating
void configuration(Configuration config) {
if(this.configurations==null) {
this.configurations = []
}
this.configurations.add(config)
boolean isShadowJar() {
shadowJar
}
private JRubyJarConfigurator(Jar a) {
archive = a
}
private Set<File> getDependencies() {
Set<File> tmp = []
if(configurations == null) {
archive.project.configurations.each { Configuration cfg ->
if( ['jrubyJar','compile','runtime'].contains(cfg.name) ) {
tmp.addAll(cfg.files)
}
}
} else {
configurations.each {
tmp.addAll(it.files)
}
if (a.class.name == SHADOW_JAR_TASK_CLASS || a.class.superclass.name == SHADOW_JAR_TASK_CLASS) {
shadowJar = true
}
return tmp
}
private void maybeAddExtraManifest() {
if(extraManifest == null) {
extraManifest = new File(archive.project.buildDir,extraManifestName)
String taskName = "${archive.name}ExtraManifest"
Task task = archive.project.tasks.create(taskName)
task << {
String libs = task.inputs.files.collect { File f -> "lib/${f.name}" }.join(' ')
extraManifest.parentFile.mkdirs()
extraManifest.text = "Class-Path: ${libs}\n"
}
task.outputs.file(extraManifest)
task.inputs.files({this.getDependencies()})
archive.dependsOn task
archive.manifest.from({task.outputs.files.singleFile})
}
}
private String getExtraManifestName() {
"tmp/${archive.name}-extraManifest.mf"
}
private Jar archive
private List<Configuration> configurations
private File extraManifest
private boolean shadowJar = false
}

View File

@ -1,22 +1,124 @@
package com.github.jrubygradle.jar
import com.github.jrubygradle.internal.WarblerBootstrap
import com.github.jrubygradle.JRubyPlugin
import groovy.transform.PackageScope
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
/**
* @author Schalk W. Cronjé
*/
class JRubyJarPlugin implements Plugin<Project> {
static final String BOOTSTRAP_TASK_NAME = 'jrubyJavaStub'
void apply(Project project) {
project.apply plugin : 'com.github.jruby-gradle.base'
project.apply plugin : 'java-base'
project.configurations.maybeCreate('jrubyEmbeds')
project.configurations.maybeCreate('jrubyJar')
updateTestTask(project)
addCodeGenerationTask(project)
addDependentTasks(project)
addJrubyExtensionToJar(project)
addAfterEvaluateHooks(project)
}
@PackageScope
void addCodeGenerationTask(Project project) {
Task stubTask = project.tasks.create( name: BOOTSTRAP_TASK_NAME, type : Copy )
stubTask.extensions.create(
'jruby',
BootstrapClassExtension,
stubTask
)
project.configure(stubTask) {
group JRubyPlugin.TASK_GROUP_NAME
description 'Generates a JRuby Java bootstrap class'
from({extensions.jruby.getSource()})
into new File(project.buildDir,'generated/java')
filter { String line ->
line.replaceAll('%%LAUNCH_SCRIPT%%',extensions.jruby.initScript)
}
rename '(.+)\\.java\\.template','$1.java'
}
project.sourceSets {
main {
java {
srcDir new File(project.buildDir,'generated/java')
}
}
}
}
@PackageScope
void addDependentTasks(Project project) {
['jar','shadowJar'].each { taskName ->
try {
Task t = project.tasks.getByName(taskName)
if( t instanceof Jar) {
t.dependsOn 'jrubyPrepareGems'
}
} catch(UnknownTaskException) {
project.tasks.whenTaskAdded { Task t ->
if (t.name == taskName && t instanceof Jar) {
t.dependsOn 'jrubyPrepareGems'
}
}
}
}
try {
Task t = project.tasks.getByName('compileJava')
if( t instanceof JavaCompile) {
t.dependsOn BOOTSTRAP_TASK_NAME
}
} catch(UnknownTaskException) {
project.tasks.whenTaskAdded { Task t ->
if (t.name == 'compileJava' && t instanceof JavaCompile) {
t.dependsOn BOOTSTRAP_TASK_NAME
}
}
}
}
@PackageScope
void addJrubyExtensionToJar(Project project) {
if(!Jar.metaClass.respondsTo(Jar.class,'jruby',Closure)) {
Jar.metaClass.jruby = { Closure extraConfig ->
JRubyJarConfigurator.configureArchive(delegate,extraConfig)
}
}
}
@PackageScope
void addAfterEvaluateHooks(Project project) {
project.afterEvaluate {
project.dependencies {
jrubyJar group: 'org.jruby', name: 'jruby-complete', version: project.jruby.defaultVersion
}
// WarblerBootstrap.addDependency(project)
JRubyJarConfigurator.afterEvaluateAction(project)
}
}
@PackageScope
void updateTestTask(Project project) {
// In order to update the testing cycle we need to tell unit tests where to
// find GEMs. We are assuming that if someone includes this plugin, that they
// will be writing tests that includes jruby and that they might need some
@ -25,6 +127,7 @@ class JRubyJarPlugin implements Plugin<Project> {
environment GEM_HOME : project.extensions.getByName('jruby').gemInstallDir
dependsOn 'jrubyPrepareGems'
}
try {
Task t = project.tasks.getByName('test')
if( t instanceof Test) {
@ -37,36 +140,5 @@ class JRubyJarPlugin implements Plugin<Project> {
}
}
}
try {
Task t = project.tasks.getByName('jar')
if( t instanceof Jar) {
t.dependsOn 'jrubyPrepareGems'
}
} catch(UnknownTaskException) {
project.tasks.whenTaskAdded { Task t ->
if (t.name == 'jar' && t instanceof Jar) {
t.dependsOn 'jrubyPrepareGems'
}
}
}
if(!Jar.metaClass.respondsTo(Jar.class,'jruby',Closure)) {
Jar.metaClass.jruby = { Closure extraConfig ->
JRubyJarConfigurator.configureArchive(delegate,extraConfig)
}
}
project.afterEvaluate {
WarblerBootstrap.addDependency(project)
JRubyJarConfigurator.afterEvaluateAction(project)
project.dependencies {
jrubyJar group: 'org.jruby', name: 'jruby-complete', version: project.jruby.defaultVersion
}
}
}
}

View File

@ -0,0 +1,39 @@
package com.github.jrubygradle.jar.bootstrap;
import org.jruby.embed.ScriptingContainer;
import static org.jruby.embed.PathType.*;
class JarMain {
private JarMain(String[] args) throws Exception {
container = new ScriptingContainer();
container.setArgv(args);
// List<String> loadPaths = new ArrayList();
// loadPaths.add(jrubyhome);
// Ruby Version
// container.setCompatVersion(CompatVersion.%%RUBY_COMPAT_VERSION%%);
// RubyInstanceConfig config = container.getProvider().getRubyInstanceConfig();
// container.setEnvironment(Map environment)
}
private int run() {
try {
Object outcome = container.runScriptlet(LAUNCH_SCRIPT /*, RELATIVE|CLASSPATH */ );
return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0;
} finally {
container.terminate();
}
}
private static final String LAUNCH_SCRIPT = "load '%%LAUNCH_SCRIPT%%'";
private ScriptingContainer container;
public static void main(String[] args) throws Exception {
JarMain jm = new JarMain(args);
System.exit(jm.run());
}
}

View File

@ -3,6 +3,7 @@ package com.github.jrubygradle.jar
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.testfixtures.ProjectBuilder
import spock.lang.Specification
@ -94,7 +95,7 @@ class JRubyJarPluginSpec extends Specification {
}
then: "Then the attribute should be set to the default in the manifest"
jarTask.manifest.attributes.'Main-Class' == JRubyJarConfigurator.DEFAULT_MAIN_CLASS
jarTask.manifest.attributes.'Main-Class' == JRubyJarConfigurator.DEFAULT_BOOTSTRAP_CLASS
}
def "Adding all defaults"() {
@ -113,11 +114,12 @@ class JRubyJarPluginSpec extends Specification {
}
}
then: "Then the attribute should be set to the default in the manifest"
jarTask.manifest.attributes.'Main-Class' == JRubyJarConfigurator.DEFAULT_MAIN_CLASS
and: "The appropriate files included"
then: "The appropriate files included"
fileNames(jarTask.source) == (['MANIFEST.MF','gems','gems/fake.txt','data','data/data.txt'] as Set<String>)
and: "Then the attribute should be set to the default in the manifest"
jarTask.manifest.attributes.'Main-Class' == JRubyJarConfigurator.DEFAULT_BOOTSTRAP_CLASS
}
def "Adding a custom main class"() {
@ -132,129 +134,7 @@ class JRubyJarPluginSpec extends Specification {
jarTask.manifest.attributes.'Main-Class' == 'org.scooby.doo.snackMain'
}
def "Building a Jar"() {
given: "A local repository"
final String jrubyTestVersion = '1.7.15'
File expectedDir= new File(TESTROOT,'libs/')
expectedDir.mkdirs()
File expectedJar= new File(expectedDir,'test.jar')
project.jruby.gemInstallDir = new File(TESTROOT,'fakeGemDir').absolutePath
new File(project.jruby.gemInstallDir,'gems').mkdirs()
new File(project.jruby.gemInstallDir,'gems/fake.txt').text = 'fake.content'
project.with {
jruby {
defaultRepositories = false
warblerBootstrapVersion = '0.1.0'
defaultVersion = jrubyTestVersion
}
repositories {
ivy {
url WARBLER_LOCATION
layout('pattern') {
artifact '[module]-[revision](.[ext])'
}
}
}
dependencies {
jrubyJar 'org.spockframework:spock-core:0.7-groovy-2.0'
}
}
when: "I set the default main class"
project.configure(jarTask) {
archiveName = 'test.jar'
destinationDir = expectedDir
jruby {
defaults 'mainClass','gems'
}
}
project.evaluate()
and: "I actually build the JAR"
project.tasks.getByName("${jarTask.name}ExtraManifest").execute()
jarTask.copy()
def builtJar = fileNames(project.zipTree(expectedJar))
then: "I expect to see the JarMain.class embedded in the JAR"
expectedJar.exists()
builtJar.contains('com/lookout/jruby/JarMain.class')
!builtJar.contains('com/lookout/jruby/WarMain.class')
and: "I expect to see jruby-complete packed in libs"
builtJar.contains("META-INF/lib/jruby-complete-${jrubyTestVersion}.jar".toString())
and: "I expect to see manifest to include it"
jarTask.manifest.effectiveManifest.attributes['Class-Path']?.contains("lib/jruby-complete-${jrubyTestVersion}.jar".toString())
}
def "Building a Jar with a custom configuration"() {
given: "A local repository"
final String jrubyTestVersion = '1.7.15'
File expectedDir= new File(TESTROOT,'libs/')
expectedDir.mkdirs()
File expectedJar= new File(expectedDir,'test.jar')
project.jruby.gemInstallDir = new File(TESTROOT,'fakeGemDir').absolutePath
new File(project.jruby.gemInstallDir,'gems').mkdirs()
new File(project.jruby.gemInstallDir,'gems/fake.txt').text = 'fake.content'
project.with {
jruby {
defaultRepositories = false
warblerBootstrapVersion = '0.1.0'
defaultVersion = jrubyTestVersion
}
repositories {
ivy {
url WARBLER_LOCATION
layout('pattern') {
artifact '[module]-[revision](.[ext])'
}
}
}
dependencies {
jrubyJar 'org.spockframework:spock-core:0.7-groovy-2.0'
}
}
when: "I set the default main class"
project.configure(jarTask) {
archiveName = 'test.jar'
destinationDir = expectedDir
jruby {
defaults 'gems'
mainClass JRubyJarConfigurator.DEFAULT_MAIN_CLASS
configuration 'jrubyJar'
}
}
project.evaluate()
and: "I actually build the JAR"
project.tasks.getByName("${jarTask.name}ExtraManifest").execute()
jarTask.copy()
def builtJar = fileNames(project.zipTree(expectedJar))
then: "I expect to see the JarMain.class embedded in the JAR"
expectedJar.exists()
builtJar.contains('com/lookout/jruby/JarMain.class')
!builtJar.contains('com/lookout/jruby/WarMain.class')
and: "I expect to see jruby-complete packed in libs"
builtJar.contains("META-INF/lib/jruby-complete-${jrubyTestVersion}.jar".toString())
and: "I expect to see manifest to include it"
jarTask.manifest.effectiveManifest.attributes['Class-Path']?.contains("lib/jruby-complete-${jrubyTestVersion}.jar".toString())
}
def "Building a Jar with a custom configuration and 'java' plugin is applied"() {
def "Building a Jar and 'java' plugin is applied"() {
given: "Java plugin applied before JRuby Jar plugin"
project = ProjectBuilder.builder().build()
project.buildDir = TESTROOT
@ -299,27 +179,142 @@ class JRubyJarPluginSpec extends Specification {
jruby {
defaults 'gems'
mainClass 'bogus.does.not.exist'
configuration 'jrubyJar'
}
}
project.evaluate()
and: "I actually build the JAR"
project.tasks.getByName("jarExtraManifest").execute()
jar.copy()
def builtJar = fileNames(project.zipTree(expectedJar))
then: "I expect to see jruby-complete packed in libs"
builtJar.contains("META-INF/lib/jruby-complete-${jrubyTestVersion}.jar".toString())
and: "I expect to see manifest to include it"
jar.manifest.effectiveManifest.attributes['Class-Path']?.contains("lib/jruby-complete-${jrubyTestVersion}.jar".toString())
then: "I don't want to see jruby-complete unpacked"
!builtJar.contains("META-INF/jruby.home/lib/ruby".toString())
and: "I expect the new main class to be listed in the manifest"
jar.manifest.effectiveManifest.attributes['Main-Class']?.contains('bogus.does.not.exist')
}
def "Setting up a java project"() {
given: "All jar, java & shadowJar plugins have been applied"
project = ProjectBuilder.builder().build()
project.buildDir = TESTROOT
project.logging.level = LIFECYCLE
project.apply plugin : 'java'
project.apply plugin: 'com.github.jruby-gradle.jar'
project.apply plugin: 'com.github.johnrengelman.shadow'
Task jar = project.tasks.getByName('jar')
Task shadowJar = project.tasks.getByName('shadowJar')
Task compileJava = project.tasks.getByName('compileJava')
expect:
compileJava.taskDependencies.getDependencies(compileJava).
contains(project.tasks.getByName(JRubyJarPlugin.BOOTSTRAP_TASK_NAME))
jar.taskDependencies.getDependencies(jar).
contains(project.tasks.getByName('jrubyPrepareGems'))
shadowJar.taskDependencies.getDependencies(shadowJar).
contains(project.tasks.getByName('jrubyPrepareGems'))
}
def "Building a ShadowJar with a custom configuration and 'java' plugin is applied"() {
given: "Java plugin applied before JRuby Jar plugin"
project = ProjectBuilder.builder().build()
project.buildDir = TESTROOT
project.logging.level = LIFECYCLE
project.apply plugin : 'java'
project.apply plugin: 'com.github.jruby-gradle.jar'
project.apply plugin: 'com.github.johnrengelman.shadow'
Task jar = project.tasks.getByName('shadowJar')
JavaCompile compile = project.tasks.getByName('compileJava') as JavaCompile
and: "A local repository"
final String jrubyTestVersion = '1.7.15'
File expectedDir= new File(TESTROOT,'libs/')
expectedDir.mkdirs()
File expectedJar= new File(expectedDir,'test-all.jar')
project.jruby.gemInstallDir = new File(TESTROOT,'fakeGemDir').absolutePath
new File(project.jruby.gemInstallDir,'gems').mkdirs()
new File(project.jruby.gemInstallDir,'gems/fake.txt').text = 'fake.content'
project.with {
jruby {
defaultRepositories = false
warblerBootstrapVersion = '0.1.0'
defaultVersion = jrubyTestVersion
}
repositories {
ivy {
url WARBLER_LOCATION
layout('pattern') {
artifact '[module]-[revision](.[ext])'
}
}
}
dependencies {
jrubyJar 'org.spockframework:spock-core:0.7-groovy-2.0'
}
}
when: "I set the default main class"
project.configure(jar) {
archiveName = 'test-all.jar'
destinationDir = expectedDir
jruby {
defaults 'gems'
mainClass 'bogus.does.not.exist'
}
}
project.evaluate()
and: "I actually build the JAR"
jar.copy()
def builtJar = fileNames(project.zipTree(expectedJar))
then: "I expect to see jruby.home unpacked "
builtJar.contains("META-INF/jruby.home/lib/ruby".toString())
and: "To see my fake files in the 'gems' folder"
builtJar.contains("gems/fake.txt".toString())
and: "I expect the new main class to be listed in the manifest"
jar.manifest.effectiveManifest.attributes['Main-Class']?.contains('bogus.does.not.exist')
}
def "Check code generation configuration"() {
given:
project = ProjectBuilder.builder().build()
project.buildDir = TESTROOT
project.logging.level = LIFECYCLE
project.apply plugin: 'com.github.jruby-gradle.jar'
Task generator = project.tasks.getByName(JRubyJarPlugin.BOOTSTRAP_TASK_NAME)
expect:
generator.jruby.getSource().toString().endsWith(BootstrapClassExtension.BOOTSTRAP_TEMPLATE_PATH)
generator.destinationDir == new File(project.buildDir,'generated/java')
}
def "Run code generation"() {
given: "That the jar plugin has been applied"
project = ProjectBuilder.builder().build()
project.buildDir = TESTROOT
project.logging.level = LIFECYCLE
project.apply plugin: 'com.github.jruby-gradle.jar'
Task generator = project.tasks.getByName(JRubyJarPlugin.BOOTSTRAP_TASK_NAME)
File expectedFile = new File(generator.destinationDir,'bootstrap.java')
when: "The task is executed"
project.evaluate()
generator.copy()
then: "Expect to find the generated file"
expectedFile.exists()
and: "The tokens has been replaced"
!expectedFile.text.contains('%%LAUNCH_SCRIPT%%')
expectedFile.text.contains(generator.jruby.initScript)
}
}