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'