diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6891e6e828..708384f2c6 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -52,6 +52,12 @@
+
+
+
+
+
+
@@ -98,6 +104,8 @@
+
+
diff --git a/launcher/build.gradle b/launcher/build.gradle
new file mode 100644
index 0000000000..0f4ad30356
--- /dev/null
+++ b/launcher/build.gradle
@@ -0,0 +1,73 @@
+group 'com.r3.corda'
+version 'R3.CORDA-3.0-SNAPSHOT'
+
+apply plugin: 'java'
+apply plugin: 'kotlin'
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
+ compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+}
+
+jar {
+ baseName 'corda-launcher'
+}
+
+//task gatherDependencies(type: Copy, dependsOn: jar) {
+// from configurations.runtime
+// from jar
+// into "$buildDir/bin/lib"
+//}
+//
+//def launcherOutputDir = null
+//
+//task makeExecutable(type: Exec, dependsOn: [gatherDependencies]) {
+// def isLinux = System.properties['os.name'].toLowerCase().contains('linux')
+// def isMac = System.properties['os.name'].toLowerCase().contains('mac')
+//
+// if (!isLinux && !isMac)
+// throw new GradleException("Preparing distribution package is currently only supported on Linux/Mac")
+//
+// def distributionDir = "${buildDir}/tmp/"
+// if (isLinux) launcherOutputDir = "${distributionDir}/bundles/launcher/"
+// else launcherOutputDir = "${distributionDir}/bundles/launcher.app/Contents"
+//
+// def classPath = []
+//
+// workingDir project.projectDir
+//
+// def extraArgs = [
+// "-BjvmOptions=-javaagent:quasar-core-${quasar_version}-jdk8.jar=${project(':node:capsule').quasarExcludeExpression}",
+// '-BuserJvmOptions=-Xmx=4g',
+// '-BuserJvmOptions=-XX\\:=+UseG1GC',
+// '-BjvmProperties=java.system.class.loader=net.corda.launcher.Loader'
+// ]
+//
+// doFirst {
+// def dependencies = []
+//
+// fileTree(gatherDependencies.destinationDir).forEach({ file ->
+// classPath.add("../../lib/" + file.getName())
+// })
+//
+// commandLine = [
+// 'javapackager',
+// '-deploy',
+// '-nosign',
+// '-native', 'image',
+// '-outdir', "$distributionDir",
+// '-outfile', 'launcher',
+// '-name', 'launcher',
+// "-BmainJar=${jar.archiveName}",
+// "-Bclasspath=${classPath.join(":")}",
+// '-appclass', 'net.corda.launcher.Launcher',
+// '-srcdir', "${gatherDependencies.destinationDir}",
+// '-srcfiles', "${jar.archiveName}"
+// ] + extraArgs
+// }
+//}
+//
+//task exportLauncher(type: Copy, dependsOn: makeExecutable ) {
+// from launcherOutputDir into "${buildDir}/bin"
+//}
+
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
new file mode 100644
index 0000000000..88ad330c89
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
@@ -0,0 +1,64 @@
+@file:JvmName("Launcher")
+
+package net.corda.launcher
+
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.system.exitProcess
+
+fun main(args: Array) {
+
+ val sysClassLoader = ClassLoader.getSystemClassLoader()
+
+ val appClassLoader = (sysClassLoader as? Loader) ?: {
+ println("WARNING: failed to overried system classloader")
+ Loader(sysClassLoader)
+ } ()
+
+ if(args.isEmpty()) {
+ println("Usage: launcher ")
+ exitProcess(0)
+ }
+
+ // Resolve plugins directory and extend classpath
+ val nodeBaseDir = Settings.WORKING_DIR
+ .resolve(getBaseDirectory(args) ?: ".")
+ .toAbsolutePath()
+
+ val pluginURLs = Settings.PLUGINS.flatMap {
+ val entry = nodeBaseDir.resolve(it)
+ if (Files.isDirectory(entry)) {
+ entry.jarFiles()
+ } else {
+ setOf(entry)
+ }
+ }.map { it.toUri().toURL() }
+
+ appClassLoader.augmentClasspath(pluginURLs)
+
+ // Propagate current working directory, as workaround for javapackager
+ // corrupting it
+ System.setProperty("corda.launcher.cwd", nodeBaseDir.toString())
+ System.setProperty("user.dir", nodeBaseDir.toString())
+
+ try {
+ appClassLoader
+ .loadClass(args[0])
+ .getMethod("main", Array::class.java)
+ .invoke(null, args.sliceArray(1..args.lastIndex))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ exitProcess(1)
+ }
+}
+
+private fun getBaseDirectory(args: Array): String? {
+ val idx = args.indexOf("--base-directory")
+ return if (idx != -1 && idx < args.lastIndex) {
+ args[idx + 1]
+ } else null
+}
+
+private fun Path.jarFiles(): List {
+ return Files.newDirectoryStream(this).filter { it.toString().endsWith(".jar") }
+}
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Loader.kt b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt
new file mode 100644
index 0000000000..c2d549b219
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt
@@ -0,0 +1,12 @@
+package net.corda.launcher
+
+import java.net.URL
+import java.net.URLClassLoader
+
+class Loader(parent: ClassLoader?):
+ URLClassLoader(Settings.CLASSPATH.toTypedArray(), parent) {
+
+ fun augmentClasspath(urls: List) {
+ urls.forEach { addURL(it) }
+ }
+}
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Settings.kt b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt
new file mode 100644
index 0000000000..1790d193f1
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt
@@ -0,0 +1,46 @@
+package net.corda.launcher
+
+import java.io.FileInputStream
+import java.net.URL
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+import kotlin.collections.HashSet
+
+object Settings {
+
+ val WORKING_DIR: Path
+ val CLASSPATH: List
+ val PLUGINS: List
+ val CORDA_RUNTIME_SETTINGS = "../runtime.properties"
+
+ init {
+ WORKING_DIR = Paths.get(System.getenv("CORDA_LAUNCHER_CWD") ?: "..")
+
+ val settings = Properties().apply {
+ load(FileInputStream(CORDA_RUNTIME_SETTINGS))
+ }
+
+ CLASSPATH = parseClasspath(settings)
+ PLUGINS = parsePlugins(settings)
+ }
+
+ private fun parseClasspath(config: Properties): List {
+ val launcherDir = Paths.get("..").toAbsolutePath()
+ val cp = config.getProperty("classpath") ?:
+ throw Error("Missing 'classpath' property from config")
+
+ return cp.split(':').map {
+ launcherDir.resolve(it).toUri().toURL()
+ }
+ }
+
+ private fun parsePlugins(config: Properties): List {
+ val ext = config.getProperty("plugins")
+
+ return ext?.let {
+ it.split(':').map { Paths.get(it) }
+ } ?: emptyList()
+ }
+}
diff --git a/node/build.gradle b/node/build.gradle
index 7800ff24c5..a51b525344 100644
--- a/node/build.gradle
+++ b/node/build.gradle
@@ -71,6 +71,8 @@ dependencies {
compile project(":confidential-identities")
compile project(':client:rpc')
compile project(':tools:shell')
+ runtime project(':launcher')
+
compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// Log4J: logging framework (with SLF4J bindings)
diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle
index aab294cda1..e4e8984eb3 100644
--- a/node/capsule/build.gradle
+++ b/node/capsule/build.gradle
@@ -28,6 +28,11 @@ dependencies {
capsuleRuntime "com.typesafe:config:$typesafe_config_version"
}
+ext {
+ quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
+ applicationClass = 'net.corda.node.Corda'
+}
+
// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards
// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
@@ -37,8 +42,8 @@ sourceCompatibility = 1.6
targetCompatibility = 1.6
task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
- applicationClass 'net.corda.node.Corda'
archiveName "corda-r3-${corda_release_version}.jar"
+ applicationClass 'net.corda.node.Corda'
applicationSource = files(
project(':node').configurations.runtime,
project(':node').jar,
@@ -54,7 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
applicationVersion = corda_release_version
// See experimental/quasar-hook/README.md for how to generate.
- def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
minJavaVersion = '1.8.0'
diff --git a/node/dist/README.md b/node/dist/README.md
new file mode 100644
index 0000000000..cea059c4d9
--- /dev/null
+++ b/node/dist/README.md
@@ -0,0 +1,5 @@
+This project adds `buildCordaTarball` task to Gradle. It prepares distributable tarball with JRE built-in, using ``javapackager``
+
+For now, it packs the whatever JRE is available in the system, but this will get standarised over time.
+
+It requires ``javapackager`` to be available in the path.
\ No newline at end of file
diff --git a/node/dist/build.gradle b/node/dist/build.gradle
new file mode 100644
index 0000000000..cf1636c907
--- /dev/null
+++ b/node/dist/build.gradle
@@ -0,0 +1,146 @@
+description 'Corda Node Executable Image'
+
+evaluationDependsOn(":node")
+evaluationDependsOn(":docs")
+evaluationDependsOn(":launcher")
+
+def outputDir = "$buildDir/release"
+
+configurations {
+ launcherClasspath
+}
+
+sourceSets {
+ binFiles {
+ resources {
+ srcDir file('src/main/resources/bin')
+ }
+ }
+ licenseFiles {
+ resources {
+ srcDir file('src/main/resources/license')
+ }
+ }
+}
+
+dependencies {
+ launcherClasspath "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
+ launcherClasspath "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+ launcherClasspath "org.slf4j:jul-to-slf4j:$slf4j_version"
+ launcherClasspath "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
+ launcherClasspath "org.apache.logging.log4j:log4j-web:${log4j_version}"
+ launcherClasspath "com.google.guava:guava:$guava_version"
+ launcherClasspath "de.javakaffee:kryo-serializers:0.41"
+ launcherClasspath project(':launcher')
+}
+
+task copyLauncherLibs(type: Copy, dependsOn: [project(':launcher').jar]) {
+ from configurations.launcherClasspath
+ into "$buildDir/tmp/launcher-lib"
+}
+
+task buildLauncher(type: Exec, dependsOn: [copyLauncherLibs]) {
+ def isLinux = System.properties['os.name'].toLowerCase().contains('linux')
+ def isMac = System.properties['os.name'].toLowerCase().contains('mac')
+
+ if (!isLinux && !isMac)
+ throw new GradleException("Preparing distribution package is currently only supported on Linux/Mac")
+
+ def distributionDir = "${buildDir}/tmp/"
+
+ def relativeDir
+ if (isLinux) relativeDir = "launcher"
+ else relativeDir = "launcher.app/Contents"
+
+ ext {
+ launcherBinDir = "${distributionDir}/bundles/$relativeDir"
+ }
+
+ workingDir project.projectDir
+
+ def extraArgs = [
+ "-BjvmOptions=-javaagent:../../lib/quasar-core-${quasar_version}-jdk8.jar=${project(':node:capsule').quasarExcludeExpression}",
+ '-BuserJvmOptions=-Xmx=4g',
+ '-BuserJvmOptions=-XX\\:=+UseG1GC',
+ '-BjvmProperties=java.system.class.loader=net.corda.launcher.Loader'
+ ]
+
+ doFirst {
+ def launcherLib = copyLauncherLibs.destinationDir
+ def srcfiles = []
+ def classpath = []
+
+ fileTree(launcherLib).forEach({ file ->
+ srcfiles.add("-srcfiles")
+ srcfiles.add(file.name)
+ classpath.add(file.name)
+ })
+
+ commandLine = [
+ 'javapackager',
+ '-deploy',
+ '-nosign',
+ '-native', 'image',
+ '-outdir', "$distributionDir",
+ '-outfile', 'launcher',
+ '-name', 'launcher',
+ "-BmainJar=${project(':launcher').jar.archiveName}",
+ "-Bclasspath=${classpath.join(":")}",
+ '-appclass', 'net.corda.launcher.Launcher',
+ '-srcdir', "$launcherLib"
+ ] + srcfiles + extraArgs
+ }
+}
+
+task installNodeLib(type: Copy, dependsOn: [project(':node').jar]) {
+ from project(':node').configurations.runtime
+ from project(':node').jar
+ into "${outputDir}/lib"
+}
+
+task installLauncher(type: Copy, dependsOn: [buildLauncher, installNodeLib]) {
+ from buildLauncher.launcherBinDir
+ into "${outputDir}/launcher"
+
+ doLast {
+ def classpath = []
+
+ fileTree("${outputDir}/lib").forEach({ file ->
+ classpath.add("../lib/" + file.getName())
+ })
+
+ new File("${outputDir}/launcher/runtime.properties").text = [
+ "classpath=${classpath.join(':')}",
+ "plugins=./drivers:./cordapps"].join("\n")
+ }
+}
+
+task installStartupScripts(type: Copy) {
+ from sourceSets.binFiles.resources
+ into "$outputDir/bin"
+}
+
+task installReadmeFiles(type: Copy) {
+ from sourceSets.licenseFiles.resources
+ into "$outputDir"
+}
+
+task installDocs(type: Copy, dependsOn: [project(':docs').tasks['makeDocs']]) {
+ from(project(':docs').buildDir)
+ into "$outputDir/docs"
+}
+
+task buildNode(dependsOn: [installLauncher,
+ installNodeLib,
+ installDocs,
+ installStartupScripts,
+ installReadmeFiles]) {
+
+ doLast {
+ new File("${outputDir}/cordapps").mkdirs()
+ new File("${outputDir}/drivers").mkdirs()
+ println ("Stand-alone Corda Node application available at:")
+ println ("${outputDir}")
+ }
+}
+
diff --git a/node/dist/src/main/resources/bin/corda b/node/dist/src/main/resources/bin/corda
new file mode 100755
index 0000000000..ad5ce399d7
--- /dev/null
+++ b/node/dist/src/main/resources/bin/corda
@@ -0,0 +1,20 @@
+#!/bin/sh
+# ------------------------
+# Corda startup script
+# -------------------------
+
+MAINCLASSNAME="net.corda.node.Corda"
+READLINK=`which readlink`
+
+# Locate this script and relative launcher executable
+SCRIPT_LOCATION=$0
+if [ -x "$READLINK" ]; then
+ while [ -L "$SCRIPT_LOCATION" ]; do
+ SCRIPT_LOCATION=`"$READLINK" -e "$SCRIPT_LOCATION"`
+ done
+fi
+SCRIPT_DIR=`dirname "$SCRIPT_LOCATION"`
+LAUNCHER_LOCATION="$SCRIPT_DIR/../launcher/launcher"
+
+# Run Corda
+CORDA_LAUNCHER_CWD="`pwd`" ${LAUNCHER_LOCATION} ${MAINCLASSNAME} "$@"
diff --git a/node/dist/src/main/resources/license/README b/node/dist/src/main/resources/license/README
new file mode 100644
index 0000000000..923133ee85
--- /dev/null
+++ b/node/dist/src/main/resources/license/README
@@ -0,0 +1,11 @@
+Welcome to Corda Enterprise!
+
+This is a distributon package containing the Java Runtime Environment for convenience.
+
+To start a node, please edit supplied node.conf file so it contains appropriate data for your organization. More - https://docs.corda.net/corda-configuration-file.html
+
+Your CordApps should be placed in cordapps directory, from which they will be loaded automatically.
+
+Linux:
+Main executable file is corda - run by simply ./corda
+MacOS: Main executable file is MacOS/corda - run simply by typing ./MacOS/corda
diff --git a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
index 4e885beea9..fcc61342de 100644
--- a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
+++ b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
@@ -68,7 +68,14 @@ class NodeArgsParser : AbstractArgsParser() {
require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) {
"${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together"
}
- val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
+ // Note: this is a workaround for javapackager misbehaving with cwd.
+ // The correct working directory is propagated from launcher via system property.
+
+ val baseDirectory = System.getProperty("corda.launcher.cwd")?.let { Paths.get(it) }
+ ?: optionSet.valueOf(baseDirectoryArg)
+ .normalize()
+ .toAbsolutePath()
+
val configFile = baseDirectory / optionSet.valueOf(configFileArg)
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
diff --git a/settings.gradle b/settings.gradle
index 82ada2052d..e9771e123c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -82,3 +82,5 @@ project(':hsm-tool').with {
name = 'sgx-hsm-tool'
projectDir = file("$settingsDir/sgx-jvm/hsm-tool")
}
+include 'launcher'
+include 'node:dist'