feat(api): package Kotlin bindings for Gradle consumers
This commit is contained in:
parent
268f2f15d9
commit
a4dba0be3d
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,6 +22,9 @@
|
|||||||
.idea/**/dbnavigator.xml
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
/build/
|
/build/
|
||||||
|
/api/build/
|
||||||
|
/api/tools/
|
||||||
|
/gradle-plugin/build/
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
.kotlin
|
.kotlin
|
||||||
|
|||||||
37
README.md
37
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.
|
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
|
- JDK 17 or later
|
||||||
- To run Gradle 9.4
|
- To run Gradle 9.4
|
||||||
- Rust
|
- 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
|
- 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:
|
Initialize the WIT submodule after cloning:
|
||||||
|
|
||||||
@ -22,15 +24,28 @@ Initialize the WIT submodule after cloning:
|
|||||||
git submodule update --init --recursive
|
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/<project-name>.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
|
## Updating the WIT
|
||||||
|
|
||||||
|
|||||||
110
api/build.gradle.kts
Normal file
110
api/build.gradle.kts
Normal file
@ -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<MavenPublication>("kotlinMultiplatform") {
|
||||||
|
artifactId = "pumpkin-api-kt"
|
||||||
|
}
|
||||||
|
publications.named<MavenPublication>("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<Jar>("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")
|
||||||
|
}
|
||||||
|
}
|
||||||
124
api/src/wasmWasiMain/kotlin/plugin/PumpkinPlugin.kt
Normal file
124
api/src/wasmWasiMain/kotlin/plugin/PumpkinPlugin.kt
Normal file
@ -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<Unit> = Result.success(Unit)
|
||||||
|
|
||||||
|
open fun onUnload(context: Context.Context): Result<Unit> = Result.success(Unit)
|
||||||
|
|
||||||
|
open fun handleCommand(
|
||||||
|
commandId: UInt,
|
||||||
|
sender: Command.CommandSender,
|
||||||
|
server: Server.Server,
|
||||||
|
args: Command.ConsumedArgs,
|
||||||
|
): Result<Int> = 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<Unit>("handleTask")
|
||||||
|
|
||||||
|
open fun handleIpcMessage(sender: String, message: List<UByte>): Result<List<UByte>> =
|
||||||
|
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<Unit>("handleAiGoalStart")
|
||||||
|
|
||||||
|
open fun handleAiGoalTick(goalId: UInt, server: Server.Server, entity: World.Entity) =
|
||||||
|
unsupportedCallback<Unit>("handleAiGoalTick")
|
||||||
|
|
||||||
|
open fun handleAiGoalStop(goalId: UInt, server: Server.Server, entity: World.Entity) =
|
||||||
|
unsupportedCallback<Unit>("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 <T> 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<UByte>) =
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,16 @@
|
|||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
import org.gradle.process.CommandLineArgumentProvider
|
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
|
import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
evaluationDependsOn(":api")
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
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 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 wasmToolsVersion = "1.258.0"
|
||||||
val wasmToolsDirectory = layout.projectDirectory.dir("tools/wasm-tools/$wasmToolsVersion")
|
val wasmToolsDirectory = layout.projectDirectory.dir("tools/wasm-tools/$wasmToolsVersion")
|
||||||
val wasmTools = wasmToolsDirectory.file("bin/wasm-tools$executableSuffix")
|
val wasmTools = wasmToolsDirectory.file("bin/wasm-tools$executableSuffix")
|
||||||
|
|
||||||
val generatedBindings = layout.buildDirectory.dir("generated/wit/wasmWasiMain/kotlin")
|
|
||||||
val projectWasmName = rootProject.name
|
val projectWasmName = rootProject.name
|
||||||
val embeddedReleaseComponent = layout.buildDirectory.file(
|
val embeddedReleaseComponent = layout.buildDirectory.file(
|
||||||
"intermediates/pumpkin-components/release/$projectWasmName-embedded.wasm"
|
"intermediates/pumpkin-components/release/$projectWasmName-embedded.wasm"
|
||||||
)
|
)
|
||||||
val releaseComponent = layout.buildDirectory.file("$projectWasmName.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) {
|
val installWasmTools by tasks.registering(Exec::class) {
|
||||||
group = "build setup"
|
group = "build setup"
|
||||||
description = "Installs the pinned wasm-tools executable."
|
description = "Installs the pinned wasm-tools executable."
|
||||||
@ -68,7 +43,10 @@ val installWasmTools by tasks.registering(Exec::class) {
|
|||||||
outputs.dir(wasmToolsDirectory)
|
outputs.dir(wasmToolsDirectory)
|
||||||
|
|
||||||
commandLine(
|
commandLine(
|
||||||
cargoExecutable.get(),
|
providers.environmentVariable("CARGO_HOME")
|
||||||
|
.orElse(providers.systemProperty("user.home").map { "$it/.cargo" })
|
||||||
|
.map { "$it/bin/cargo$executableSuffix" }
|
||||||
|
.get(),
|
||||||
"install",
|
"install",
|
||||||
"wasm-tools",
|
"wasm-tools",
|
||||||
"--version", wasmToolsVersion,
|
"--version", wasmToolsVersion,
|
||||||
@ -77,40 +55,21 @@ val installWasmTools by tasks.registering(Exec::class) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val generateWitBindings by tasks.registering(Exec::class) {
|
val apiSourcesJar = project(":api").tasks.named<Jar>("wasmWasiSourcesJar")
|
||||||
group = "build"
|
val unpackApiSources by tasks.registering(Sync::class) {
|
||||||
description = "Generates Kotlin bindings from Pumpkin's WIT definitions."
|
group = "build setup"
|
||||||
|
description = "Unpacks the published Pumpkin API source snapshot for component compilation."
|
||||||
|
|
||||||
dependsOn(installWitBindgen)
|
dependsOn(apiSourcesJar)
|
||||||
inputs.dir(witDirectory)
|
from(apiSourcesJar.flatMap { it.archiveFile }.map { zipTree(it.asFile) })
|
||||||
inputs.file(witBindgen)
|
into(layout.buildDirectory.dir("generated/pumpkin-api"))
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val witDirectory = unpackApiSources.map { it.destinationDir.resolve("wit/v0.1") }
|
||||||
|
val reactorAdapter = unpackApiSources.map { it.destinationDir.resolve("wasi/wasi_snapshot_preview1.reactor.wasm") }
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets.named("wasmWasiMain") {
|
sourceSets.named("wasmWasiMain") {
|
||||||
kotlin.srcDir(generateWitBindings)
|
kotlin.srcDir(unpackApiSources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +94,7 @@ val embedComponentWitRelease by tasks.registering(Exec::class) {
|
|||||||
override fun asArguments() = listOf(
|
override fun asArguments() = listOf(
|
||||||
"component",
|
"component",
|
||||||
"embed",
|
"embed",
|
||||||
witDirectory.asFile.absolutePath,
|
witDirectory.get().absolutePath,
|
||||||
releaseCoreWasm.get().asFile.absolutePath,
|
releaseCoreWasm.get().asFile.absolutePath,
|
||||||
"-o", embeddedReleaseComponent.get().asFile.absolutePath,
|
"-o", embeddedReleaseComponent.get().asFile.absolutePath,
|
||||||
)
|
)
|
||||||
@ -165,7 +124,7 @@ val assemblePluginRelease by tasks.registering(Exec::class) {
|
|||||||
"component",
|
"component",
|
||||||
"new",
|
"new",
|
||||||
embeddedReleaseComponent.get().asFile.absolutePath,
|
embeddedReleaseComponent.get().asFile.absolutePath,
|
||||||
"--adapt", "wasi_snapshot_preview1=${reactorAdapter.asFile.absolutePath}",
|
"--adapt", "wasi_snapshot_preview1=${reactorAdapter.get().absolutePath}",
|
||||||
"-o", releaseComponent.get().asFile.absolutePath,
|
"-o", releaseComponent.get().asFile.absolutePath,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
36
gradle-plugin/build.gradle.kts
Normal file
36
gradle-plugin/build.gradle.kts
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String> = objects.property(String::class.java).convention("io.github.pumpkin-mc")
|
||||||
|
val apiArtifact: Property<String> = objects.property(String::class.java).convention("pumpkin-api-kt")
|
||||||
|
val apiVersion: Property<String> = objects.property(String::class.java).convention("0.1.0-dev") // Replace
|
||||||
|
val wasmToolsVersion: Property<String> = objects.property(String::class.java).convention("1.258.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
class PumpkinPlugin : Plugin<Project> {
|
||||||
|
override fun apply(project: Project) {
|
||||||
|
val extension = project.extensions.create<PumpkinExtension>("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<Sync>("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<BinaryenExec>(
|
||||||
|
"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<java.io.File>,
|
||||||
|
releaseCoreWasm: org.gradle.api.provider.Provider<RegularFile>,
|
||||||
|
embeddedReleaseComponent: org.gradle.api.provider.Provider<RegularFile>,
|
||||||
|
) = 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<RegularFile>,
|
||||||
|
reactorAdapter: org.gradle.api.provider.Provider<java.io.File>,
|
||||||
|
releaseComponent: org.gradle.api.provider.Provider<RegularFile>,
|
||||||
|
) = CommandLineArgumentProvider {
|
||||||
|
listOf(
|
||||||
|
"component", "new",
|
||||||
|
embeddedReleaseComponent.get().asFile.absolutePath,
|
||||||
|
"--adapt", "wasi_snapshot_preview1=${reactorAdapter.get().absolutePath}",
|
||||||
|
"-o", releaseComponent.get().asFile.absolutePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,3 +7,6 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include("api")
|
||||||
|
include("gradle-plugin")
|
||||||
|
|||||||
@ -1,79 +1,33 @@
|
|||||||
package plugin;
|
package plugin
|
||||||
|
|
||||||
import pumpkin.*
|
import pumpkin.Context
|
||||||
|
import pumpkin.Logging
|
||||||
|
|
||||||
class PluginRootFunctionsExportsImpl {
|
class ExamplePlugin : PumpkinPlugin() {
|
||||||
companion object : PluginRootFunctions.Exports {
|
override fun metadata() = PluginMetadata(
|
||||||
override fun initPlugin() {
|
name = "Example Kotlin Plugin",
|
||||||
Logging.log(Logging.Level.INFO, "Init 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<Unit> {
|
override fun initPlugin() {
|
||||||
Logging.log(Logging.Level.INFO, "Load Kotlin plugin!")
|
Logging.log(Logging.Level.INFO, "Init Kotlin plugin!")
|
||||||
return Result.success(Unit)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnload(context: Context.Context): Result<Unit> {
|
override fun onLoad(context: Context.Context): Result<Unit> {
|
||||||
Logging.log(Logging.Level.INFO, "Unload Kotlin plugin!")
|
Logging.log(Logging.Level.INFO, "Load Kotlin plugin!")
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCommand(
|
override fun onUnload(context: Context.Context): Result<Unit> {
|
||||||
commandId: UInt,
|
Logging.log(Logging.Level.INFO, "Unload Kotlin plugin!")
|
||||||
sender: Command.CommandSender,
|
return Result.success(Unit)
|
||||||
server: Server.Server,
|
|
||||||
args: Command.ConsumedArgs
|
|
||||||
): Result<Int> {
|
|
||||||
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<UByte>): Result<List<UByte>> {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataImpl {
|
fun main() {
|
||||||
companion object : Metadata {
|
registerPlugin(ExamplePlugin())
|
||||||
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user