Revert "Removed gradle plugins from repo to ensure plugins are never changed in R3 corda"

This reverts commit c91d9b5507.
This commit is contained in:
Clinton Alexander 2018-01-26 16:06:46 +00:00
parent c91d9b5507
commit e8f8ff7c94
46 changed files with 2704 additions and 0 deletions

29
gradle-plugins/README.rst Normal file
View File

@ -0,0 +1,29 @@
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.
Version number
--------------
To modify the version number edit constants.properties in root dir
Installing
----------
If you need to bootstrap the corda repository you can install these plugins with
.. code-block:: text
cd publish-utils
../../gradlew -u install
cd ../
../gradlew install

View File

@ -0,0 +1,79 @@
# API Scanner
Generates a text summary of Corda's public API that we can check for API-breaking changes.
```bash
$ gradlew generateApi
```
See [here](../../docs/source/corda-api.rst) for Corda's public API strategy. We will need to
apply this plugin to other modules in future Corda releases as those modules' APIs stabilise.
Basically, this plugin will document a module's `public` and `protected` classes/methods/fields,
excluding those from our `*.internal.*` packgages, any synthetic methods, bridge methods, or methods
identified as having Kotlin's `internal` scope. (Kotlin doesn't seem to have implemented `internal`
scope for classes or fields yet as these are currently `public` inside the `.class` file.)
## Usage
Include this line in the `build.gradle` file of every Corda module that exports public API:
```gradle
apply plugin: 'net.corda.plugins.api-scanner'
```
This will create a Gradle task called `scanApi` which will analyse that module's Jar artifacts. More precisely,
it will analyse all of the Jar artifacts that have not been assigned a Maven classifier, on the basis
that these should be the module's main artifacts.
The `scanApi` task supports the following configuration options:
```gradle
scanApi {
// Make the classpath-scanning phase more verbose.
verbose = {true|false}
// Enable / disable the task within this module.
enabled = {true|false}
// Names of classes that should be excluded from the output.
excludeClasses = [
...
]
}
```
All of the `ScanApi` tasks write their output files to their own `$buildDir/api` directory, where they
are collated into a single output file by the `GenerateApi` task. The `GenerateApi` task is declared
in the root project's `build.gradle` file:
```gradle
task generateApi(type: net.corda.plugins.GenerateApi){
baseName = "api-corda"
}
```
The final API file is written to `$buildDir/api/$baseName-$project.version.txt`
### Sample Output
```
public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash
public abstract void extractFile(String, java.io.OutputStream)
@org.jetbrains.annotations.NotNull public abstract List getSigners()
@org.jetbrains.annotations.NotNull public abstract java.io.InputStream open()
@org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR()
##
public interface net.corda.core.contracts.AttachmentConstraint
public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
##
public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException
public <init>(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash()
##
```
#### Notes
The `GenerateApi` task will collate the output of every `ScanApi` task found either in the same project,
or in any of that project's subprojects. So it is _theoretically_ possible also to collate the API output
from subtrees of modules simply by defining a new `GenerateApi` task at the root of that subtree.
## Plugin Installation
See [here](../README.rst) for full installation instructions.

View File

@ -0,0 +1,19 @@
apply plugin: 'java'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description "Generates a summary of the artifact's public API"
repositories {
mavenCentral()
}
dependencies {
compile gradleApi()
compile "io.github.lukehutch:fast-classpath-scanner:2.7.0"
testCompile "junit:junit:4.12"
}
publish {
name project.name
}

View File

@ -0,0 +1,70 @@
package net.corda.plugins;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.TaskCollection;
import org.gradle.jvm.tasks.Jar;
public class ApiScanner implements Plugin<Project> {
/**
* Identify the Gradle Jar tasks creating jars
* without Maven classifiers, and generate API
* documentation for them.
* @param p Current project.
*/
@Override
public void apply(Project p) {
p.getLogger().info("Applying API scanner to {}", p.getName());
ScannerExtension extension = p.getExtensions().create("scanApi", ScannerExtension.class);
p.afterEvaluate(project -> {
TaskCollection<Jar> jarTasks = project.getTasks()
.withType(Jar.class)
.matching(jarTask -> jarTask.getClassifier().isEmpty() && jarTask.isEnabled());
if (jarTasks.isEmpty()) {
return;
}
project.getLogger().info("Adding scanApi task to {}", project.getName());
project.getTasks().create("scanApi", ScanApi.class, scanTask -> {
scanTask.setClasspath(compilationClasspath(project.getConfigurations()));
// Automatically creates a dependency on jar tasks.
scanTask.setSources(project.files(jarTasks));
scanTask.setExcludeClasses(extension.getExcludeClasses());
scanTask.setVerbose(extension.isVerbose());
scanTask.setEnabled(extension.isEnabled());
// Declare this ScanApi task to be a dependency of any
// GenerateApi tasks belonging to any of our ancestors.
project.getRootProject().getTasks()
.withType(GenerateApi.class)
.matching(generateTask -> isAncestorOf(generateTask.getProject(), project))
.forEach(generateTask -> generateTask.dependsOn(scanTask));
});
});
}
/*
* Recurse through a child project's parents until we reach the root,
* and return true iff we find our target project along the way.
*/
private static boolean isAncestorOf(Project target, Project child) {
Project p = child;
while (p != null) {
if (p == target) {
return true;
}
p = p.getParent();
}
return false;
}
private static FileCollection compilationClasspath(ConfigurationContainer configurations) {
return configurations.getByName("compile")
.plus(configurations.getByName("compileOnly"));
}
}

View File

@ -0,0 +1,61 @@
package net.corda.plugins;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import java.io.*;
import java.nio.file.Files;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
@SuppressWarnings("unused")
public class GenerateApi extends DefaultTask {
private final File outputDir;
private String baseName;
public GenerateApi() {
outputDir = new File(getProject().getBuildDir(), "api");
baseName = "api-" + getProject().getName();
}
public void setBaseName(String baseName) {
this.baseName = baseName;
}
@InputFiles
public FileCollection getSources() {
return getProject().files(getProject().getAllprojects().stream()
.flatMap(project -> project.getTasks()
.withType(ScanApi.class)
.matching(ScanApi::isEnabled)
.stream())
.flatMap(scanTask -> scanTask.getTargets().getFiles().stream())
.sorted(comparing(File::getName))
.collect(toList())
);
}
@OutputFile
public File getTarget() {
return new File(outputDir, String.format("%s-%s.txt", baseName, getProject().getVersion()));
}
@TaskAction
public void generate() {
FileCollection apiFiles = getSources();
if (!apiFiles.isEmpty()) {
try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) {
for (File apiFile : apiFiles) {
Files.copy(apiFile.toPath(), output);
}
} catch (IOException e) {
getLogger().error("Failed to generate API file", e);
}
}
}
}

View File

@ -0,0 +1,389 @@
package net.corda.plugins;
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo;
import io.github.lukehutch.fastclasspathscanner.scanner.FieldInfo;
import io.github.lukehutch.fastclasspathscanner.scanner.MethodInfo;
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.CompileClasspath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.TaskAction;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.stream.StreamSupport;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
@SuppressWarnings("unused")
public class ScanApi extends DefaultTask {
private static final int CLASS_MASK = Modifier.classModifiers();
private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT;
private static final int METHOD_MASK = Modifier.methodModifiers();
private static final int FIELD_MASK = Modifier.fieldModifiers();
private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED;
private static final Set<String> ANNOTATION_BLACKLIST;
static {
Set<String> blacklist = new LinkedHashSet<>();
blacklist.add("kotlin.jvm.JvmOverloads");
ANNOTATION_BLACKLIST = unmodifiableSet(blacklist);
}
/**
* This information has been lifted from:
* @link <a href="https://github.com/JetBrains/kotlin/blob/master/core/runtime.jvm/src/kotlin/Metadata.kt">Metadata.kt</a>
*/
private static final String KOTLIN_METADATA = "kotlin.Metadata";
private static final String KOTLIN_CLASSTYPE_METHOD = "k";
private static final int KOTLIN_SYNTHETIC = 3;
private final ConfigurableFileCollection sources;
private final ConfigurableFileCollection classpath;
private final Set<String> excludeClasses;
private final File outputDir;
private boolean verbose;
public ScanApi() {
sources = getProject().files();
classpath = getProject().files();
excludeClasses = new LinkedHashSet<>();
outputDir = new File(getProject().getBuildDir(), "api");
}
@InputFiles
public FileCollection getSources() {
return sources;
}
void setSources(FileCollection sources) {
this.sources.setFrom(sources);
}
@CompileClasspath
@InputFiles
public FileCollection getClasspath() {
return classpath;
}
void setClasspath(FileCollection classpath) {
this.classpath.setFrom(classpath);
}
@Input
public Collection<String> getExcludeClasses() {
return unmodifiableSet(excludeClasses);
}
void setExcludeClasses(Collection<String> excludeClasses) {
this.excludeClasses.clear();
this.excludeClasses.addAll(excludeClasses);
}
@OutputFiles
public FileCollection getTargets() {
return getProject().files(
StreamSupport.stream(sources.spliterator(), false)
.map(this::toTarget)
.collect(toList())
);
}
public boolean isVerbose() {
return verbose;
}
void setVerbose(boolean verbose) {
this.verbose = verbose;
}
private File toTarget(File source) {
return new File(outputDir, source.getName().replaceAll(".jar$", ".txt"));
}
@TaskAction
public void scan() {
try (Scanner scanner = new Scanner(classpath)) {
for (File source : sources) {
scanner.scan(source);
}
} catch (IOException e) {
getLogger().error("Failed to write API file", e);
}
}
class Scanner implements Closeable {
private final URLClassLoader classpathLoader;
private final Class<? extends Annotation> metadataClass;
private final Method classTypeMethod;
@SuppressWarnings("unchecked")
Scanner(URLClassLoader classpathLoader) {
this.classpathLoader = classpathLoader;
Class<? extends Annotation> kClass;
Method kMethod;
try {
kClass = (Class<Annotation>) Class.forName(KOTLIN_METADATA, true, classpathLoader);
kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD);
} catch (ClassNotFoundException | NoSuchMethodException e) {
kClass = null;
kMethod = null;
}
metadataClass = kClass;
classTypeMethod = kMethod;
}
Scanner(FileCollection classpath) throws MalformedURLException {
this(new URLClassLoader(toURLs(classpath)));
}
@Override
public void close() throws IOException {
classpathLoader.close();
}
void scan(File source) {
File target = toTarget(source);
try (
URLClassLoader appLoader = new URLClassLoader(new URL[]{ toURL(source) }, classpathLoader);
PrintWriter writer = new PrintWriter(target, "UTF-8")
) {
scan(writer, appLoader);
} catch (IOException e) {
getLogger().error("API scan has failed", e);
}
}
void scan(PrintWriter writer, ClassLoader appLoader) {
ScanResult result = new FastClasspathScanner(getScanSpecification())
.overrideClassLoaders(appLoader)
.ignoreParentClassLoaders()
.ignoreMethodVisibility()
.ignoreFieldVisibility()
.enableMethodInfo()
.enableFieldInfo()
.verbose(verbose)
.scan();
writeApis(writer, result);
}
private String[] getScanSpecification() {
String[] spec = new String[2 + excludeClasses.size()];
spec[0] = "!"; // Don't blacklist system classes from the output.
spec[1] = "-dir:"; // Ignore classes on the filesystem.
int i = 2;
for (String excludeClass : excludeClasses) {
spec[i++] = '-' + excludeClass;
}
return spec;
}
private void writeApis(PrintWriter writer, ScanResult result) {
Map<String, ClassInfo> allInfo = result.getClassNameToClassInfo();
result.getNamesOfAllClasses().forEach(className -> {
if (className.contains(".internal.")) {
// These classes belong to internal Corda packages.
return;
}
ClassInfo classInfo = allInfo.get(className);
if (classInfo.getClassLoaders() == null) {
// Ignore classes that belong to one of our target ClassLoader's parents.
return;
}
Class<?> javaClass = result.classNameToClassRef(className);
if (!isVisible(javaClass.getModifiers())) {
// Excludes private and package-protected classes
return;
}
int kotlinClassType = getKotlinClassType(javaClass);
if (kotlinClassType == KOTLIN_SYNTHETIC) {
// Exclude classes synthesised by the Kotlin compiler.
return;
}
writeClass(writer, classInfo, javaClass.getModifiers());
writeMethods(writer, classInfo.getMethodAndConstructorInfo());
writeFields(writer, classInfo.getFieldInfo());
writer.println("##");
});
}
private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) {
if (classInfo.isAnnotation()) {
/*
* Annotation declaration.
*/
writer.append(Modifier.toString(modifiers & INTERFACE_MASK));
writer.append(" @interface ").print(classInfo);
} else if (classInfo.isStandardClass()) {
/*
* Class declaration.
*/
List<String> annotationNames = toNames(readClassAnnotationsFor(classInfo));
if (!annotationNames.isEmpty()) {
writer.append(asAnnotations(annotationNames));
}
writer.append(Modifier.toString(modifiers & CLASS_MASK));
writer.append(" class ").print(classInfo);
Set<ClassInfo> superclasses = classInfo.getDirectSuperclasses();
if (!superclasses.isEmpty()) {
writer.append(" extends ").print(stringOf(superclasses));
}
Set<ClassInfo> interfaces = classInfo.getDirectlyImplementedInterfaces();
if (!interfaces.isEmpty()) {
writer.append(" implements ").print(stringOf(interfaces));
}
} else {
/*
* Interface declaration.
*/
List<String> annotationNames = toNames(readInterfaceAnnotationsFor(classInfo));
if (!annotationNames.isEmpty()) {
writer.append(asAnnotations(annotationNames));
}
writer.append(Modifier.toString(modifiers & INTERFACE_MASK));
writer.append(" interface ").print(classInfo);
Set<ClassInfo> superinterfaces = classInfo.getDirectSuperinterfaces();
if (!superinterfaces.isEmpty()) {
writer.append(" extends ").print(stringOf(superinterfaces));
}
}
writer.println();
}
private void writeMethods(PrintWriter writer, List<MethodInfo> methods) {
sort(methods);
for (MethodInfo method : methods) {
if (isVisible(method.getAccessFlags()) // Only public and protected methods
&& isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods
&& !isKotlinInternalScope(method)) {
writer.append(" ").println(filterAnnotationsFor(method));
}
}
}
private void writeFields(PrintWriter output, List<FieldInfo> fields) {
sort(fields);
for (FieldInfo field : fields) {
if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) {
output.append(" ").println(field);
}
}
}
private int getKotlinClassType(Class<?> javaClass) {
if (metadataClass != null) {
Annotation metadata = javaClass.getAnnotation(metadataClass);
if (metadata != null) {
try {
return (int) classTypeMethod.invoke(metadata);
} catch (IllegalAccessException | InvocationTargetException e) {
getLogger().error("Failed to read Kotlin annotation", e);
}
}
}
return 0;
}
private List<String> toNames(Collection<ClassInfo> classes) {
return classes.stream()
.map(ClassInfo::toString)
.filter(ScanApi::isApplicationClass)
.collect(toList());
}
private Set<ClassInfo> readClassAnnotationsFor(ClassInfo classInfo) {
Set<ClassInfo> annotations = new HashSet<>(classInfo.getAnnotations());
annotations.addAll(selectInheritedAnnotations(classInfo.getSuperclasses()));
annotations.addAll(selectInheritedAnnotations(classInfo.getImplementedInterfaces()));
return annotations;
}
private Set<ClassInfo> readInterfaceAnnotationsFor(ClassInfo classInfo) {
Set<ClassInfo> annotations = new HashSet<>(classInfo.getAnnotations());
annotations.addAll(selectInheritedAnnotations(classInfo.getSuperinterfaces()));
return annotations;
}
/**
* Returns those annotations which have themselves been annotated as "Inherited".
*/
private List<ClassInfo> selectInheritedAnnotations(Collection<ClassInfo> classes) {
return classes.stream()
.flatMap(cls -> cls.getAnnotations().stream())
.filter(ann -> ann.hasMetaAnnotation(Inherited.class.getName()))
.collect(toList());
}
private MethodInfo filterAnnotationsFor(MethodInfo method) {
return new MethodInfo(
method.getClassName(),
method.getMethodName(),
method.getAccessFlags(),
method.getTypeDescriptor(),
method.getAnnotationNames().stream()
.filter(ScanApi::isVisibleAnnotation)
.collect(toList())
);
}
}
private static boolean isVisibleAnnotation(String annotationName) {
return !ANNOTATION_BLACKLIST.contains(annotationName);
}
private static boolean isKotlinInternalScope(MethodInfo method) {
return method.getMethodName().indexOf('$') >= 0;
}
private static boolean isValid(int modifiers, int mask) {
return (modifiers & mask) == modifiers;
}
private static boolean isVisible(int accessFlags) {
return (accessFlags & VISIBILITY_MASK) != 0;
}
private static String stringOf(Collection<ClassInfo> items) {
return items.stream().map(ClassInfo::toString).collect(joining(", "));
}
private static String asAnnotations(Collection<String> items) {
return items.stream().collect(joining(" @", "@", " "));
}
private static boolean isApplicationClass(String typeName) {
return !typeName.startsWith("java.") && !typeName.startsWith("kotlin.");
}
private static URL toURL(File file) throws MalformedURLException {
return file.toURI().toURL();
}
private static URL[] toURLs(Iterable<File> files) throws MalformedURLException {
List<URL> urls = new LinkedList<>();
for (File file : files) {
urls.add(toURL(file));
}
return urls.toArray(new URL[urls.size()]);
}
}

View File

@ -0,0 +1,37 @@
package net.corda.plugins;
import java.util.List;
import static java.util.Collections.emptyList;
@SuppressWarnings("unused")
public class ScannerExtension {
private boolean verbose;
private boolean enabled = true;
private List<String> excludeClasses = emptyList();
public boolean isVerbose() {
return verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getExcludeClasses() {
return excludeClasses;
}
public void setExcludeClasses(List<String> excludeClasses) {
this.excludeClasses = excludeClasses;
}
}

View File

@ -0,0 +1 @@
implementation-class=net.corda.plugins.ApiScanner

View File

@ -0,0 +1,83 @@
// This script exists just to allow bootstrapping the gradle plugins if maven central or jcenter are unavailable
// or if you are developing these plugins. See the readme for more information.
buildscript {
// For sharing constants between builds
Properties constants = new Properties()
file("$projectDir/../constants.properties").withInputStream { constants.load(it) }
// If you bump this version you must re-bootstrap the codebase. See the README for more information.
ext {
gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
bouncycastle_version = constants.getProperty("bouncycastleVersion")
typesafe_config_version = constants.getProperty("typesafeConfigVersion")
jsr305_version = constants.getProperty("jsr305Version")
kotlin_version = constants.getProperty("kotlinVersion")
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
}
repositories {
mavenLocal()
jcenter()
}
dependencies {
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
}
}
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
allprojects {
version gradle_plugins_version
group 'net.corda.plugins'
}
bintrayConfig {
user = System.getenv('CORDA_BINTRAY_USER')
key = System.getenv('CORDA_BINTRAY_KEY')
repo = 'corda'
org = 'r3'
licenses = ['Apache-2.0']
vcsUrl = 'https://github.com/corda/corda'
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner', 'cordapp']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'
distribution = 'repo'
}
developer {
id = 'R3'
name = 'R3'
email = 'dev@corda.net'
}
}
artifactory {
publish {
contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory'
repository {
repoKey = 'corda-dev'
username = 'teamcity'
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
}
defaults {
// Publish utils does not have a publish block because it would be circular for it to apply it's own
// extensions to itself
if(project.name == 'publish-utils') {
publications('publishUtils')
// Root project applies the plugin (for this block) but does not need to be published
} else if(project != rootProject) {
publications(project.extensions.publish.name())
}
}
}
}

View File

@ -0,0 +1,10 @@
# Cordapp Gradle Plugin
## Purpose
To transform any project this plugin is applied to into a cordapp project that generates a cordapp JAR.
## Effects
Will modify the default JAR task to create a CorDapp format JAR instead [see here](https://docs.corda.net/cordapp-build-systems.html)
for more information.

View File

@ -0,0 +1,19 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Turns a project into a cordapp project that produces cordapp fat JARs'
repositories {
mavenCentral()
jcenter()
}
dependencies {
compile gradleApi()
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
}
publish {
name project.name
}

View File

@ -0,0 +1,75 @@
package net.corda.plugins
import org.gradle.api.*
import org.gradle.api.artifacts.*
import org.gradle.jvm.tasks.Jar
import java.io.File
/**
* The Cordapp plugin will turn a project into a cordapp project which builds cordapp JARs with the correct format
* and with the information needed to run on Corda.
*/
class CordappPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.logger.info("Configuring ${project.name} as a cordapp")
Utils.createCompileConfiguration("cordapp", project)
Utils.createCompileConfiguration("cordaCompile", project)
val configuration: Configuration = project.configurations.create("cordaRuntime")
configuration.isTransitive = false
project.configurations.single { it.name == "runtime" }.extendsFrom(configuration)
configureCordappJar(project)
}
/**
* Configures this project's JAR as a Cordapp JAR
*/
private fun configureCordappJar(project: Project) {
// Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead
val task = project.task("configureCordappFatJar")
val jarTask = project.tasks.getByName("jar") as Jar
task.doLast {
jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply {
exclude("META-INF/*.SF")
exclude("META-INF/*.DSA")
exclude("META-INF/*.RSA")
}
}
jarTask.dependsOn(task)
}
private fun getDirectNonCordaDependencies(project: Project): Set<File> {
project.logger.info("Finding direct non-corda dependencies for inclusion in CorDapp JAR")
val excludes = listOf(
mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib"),
mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib-jre8"),
mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-reflect"),
mapOf("group" to "co.paralleluniverse", "name" to "quasar-core")
)
val runtimeConfiguration = project.configuration("runtime")
// The direct dependencies of this project
val excludeDeps = project.configuration("cordapp").allDependencies +
project.configuration("cordaCompile").allDependencies +
project.configuration("cordaRuntime").allDependencies
val directDeps = runtimeConfiguration.allDependencies - excludeDeps
// We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar
val filteredDeps = directDeps.filter { dep ->
excludes.none { exclude -> (exclude["group"] == dep.group) && (exclude["name"] == dep.name) }
}
filteredDeps.forEach {
// net.corda or com.r3.corda may be a core dependency which shouldn't be included in this cordapp so give a warning
val group = it.group?.toString() ?: ""
if (group.startsWith("net.corda.") || group.startsWith("com.r3.corda.")) {
project.logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." +
"This can cause node stability problems. Please use 'corda' instead." +
"See http://docs.corda.net/cordapp-build-systems.html")
} else {
project.logger.info("Including dependency in CorDapp JAR: $it")
}
}
return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet()
}
}

View File

@ -0,0 +1,35 @@
package net.corda.plugins
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.plugins.ExtraPropertiesExtension
/**
* Mimics the "project.ext" functionality in groovy which provides a direct
* accessor to the "ext" extention (See: ExtraPropertiesExtension)
*/
@Suppress("UNCHECKED_CAST")
fun <T : Any> Project.ext(name: String): T = (extensions.findByName("ext") as ExtraPropertiesExtension).get(name) as T
fun Project.configuration(name: String): Configuration = configurations.single { it.name == name }
class Utils {
companion object {
@JvmStatic
fun createCompileConfiguration(name: String, project: Project) {
if(!project.configurations.any { it.name == name }) {
val configuration = project.configurations.create(name)
configuration.isTransitive = false
project.configurations.single { it.name == "compile" }.extendsFrom(configuration)
}
}
fun createRuntimeConfiguration(name: String, project: Project) {
if(!project.configurations.any { it.name == name }) {
val configuration = project.configurations.create(name)
configuration.isTransitive = false
project.configurations.single { it.name == "runtime" }.extendsFrom(configuration)
}
}
}
}

View File

@ -0,0 +1 @@
implementation-class=net.corda.plugins.CordappPlugin

View File

@ -0,0 +1,4 @@
# Cordform Common
This project contains common node types that both the Corda gradle plugin suite and Corda project
require in order to build Corda nodes.

View File

@ -0,0 +1,24 @@
apply plugin: 'java'
apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
repositories {
mavenCentral()
}
// This tracks the gradle plugins version and not Corda
version gradle_plugins_version
group 'net.corda.plugins'
dependencies {
// JSR 305: Nullability annotations
compile "com.google.code.findbugs:jsr305:$jsr305_version"
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version"
}
publish {
name project.name
}

View File

@ -0,0 +1,7 @@
package net.corda.cordform;
import java.nio.file.Path;
public interface CordformContext {
Path baseDirectory(String nodeName);
}

View File

@ -0,0 +1,40 @@
package net.corda.cordform;
import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public abstract class CordformDefinition {
private Path nodesDirectory = Paths.get("build", "nodes");
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
private final List<String> cordappPackages = new ArrayList<>();
public Path getNodesDirectory() {
return nodesDirectory;
}
public void setNodesDirectory(Path nodesDirectory) {
this.nodesDirectory = nodesDirectory;
}
public List<Consumer<CordformNode>> getNodeConfigurers() {
return nodeConfigurers;
}
public void addNode(Consumer<CordformNode> configurer) {
nodeConfigurers.add(configurer);
}
public List<String> getCordappPackages() {
return cordappPackages;
}
/**
* Make arbitrary changes to the node directories before they are started.
* @param context Lookup of node directory by node name.
*/
public abstract void setup(@Nonnull CordformContext context);
}

View File

@ -0,0 +1,173 @@
package net.corda.cordform;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import static java.util.Collections.emptyList;
public class CordformNode implements NodeDefinition {
/**
* Path relative to the running node where the serialized NodeInfos are stored.
*/
public static final String NODE_INFO_DIRECTORY = "additional-node-infos";
protected static final String DEFAULT_HOST = "localhost";
/**
* Name of the node.
*/
private String name;
public String getName() {
return name;
}
/**
* Set the RPC users for this node. This configuration block allows arbitrary configuration.
* The recommended current structure is:
* [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]]
* The above is a list to a map of keys to values using Groovy map and list shorthands.
*
* Incorrect configurations will not cause a DSL error.
*/
public List<Map<String, Object>> rpcUsers = emptyList();
/**
* Apply the notary configuration if this node is a notary. The map is the config structure of
* net.corda.node.services.config.NotaryConfig
*/
public Map<String, Object> notary = null;
public Map<String, Object> extraConfig = null;
protected Config config = ConfigFactory.empty();
public Config getConfig() {
return config;
}
/**
* Set the name of the node.
*
* @param name The node name.
*/
public void name(String name) {
this.name = name;
setValue("myLegalName", name);
}
/**
* Get the artemis address for this node.
*
* @return This node's P2P address.
*/
@Nonnull
public String getP2pAddress() {
return config.getString("p2pAddress");
}
/**
* Set the Artemis P2P port for this node on localhost.
*
* @param p2pPort The Artemis messaging queue port.
*/
public void p2pPort(int p2pPort) {
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
}
/**
* Set the Artemis P2P address for this node.
*
* @param p2pAddress The Artemis messaging queue host and port.
*/
public void p2pAddress(String p2pAddress) {
setValue("p2pAddress", p2pAddress);
}
/**
* Returns the RPC address for this node, or null if one hasn't been specified.
*/
@Nullable
public String getRpcAddress() {
if (config.hasPath("rpcSettings.address")) {
return config.getConfig("rpcSettings").getString("address");
}
return getOptionalString("rpcAddress");
}
/**
* Set the Artemis RPC port for this node on localhost.
*
* @param rpcPort The Artemis RPC queue port.
* @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead.
*/
@Deprecated
public void rpcPort(int rpcPort) {
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
}
/**
* Set the Artemis RPC address for this node.
*
* @param rpcAddress The Artemis RPC queue host and port.
* @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead.
*/
@Deprecated
public void rpcAddress(String rpcAddress) {
setValue("rpcAddress", rpcAddress);
}
/**
* Returns the address of the web server that will connect to the node, or null if one hasn't been specified.
*/
@Nullable
public String getWebAddress() {
return getOptionalString("webAddress");
}
/**
* Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node
* must have an RPC address configured.
*/
public void webPort(int webPort) {
webAddress(DEFAULT_HOST + ':' + webPort);
}
/**
* Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node
* must have an RPC address configured.
*/
public void webAddress(String webAddress) {
setValue("webAddress", webAddress);
}
/**
* Specifies RPC settings for the node.
*/
public void rpcSettings(RpcSettings settings) {
config = settings.addTo("rpcSettings", config);
}
/**
* Set the path to a file with optional properties, which are appended to the generated node.conf file.
*
* @param configFile The file path.
*/
public void configFile(String configFile) {
setValue("configFile", configFile);
}
private String getOptionalString(String path) {
return config.hasPath(path) ? config.getString(path) : null;
}
private void setValue(String path, Object value) {
config = config.withValue(path, ConfigValueFactory.fromAnyRef(value));
}
}

View File

@ -0,0 +1,9 @@
package net.corda.cordform;
import com.typesafe.config.Config;
public interface NodeDefinition {
String getName();
Config getConfig();
}

View File

@ -0,0 +1,56 @@
package net.corda.cordform;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
public final class RpcSettings {
private Config config = ConfigFactory.empty();
/**
* RPC address for the node.
*/
public final void address(final String value) {
setValue("address", value);
}
/**
* RPC admin address for the node (necessary if [useSsl] is false or unset).
*/
public final void adminAddress(final String value) {
setValue("adminAddress", value);
}
/**
* Specifies whether the node RPC layer will require SSL from clients.
*/
public final void useSsl(final Boolean value) {
setValue("useSsl", value);
}
/**
* Specifies whether the RPC broker is separate from the node.
*/
public final void standAloneBroker(final Boolean value) {
setValue("standAloneBroker", value);
}
/**
* Specifies SSL certificates options for the RPC layer.
*/
public final void ssl(final SslOptions options) {
config = options.addTo("ssl", config);
}
final Config addTo(final String key, final Config config) {
if (this.config.isEmpty()) {
return config;
}
return config.withValue(key, this.config.root());
}
private void setValue(String path, Object value) {
config = config.withValue(path, ConfigValueFactory.fromAnyRef(value));
}
}

View File

@ -0,0 +1,56 @@
package net.corda.cordform;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
public final class SslOptions {
private Config config = ConfigFactory.empty();
/**
* Password for the keystore.
*/
public final void keyStorePassword(final String value) {
setValue("keyStorePassword", value);
}
/**
* Password for the truststore.
*/
public final void trustStorePassword(final String value) {
setValue("trustStorePassword", value);
}
/**
* Directory under which key stores are to be placed.
*/
public final void certificatesDirectory(final String value) {
setValue("certificatesDirectory", value);
}
/**
* Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks"
*/
public final void sslKeystore(final String value) {
setValue("sslKeystore", value);
}
/**
* Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks"
*/
public final void trustStoreFile(final String value) {
setValue("trustStoreFile", value);
}
final Config addTo(final String key, final Config config) {
if (this.config.isEmpty()) {
return config;
}
return config.withValue(key, this.config.root());
}
private void setValue(String path, Object value) {
config = config.withValue(path, ConfigValueFactory.fromAnyRef(value));
}
}

View File

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

View File

@ -0,0 +1,62 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'
repositories {
mavenCentral()
}
configurations {
noderunner
compile.extendsFrom noderunner
}
sourceSets {
runnodes {
kotlin {
srcDir file('src/noderunner/kotlin')
compileClasspath += configurations.noderunner
}
}
}
dependencies {
compile gradleApi()
compile project(":cordapp")
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile project(':cordform-common')
}
task createNodeRunner(type: Jar, dependsOn: [classes]) {
manifest {
attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt')
}
classifier = 'fatjar'
from { configurations.noderunner.collect { it.isDirectory() ? it : zipTree(it) } }
from sourceSets.runnodes.output
}
jar {
from(createNodeRunner) {
rename { 'net/corda/plugins/runnodes.jar' }
}
}
publish {
name project.name
}

View File

@ -0,0 +1,218 @@
package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.jar.JarInputStream
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
@Suppress("unused")
open class Cordform : DefaultTask() {
private companion object {
val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "nodes")
}
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
private var directory = defaultDirectory
private val nodes = mutableListOf<Node>()
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
*/
fun directory(directory: String) {
this.directory = Paths.get(directory)
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureClosure: Closure<in Node>) {
nodes += project.configure(Node(project), configureClosure) as Node
}
/**
* Add a node configuration
*
* @param configureFunc A node configuration that will be deployed
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureFunc: Node.() -> Any?): Node {
val node = Node(project).apply { configureFunc() }
nodes += node
return node
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
/**
* Installs the run script into the nodes directory.
*/
private fun installRunScript() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar"))
fileMode = Cordformation.executableFileMode
into("$directory/")
}
}
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes"))
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java)
fileMode = Cordformation.executableFileMode
into("$directory/")
}
}
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat"))
into("$directory/")
}
}
}
/**
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadCordformDefinition(): CordformDefinition {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
.loadClass(definitionClass)
.asSubclass(CordformDefinition::class.java)
.newInstance()
}
/**
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadNetworkBootstrapperClass(): Class<*> {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
}
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@TaskAction
fun build() {
project.logger.info("Running Cordform task")
initializeConfiguration()
nodes.forEach(Node::installConfig)
installCordaJar()
installRunScript()
bootstrapNetwork()
nodes.forEach(Node::build)
}
/**
* Installs the corda fat JAR to the root directory, for the network bootstrapper to use.
*/
private fun installCordaJar() {
val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda")
project.copy {
it.apply {
from(cordaJar)
into(directory)
rename(cordaJar.name, nodeJarName)
fileMode = Cordformation.executableFileMode
}
}
}
private fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
// If the user has specified their own directory (even if it's the same default path) then let them know
// it's not used and should just rely on the one in CordformDefinition
require(directory === defaultDirectory) {
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
}
directory = cd.nodesDirectory
val cordapps = cd.getMatchingCordapps()
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.additionalCordapps.addAll(cordapps)
node.rootDir(directory)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
} else {
nodes.forEach {
it.rootDir(directory)
}
}
}
private fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
// Call NetworkBootstrapper.bootstrap
try {
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
bootstrapMethod.invoke(networkBootstrapper, rootDir)
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
val cordappJars = project.configuration("cordapp").files
return cordappPackages.map { `package` ->
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
when (cordappsWithPackage.size) {
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
1 -> cordappsWithPackage[0]
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
}
}
}
private fun File.containsPackage(`package`: String): Boolean {
JarInputStream(inputStream()).use {
while (true) {
val name = it.nextJarEntry?.name ?: break
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
return true
}
}
return false
}
}
}

View File

@ -0,0 +1,62 @@
package net.corda.plugins
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
/**
* 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 : Plugin<Project> {
internal companion object {
const val CORDFORMATION_TYPE = "cordformationInternal"
/**
* 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.
*/
fun getPluginFile(project: Project, filePathInJar: String): File {
val archive = project.rootProject.buildscript.configurations
.single { it.name == "classpath" }
.first { it.name.contains("cordformation") }
return project.rootProject.resources.text
.fromArchiveEntry(archive, filePathInJar)
.asFile()
}
/**
* Gets a current built corda jar file
*
* @param project The project environment this plugin executes in.
* @param jarName The name of the JAR you wish to access.
* @return A file handle to the file in the JAR.
*/
fun verifyAndGetRuntimeJar(project: Project, jarName: String): File {
val releaseVersion = project.rootProject.ext<String>("corda_release_version")
val maybeJar = project.configuration("runtime").filter {
"$jarName-$releaseVersion.jar" in it.toString() || "$jarName-r3-$releaseVersion.jar" in it.toString()
}
if (maybeJar.isEmpty) {
throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"")
} else {
val jar = maybeJar.singleFile
require(jar.isFile)
return jar
}
}
val executableFileMode = "0755".toInt(8)
}
override fun apply(project: Project) {
Utils.createCompileConfiguration("cordapp", project)
Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project)
// TODO: improve how we re-use existing declared external variables from root gradle.build
val jolokiaVersion = try { project.rootProject.ext<String>("jolokia_version") } catch (e: Exception) { "1.3.7" }
project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent")
}
}

View File

@ -0,0 +1,239 @@
package net.corda.plugins
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import groovy.lang.Closure
import net.corda.cordform.CordformNode
import org.gradle.api.Project
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
/**
* Represents a node that will be installed.
*/
class Node(private val project: Project) : CordformNode() {
companion object {
@JvmStatic
val webJarName = "corda-webserver.jar"
private val configFileProperty = "configFile"
}
/**
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
* dependency name, eg: com.example:product-name:0.1
*
* @note Your app will be installed by default and does not need to be included here.
* @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it
*/
var cordapps = mutableListOf<Any>()
internal var additionalCordapps = mutableListOf<File>()
internal lateinit var nodeDir: File
private set
internal lateinit var rootDir: File
private set
/**
* Sets whether this node will use HTTPS communication.
*
* @param isHttps True if this node uses HTTPS communication.
*/
fun https(isHttps: Boolean) {
config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps))
}
/**
* Sets the H2 port for this node
*/
fun h2Port(h2Port: Int) {
config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port))
}
fun useTestClock(useTestClock: Boolean) {
config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock))
}
/**
* Specifies RPC settings for the node.
*/
fun rpcSettings(configureClosure: Closure<in RpcSettings>) {
val rpcSettings = project.configure(RpcSettings(project), configureClosure) as RpcSettings
config = rpcSettings.addTo("rpcSettings", config)
}
/**
* Enables SSH access on given port
*
* @param sshdPort The port for SSH server to listen on
*/
fun sshdPort(sshdPort: Int?) {
config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort))
}
internal fun build() {
if (config.hasPath("webAddress")) {
installWebserverJar()
}
installAgentJar()
installBuiltCordapp()
installCordapps()
}
internal fun rootDir(rootDir: Path) {
if (name == null) {
project.logger.error("Node has a null name - cannot create node")
throw IllegalStateException("Node has a null name - cannot create node")
}
// Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems
// with loading our custom X509EdDSAEngine.
val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
val dirName = organizationName ?: name
this.rootDir = rootDir.toFile()
nodeDir = File(this.rootDir, dirName.replace("\\s", ""))
Files.createDirectories(nodeDir.toPath())
}
private fun configureProperties() {
config = config.withValue("database.runMigration", ConfigValueFactory.fromAnyRef(true))
config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers))
if (notary != null) {
config = config.withValue("notary", ConfigValueFactory.fromMap(notary))
}
if (extraConfig != null) {
config = config.withFallback(ConfigFactory.parseMap(extraConfig))
}
}
/**
* Installs the corda webserver JAR to the node directory
*/
private fun installWebserverJar() {
val webJar = Cordformation.verifyAndGetRuntimeJar(project, "corda-webserver")
project.copy {
it.apply {
from(webJar)
into(nodeDir)
rename(webJar.name, webJarName)
}
}
}
/**
* Installs this project's cordapp to this directory.
*/
private fun installBuiltCordapp() {
val cordappsDir = File(nodeDir, "cordapps")
project.copy {
it.apply {
from(project.tasks.getByName("jar"))
into(cordappsDir)
}
}
}
/**
* Installs the jolokia monitoring agent JAR to the node/drivers directory
*/
private fun installAgentJar() {
// TODO: improve how we re-use existing declared external variables from root gradle.build
val jolokiaVersion = try { project.rootProject.ext<String>("jolokia_version") } catch (e: Exception) { "1.3.7" }
val agentJar = project.configuration("runtime").files {
(it.group == "org.jolokia") &&
(it.name == "jolokia-jvm") &&
(it.version == jolokiaVersion)
// TODO: revisit when classifier attribute is added. eg && (it.classifier = "agent")
}.first() // should always be the jolokia agent fat jar: eg. jolokia-jvm-1.3.7-agent.jar
project.logger.info("Jolokia agent jar: $agentJar")
if (agentJar.isFile) {
val driversDir = File(nodeDir, "drivers")
project.copy {
it.apply {
from(agentJar)
into(driversDir)
}
}
}
}
private fun createTempConfigFile(): File {
val options = ConfigRenderOptions
.defaults()
.setOriginComments(false)
.setComments(false)
.setFormatted(true)
.setJson(false)
val configFileText = config.root().render(options).split("\n").toList()
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.
val tmpDir = File(project.buildDir, "tmp")
Files.createDirectories(tmpDir.toPath())
var fileName = "${nodeDir.name}.conf"
val tmpConfFile = File(tmpDir, fileName)
Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8)
return tmpConfFile
}
/**
* Installs the configuration file to the root directory and detokenises it.
*/
internal fun installConfig() {
configureProperties()
val tmpConfFile = createTempConfigFile()
appendOptionalConfig(tmpConfFile)
project.copy {
it.apply {
from(tmpConfFile)
into(rootDir)
}
}
}
/**
* Appends installed config file with properties from an optional file.
*/
private fun appendOptionalConfig(confFile: File) {
val optionalConfig: File? = when {
project.findProperty(configFileProperty) != null -> //provided by -PconfigFile command line property when running Gradle task
File(project.findProperty(configFileProperty) as String)
config.hasPath(configFileProperty) -> File(config.getString(configFileProperty))
else -> null
}
if (optionalConfig != null) {
if (!optionalConfig.exists()) {
project.logger.error("$configFileProperty '$optionalConfig' not found")
} else {
confFile.appendBytes(optionalConfig.readBytes())
}
}
}
/**
* Installs other cordapps to this node's cordapps directory.
*/
internal fun installCordapps() {
additionalCordapps.addAll(getCordappList())
val cordappsDir = File(nodeDir, "cordapps")
project.copy {
it.apply {
from(additionalCordapps)
into(cordappsDir)
}
}
}
/**
* Gets a list of cordapps based on what dependent cordapps were specified.
*
* @return List of this node's cordapps.
*/
private fun getCordappList(): Collection<File> {
// Cordapps can sometimes contain a GString instance which fails the equality test with the Java string
@Suppress("RemoveRedundantCallsOfConversionMethods")
val cordapps: List<String> = cordapps.map { it.toString() }
return project.configuration("cordapp").files {
cordapps.contains(it.group + ":" + it.name + ":" + it.version)
}
}
}

View File

@ -0,0 +1,59 @@
package net.corda.plugins
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValueFactory
import groovy.lang.Closure
import org.gradle.api.Project
class RpcSettings(private val project: Project) {
private var config: Config = ConfigFactory.empty()
/**
* RPC address for the node.
*/
fun address(value: String) {
config += "address" to value
}
/**
* RPC admin address for the node (necessary if [useSsl] is false or unset).
*/
fun adminAddress(value: String) {
config += "adminAddress" to value
}
/**
* Specifies whether the node RPC layer will require SSL from clients.
*/
fun useSsl(value: Boolean) {
config += "useSsl" to value
}
/**
* Specifies whether the RPC broker is separate from the node.
*/
fun standAloneBroker(value: Boolean) {
config += "standAloneBroker" to value
}
/**
* Specifies SSL certificates options for the RPC layer.
*/
fun ssl(configureClosure: Closure<in SslOptions>) {
val sslOptions = project.configure(SslOptions(), configureClosure) as SslOptions
config = sslOptions.addTo("ssl", config)
}
internal fun addTo(key: String, config: Config): Config {
if (this.config.isEmpty) {
return config
}
return config + (key to this.config.root())
}
}
internal operator fun Config.plus(entry: Pair<String, Any>): Config {
return withValue(entry.first, ConfigValueFactory.fromAnyRef(entry.second))
}

View File

@ -0,0 +1,50 @@
package net.corda.plugins
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
class SslOptions {
private var config: Config = ConfigFactory.empty()
/**
* Password for the keystore.
*/
fun keyStorePassword(value: String) {
config += "keyStorePassword" to value
}
/**
* Password for the truststore.
*/
fun trustStorePassword(value: String) {
config += "trustStorePassword" to value
}
/**
* Directory under which key stores are to be placed.
*/
fun certificatesDirectory(value: String) {
config += "certificatesDirectory" to value
}
/**
* Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks"
*/
fun sslKeystore(value: String) {
config += "sslKeystore" to value
}
/**
* Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks"
*/
fun trustStoreFile(value: String) {
config += "trustStoreFile" to value
}
internal fun addTo(key: String, config: Config): Config {
if (this.config.isEmpty) {
return config
}
return config + (key to this.config.root())
}
}

View File

@ -0,0 +1 @@
implementation-class=net.corda.plugins.Cordformation

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
# Allow the script to be run from outside the nodes directory.
basedir=$( dirname "$0" )
cd "$basedir"
if which osascript >/dev/null; then
/usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@"
else
"${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar runnodes.jar "$@"
fi

View File

@ -0,0 +1,8 @@
@echo off
REM Change to the directory of this script (%~dp0)
Pushd %~dp0
java -jar runnodes.jar %*
Popd

View File

@ -0,0 +1,151 @@
package net.corda.plugins
import java.awt.GraphicsEnvironment
import java.io.File
import java.nio.file.Files
import java.util.*
private val HEADLESS_FLAG = "--headless"
private val CAPSULE_DEBUG_FLAG = "--capsule-debug"
private val os by lazy {
val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH)
if ("mac" in osName || "darwin" in osName) OS.MACOS
else if ("win" in osName) OS.WINDOWS
else OS.LINUX
}
private enum class OS { MACOS, WINDOWS, LINUX }
private object debugPortAlloc {
private var basePort = 5005
internal fun next() = basePort++
}
private object monitoringPortAlloc {
private var basePort = 7005
internal fun next() = basePort++
}
fun main(args: Array<String>) {
val startedProcesses = mutableListOf<Process>()
val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG)
val capsuleDebugMode = args.contains(CAPSULE_DEBUG_FLAG)
val workingDir = File(System.getProperty("user.dir"))
val javaArgs = args.filter { it != HEADLESS_FLAG && it != CAPSULE_DEBUG_FLAG }
val jvmArgs = if (capsuleDebugMode) listOf("-Dcapsule.log=verbose") else emptyList<String>()
println("Starting nodes in $workingDir")
workingDir.listFiles { file -> file.isDirectory }.forEach { dir ->
listOf(NodeJarType, WebJarType).forEach { jarType ->
jarType.acceptDirAndStartProcess(dir, headless, javaArgs, jvmArgs)?.let { startedProcesses += it }
}
}
println("Started ${startedProcesses.size} processes")
println("Finished starting nodes")
}
private abstract class JarType(private val jarName: String) {
internal abstract fun acceptNodeConf(nodeConf: File): Boolean
internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List<String>, jvmArgs: List<String>): Process? {
if (!File(dir, jarName).exists()) {
return null
}
if (!File(dir, "node.conf").let { it.exists() && acceptNodeConf(it) }) {
return null
}
val debugPort = debugPortAlloc.next()
val monitoringPort = monitoringPortAlloc.next()
println("Starting $jarName in $dir on debug port $debugPort")
val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, monitoringPort, javaArgs, jvmArgs).start()
if (os == OS.MACOS) Thread.sleep(1000)
return process
}
}
private object NodeJarType : JarType("corda.jar") {
override fun acceptNodeConf(nodeConf: File) = true
}
private object WebJarType : JarType("corda-webserver.jar") {
// TODO: Add a webserver.conf, or use TypeSafe config instead of this hack
override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it }
}
private abstract class JavaCommand(
jarName: String,
internal val dir: File,
debugPort: Int?,
monitoringPort: Int?,
internal val nodeName: String,
init: MutableList<String>.() -> Unit, args: List<String>,
jvmArgs: List<String>
) {
private val jolokiaJar by lazy {
File("$dir/drivers").listFiles { _, filename ->
filename.matches("jolokia-jvm-.*-agent\\.jar$".toRegex())
}.first().name
}
internal val command: List<String> = mutableListOf<String>().apply {
add(getJavaPath())
addAll(jvmArgs)
add("-Dname=$nodeName")
val jvmArgs: MutableList<String> = mutableListOf()
null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
if (jvmArgs.isNotEmpty()) {
add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}")
}
add("-jar")
add(jarName)
init()
addAll(args)
}
internal abstract fun processBuilder(): ProcessBuilder
internal fun start() = processBuilder().directory(dir).start()
internal abstract fun getJavaPath(): String
}
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) {
override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path
}
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) {
override fun processBuilder() = ProcessBuilder(when (os) {
OS.MACOS -> {
listOf("osascript", "-e", """tell app "Terminal"
activate
delay 0.5
tell app "System Events" to tell process "Terminal" to keystroke "t" using command down
delay 0.5
do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window
end tell""")
}
OS.WINDOWS -> {
listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}")
}
OS.LINUX -> {
// Start shell to keep window open unless java terminated normally or due to SIGTERM:
val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh"
if (isTmux()) {
listOf("tmux", "new-window", "-n", nodeName, command)
} else {
listOf("xterm", "-T", nodeName, "-e", command)
}
}
})
private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ")
override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path
// Replace below is to fix an issue with spaces in paths on Windows.
// Quoting the entire path does not work, only the space or directory within the path.
private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"")
}
private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells.
private fun isTmux() = System.getenv("TMUX")?.isNotEmpty() ?: false

View File

@ -0,0 +1,92 @@
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
}
}
}
Bintray Publishing
------------------
For large multibuild projects it can be inconvenient to store the entire configuration for bintray and maven central
per project (with a bintray and publishing block with extended POM information). Publish utils can bring the number of
configuration blocks down to one in the ideal scenario.
To use this plugin you must first apply it to both the root project and any project that will be published with
.. code-block:: text
apply plugin: 'net.corda.plugins.publish-utils'
Next you must setup the general bintray configuration you wish to use project wide, for example:
.. code-block:: text
bintrayConfig {
user = <your bintray username>
key = <your bintray user key>
repo = 'example repo'
org = 'example organisation'
licenses = ['a license']
vcsUrl = 'https://example.com'
projectUrl = 'https://example.com'
gpgSign = true // Whether to GPG sign
gpgPassphrase = <your bintray GPG key passphrase> // Only required if gpgSign is true and your key is passworded
publications = ['example'] // a list of publications (see below)
license {
name = 'example'
url = 'https://example.com'
distribution = 'repo'
}
developer {
id = 'a developer id'
name = 'a developer name'
email = 'example@example.com'
}
}
.. note:: You can currently only have one license and developer in the maven POM sections
**Publications**
This plugin assumes, by default, that publications match the name of the project. This means, by default, you can
just list the names of the projects you wish to publish (e.g. to publish `test:myapp` you need `publications = ['myapp']`.
If a project requires a different name you can configure it *per project* with the project configuration block.
The project configuration block has the following structure:
.. code-block:: text
publish {
disableDefaultJar = false // set to true to disable the default JAR being created (e.g. when creating a fat JAR)
name 'non-default-project-name' // Always put this last because it causes configuration to happen
}
**Artifacts**
To add additional artifacts to the project you can use the default gradle `artifacts` block with the `publish`
configuration. For example:
artifacts {
publish buildFatJar {
// You can configure this as a regular maven publication
}
}

View File

@ -0,0 +1,109 @@
apply plugin: 'groovy'
apply plugin: 'maven-publish'
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.jfrog.artifactory'
// Used for bootstrapping project
buildscript {
Properties constants = new Properties()
file("../../constants.properties").withInputStream { constants.load(it) }
ext {
gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
}
repositories {
jcenter()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
}
}
version "$gradle_plugins_version"
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
task("sourceJar", type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}
task("javadocJar", type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
bintray {
user = System.getenv('CORDA_BINTRAY_USER')
key = System.getenv('CORDA_BINTRAY_KEY')
publications = ['publishUtils']
dryRun = false
pkg {
repo = 'corda'
name = 'publish-utils'
userOrg = 'r3'
licenses = ['Apache-2.0']
version {
gpg {
sign = true
passphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
}
}
}
}
publishing {
publications {
publishUtils(MavenPublication) {
from components.java
groupId 'net.corda.plugins'
artifactId 'publish-utils'
artifact sourceJar
artifact javadocJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
name 'publish-utils'
description 'A small gradle plugin that adds a couple of convenience functions for publishing to Maven'
url 'https://github.com/corda/corda'
scm {
url 'https://github.com/corda/corda'
}
licenses {
license {
name 'Apache-2.0'
url 'https://www.apache.org/licenses/LICENSE-2.0'
distribution 'repo'
}
}
developers {
developer {
id 'R3'
name 'R3'
email 'dev@corda.net'
}
}
}
}
}
}
}
// Aliasing the publishToMavenLocal for simplicity.
task(install, dependsOn: 'publishToMavenLocal')

View File

@ -0,0 +1,39 @@
package net.corda.plugins
class ProjectPublishExtension {
private PublishTasks task
void setPublishTask(PublishTasks task) {
this.task = task
}
/**
* Use a different name from the current project name for publishing.
* Set this after all other settings that need to be configured
*/
void name(String name) {
task.setPublishName(name)
}
/**
* Get the publishing name for this project.
*/
String name() {
return task.getPublishName()
}
/**
* True when we do not want to publish default Java components
*/
Boolean disableDefaultJar = false
/**
* True if publishing a WAR instead of a JAR. Forces disableDefaultJAR to "true" when true
*/
Boolean publishWar = false
/**
* True if publishing sources to remote repositories
*/
Boolean publishSources = true
}

View File

@ -0,0 +1,161 @@
package net.corda.plugins
import org.gradle.api.*
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.Project
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.MavenPom
import net.corda.plugins.bintray.*
/**
* A utility plugin that when applied will automatically create source and javadoc publishing tasks
* To apply this plugin you must also add 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' to your
* buildscript's classpath dependencies.
*
* To use this plugin you can add a new configuration block (extension) to your root build.gradle. See the fields
* in BintrayConfigExtension.
*/
class PublishTasks implements Plugin<Project> {
Project project
String publishName
ProjectPublishExtension publishConfig
void apply(Project project) {
this.project = project
this.publishName = project.name
createTasks()
createExtensions()
createConfigurations()
}
void setPublishName(String publishName) {
project.logger.info("Changing publishing name from ${project.name} to ${publishName}")
this.publishName = publishName
checkAndConfigurePublishing()
}
void checkAndConfigurePublishing() {
project.logger.info("Checking whether to publish $publishName")
def bintrayConfig = project.rootProject.extensions.findByType(BintrayConfigExtension.class)
if((bintrayConfig != null) && (bintrayConfig.publications) && (bintrayConfig.publications.findAll { it == publishName }.size() > 0)) {
configurePublishing(bintrayConfig)
}
}
void configurePublishing(BintrayConfigExtension bintrayConfig) {
project.logger.info("Configuring bintray for ${publishName}")
configureMavenPublish(bintrayConfig)
configureBintray(bintrayConfig)
}
void configureMavenPublish(BintrayConfigExtension bintrayConfig) {
project.apply([plugin: 'maven-publish'])
project.publishing.publications.create(publishName, MavenPublication) {
groupId project.group
artifactId publishName
if (publishConfig.publishSources) {
artifact project.tasks.sourceJar
}
artifact project.tasks.javadocJar
project.configurations.publish.artifacts.each {
project.logger.debug("Adding artifact: $it")
delegate.artifact it
}
if (!publishConfig.disableDefaultJar && !publishConfig.publishWar) {
from project.components.java
} else if (publishConfig.publishWar) {
from project.components.web
}
extendPomForMavenCentral(pom, bintrayConfig)
}
project.task("install", dependsOn: "publishToMavenLocal")
}
// Maven central requires all of the below fields for this to be a valid POM
void extendPomForMavenCentral(MavenPom pom, BintrayConfigExtension config) {
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
name publishName
description project.description
url config.projectUrl
scm {
url config.vcsUrl
}
licenses {
license {
name config.license.name
url config.license.url
distribution config.license.url
}
}
developers {
developer {
id config.developer.id
name config.developer.name
email config.developer.email
}
}
}
}
}
void configureBintray(BintrayConfigExtension bintrayConfig) {
project.apply([plugin: 'com.jfrog.bintray'])
project.bintray {
user = bintrayConfig.user
key = bintrayConfig.key
publications = [ publishName ]
dryRun = bintrayConfig.dryRun ?: false
pkg {
repo = bintrayConfig.repo
name = publishName
userOrg = bintrayConfig.org
licenses = bintrayConfig.licenses
version {
gpg {
sign = bintrayConfig.gpgSign ?: false
passphrase = bintrayConfig.gpgPassphrase
}
}
}
}
}
void createTasks() {
if(project.hasProperty('classes')) {
project.task("sourceJar", type: Jar, dependsOn: project.classes) {
classifier = 'sources'
from project.sourceSets.main.allSource
}
}
if(project.hasProperty('javadoc')) {
project.task("javadocJar", type: Jar, dependsOn: project.javadoc) {
classifier = 'javadoc'
from project.javadoc.destinationDir
}
}
}
void createExtensions() {
if(project == project.rootProject) {
project.extensions.create("bintrayConfig", BintrayConfigExtension)
}
publishConfig = project.extensions.create("publish", ProjectPublishExtension)
publishConfig.setPublishTask(this)
}
void createConfigurations() {
project.configurations.create("publish")
}
}

View File

@ -0,0 +1,70 @@
package net.corda.plugins.bintray
import org.gradle.util.ConfigureUtil
class BintrayConfigExtension {
/**
* Bintray username
*/
String user
/**
* Bintray access key
*/
String key
/**
* Bintray repository
*/
String repo
/**
* Bintray organisation
*/
String org
/**
* Licenses for packages uploaded by this configuration
*/
String[] licenses
/**
* Whether to sign packages uploaded by this configuration
*/
Boolean gpgSign
/**
* The passphrase for the key used to sign releases.
*/
String gpgPassphrase
/**
* VCS URL
*/
String vcsUrl
/**
* Project URL
*/
String projectUrl
/**
* The publications that will be uploaded as a part of this configuration. These must match both the name on
* bintray and the gradle module name. ie; it must be "some-package" as a gradle sub-module (root project not
* supported, this extension is to improve multi-build bintray uploads). The publication must also be called
* "some-package". Only one publication can be uploaded per module (a bintray plugin restriction(.
* If any of these conditions are not met your package will not be uploaded.
*/
String[] publications
/**
* Whether to test the publication without uploading to bintray.
*/
Boolean dryRun
/**
* The license this project will use (currently limited to one)
*/
License license = new License()
/**
* The developer of this project (currently limited to one)
*/
Developer developer = new Developer()
void license(Closure closure) {
ConfigureUtil.configure(closure, license)
}
void developer(Closure closure) {
ConfigureUtil.configure(closure, developer)
}
}

View File

@ -0,0 +1,16 @@
package net.corda.plugins.bintray
class Developer {
/**
* A unique identifier the developer (eg; organisation ID)
*/
String id
/**
* The full name of the developer
*/
String name
/**
* An email address for contacting the developer
*/
String email
}

View File

@ -0,0 +1,16 @@
package net.corda.plugins.bintray
class License {
/**
* The name of license (eg; Apache 2.0)
*/
String name
/**
* URL to the full license file
*/
String url
/**
* The distribution level this license corresponds to (eg: repo)
*/
String distribution
}

View File

@ -0,0 +1 @@
implementation-class=net.corda.plugins.PublishTasks

View File

@ -0,0 +1,4 @@
Quasar Utils
============
Quasar utilities adds several tasks and configuration that provide a default Quasar setup and removes some boilerplate.

View File

@ -0,0 +1,19 @@
apply plugin: 'groovy'
apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'
repositories {
mavenCentral()
}
dependencies {
compile gradleApi()
compile localGroovy()
}
publish {
name project.name
}

View File

@ -0,0 +1,28 @@
package net.corda.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 and adds quasar as a dependency.
*/
class QuasarPlugin implements Plugin<Project> {
void apply(Project project) {
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("runtime", 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"
}
}
}

View File

@ -0,0 +1 @@
implementation-class=net.corda.plugins.QuasarPlugin

View File

@ -0,0 +1,7 @@
rootProject.name = 'corda-gradle-plugins'
include 'publish-utils'
include 'quasar-utils'
include 'cordformation'
include 'cordform-common'
include 'api-scanner'
include 'cordapp'