diff --git a/.gitignore b/.gitignore index 0c3abc5..2621d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ .idea/**/dbnavigator.xml /build/ +/api/build/ +/api/tools/ +/gradle-plugin/build/ .idea .kotlin diff --git a/README.md b/README.md index 7e674c2..a84bd9b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ -# Pumpkin Plugin Template for Kotlin +# Pumpkin Plugin API for Kotlin -This repository provides a template to get started making [Pumpkin](https://github.com/Pumpkin-MC/Pumpkin) plugins using WebAssembly (Wasm) components. +This repository builds the Kotlin bindings for [Pumpkin](https://github.com/Pumpkin-MC/Pumpkin) plugins using WebAssembly (Wasm) components. -Unlike many of the other available language bindings, this repo is a template, not a package. You should clone this repository as a starting point. +The `api` Gradle module is a publishable Kotlin/WASI library and generated-source snapshot. It generates the WIT bindings only while API maintainers build or publish the library; the consumer Gradle plugin will unpack that snapshot before compiling a plugin, so plugin authors do not install or run `wit-bindgen`. -## Requirements +The root component template remains available while the consumer Gradle plugin is being moved into `gradle-plugin`. + +## API maintainer requirements Kotlin/Wasm + components it's still in it's early stages, and not as straightforward as could be. You will need to have several things installed for everything to work. - JDK 17 or later - To run Gradle 9.4 - Rust - - This is required as a key component (wit-bindgen) is written and Rust must be built from a particular Kotlin-enabled fork. + - This is required only to build or publish the API, because `wit-bindgen` is built from a particular Kotlin-enabled fork. - You only need a default Rust install for your host platform. NOT for any WebAssembly targets -## Initial setup +## Building the API Initialize the WIT submodule after cloning: @@ -22,15 +24,28 @@ Initialize the WIT submodule after cloning: git submodule update --init --recursive ``` -Then adjust the project name in `settings.gradle.kts`. The generated component is named after that project. +Publish a local development artifact with: -Run `./gradlew build` to install the pinned build tools, generate the `pumpkin` package bindings, compile the Kotlin/Wasm module, and produce a validated component at `build/.wasm`, ready to be installed into Pumpkin. Generated bindings live under `build/generated/`. +```sh +./gradlew :api:publishToMavenLocal +``` -Then you can tweak the plugin source in `src/wasmWasiMain/kotlin/plugin/Plugin.kt`. +This generates the `pumpkin` package bindings under `api/build/generated/`, compiles them with the package-owned WIT export bridge, and publishes the KLIB plus a generated-source archive. The source archive includes the pinned WIT snapshot and Preview 1 reactor adapter needed to assemble a component. -## Rebuilding +```kotlin +repositories { + mavenLocal() + mavenCentral() +} -Rebuild with `./gradlew build`. Gradle skips tool installation, binding generation, and component assembly when their declared inputs are unchanged. Use `./gradlew clean` to remove generated build output; it intentionally retains the installed tool cache under `tools/`. +// The consumer Gradle plugin resolves the matching generated-source archive. +``` + +The API artifact exposes `PumpkinPlugin` and `registerPlugin(...)`; plugin authors should implement and register that class rather than defining the generator-specific `PluginRootFunctionsExportsImpl` or `MetadataImpl` types. + +## Root template build + +The original root template still builds a complete component with `./gradlew build`. Gradle skips tool installation, binding generation, and component assembly when their declared inputs are unchanged. Use `./gradlew clean` to remove generated build output; it intentionally retains the installed tool cache under `tools/`. ## Updating the WIT diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..e452733 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,110 @@ +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.jvm.tasks.Jar +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + `maven-publish` +} + +group = "io.github.pumpkin-mc" +version = providers.gradleProperty("pumpkinApiVersion").getOrElse("0.1.0-dev") + +base { + archivesName.set("pumpkin-api-kt") +} + +repositories { + mavenCentral() +} + +kotlin { + @OptIn(ExperimentalWasmDsl::class) + wasmWasi { + nodejs() + } +} + +publishing { + publications.named("kotlinMultiplatform") { + artifactId = "pumpkin-api-kt" + } + publications.named("wasmWasi") { + artifactId = "pumpkin-api-kt-wasm-wasi" + } +} + +val cargoHome = providers.environmentVariable("CARGO_HOME") + .orElse(providers.systemProperty("user.home").map { "$it/.cargo" }) +val executableSuffix = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) ".exe" else "" +val cargoExecutable = cargoHome.map { "$it/bin/cargo$executableSuffix" } + +val witDirectory = rootProject.layout.projectDirectory.dir("wit/v0.1") +val witBindgenRevision = "700f2db5e1d01f7bee8d756750c6f631171f520e" +val witBindgenDirectory = layout.projectDirectory.dir("tools/wit-bindgen/$witBindgenRevision") +val witBindgen = witBindgenDirectory.file("bin/wit-bindgen$executableSuffix") +val generatedBindings = layout.buildDirectory.dir("generated/wit/wasmWasiMain/kotlin") + +val installWitBindgen by tasks.registering(Exec::class) { + group = "build setup" + description = "Installs Pumpkin's pinned Kotlin binding generator." + + inputs.property("revision", witBindgenRevision) + outputs.dir(witBindgenDirectory) + + commandLine( + cargoExecutable.get(), + "install", + "wit-bindgen-cli", + "--git", "https://github.com/Kotlin/wit-bindgen", + "--rev", witBindgenRevision, + "--locked", + "--root", witBindgenDirectory.asFile.absolutePath, + ) +} + +val generateWitBindings by tasks.registering(Exec::class) { + group = "build" + description = "Generates Kotlin bindings for the published Pumpkin API." + + dependsOn(installWitBindgen) + inputs.dir(witDirectory) + inputs.file(witBindgen) + inputs.property("kotlinPackage", "pumpkin") + inputs.property("kotlinImports", "plugin.*") + outputs.dir(generatedBindings) + + doFirst { + val outputDirectory = generatedBindings.get().asFile + check(outputDirectory.deleteRecursively() || !outputDirectory.exists()) { + "Could not remove stale bindings from $outputDirectory" + } + check(outputDirectory.mkdirs() || outputDirectory.isDirectory) { + "Could not create bindings directory $outputDirectory" + } + } + + commandLine( + witBindgen.asFile.absolutePath, + "kotlin", + "--kotlin-imports", "plugin.*", + "--kotlin-package-name", "pumpkin", + witDirectory.asFile.absolutePath, + "--out-dir", generatedBindings.get().asFile.absolutePath, + ) +} + +kotlin { + sourceSets.named("wasmWasiMain") { + kotlin.srcDir(generateWitBindings) + } +} + +tasks.named("wasmWasiSourcesJar") { + from(rootProject.layout.projectDirectory.dir("wit/v0.1")) { + into("wit/v0.1") + } + from(rootProject.layout.projectDirectory.file("wasi_snapshot_preview1.reactor.wasm")) { + into("wasi") + } +} diff --git a/api/src/wasmWasiMain/kotlin/plugin/PumpkinPlugin.kt b/api/src/wasmWasiMain/kotlin/plugin/PumpkinPlugin.kt new file mode 100644 index 0000000..da5632c --- /dev/null +++ b/api/src/wasmWasiMain/kotlin/plugin/PumpkinPlugin.kt @@ -0,0 +1,124 @@ +package plugin + +import pumpkin.Command +import pumpkin.Context +import pumpkin.Event +import pumpkin.Metadata +import pumpkin.PluginRootFunctions +import pumpkin.Server +import pumpkin.World +import pumpkin.runtime.ComponentException + +/** The implementation supplied by a Kotlin Pumpkin plugin. */ +abstract class PumpkinPlugin { + abstract fun metadata(): PluginMetadata + + open fun initPlugin() = Unit + + open fun onLoad(context: Context.Context): Result = Result.success(Unit) + + open fun onUnload(context: Context.Context): Result = Result.success(Unit) + + open fun handleCommand( + commandId: UInt, + sender: Command.CommandSender, + server: Server.Server, + args: Command.ConsumedArgs, + ): Result = unsupportedCallback("handleCommand") + + open fun handleEvent( + eventId: UInt, + server: Server.Server, + event: Event.Event, + ): Event.Event = unsupportedCallback("handleEvent") + + open fun handleTask(handlerId: UInt, server: Server.Server) = + unsupportedCallback("handleTask") + + open fun handleIpcMessage(sender: String, message: List): Result> = + Result.failure(ComponentException("This plugin cannot receive messages.")) + + open fun handleAiGoalCanStart(goalId: UInt, server: Server.Server, entity: World.Entity): Boolean = + unsupportedCallback("handleAiGoalCanStart") + + open fun handleAiGoalShouldContinue(goalId: UInt, server: Server.Server, entity: World.Entity): Boolean = + unsupportedCallback("handleAiGoalShouldContinue") + + open fun handleAiGoalStart(goalId: UInt, server: Server.Server, entity: World.Entity) = + unsupportedCallback("handleAiGoalStart") + + open fun handleAiGoalTick(goalId: UInt, server: Server.Server, entity: World.Entity) = + unsupportedCallback("handleAiGoalTick") + + open fun handleAiGoalStop(goalId: UInt, server: Server.Server, entity: World.Entity) = + unsupportedCallback("handleAiGoalStop") +} + +typealias PluginMetadata = Metadata.PluginMetadata + +private var registeredPlugin: PumpkinPlugin? = null + +/** Registers the plugin instance exported by this WebAssembly component. */ +fun registerPlugin(plugin: PumpkinPlugin) { + check(registeredPlugin == null) { "A Pumpkin plugin has already been registered." } + registeredPlugin = plugin +} + +private fun requirePlugin(): PumpkinPlugin = checkNotNull(registeredPlugin) { + "No Pumpkin plugin has been registered. Call registerPlugin(...) during initialization." +} + +private fun unsupportedCallback(name: String): T = + error("PumpkinPlugin.$name has not been implemented.") + +/** + * WIT export bridge used by the generated bindings. Plugin authors use [PumpkinPlugin] and + * [registerPlugin] instead of implementing this generated interface directly. + */ +internal class PluginRootFunctionsExportsImpl { + companion object : PluginRootFunctions.Exports { + override fun initPlugin() = requirePlugin().initPlugin() + + override fun onLoad(context: Context.Context) = requirePlugin().onLoad(context) + + override fun onUnload(context: Context.Context) = requirePlugin().onUnload(context) + + override fun handleCommand( + commandId: UInt, + sender: Command.CommandSender, + server: Server.Server, + args: Command.ConsumedArgs, + ) = requirePlugin().handleCommand(commandId, sender, server, args) + + override fun handleEvent(eventId: UInt, server: Server.Server, event: Event.Event) = + requirePlugin().handleEvent(eventId, server, event) + + override fun handleTask(handlerId: UInt, server: Server.Server) = + requirePlugin().handleTask(handlerId, server) + + override fun handleIpcMessage(sender: String, message: List) = + requirePlugin().handleIpcMessage(sender, message) + + override fun handleAiGoalCanStart(goalId: UInt, server: Server.Server, entity: World.Entity) = + requirePlugin().handleAiGoalCanStart(goalId, server, entity) + + override fun handleAiGoalShouldContinue(goalId: UInt, server: Server.Server, entity: World.Entity) = + requirePlugin().handleAiGoalShouldContinue(goalId, server, entity) + + override fun handleAiGoalStart(goalId: UInt, server: Server.Server, entity: World.Entity) = + requirePlugin().handleAiGoalStart(goalId, server, entity) + + override fun handleAiGoalTick(goalId: UInt, server: Server.Server, entity: World.Entity) = + requirePlugin().handleAiGoalTick(goalId, server, entity) + + override fun handleAiGoalStop(goalId: UInt, server: Server.Server, entity: World.Entity) = + requirePlugin().handleAiGoalStop(goalId, server, entity) + } +} + +/** WIT metadata export bridge used by the generated bindings. */ +internal class MetadataImpl { + companion object : Metadata { + override fun getMetadata() = requirePlugin().metadata() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 74aa151..2ae01f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,16 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.gradle.process.CommandLineArgumentProvider +import org.gradle.api.tasks.Sync +import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec plugins { alias(libs.plugins.kotlinMultiplatform) } +evaluationDependsOn(":api") + repositories { mavenCentral() } @@ -19,47 +23,18 @@ kotlin { } } -val cargoHome = providers.environmentVariable("CARGO_HOME") - .orElse(providers.systemProperty("user.home").map { "$it/.cargo" }) val executableSuffix = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) ".exe" else "" -val cargoExecutable = cargoHome.map { "$it/bin/cargo$executableSuffix" } - -val witDirectory = layout.projectDirectory.dir("wit/v0.1") -val reactorAdapter = layout.projectDirectory.file("wasi_snapshot_preview1.reactor.wasm") - -val witBindgenRevision = "700f2db5e1d01f7bee8d756750c6f631171f520e" -val witBindgenDirectory = layout.projectDirectory.dir("tools/wit-bindgen/$witBindgenRevision") -val witBindgen = witBindgenDirectory.file("bin/wit-bindgen") val wasmToolsVersion = "1.258.0" val wasmToolsDirectory = layout.projectDirectory.dir("tools/wasm-tools/$wasmToolsVersion") val wasmTools = wasmToolsDirectory.file("bin/wasm-tools$executableSuffix") -val generatedBindings = layout.buildDirectory.dir("generated/wit/wasmWasiMain/kotlin") val projectWasmName = rootProject.name val embeddedReleaseComponent = layout.buildDirectory.file( "intermediates/pumpkin-components/release/$projectWasmName-embedded.wasm" ) val releaseComponent = layout.buildDirectory.file("$projectWasmName.wasm") -val installWitBindgen by tasks.registering(Exec::class) { - group = "build setup" - description = "Installs Pumpkin's pinned Kotlin binding generator." - - inputs.property("revision", witBindgenRevision) - outputs.dir(witBindgenDirectory) - - commandLine( - cargoExecutable.get(), - "install", - "wit-bindgen-cli", - "--git", "https://github.com/Kotlin/wit-bindgen", - "--rev", witBindgenRevision, - "--locked", - "--root", witBindgenDirectory.asFile.absolutePath, - ) -} - val installWasmTools by tasks.registering(Exec::class) { group = "build setup" description = "Installs the pinned wasm-tools executable." @@ -68,7 +43,10 @@ val installWasmTools by tasks.registering(Exec::class) { outputs.dir(wasmToolsDirectory) commandLine( - cargoExecutable.get(), + providers.environmentVariable("CARGO_HOME") + .orElse(providers.systemProperty("user.home").map { "$it/.cargo" }) + .map { "$it/bin/cargo$executableSuffix" } + .get(), "install", "wasm-tools", "--version", wasmToolsVersion, @@ -77,40 +55,21 @@ val installWasmTools by tasks.registering(Exec::class) { ) } -val generateWitBindings by tasks.registering(Exec::class) { - group = "build" - description = "Generates Kotlin bindings from Pumpkin's WIT definitions." +val apiSourcesJar = project(":api").tasks.named("wasmWasiSourcesJar") +val unpackApiSources by tasks.registering(Sync::class) { + group = "build setup" + description = "Unpacks the published Pumpkin API source snapshot for component compilation." - dependsOn(installWitBindgen) - inputs.dir(witDirectory) - inputs.file(witBindgen) - inputs.property("kotlinPackage", "pumpkin") - inputs.property("kotlinImports", "plugin.*") - outputs.dir(generatedBindings) - - doFirst { - val outputDirectory = generatedBindings.get().asFile - check(outputDirectory.deleteRecursively() || !outputDirectory.exists()) { - "Could not remove stale bindings from $outputDirectory" - } - check(outputDirectory.mkdirs() || outputDirectory.isDirectory) { - "Could not create bindings directory $outputDirectory" - } - } - - commandLine( - witBindgen.asFile.absolutePath, - "kotlin", - "--kotlin-imports", "plugin.*", - "--kotlin-package-name", "pumpkin", - witDirectory.asFile.absolutePath, - "--out-dir", generatedBindings.get().asFile.absolutePath, - ) + dependsOn(apiSourcesJar) + from(apiSourcesJar.flatMap { it.archiveFile }.map { zipTree(it.asFile) }) + into(layout.buildDirectory.dir("generated/pumpkin-api")) } +val witDirectory = unpackApiSources.map { it.destinationDir.resolve("wit/v0.1") } +val reactorAdapter = unpackApiSources.map { it.destinationDir.resolve("wasi/wasi_snapshot_preview1.reactor.wasm") } kotlin { sourceSets.named("wasmWasiMain") { - kotlin.srcDir(generateWitBindings) + kotlin.srcDir(unpackApiSources) } } @@ -135,7 +94,7 @@ val embedComponentWitRelease by tasks.registering(Exec::class) { override fun asArguments() = listOf( "component", "embed", - witDirectory.asFile.absolutePath, + witDirectory.get().absolutePath, releaseCoreWasm.get().asFile.absolutePath, "-o", embeddedReleaseComponent.get().asFile.absolutePath, ) @@ -165,7 +124,7 @@ val assemblePluginRelease by tasks.registering(Exec::class) { "component", "new", embeddedReleaseComponent.get().asFile.absolutePath, - "--adapt", "wasi_snapshot_preview1=${reactorAdapter.asFile.absolutePath}", + "--adapt", "wasi_snapshot_preview1=${reactorAdapter.get().absolutePath}", "-o", releaseComponent.get().asFile.absolutePath, ) } diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts new file mode 100644 index 0000000..67edbba --- /dev/null +++ b/gradle-plugin/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + `kotlin-dsl` + `java-gradle-plugin` + `maven-publish` +} + +group = "io.github.pumpkin-mc" +version = providers.gradleProperty("pumpkinApiVersion").getOrElse("0.1.0-dev") + +base { + archivesName.set("pumpkin-api-kt-gradle-plugin") +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0") +} + +kotlin { + jvmToolchain(17) +} + +gradlePlugin { + plugins { + create("pumpkinPlugin") { + id = "io.github.pumpkin-mc.plugin" + implementationClass = "io.github.pumpkinmc.gradle.PumpkinPlugin" + displayName = "Pumpkin Kotlin plugin build" + description = "Builds Kotlin/Wasm Pumpkin plugins from the published API source snapshot." + } + } +} diff --git a/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt b/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt new file mode 100644 index 0000000..c825c65 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt @@ -0,0 +1,174 @@ +package io.github.pumpkinmc.gradle + +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import org.gradle.process.CommandLineArgumentProvider +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec + +abstract class PumpkinExtension @Inject constructor(objects: ObjectFactory) { + val apiGroup: Property = objects.property(String::class.java).convention("io.github.pumpkin-mc") + val apiArtifact: Property = objects.property(String::class.java).convention("pumpkin-api-kt") + val apiVersion: Property = objects.property(String::class.java).convention("0.1.0-dev") // Replace + val wasmToolsVersion: Property = objects.property(String::class.java).convention("1.258.0") +} + +class PumpkinPlugin : Plugin { + override fun apply(project: Project) { + val extension = project.extensions.create("pumpkin") + val apiSources = project.configurations.create("pumpkinApiSources") { + isCanBeConsumed = false + isCanBeResolved = true + isTransitive = false + description = "The generated Pumpkin Kotlin API source snapshot." + } + + project.afterEvaluate { + configurePlugin(project, extension, apiSources) + } + } + + private fun configurePlugin( + project: Project, + extension: PumpkinExtension, + apiSources: Configuration, + ) { + require(project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + "io.github.pumpkin-mc.plugin requires org.jetbrains.kotlin.multiplatform." + } + + project.dependencies.add( + apiSources.name, + "${extension.apiGroup.get()}:${extension.apiArtifact.get()}-wasm-wasi:${extension.apiVersion.get()}:sources@jar", + ) + + val unpackApiSources = project.tasks.register("unpackPumpkinApiSources") { + group = "build setup" + description = "Unpacks the published Pumpkin API source snapshot for component compilation." + from(project.provider { project.zipTree(apiSources.singleFile) }) + into(project.layout.buildDirectory.dir("generated/pumpkin-api")) + } + + project.extensions.getByType(KotlinMultiplatformExtension::class.java) + .sourceSets.named("wasmWasiMain") { + kotlin.srcDir(unpackApiSources) + } + + val unpackedApiDirectory = unpackApiSources.map { it.destinationDir } + val witDirectory = unpackedApiDirectory.map { it.resolve("wit/v0.1") } + val reactorAdapter = unpackedApiDirectory.map { it.resolve("wasi/wasi_snapshot_preview1.reactor.wasm") } + val executableSuffix = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) ".exe" else "" + val wasmToolsDirectory = project.layout.buildDirectory.dir( + "tools/wasm-tools/${extension.wasmToolsVersion.get()}", + ) + val wasmTools = wasmToolsDirectory.map { it.file("bin/wasm-tools$executableSuffix") } + + val installWasmTools = project.tasks.register("installWasmTools", org.gradle.api.tasks.Exec::class.java) { + group = "build setup" + description = "Installs the pinned wasm-tools executable." + inputs.property("version", extension.wasmToolsVersion) + outputs.dir(wasmToolsDirectory) + + val cargoHome = project.providers.environmentVariable("CARGO_HOME") + .orElse(project.providers.systemProperty("user.home").map { "$it/.cargo" }) + commandLine( + cargoHome.map { "$it/bin/cargo$executableSuffix" }.get(), + "install", + "wasm-tools", + "--version", extension.wasmToolsVersion.get(), + "--locked", + "--root", wasmToolsDirectory.get().asFile.absolutePath, + ) + } + + val projectWasmName = project.name + val compileReleaseWasm = project.tasks.named( + "compileProductionExecutableKotlinWasmWasiOptimize", + ) + val releaseCoreWasm = compileReleaseWasm.flatMap { + it.outputDirectory.file("$projectWasmName.wasm") + } + val embeddedReleaseComponent = project.layout.buildDirectory.file( + "intermediates/pumpkin-components/release/$projectWasmName-embedded.wasm", + ) + val releaseComponent = project.layout.buildDirectory.file("$projectWasmName.wasm") + + val embedComponentWitRelease = project.tasks.register("embedComponentWitRelease", org.gradle.api.tasks.Exec::class.java) { + group = "build" + description = "Embeds Pumpkin's WIT into the release Kotlin/Wasm module." + dependsOn(compileReleaseWasm, installWasmTools) + inputs.dir(witDirectory) + inputs.file(releaseCoreWasm) + inputs.file(wasmTools) + outputs.file(embeddedReleaseComponent) + + executable = wasmTools.get().asFile.absolutePath + argumentProviders.add(componentEmbedArguments(witDirectory, releaseCoreWasm, embeddedReleaseComponent)) + doFirst { + embeddedReleaseComponent.get().asFile.parentFile.mkdirs() + } + } + + val assemblePluginRelease = project.tasks.register("assemblePluginRelease", org.gradle.api.tasks.Exec::class.java) { + group = "build" + description = "Creates the release WebAssembly component that Pumpkin can load." + dependsOn(embedComponentWitRelease) + inputs.file(embeddedReleaseComponent) + inputs.file(reactorAdapter) + inputs.file(wasmTools) + outputs.file(releaseComponent) + + executable = wasmTools.get().asFile.absolutePath + argumentProviders.add(componentNewArguments(embeddedReleaseComponent, reactorAdapter, releaseComponent)) + } + + val validatePluginRelease = project.tasks.register("validatePluginRelease", org.gradle.api.tasks.Exec::class.java) { + group = "verification" + description = "Validates the release WebAssembly component." + dependsOn(assemblePluginRelease) + inputs.file(releaseComponent) + inputs.file(wasmTools) + commandLine(wasmTools.get().asFile.absolutePath, "validate", releaseComponent.get().asFile.absolutePath) + } + + project.tasks.named("assemble") { dependsOn(assemblePluginRelease) } + project.tasks.named("check") { dependsOn(validatePluginRelease) } + } + + private fun componentEmbedArguments( + witDirectory: org.gradle.api.provider.Provider, + releaseCoreWasm: org.gradle.api.provider.Provider, + embeddedReleaseComponent: org.gradle.api.provider.Provider, + ) = CommandLineArgumentProvider { + listOf( + "component", "embed", + witDirectory.get().absolutePath, + releaseCoreWasm.get().asFile.absolutePath, + "-o", embeddedReleaseComponent.get().asFile.absolutePath, + ) + } + + private fun componentNewArguments( + embeddedReleaseComponent: org.gradle.api.provider.Provider, + reactorAdapter: org.gradle.api.provider.Provider, + releaseComponent: org.gradle.api.provider.Provider, + ) = CommandLineArgumentProvider { + listOf( + "component", "new", + embeddedReleaseComponent.get().asFile.absolutePath, + "--adapt", "wasi_snapshot_preview1=${reactorAdapter.get().absolutePath}", + "-o", releaseComponent.get().asFile.absolutePath, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 626cba4..67f3936 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,3 +7,6 @@ pluginManagement { } } } + +include("api") +include("gradle-plugin") diff --git a/src/wasmWasiMain/kotlin/plugin/Plugin.kt b/src/wasmWasiMain/kotlin/plugin/Plugin.kt index 81f2bc0..3c48744 100644 --- a/src/wasmWasiMain/kotlin/plugin/Plugin.kt +++ b/src/wasmWasiMain/kotlin/plugin/Plugin.kt @@ -1,79 +1,33 @@ -package plugin; +package plugin -import pumpkin.* +import pumpkin.Context +import pumpkin.Logging -class PluginRootFunctionsExportsImpl { - companion object : PluginRootFunctions.Exports { - override fun initPlugin() { - Logging.log(Logging.Level.INFO, "Init Kotlin plugin!") - } +class ExamplePlugin : PumpkinPlugin() { + override fun metadata() = PluginMetadata( + name = "Example Kotlin Plugin", + version = "0.1.0", + authors = listOf("You"), + description = "An example plugin written in Kotlin", + dependencies = listOf(), + permissions = listOf(), + ) - override fun onLoad(context: Context.Context): Result { - Logging.log(Logging.Level.INFO, "Load Kotlin plugin!") - return Result.success(Unit) - } + override fun initPlugin() { + Logging.log(Logging.Level.INFO, "Init Kotlin plugin!") + } - override fun onUnload(context: Context.Context): Result { - Logging.log(Logging.Level.INFO, "Unload Kotlin plugin!") - return Result.success(Unit) - } + override fun onLoad(context: Context.Context): Result { + Logging.log(Logging.Level.INFO, "Load Kotlin plugin!") + return Result.success(Unit) + } - override fun handleCommand( - commandId: UInt, - sender: Command.CommandSender, - server: Server.Server, - args: Command.ConsumedArgs - ): Result { - TODO("Not yet implemented") - } - - override fun handleEvent(eventId: UInt, server: Server.Server, event: Event.Event): Event.Event { - TODO("Not yet implemented") - } - - override fun handleTask(handlerId: UInt, server: Server.Server) { - TODO("Not yet implemented") - } - - override fun handleIpcMessage(sender: String, message: List): Result> { - return Result.failure( - pumpkin.runtime.ComponentException("This plugin cannot recieve messages.") - ); - } - - override fun handleAiGoalCanStart(goalId: UInt, server: Server.Server, entity: World.Entity): Boolean { - TODO("Not yet implemented"); - } - - override fun handleAiGoalShouldContinue(goalId: UInt, server: Server.Server, entity: World.Entity): Boolean { - TODO("Not yet implemented"); - } - - override fun handleAiGoalStart(goalId: UInt, server: Server.Server, entity: World.Entity): Unit { - TODO("Not yet implemented"); - } - - override fun handleAiGoalTick(goalId: UInt, server: Server.Server, entity: World.Entity): Unit { - TODO("Not yet implemented"); - } - - override fun handleAiGoalStop(goalId: UInt, server: Server.Server, entity: World.Entity): Unit { - TODO("Not yet implemented"); - } + override fun onUnload(context: Context.Context): Result { + Logging.log(Logging.Level.INFO, "Unload Kotlin plugin!") + return Result.success(Unit) } } -class MetadataImpl { - companion object : Metadata { - override fun getMetadata(): Metadata.PluginMetadata { - return Metadata.PluginMetadata( - name = "Example Kotlin Plugin", - version = "0.1.0", - authors = listOf("You"), - description = "An example plugin written in Kotlin", - dependencies = listOf(), - permissions = listOf() - ); - } - } +fun main() { + registerPlugin(ExamplePlugin()) }