Skip to content

Commit

Permalink
Add options for Really Executable Capsules
Browse files Browse the repository at this point in the history
Fixes #3
  • Loading branch information
danthegoodman committed Sep 11, 2014
1 parent 56c0926 commit 3871554
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 53 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
}
}
}
34 changes: 34 additions & 0 deletions src/main/groovy/us/kirchmeier/capsule/task/Capsule.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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() }
}
}
}

Expand All @@ -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()
Expand All @@ -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)
}
}
146 changes: 93 additions & 53 deletions src/test/gradle/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import java.util.jar.Manifest as JarManifest
import java.util.jar.JarFile

buildscript {
dependencies {
Expand All @@ -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'
}
Expand All @@ -24,75 +27,112 @@ 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'
assert jm.getValue('Application-Class') == 'com.foo.Main'
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',
])

0 comments on commit 3871554

Please sign in to comment.