Merged in clint-gradleplugins (pull request )

Added Gradle Plugins required for Cordapp development
This commit is contained in:
Clinton Alexander 2016-10-11 16:57:25 +01:00
commit d2983d6a7a
21 changed files with 718 additions and 5 deletions
build.gradle
core
docs/source
gradle-plugins
README.rst
cordformation
README.rstbuild.gradle
src/main
groovy/com/r3corda/plugins
resources
META-INF/gradle-plugins
com/r3corda/plugins
publish-utils
README.rstbuild.gradle
src/main
groovy/com/r3corda/plugins
resources/META-INF/gradle-plugins
quasar-utils
README.rstbuild.gradle
src/main
groovy/com/r3corda/plugins
resources/META-INF/gradle-plugins
settings.gradle
src/main/java

@ -264,6 +264,9 @@ task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
}
}
// Aliasing the publishToMavenLocal for simplicity.
task(install, dependsOn: 'publishToMavenLocal')
publishing {
publications {
corda(MavenPublication) {

@ -1,7 +1,6 @@
apply plugin: 'kotlin'
apply plugin: QuasarPlugin
// Applying the maven plugin means this will get installed locally when running "gradle install"
apply plugin: 'maven'
apply plugin: DefaultPublishTasks
buildscript {

@ -106,10 +106,58 @@ root directory of Corda
This will publish corda-$version.jar, contracts-$version.jar, core-$version.jar and node-$version.jar to the
group com.r3corda. You can now depend on these as you normally would a Maven dependency.
In Gradle you can depend on these by adding/modifying your build.gradle file to contain the following:
Gradle Plugins for Cordapps
===========================
There are several Gradle plugins that reduce your build.gradle boilerplate and make development of Cordapps easier.
The available plugins are in the gradle-plugins directory of the Corda repository.
Building Gradle Plugins
-----------------------
To install to your local Maven repository the plugins that Cordapp gradle files require, run the following from the
root of the Corda project:
.. code-block:: text
./gradlew publishToMavenLocal
The plugins will now be installed to your local Maven repository in ~/.m2 on Unix and %HOMEPATH%\.m2 on Windows.
Using Gradle Plugins
--------------------
To use the plugins, if you are not already using the Cordapp template project, you must modify your build.gradle. Add
the following segments to the relevant part of your build.gradle.
Template build.gradle
=====================
To build against Corda and the plugins that cordapps use, update your build.gradle to contain the following:
.. code-block:: groovy
buildscript {
ext.corda_version = '<enter the corda version you build against here>'
... your buildscript ...
repositories {
... other repositories ...
mavenLocal()
}
dependencies {
... your dependencies ...
classpath "com.r3corda.plugins:cordformation:$corda_version"
classpath "com.r3corda.plugins:quasar-utils:$corda_version"
classpath "com.r3corda.plugins:publish-utils:$corda_version"
}
}
apply plugin: 'com.r3corda.plugins.cordformation'
apply plugin: 'com.r3corda.plugins.quasar-utils'
apply plugin: 'com.r3corda.plugins.publish-utils'
repositories {
mavenLocal()
... other repositories here ...
@ -122,3 +170,78 @@ In Gradle you can depend on these by adding/modifying your build.gradle file to
compile "com.r3corda:corda:$corda_version"
... other dependencies here ...
}
... your tasks ...
// Sets the classes for Quasar to scan. Loaded by the the quasar-utils plugin.
quasarScan.dependsOn('classes', ... your dependent subprojects...)
// Standard way to publish Cordapps to maven local with the maven-publish and publish-utils plugin.
publishing {
publications {
jarAndSources(MavenPublication) {
from components.java
// The two lines below are the tasks added by this plugin.
artifact sourceJar
artifact javadocJar
}
}
}
Cordformation
=============
Cordformation is the local node deployment system for Cordapps, the nodes generated are intended to be used for
experimenting, debugging, and testing node configurations and setups but not intended for production or testnet
deployment.
To use this gradle plugin you must add a new task that is of the type `com.r3corda.plugins.Cordform` to your
build.gradle and then configure the nodes you wish to deploy with the Node and nodes configuration DSL.
This DSL is specified in the `JavaDoc <api/index.html>`_. An example of this is in the template-cordapp and below
is a three node example;
.. code-block:: text
task deployNodes(type: com.r3corda.plugins.Cordform, dependsOn: ['build']) {
directory "./build/nodes" // The output directory
networkMap "Controller" // The artemis address of the node named here will be used as the networkMapAddress on all other nodes.
node {
name "Controller"
dirName "controller"
nearestCity "London"
notary true // Sets this node to be a notary
advertisedServices []
artemisPort 12345
webPort 12346
cordapps []
}
node {
name "NodeA"
dirName "nodea"
nearestCity "London"
advertisedServices []
artemisPort 31337
webPort 31339
cordapps []
}
node {
name "NodeB"
dirName "nodeb"
nearestCity "New York"
advertisedServices []
artemisPort 31338
webPort 31340
cordapps []
}
}
You can create more configurations with new tasks that extend Cordform.
New nodes can be added by simply adding another node block and giving it a different name, directory and ports. When you
run this task it will install the nodes to the directory specified and a script will be generated (for UNIX users only
at present) to run the nodes with one command.
Other cordapps can also be specified if they are already specified as classpath or compile dependencies in your
build.gradle.

11
gradle-plugins/README.rst Normal file

@ -0,0 +1,11 @@
Gradle Plugins for Cordapps
===========================
The projects at this level of the project are gradle plugins for cordapps and are published to Maven Local with
the rest of the Corda libraries.
.. note::
Some of the plugins here are duplicated with the ones in buildSrc. While the duplication is unwanted any
currently known solution (such as publishing from buildSrc or setting up a separate project/repo) would
introduce a two step build which is less convenient.

@ -0,0 +1 @@
Please refer to the documentation in <corda-root>/doc/build/html/creating-a-cordapp.html#cordformation.

@ -0,0 +1,23 @@
apply plugin: 'maven-publish'
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
compile "com.typesafe:config:1.3.0"
}
repositories {
mavenCentral()
}
publishing {
publications {
plugin(MavenPublication) {
from components.java
groupId 'com.r3corda.plugins'
artifactId 'cordformation'
}
}
}

@ -0,0 +1,92 @@
package com.r3corda.plugins
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.nio.file.Path
import java.nio.file.Paths
import org.gradle.api.Project
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
class Cordform extends DefaultTask {
protected Path directory = Paths.get("./build/nodes")
protected List<Node> nodes = new ArrayList<Node>()
protected String networkMapNodeName
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
* @return
*/
public void directory(String directory) {
this.directory = Paths.get(directory)
}
/**
* Set the network map node.
*
* @warning Ensure the node name is one of the configured nodes.
* @param nodeName The name of the node that will host the network map.
*/
public void networkMap(String nodeName) {
networkMapNodeName = nodeName
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
public void node(Closure configureClosure) {
nodes << project.configure(new Node(project), configureClosure)
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
protected Node getNodeByName(String name) {
for(Node node : nodes) {
if(node.name.equals(networkMapNodeName)) {
return node
}
}
return null
}
/**
* Installs the run script into the nodes directory.
*/
protected void installRunScript() {
project.copy {
from Cordformation.getPluginFile(project, "com/r3corda/plugins/runnodes")
filter { String line -> line.replace("JAR_NAME", Node.JAR_NAME) }
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
into "${directory}/"
}
}
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@TaskAction
void build() {
installRunScript()
Node networkMapNode = getNodeByName(networkMapNodeName)
nodes.each {
if(it != networkMapNode) {
it.networkMapAddress(networkMapNode.getArtemisAddress())
}
it.build(directory.toFile())
}
}
}

@ -0,0 +1,27 @@
package com.r3corda.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation,
* testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner.
*/
class Cordformation implements Plugin<Project> {
void apply(Project project) {
}
/**
* Gets a resource file from this plugin's JAR file.
*
* @param project The project environment this plugin executes in.
* @param filePathInJar The file in the JAR, relative to root, you wish to access.
* @return A file handle to the file in the JAR.
*/
static File getPluginFile(Project project, String filePathInJar) {
return project.resources.text.fromArchiveEntry(project.buildscript.configurations.classpath.find {
it.name.contains('cordformation')
}, filePathInJar).asFile()
}
}

@ -0,0 +1,240 @@
package com.r3corda.plugins
import org.gradle.api.internal.file.AbstractFileCollection
import org.gradle.api.Project
import java.nio.file.Files
import java.nio.charset.StandardCharsets
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.ConfigRenderOptions
/**
* Represents a node that will be installed.
*/
class Node {
static final String JAR_NAME = 'corda.jar'
static final String DEFAULT_HOST = 'localhost'
/**
* Name of the node.
*/
public String name
/**
* A list of advertised services ID strings.
*/
protected List<String> advertisedServices = []
/**
* Set thThe list of cordapps to install to the plugins directory.
*
* @note Your app will be installed by default and does not need to be included here.
*/
protected List<String> cordapps = []
private String dirName
private Config config = ConfigFactory.empty()
//private Map<String, Object> config = new HashMap<String, Object>()
private File nodeDir
private def project
/**
* Set the name of the node.
*
* @param name The node name.
*/
void name(String name) {
this.name = name
config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name))
}
/**
* Set the directory the node will be installed to relative to the directory specified in Cordform task.
*
* @param dirName Subdirectory name for node to be installed to. Must be valid directory name on all OSes.
*/
void dirName(String dirName) {
this.dirName = dirName
config = config.withValue("basedir", ConfigValueFactory.fromAnyRef(dirName))
}
/**
* Set the nearest city to the node.
*
* @param nearestCity The name of the nearest city to the node.
*/
void nearestCity(String nearestCity) {
config = config.withValue("nearestCity", ConfigValueFactory.fromAnyRef(nearestCity))
}
/**
* Sets whether this node will use HTTPS communication.
*
* @param isHttps True if this node uses HTTPS communication.
*/
void https(Boolean isHttps) {
config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps))
}
/**
* Set the artemis port for this node.
*
* @param artemisPort The artemis messaging queue port.
*/
void artemisPort(Integer artemisPort) {
config = config.withValue("artemisAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$artemisPort".toString()))
}
/**
* Set the HTTP web server port for this node.
*
* @param webPort The web port number for this node.
*/
void webPort(Integer webPort) {
config = config.withValue("webAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort".toString()))
}
/**
* Set the network map address for this node.
*
* @warning This should not be directly set unless you know what you are doing. Use the networkMapName in the
* Cordform task instead.
* @param networkMapAddress Network map address.
*/
void networkMapAddress(String networkMapAddress) {
config = config.withValue("networkMapAddress",
ConfigValueFactory.fromAnyRef(networkMapAddress))
}
Node(Project project) {
this.project = project
}
/**
* Install the nodes to the given base directory.
*
* @param baseDir The base directory for this node. All other paths are relative to it + this nodes dir name.
*/
void build(File baseDir) {
nodeDir = new File(baseDir, dirName)
installCordaJAR()
installBuiltPlugin()
installCordapps()
installDependencies()
installConfig()
}
/**
* Get the artemis address for this node.
*
* @return This node's artemis address.
*/
String getArtemisAddress() {
return config.getString("artemisAddress")
}
/**
* Installs the corda fat JAR to the node directory.
*/
private void installCordaJAR() {
def cordaJar = verifyAndGetCordaJar()
project.copy {
from cordaJar
into nodeDir
rename cordaJar.name, JAR_NAME
}
}
/**
* Installs this project's cordapp to this directory.
*/
private void installBuiltPlugin() {
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins")
project.copy {
from project.jar
into pluginsDir
}
}
/**
* Installs other cordapps to this node's plugins directory.
*/
private void installCordapps() {
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins")
def cordapps = getCordappList()
project.copy {
from cordapps
into pluginsDir
}
}
/**
* Installs other dependencies to this node's dependencies directory.
*/
private void installDependencies() {
def cordaJar = verifyAndGetCordaJar()
def cordappList = getCordappList()
def depsDir = getAndCreateDirectory(nodeDir, "dependencies")
def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappList.contains(it) }
project.copy {
from appDeps
into depsDir
}
}
/**
* Installs the configuration file to this node's directory and detokenises it.
*/
private void installConfig() {
// Adding required default values
config = config.withValue('extraAdvertisedServiceIds',
ConfigValueFactory.fromAnyRef(advertisedServices.join(',')))
def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList()
Files.write(new File(nodeDir, 'node.conf').toPath(), configFileText, StandardCharsets.UTF_8)
}
/**
* Find the corda JAR amongst the dependencies.
*
* @return A file representing the Corda JAR.
*/
private File verifyAndGetCordaJar() {
def maybeCordaJAR = project.configurations.runtime.filter { it.toString().contains("corda-${project.corda_version}.jar")}
if(maybeCordaJAR.size() == 0) {
throw new RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven?")
} else {
def cordaJar = maybeCordaJAR.getSingleFile()
assert(cordaJar.isFile())
return cordaJar
}
}
/**
* Gets a list of cordapps based on what dependent cordapps were specified.
*
* @return List of this node's cordapps.
*/
private AbstractFileCollection getCordappList() {
def cordaJar = verifyAndGetCordaJar()
return project.configurations.runtime.filter {
def jarName = it.name.split('-').first()
return (it != cordaJar) && cordapps.contains(jarName)
}
}
/**
* Create a directory if it doesn't exist and return the file representation of it.
*
* @param baseDir The base directory to create the directory at.
* @param subDirName A valid name of the subdirectory to get and create if not exists.
* @return A file representing the subdirectory.
*/
private static File getAndCreateDirectory(File baseDir, String subDirName) {
File dir = new File(baseDir, subDirName)
assert(!dir.exists() || dir.isDirectory())
dir.mkdirs()
return dir
}
}

@ -0,0 +1 @@
implementation-class=com.r3corda.plugins.Cordformation

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Will attempt to execute a corda node within all subdirectories in the current working directory.
# TODO: Use screens or separate windows when starting instances.
set -euo pipefail
trap 'kill $(jobs -p)' EXIT
export CAPSULE_CACHE_DIR=cache
function runNode {
pushd $1
( java -jar JAR_NAME )&
popd
}
for dir in `ls`; do
if [ -d $dir ]; then
runNode $dir
fi
done
read -p 'Any key to exit'
kill $(jobs -p)

@ -0,0 +1,25 @@
Publish Utils
=============
Publishing utilities adds a couple of tasks to any project it is applied to that hide some boilerplate that would
otherwise be placed in the Cordapp template's build.gradle.
There are two tasks exposed: `sourceJar` and `javadocJar` and both return a `FileCollection`.
It is used within the `publishing` block of a build.gradle as such;
.. code-block:: text
// This will publish the sources, javadoc, and Java components to Maven.
// See the `maven-publish` plugin for more info: https://docs.gradle.org/current/userguide/publishing_maven.html
publishing {
publications {
jarAndSources(MavenPublication) {
from components.java
// The two lines below are the tasks added by this plugin.
artifact sourceJar
artifact javadocJar
}
}
}

@ -0,0 +1,21 @@
apply plugin: 'maven-publish'
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
publishing {
publications {
plugin(MavenPublication) {
from components.java
groupId 'com.r3corda.plugins'
artifactId 'publish-utils'
}
}
}

@ -0,0 +1,23 @@
package com.r3corda.plugins
import org.gradle.api.*
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.Project
/**
* A utility plugin that when applied will automatically create source and javadoc publishing tasks
*/
class DefaultPublishTasks implements Plugin<Project> {
void apply(Project project) {
project.task("sourceJar", type: Jar, dependsOn: project.classes) {
classifier = 'sources'
from project.sourceSets.main.allSource
}
project.task("javadocJar", type: Jar, dependsOn: project.javadoc) {
classifier = 'javadoc'
from project.javadoc.destinationDir
}
}
}

@ -0,0 +1 @@
implementation-class=com.r3corda.plugins.DefaultPublishTasks

@ -0,0 +1,16 @@
Quasar Utils
============
Quasar utilities adds several tasks and configuration that provide a default Quasar setup and removes some boilerplate.
One line must be added to your build.gradle once you apply this plugin:
.. code-block:: text
quasarScan.dependsOn('classes')
If any sub-projects are added that this project depends on then add the gradle target for that project to the depends
on statement. eg:
.. code-block:: text
quasarScan.dependsOn('classes', 'subproject:subsubproject', ...)

@ -0,0 +1,21 @@
apply plugin: 'maven-publish'
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
publishing {
publications {
plugin(MavenPublication) {
from components.java
groupId 'com.r3corda.plugins'
artifactId 'quasar-utils'
}
}
}

@ -0,0 +1,60 @@
package com.r3corda.plugins
import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.JavaExec
/**
* QuasarPlugin creates a "quasar" configuration, adds quasar as a dependency and creates a "quasarScan" task that scans
* for `@Suspendable`s in the code
*/
class QuasarPlugin implements Plugin<Project> {
void apply(Project project) {
project.repositories {
mavenCentral()
}
project.configurations.create("quasar")
// To add a local .jar dependency:
// project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar"))
project.dependencies.add("quasar", "co.paralleluniverse:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar")
project.dependencies.add("compile", project.configurations.getByName("quasar"))
project.tasks.withType(Test) {
jvmArgs "-javaagent:${project.configurations.quasar.singleFile}"
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
}
project.tasks.withType(JavaExec) {
jvmArgs "-javaagent:${project.configurations.quasar.singleFile}"
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
}
project.task("quasarScan") {
inputs.files(project.sourceSets.main.output)
outputs.files(
"$project.sourceSets.main.output.resourcesDir/META-INF/suspendables",
"$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers"
)
} << {
// These lines tell gradle to run the Quasar suspendables scanner to look for unannotated super methods
// that have @Suspendable sub implementations. These tend to cause NPEs and are not caught by the verifier
// NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so
// we explicitly delete to avoid that happening. We also need to turn off what seems to be a spurious warning in the IDE
ant.taskdef(name:'scanSuspendables', classname:'co.paralleluniverse.fibers.instrument.SuspendablesScanner',
classpath: "${project.sourceSets.main.output.classesDir}:${project.sourceSets.main.output.resourcesDir}:${project.configurations.runtime.asPath}")
project.delete "$project.sourceSets.main.output.resourcesDir/META-INF/suspendables", "$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers"
ant.scanSuspendables(
auto:false,
suspendablesFile: "$project.sourceSets.main.output.resourcesDir/META-INF/suspendables",
supersFile: "$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers") {
fileset(dir: project.sourceSets.main.output.classesDir)
}
}
project.jar.dependsOn project.quasarScan
}
}

@ -0,0 +1 @@
implementation-class=com.r3corda.plugins.QuasarPlugin

@ -8,4 +8,7 @@ include 'experimental'
include 'test-utils'
include 'network-simulator'
include 'explorer'
include 'gradle-plugins:quasar-utils'
include 'gradle-plugins:publish-utils'
include 'gradle-plugins:cordformation'
include 'docs/source/example-code'

@ -25,15 +25,15 @@ public class CordaCaplet extends Capsule {
// defined as public static final fields on the Capsule class, therefore referential equality is safe.
if(ATTR_APP_CLASS_PATH == attr) {
T cp = super.attribute(attr);
List<Path> classpath = (List<Path>) cp;
return (T) augmentClasspath(classpath);
List<Path> classpath = augmentClasspath((List<Path>) cp, "plugins");
return (T) augmentClasspath(classpath, "dependencies");
}
return super.attribute(attr);
}
// TODO: Make directory configurable via the capsule manifest.
// TODO: Add working directory variable to capsules string replacement variables.
private List<Path> augmentClasspath(List<Path> classpath) {
private List<Path> augmentClasspath(List<Path> classpath, String dirName) {
File dir = new File("plugins");
if(!dir.exists()) {
dir.mkdir();