Treat each plugin as an individual source set

Adds separate build tasks for each plugin by auto-discovering plugin meta
files in the build script.  Each plugin will automatically have its
main sources and tests compiled, and then it's output added to the game
modules classpath.

This enables support for incremental compilation of scripts, as well as
unit testing using Gradle's test framework.
This commit is contained in:
Gary Tierney
2017-05-30 02:19:09 +01:00
parent 4ee123a59d
commit 3fb6d3f792
23 changed files with 225 additions and 80 deletions
+6 -42
View File
@@ -7,59 +7,23 @@ buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath group: 'com.moandjiezana.toml', name: 'toml4j', version: '0.7.1'
}
}
apply plugin: 'kotlin'
ext.pluginsDir = "$projectDir/src/plugins"
sourceSets {
plugins {
kotlin {
srcDir "$pluginsDir"
exclude 'stub.kt'
exclude '**/*.kts'
}
}
}
apply plugin: 'kotlin'
apply from: 'plugins.gradle'
dependencies {
compile project(':cache')
compile project(':net')
compile project(':util')
pluginsCompile(configurations.compile)
pluginsCompile(sourceSets.main.output)
compile group: 'io.github.lukehutch', name: 'fast-classpath-scanner', version: '2.0.21'
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8', version: "$kotlinVersion"
compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler', version: "$kotlinVersion"
runtime files("$buildDir/plugins")
runtime sourceSets.plugins.output
}
task compilePluginScripts(type: JavaExec, dependsOn: [classes, pluginsClasses]) {
group = LifecycleBasePlugin.BUILD_GROUP
description = 'Compile plugin script files (.plugin.kts) to java bytecode'
def compilerClasspath = [
configurations.compile.asPath,
configurations.runtime.asPath,
sourceSets.main.compileClasspath.asPath,
sourceSets.main.runtimeClasspath.asPath
]
def outputDir = "$buildDir/plugins"
def manifestPath = "$buildDir/plugins/manifest.txt"
inputs.source "$pluginsDir"
outputs.dir outputDir
classpath = sourceSets.main.compileClasspath + sourceSets.main.runtimeClasspath
main = 'org.apollo.game.plugin.kotlin.KotlinPluginCompiler'
args = ["$pluginsDir", outputDir, manifestPath, compilerClasspath.join(':')]
}
assemble.dependsOn compilePluginScripts
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
}
+161
View File
@@ -0,0 +1,161 @@
import com.moandjiezana.toml.Toml
import java.nio.file.Paths
def PLUGIN_VERIFICATION_GROUP = "plugin-verification"
def PLUGIN_BUILD_GROUP = "plugin-build"
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'com.moandjiezana.toml', name: 'toml4j', version: '0.7.1'
}
}
sourceSets {
pluginStub {
kotlin {
srcDir "$pluginsDir/stub"
exclude 'stub.kt'
}
}
}
dependencies {
pluginStubCompile(project(":game"))
}
task pluginTests {
group = "plugin-verification"
doLast {
println("Finished executing plugin tests")
}
}
class PluginBuildData {
PluginBuildData(String normalizedName, SourceSet mainSources, SourceSet testSources,
FileCollection scriptFiles, List<String> dependencyNames) {
this.normalizedName = normalizedName
this.mainSources = mainSources
this.testSources = testSources
this.scriptFiles = scriptFiles
this.dependencyNames = dependencyNames
}
String normalizedName
SourceSet mainSources
SourceSet testSources
FileCollection scriptFiles
List<String> dependencyNames
}
Map<String, PluginBuildData> pluginMap = new HashMap<>()
def configurePluginDependencies(SourceSet mainSources, SourceSet testSources,
List<PluginBuildData> pluginDependencies) {
def testConfiguration = testSources.compileConfigurationName
def mainConfiguration = mainSources.compileConfigurationName
def runtimeConfiguration = mainSources.runtimeConfigurationName
// Add this plugin as a runtime dependency to the main game project
dependencies.add(configurations.runtime.name, mainSources.output)
pluginDependencies.each {
dependencies.add(mainConfiguration, it.mainSources.output)
dependencies.add(testConfiguration, it.testSources.output)
}
dependencies.add(mainConfiguration, configurations.compile)
dependencies.add(mainConfiguration, sourceSets.main.output)
dependencies.add(runtimeConfiguration, sourceSets.pluginStub.output)
dependencies.add(testConfiguration, mainSources.output)
dependencies.add(testConfiguration, configurations.testCompile)
dependencies.add(testConfiguration, sourceSets.test.output)
}
def configurePluginTasks(String name, SourceSet mainSources, SourceSet testSources,
FileCollection scriptFiles, List<PluginBuildData> pluginDependencies) {
task("${name}Tests", type: Test) {
group = "plugin-verification"
testClassesDir = testSources.output.classesDir
classpath = testSources.runtimeClasspath + mainSources.runtimeClasspath
binResultsDir = file("$buildDir/plugin-test-results/binary/$name")
reports {
html.destination = "$buildDir/reports/plugin-tests/$name"
junitXml.destination = "$buildDir/plugin-tests/$name"
}
}
task("compile${name}Scripts", type: JavaExec) {
group = "plugin-compile"
def outputDir = mainSources.output.classesDir.toString()
inputs.files scriptFiles
outputs.dir outputDir
classpath = sourceSets.main.compileClasspath + sourceSets.main.runtimeClasspath
main = 'org.apollo.game.plugin.kotlin.KotlinPluginCompiler'
args = [outputDir] + scriptFiles.collect { it.absoluteFile.toString() }
}
def testsTask = tasks["${name}Tests"]
pluginTests.dependsOn testsTask
}
def pluginTree = fileTree(dir: "$pluginsDir")
def pluginDefinitions = pluginTree.matching {
include '**/meta.toml'
}
pluginDefinitions.each { file ->
def meta = new Toml()
meta.read(file.absoluteFile)
def pluginFolder = Paths.get(file.parentFile.absolutePath)
def name = meta.getString("name", pluginFolder.getFileName().toString())
def normalizedName = name.replaceAll("[^a-zA-Z0-9_]", '_').toLowerCase()
def packageName = meta.getString("package", "org.apollo.game.plugin")
def authors = meta.getList("authors", new ArrayList())
def dependencies = meta.getList("dependencies", new ArrayList())
def scripts = fileTree(file.parentFile) {
include '**/*.plugin.kts'
}
def srcsDir = meta.getString("config.src", "src/")
def testDir = meta.getString("config.test", "test/")
def mainSources = sourceSets.create("${normalizedName}_main") {
kotlin {
srcDir pluginFolder.resolve(srcsDir).toString()
exclude '*.kts'
}
}
def testSources = sourceSets.create("${normalizedName}_test") {
kotlin {
srcDir pluginFolder.resolve(testDir).toString()
}
}
def pluginData = new PluginBuildData(normalizedName, mainSources, testSources, scripts, dependencies)
pluginMap.put(normalizedName, pluginData)
}
pluginMap.values().each {
def dependencies = it.dependencyNames.collect { name -> pluginMap.get(name) }
configurePluginDependencies(it.mainSources, it.testSources, dependencies)
configurePluginTasks(it.normalizedName, it.mainSources, it.testSources, it.scriptFiles, dependencies)
}
@@ -15,6 +15,9 @@ import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult;
import org.apollo.game.model.World;
import org.apollo.game.plugin.kotlin.KotlinPluginCompiler;
import org.apollo.game.plugin.kotlin.KotlinPluginScript;
@@ -37,15 +40,14 @@ public class KotlinPluginEnvironment implements PluginEnvironment {
@Override
public void load(Collection<PluginMetaData> plugins) {
List<KotlinPluginScript> pluginScripts = new ArrayList<>();
List<Class<? extends KotlinPluginScript>> pluginClasses = new ArrayList<>();
try (InputStream resource = KotlinPluginEnvironment.class.getResourceAsStream("/manifest.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(resource));
List<String> pluginClassNames = reader.lines().collect(Collectors.toList());
for (String pluginClassName : pluginClassNames) {
Class<? extends KotlinPluginScript> pluginClass =
(Class<? extends KotlinPluginScript>) Class.forName(pluginClassName);
new FastClasspathScanner()
.matchSubclassesOf(KotlinPluginScript.class, pluginClasses::add)
.scan();
try {
for (Class<? extends KotlinPluginScript> pluginClass : pluginClasses) {
Constructor<? extends KotlinPluginScript> pluginConstructor =
pluginClass.getConstructor(World.class, PluginContext.class);
@@ -55,7 +57,10 @@ public class KotlinPluginEnvironment implements PluginEnvironment {
throw new RuntimeException(e);
}
pluginScripts.forEach(script -> script.doStart(world));
pluginScripts.forEach(script -> {
logger.info("Starting script: " + script.getClass().getName());
script.doStart(world);
});
}
@Override
@@ -43,8 +43,6 @@ class KotlinPluginCompiler(val classpath: List<File>, val messageCollector: Mess
companion object {
private val maxSearchDepth = 1024;
fun currentClasspath(): List<File> {
val classLoader = Thread.currentThread().contextClassLoader as? URLClassLoader ?:
throw RuntimeException("Unable to resolve classpath for current ClassLoader")
@@ -66,12 +64,10 @@ class KotlinPluginCompiler(val classpath: List<File>, val messageCollector: Mess
@JvmStatic
fun main(args: Array<String>) {
if (args.size < 4) throw RuntimeException("Usage: <inputDir> <outputDir> <manifestPath> <classpath>")
if (args.size < 2) throw RuntimeException("Usage: <outputDirectory> script1.kts script2.kts ...")
val inputDir = Paths.get(args[0])
val outputDir = Paths.get(args[1])
val manifestPath = Paths.get(args[2])
val classpathEntries = args[3].split(':')
val outputDir = Paths.get(args[0])
val inputScripts = args.slice(1..args.size - 1).map { Paths.get(it) }
val classpath = mutableListOf<File>()
val runtimeBean = ManagementFactory.getRuntimeMXBean()
@@ -83,15 +79,11 @@ class KotlinPluginCompiler(val classpath: List<File>, val messageCollector: Mess
}
/**
* Classpath entries on the command line contain the kotlin runtime
* and plugin API code. We use our current classpath to provide
* Kotlin with access to the JRE and apollo modules.
* Our current classpath should contain all compile time dependencies for the plugin as well as Apollo's
* own sources. We can also achieve this via Gradle but doing it at runtime prevents Gradle from thinking
* the build has been modified after evaluation.
*/
classpath.addAll(currentClasspath())
classpath.addAll(classpathEntries.map { File(it) })
val inputScriptsMatcher = { path: Path, _: BasicFileAttributes -> path.toString().endsWith(".plugin.kts") }
val inputScripts = Files.find(inputDir, maxSearchDepth, BiPredicate(inputScriptsMatcher))
val compiler = KotlinPluginCompiler(classpath, MessageCollector.NONE)
val compiledScriptClasses = mutableListOf<String>()
@@ -106,8 +98,6 @@ class KotlinPluginCompiler(val classpath: List<File>, val messageCollector: Mess
inputScripts.forEach {
compiledScriptClasses.add(compiler.compile(it, outputDir).fqName)
}
Files.write(manifestPath, compiledScriptClasses, CREATE, TRUNCATE_EXISTING)
} catch (t: Throwable) {
t.printStackTrace()
System.exit(1)
+7
View File
@@ -0,0 +1,7 @@
name = "Banking"
package = "org.apollo.game.plugin.banking"
authors = [ "Major" ]
[config]
srcDir = "src/"
testDir = "test/"
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0"?>
<plugin>
<id>bank</id>
<version>1</version>
<name>Bank</name>
<description>Opens the bank interface when players select 'use-quickly' on a bank booth.</description>
<authors>
<author>Major</author>
</authors>
<scripts>
<script>bank.plugin.kts</script>
</scripts>
<dependencies />
</plugin>
View File
+8
View File
@@ -0,0 +1,8 @@
name = "spawning"
package = "org.apollo.game.plugin.entity"
authors = [ "Gary Tierney" ]
dependencies = [ "entity_lookup" ]
[config]
srcDir = "src/"
testDir = "test/"
@@ -0,0 +1,8 @@
name = "lumbridge npc spawns"
package = "org.apollo.game.plugin.locations"
authors = [ "Gary Tierney" ]
dependencies = [ "spawning" ]
[config]
srcDir = "src/"
testDir = "test/"
+2
View File
@@ -0,0 +1,2 @@
name = "entity lookup"
package = "org.apollo.game.plugins.util"
@@ -0,0 +1,14 @@
import org.apollo.cache.def.ItemDefinition
import org.junit.Test
import kotlin.test.assertEquals
class LookupTests {
@Test fun itemLookup() {
val testItem = ItemDefinition(0)
testItem.name = "sword"
ItemDefinition.init(arrayOf(testItem))
assertEquals(testItem, lookup_item("sword"))
}
}