diff --git a/README.md b/README.md index ae04a36..e7b3490 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,36 @@ task myCapsule(type:ThinCapsule){ } ``` +## "Really Executable" Capsules + +`reallyExecutable` will make a capsule executable as a script in unix environments. +You may read more in the [capsule documentation][reallyexec]. + +`reallyExecutable.regular()` is the default and uses a plan execution script. +`reallyExecutable.trampoline()` will use the trompoline script. +`reallyExecutable.script(file)` may be set to define your own script. + +[reallyexec]:https://github.com/puniverse/capsule#really-executable-capsules + +```groovy +task executableCapsule(type:FatCapsule){ + applicationClass 'com.foo.CoolCalculator' + reallyExecutable //implies regular() +} + +task trampolineCapsule(type:ThinCapsule){ + applicationClass 'com.foo.CoolCalculator' + reallyExecutable { trampoline() } +} + +task myExecutableCapsule(type:FatCapsule){ + applicationClass 'com.foo.CoolCalculator' + reallyExecutable { + script file('my_script.sh') + } +} +``` + ## Changing the capsule implementation For advanced usage, `capsuleConfiguration` and `capsuleFilter` control where the capsule implementation comes from. diff --git a/src/main/groovy/us/kirchmeier/capsule/spec/ReallyExecutableSpec.groovy b/src/main/groovy/us/kirchmeier/capsule/spec/ReallyExecutableSpec.groovy new file mode 100644 index 0000000..7cfc8db --- /dev/null +++ b/src/main/groovy/us/kirchmeier/capsule/spec/ReallyExecutableSpec.groovy @@ -0,0 +1,51 @@ +package us.kirchmeier.capsule.spec + +import org.gradle.api.Project + +class ReallyExecutableSpec { + + File script + protected boolean _regular = true + protected boolean _trampoline = false + + ReallyExecutableSpec regular() { + _regular = true + _trampoline = false + script = null + return this + } + + ReallyExecutableSpec trampoline() { + _regular = false + _trampoline = true + script = null + return this + } + + void setScript(File file) { + if (file != null) { + _regular = false + _trampoline = false + script = file + } + } + + ReallyExecutableSpec script(File file) { + script = file + return this + } + + def buildAntResource(Project project, AntBuilder ant) { + if (script != null) { + return ant.file(file: script) + } + + def cap = project.configurations.capsule.files.first() + + if (_trampoline) { + return ant.zipentry(zipfile: cap, name: 'capsule/trampoline-execheader.sh') + } else if (_regular) { + return ant.zipentry(zipfile: cap, name: 'capsule/execheader.sh') + } + } +} diff --git a/src/main/groovy/us/kirchmeier/capsule/task/Capsule.groovy b/src/main/groovy/us/kirchmeier/capsule/task/Capsule.groovy index e29ee2e..4b9160c 100644 --- a/src/main/groovy/us/kirchmeier/capsule/task/Capsule.groovy +++ b/src/main/groovy/us/kirchmeier/capsule/task/Capsule.groovy @@ -4,6 +4,7 @@ import org.gradle.api.artifacts.Configuration import org.gradle.api.tasks.bundling.Jar import org.gradle.util.ConfigureUtil import us.kirchmeier.capsule.manifest.CapsuleManifest +import us.kirchmeier.capsule.spec.ReallyExecutableSpec class Capsule extends Jar { /** @@ -37,12 +38,17 @@ class Capsule extends Jar { CapsuleManifest capsuleManifest = new CapsuleManifest() + protected ReallyExecutableSpec _reallyExecutable = null + Capsule() { capsuleConfiguration = project.configurations.capsule classifier = 'capsule' project.gradle.afterProject { finalizeSettings() + if(_reallyExecutable != null){ + doLast { makeReallyExecutable() } + } } } @@ -69,6 +75,22 @@ class Capsule extends Jar { return this; } + public ReallyExecutableSpec getReallyExecutable(){ + if(_reallyExecutable == null){ + _reallyExecutable = new ReallyExecutableSpec(); + } + return _reallyExecutable; + } + + public void setReallyExecutable(ReallyExecutableSpec spec) { + _reallyExecutable = spec + } + + public Capsule reallyExecutable(@DelegatesTo(ReallyExecutableSpec) Closure configureClosure) { + ConfigureUtil.configure(configureClosure, getReallyExecutable()); + return this; + } + protected void finalizeSettings() { applyDefaultCapsuleSet() applyApplicationSource() @@ -94,4 +116,16 @@ class Capsule extends Jar { from { embedConfiguration } } + + protected void makeReallyExecutable() { + def f = File.createTempFile("cap", null) + ant.concat(destfile: f, binary: true) { + _reallyExecutable.buildAntResource(project, ant) + fileset(dir: destinationDir) { + include(name: archiveName) + } + } + ant.chmod(file: f, perm: 'ug+x', osfamily: 'unix') + f.renameTo(outputs.files.singleFile) + } } diff --git a/src/test/gradle/build.gradle b/src/test/gradle/build.gradle index 92809e3..10c7e31 100644 --- a/src/test/gradle/build.gradle +++ b/src/test/gradle/build.gradle @@ -1,4 +1,4 @@ -import java.util.jar.Manifest as JarManifest +import java.util.jar.JarFile buildscript { dependencies { @@ -9,13 +9,16 @@ buildscript { apply plugin: 'java' apply plugin: 'us.kirchmeier.capsule' +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + repositories { mavenCentral() } dependencies { compile('org.apache.ant:ant:1.9.3') { - exclude module: 'ant-launcher' + exclude module: 'ant-launcher' } runtime 'junit:junit:4.11' } @@ -24,66 +27,108 @@ jar { baseName 'test-project' } +ext.selfTest = task('self-test') + task fatCapsule(type: FatCapsule) { applicationClass 'com.foo.Main' classifier 'fat' + buildTests(delegate, executable: false, fat: true) +} + +task fatCapsuleExecutable(type: FatCapsule) { + applicationClass 'com.foo.Main' + classifier 'fatExec' + reallyExecutable + buildTests(delegate, executable: true, fat: true) } task thinCapsule(type: ThinCapsule) { applicationClass 'com.foo.Main' classifier 'thin' + buildTests(delegate, executable: false, thin: true) +} +task thinCapsuleExecutable(type: ThinCapsule) { + applicationClass 'com.foo.Main' + classifier 'thinExec' + reallyExecutable { trampoline() } + buildTests(delegate, executable: true, thin: true) +} + +private void buildTests(Map options, Task t) { + def name = t.name + def f = t.outputs.files.singleFile + + project.task("test-output-$name", dependsOn: [t]) << { + testOutput(f, options) + } + project.task("test-contents-$name", dependsOn: [t]) << { + testContents(f, options) + } + selfTest.dependsOn.addAll([ + "test-output-$name", + "test-contents-$name" + ]) } -task 'test-fatCapsule'(dependsOn: 'fatCapsule') << { - def t = tasks.getByName('fatCapsule') - def tree = zipTree(t.outputs.files.singleFile) - - def paths = [] - tree.visit { paths << it.path.toString() } - paths.sort() - assert paths == [ - 'Capsule.class', - 'META-INF', - 'META-INF/MANIFEST.MF', - 'ant-1.9.3.jar', - 'hamcrest-core-1.3.jar', - 'junit-4.11.jar', - 'test-project.jar', - ] - - def manifest = tree.matching({ include 'META-INF/MANIFEST.MF' }).singleFile - manifest.withInputStream { is -> - def jm = new JarManifest(is).mainAttributes +private void testOutput(f, options) { + def cmd = options.executable ? [f.absolutePath] : ['java', '-jar', f.absolutePath] + def proc = cmd.execute(null, null) + def out = new ByteArrayOutputStream() + proc.consumeProcessOutput(out, System.err) + int exitCode = proc.waitFor() + def strOut = new String(out.toByteArray()); + assert strOut == 'Hello World\n' + assert exitCode == 0 +} + +private void testContents(file, options) { + def f = new JarFile(file, false) + def capsuleClasses = 0 + def projectFiles = [] + f.entries().each { + if (it.name.startsWith('capsule/')) { + capsuleClasses += 1 + } else { + projectFiles << it.name + } + } + + projectFiles.sort() + if (options.fat) { + assert projectFiles == [ + 'Capsule.class', + 'META-INF/', + 'META-INF/MANIFEST.MF', + 'ant-1.9.3.jar', + 'hamcrest-core-1.3.jar', + 'junit-4.11.jar', + 'test-project.jar', + ] + } else if (options.thin) { + assert projectFiles == [ + 'Capsule.class', + 'META-INF/', + 'META-INF/MANIFEST.MF', + 'com/', + 'com/foo/', + 'com/foo/HelloWorld.class', + 'com/foo/Main.class', + ] + } + + if (options.fat) { + assert capsuleClasses == 0 + } else if (options.thin) { + assert capsuleClasses > 1000 + } + + def jm = f.manifest.mainAttributes + if (options.fat) { assert jm.size() == 3 assert jm.getValue('Manifest-Version') != null assert jm.getValue('Main-Class') == 'Capsule' assert jm.getValue('Application-Class') == 'com.foo.Main' - } -} - -task 'test-thinCapsule'(dependsOn: 'thinCapsule') << { - def t = tasks.getByName('thinCapsule') - def tree = zipTree(t.outputs.files.singleFile) - - def paths = [] - tree.matching({ exclude 'capsule/' }) visit { paths << it.path.toString() } - paths.sort() - assert paths == [ - 'Capsule.class', - 'META-INF', - 'META-INF/MANIFEST.MF', - 'com', - 'com/foo', - 'com/foo/HelloWorld.class', - 'com/foo/Main.class', - ] - - def capsuleClases = tree.matching({ include 'capsule/' }).files.size() - assert capsuleClases > 1000 - - def manifest = tree.matching({ include 'META-INF/MANIFEST.MF' }).singleFile - manifest.withInputStream { is -> - def jm = new JarManifest(is).mainAttributes + } else if (options.thin) { assert jm.size() == 4 assert jm.getValue('Manifest-Version') != null assert jm.getValue('Main-Class') == 'Capsule' @@ -91,8 +136,3 @@ task 'test-thinCapsule'(dependsOn: 'thinCapsule') << { assert jm.getValue('Dependencies') == 'junit:junit:4.11 org.apache.ant:ant:1.9.3(*:ant-launcher)' } } - -task 'self-test'(dependsOn: [ - 'test-fatCapsule', - 'test-thinCapsule', -])