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
|
||||
|
||||
/build/
|
||||
/api/build/
|
||||
/api/tools/
|
||||
/gradle-plugin/build/
|
||||
|
||||
.idea
|
||||
.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.
|
||||
|
||||
- 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/<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
|
||||
|
||||
|
||||
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.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<Jar>("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,
|
||||
)
|
||||
}
|
||||
|
||||
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,9 +1,18 @@
|
||||
package plugin;
|
||||
package plugin
|
||||
|
||||
import pumpkin.*
|
||||
import pumpkin.Context
|
||||
import pumpkin.Logging
|
||||
|
||||
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(),
|
||||
)
|
||||
|
||||
class PluginRootFunctionsExportsImpl {
|
||||
companion object : PluginRootFunctions.Exports {
|
||||
override fun initPlugin() {
|
||||
Logging.log(Logging.Level.INFO, "Init Kotlin plugin!")
|
||||
}
|
||||
@ -17,63 +26,8 @@ class PluginRootFunctionsExportsImpl {
|
||||
Logging.log(Logging.Level.INFO, "Unload Kotlin plugin!")
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override fun handleCommand(
|
||||
commandId: UInt,
|
||||
sender: Command.CommandSender,
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user