From 3fb6d3f792fd3558f33aae71a478843cd62e5179 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Tue, 30 May 2017 02:19:09 +0100 Subject: [PATCH] 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. --- game/build.gradle | 48 +----- game/plugins.gradle | 161 ++++++++++++++++++ .../game/plugin/KotlinPluginEnvironment.java | 21 ++- .../plugin/kotlin/KotlinPluginCompiler.kt | 22 +-- game/src/plugins/bank/meta.toml | 7 + game/src/plugins/bank/plugin.xml | 14 -- .../plugins/bank/{ => src}/bank.plugin.kts | 0 game/src/plugins/bank/test/BankingTests.kt | 0 .../plugins/chat/private-messaging/meta.toml | 0 .../{ => src}/friends.plugin.kts | 0 .../{ => src}/ignores.plugin.kts | 0 .../{ => src}/messaging.plugin.kts | 0 game/src/plugins/dummy/meta.toml | 0 .../plugins/dummy/{ => src}/dummy.plugin.kts | 0 game/src/plugins/entity/spawn/meta.toml | 8 + .../plugins/entity/spawn/{ => src}/spawn.kt | 0 .../entity/spawn/{ => src}/spawn.plugin.kts | 0 .../src/plugins/locations/lumbridge/meta.toml | 8 + .../lumbridge/{ => src}/lumbridge-npcs.kts | 0 game/src/plugins/{ => stub}/stub.kt | 0 game/src/plugins/util/lookup/meta.toml | 2 + .../plugins/util/lookup/{ => src}/lookup.kt | 0 .../plugins/util/lookup/test/LookupTests.kt | 14 ++ 23 files changed, 225 insertions(+), 80 deletions(-) create mode 100644 game/plugins.gradle create mode 100644 game/src/plugins/bank/meta.toml delete mode 100644 game/src/plugins/bank/plugin.xml rename game/src/plugins/bank/{ => src}/bank.plugin.kts (100%) create mode 100644 game/src/plugins/bank/test/BankingTests.kt create mode 100644 game/src/plugins/chat/private-messaging/meta.toml rename game/src/plugins/chat/private-messaging/{ => src}/friends.plugin.kts (100%) rename game/src/plugins/chat/private-messaging/{ => src}/ignores.plugin.kts (100%) rename game/src/plugins/chat/private-messaging/{ => src}/messaging.plugin.kts (100%) create mode 100644 game/src/plugins/dummy/meta.toml rename game/src/plugins/dummy/{ => src}/dummy.plugin.kts (100%) create mode 100644 game/src/plugins/entity/spawn/meta.toml rename game/src/plugins/entity/spawn/{ => src}/spawn.kt (100%) rename game/src/plugins/entity/spawn/{ => src}/spawn.plugin.kts (100%) create mode 100644 game/src/plugins/locations/lumbridge/meta.toml rename game/src/plugins/locations/lumbridge/{ => src}/lumbridge-npcs.kts (100%) rename game/src/plugins/{ => stub}/stub.kt (100%) create mode 100644 game/src/plugins/util/lookup/meta.toml rename game/src/plugins/util/lookup/{ => src}/lookup.kt (100%) create mode 100644 game/src/plugins/util/lookup/test/LookupTests.kt diff --git a/game/build.gradle b/game/build.gradle index bcce4790..f6581a2e 100644 --- a/game/build.gradle +++ b/game/build.gradle @@ -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 \ No newline at end of file + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" +} \ No newline at end of file diff --git a/game/plugins.gradle b/game/plugins.gradle new file mode 100644 index 00000000..fb405acf --- /dev/null +++ b/game/plugins.gradle @@ -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 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 dependencyNames +} + +Map pluginMap = new HashMap<>() + +def configurePluginDependencies(SourceSet mainSources, SourceSet testSources, + List 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 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) +} diff --git a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java index b2e74222..dba14469 100644 --- a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java +++ b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java @@ -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 plugins) { List pluginScripts = new ArrayList<>(); + List> pluginClasses = new ArrayList<>(); - try (InputStream resource = KotlinPluginEnvironment.class.getResourceAsStream("/manifest.txt")) { - BufferedReader reader = new BufferedReader(new InputStreamReader(resource)); - List pluginClassNames = reader.lines().collect(Collectors.toList()); - - for (String pluginClassName : pluginClassNames) { - Class pluginClass = - (Class) Class.forName(pluginClassName); + new FastClasspathScanner() + .matchSubclassesOf(KotlinPluginScript.class, pluginClasses::add) + .scan(); + try { + for (Class pluginClass : pluginClasses) { Constructor 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 diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt index 4c615583..ee655af3 100644 --- a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt +++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt @@ -43,8 +43,6 @@ class KotlinPluginCompiler(val classpath: List, val messageCollector: Mess companion object { - private val maxSearchDepth = 1024; - fun currentClasspath(): List { 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, val messageCollector: Mess @JvmStatic fun main(args: Array) { - if (args.size < 4) throw RuntimeException("Usage: ") + if (args.size < 2) throw RuntimeException("Usage: 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() val runtimeBean = ManagementFactory.getRuntimeMXBean() @@ -83,15 +79,11 @@ class KotlinPluginCompiler(val classpath: List, 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() @@ -106,8 +98,6 @@ class KotlinPluginCompiler(val classpath: List, 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) diff --git a/game/src/plugins/bank/meta.toml b/game/src/plugins/bank/meta.toml new file mode 100644 index 00000000..e7895a9e --- /dev/null +++ b/game/src/plugins/bank/meta.toml @@ -0,0 +1,7 @@ +name = "Banking" +package = "org.apollo.game.plugin.banking" +authors = [ "Major" ] + +[config] +srcDir = "src/" +testDir = "test/" \ No newline at end of file diff --git a/game/src/plugins/bank/plugin.xml b/game/src/plugins/bank/plugin.xml deleted file mode 100644 index 4402e5f0..00000000 --- a/game/src/plugins/bank/plugin.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - bank - 1 - Bank - Opens the bank interface when players select 'use-quickly' on a bank booth. - - Major - - - - - - diff --git a/game/src/plugins/bank/bank.plugin.kts b/game/src/plugins/bank/src/bank.plugin.kts similarity index 100% rename from game/src/plugins/bank/bank.plugin.kts rename to game/src/plugins/bank/src/bank.plugin.kts diff --git a/game/src/plugins/bank/test/BankingTests.kt b/game/src/plugins/bank/test/BankingTests.kt new file mode 100644 index 00000000..e69de29b diff --git a/game/src/plugins/chat/private-messaging/meta.toml b/game/src/plugins/chat/private-messaging/meta.toml new file mode 100644 index 00000000..e69de29b diff --git a/game/src/plugins/chat/private-messaging/friends.plugin.kts b/game/src/plugins/chat/private-messaging/src/friends.plugin.kts similarity index 100% rename from game/src/plugins/chat/private-messaging/friends.plugin.kts rename to game/src/plugins/chat/private-messaging/src/friends.plugin.kts diff --git a/game/src/plugins/chat/private-messaging/ignores.plugin.kts b/game/src/plugins/chat/private-messaging/src/ignores.plugin.kts similarity index 100% rename from game/src/plugins/chat/private-messaging/ignores.plugin.kts rename to game/src/plugins/chat/private-messaging/src/ignores.plugin.kts diff --git a/game/src/plugins/chat/private-messaging/messaging.plugin.kts b/game/src/plugins/chat/private-messaging/src/messaging.plugin.kts similarity index 100% rename from game/src/plugins/chat/private-messaging/messaging.plugin.kts rename to game/src/plugins/chat/private-messaging/src/messaging.plugin.kts diff --git a/game/src/plugins/dummy/meta.toml b/game/src/plugins/dummy/meta.toml new file mode 100644 index 00000000..e69de29b diff --git a/game/src/plugins/dummy/dummy.plugin.kts b/game/src/plugins/dummy/src/dummy.plugin.kts similarity index 100% rename from game/src/plugins/dummy/dummy.plugin.kts rename to game/src/plugins/dummy/src/dummy.plugin.kts diff --git a/game/src/plugins/entity/spawn/meta.toml b/game/src/plugins/entity/spawn/meta.toml new file mode 100644 index 00000000..3551199d --- /dev/null +++ b/game/src/plugins/entity/spawn/meta.toml @@ -0,0 +1,8 @@ +name = "spawning" +package = "org.apollo.game.plugin.entity" +authors = [ "Gary Tierney" ] +dependencies = [ "entity_lookup" ] + +[config] +srcDir = "src/" +testDir = "test/" \ No newline at end of file diff --git a/game/src/plugins/entity/spawn/spawn.kt b/game/src/plugins/entity/spawn/src/spawn.kt similarity index 100% rename from game/src/plugins/entity/spawn/spawn.kt rename to game/src/plugins/entity/spawn/src/spawn.kt diff --git a/game/src/plugins/entity/spawn/spawn.plugin.kts b/game/src/plugins/entity/spawn/src/spawn.plugin.kts similarity index 100% rename from game/src/plugins/entity/spawn/spawn.plugin.kts rename to game/src/plugins/entity/spawn/src/spawn.plugin.kts diff --git a/game/src/plugins/locations/lumbridge/meta.toml b/game/src/plugins/locations/lumbridge/meta.toml new file mode 100644 index 00000000..17ab1025 --- /dev/null +++ b/game/src/plugins/locations/lumbridge/meta.toml @@ -0,0 +1,8 @@ +name = "lumbridge npc spawns" +package = "org.apollo.game.plugin.locations" +authors = [ "Gary Tierney" ] +dependencies = [ "spawning" ] + +[config] +srcDir = "src/" +testDir = "test/" \ No newline at end of file diff --git a/game/src/plugins/locations/lumbridge/lumbridge-npcs.kts b/game/src/plugins/locations/lumbridge/src/lumbridge-npcs.kts similarity index 100% rename from game/src/plugins/locations/lumbridge/lumbridge-npcs.kts rename to game/src/plugins/locations/lumbridge/src/lumbridge-npcs.kts diff --git a/game/src/plugins/stub.kt b/game/src/plugins/stub/stub.kt similarity index 100% rename from game/src/plugins/stub.kt rename to game/src/plugins/stub/stub.kt diff --git a/game/src/plugins/util/lookup/meta.toml b/game/src/plugins/util/lookup/meta.toml new file mode 100644 index 00000000..91b1757b --- /dev/null +++ b/game/src/plugins/util/lookup/meta.toml @@ -0,0 +1,2 @@ +name = "entity lookup" +package = "org.apollo.game.plugins.util" diff --git a/game/src/plugins/util/lookup/lookup.kt b/game/src/plugins/util/lookup/src/lookup.kt similarity index 100% rename from game/src/plugins/util/lookup/lookup.kt rename to game/src/plugins/util/lookup/src/lookup.kt diff --git a/game/src/plugins/util/lookup/test/LookupTests.kt b/game/src/plugins/util/lookup/test/LookupTests.kt new file mode 100644 index 00000000..545cbc41 --- /dev/null +++ b/game/src/plugins/util/lookup/test/LookupTests.kt @@ -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")) + } +} \ No newline at end of file