build: add example and version-based tool caching

This commit is contained in:
Cayden Tisdale 2026-09-07 18:39:58 -04:00
parent 0b06401215
commit c1c966d4d6
No known key found for this signature in database
19 changed files with 494 additions and 293 deletions

View File

@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
: "${PUMPKIN_CI_CONSUMER:?Set PUMPKIN_CI_CONSUMER to the temporary consumer directory}"
mkdir -p "$PUMPKIN_CI_CONSUMER/src/wasmWasiMain/kotlin/ci"
cat > "$PUMPKIN_CI_CONSUMER/settings.gradle.kts" <<'EOF'
pluginManagement {
repositories {
maven { url = uri(providers.environmentVariable("PUMPKIN_CI_REPOSITORY").get()) }
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = "pumpkin-ci-consumer"
EOF
cat > "$PUMPKIN_CI_CONSUMER/build.gradle.kts" <<'EOF'
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "2.4.0"
id("io.github.pumpkin-mc.plugin") version "0.1.0-dev"
}
repositories {
maven { url = uri(providers.environmentVariable("PUMPKIN_CI_REPOSITORY").get()) }
mavenCentral()
}
kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmWasi {
nodejs()
binaries.executable()
}
}
pumpkin {
apiVersion.set("0.1.0-dev")
pluginClass.set("ci.SmokePlugin")
}
EOF
cat > "$PUMPKIN_CI_CONSUMER/gradle.properties" <<'EOF'
kotlin.daemon.jvmargs=-Xmx2g
EOF
cat > "$PUMPKIN_CI_CONSUMER/src/wasmWasiMain/kotlin/ci/SmokePlugin.kt" <<'EOF'
package ci
import plugin.PluginMetadata
import plugin.PumpkinPlugin
class SmokePlugin : PumpkinPlugin() {
override fun metadata() = PluginMetadata(
name = "ci-smoke-plugin",
version = "0.1.0",
authors = listOf("Pumpkin"),
description = "Verifies consumer compilation with the generated plugin factory.",
dependencies = emptyList(),
permissions = emptyList(),
)
}
EOF

View File

@ -19,7 +19,7 @@ concurrency:
jobs:
main:
name: Build API and verify consumer
name: Build, test, and release
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
@ -31,7 +31,13 @@ jobs:
- name: Set temporary build paths
run: |
echo "PUMPKIN_CI_REPOSITORY=$RUNNER_TEMP/pumpkin-maven" >> "$GITHUB_ENV"
echo "PUMPKIN_CI_CONSUMER=$RUNNER_TEMP/pumpkin-consumer" >> "$GITHUB_ENV"
- name: Read pinned tool versions
id: tools
run: |
while IFS='=' read -r name version; do
echo "$name=$version" >> "$GITHUB_OUTPUT"
done < gradle/tool-versions.properties
- name: Set up Java/Kotlin
uses: actions/setup-java@v6
@ -45,35 +51,22 @@ jobs:
# PRs reuse the default-branch cache without updating it.
cache-read-only: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Prepare consumer fixture
run: bash .github/scripts/prepare-consumer.sh
- name: Cache binding generator
uses: actions/cache@v6
with:
path: api/tools
# GitHub scopes writes by ref; the hash prevents reuse after tool configuration changes.
key: ${{ runner.os }}-${{ runner.arch }}-wit-bindgen-${{ hashFiles('api/build.gradle.kts') }}
path: api/tools/wit-bindgen/${{ steps.tools.outputs.witBindgenRevision }}
key: ${{ runner.os }}-${{ runner.arch }}-wit-bindgen-${{ steps.tools.outputs.witBindgenRevision }}
- name: Cache consumer tools
id: consumer-tools
uses: actions/cache@v6
with:
path: ${{ env.PUMPKIN_CI_CONSUMER }}/build/tools
key: ${{ runner.os }}-${{ runner.arch }}-consumer-tools-${{ hashFiles('gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt') }}
path: example/build/tools/wasm-tools/${{ steps.tools.outputs.wasmToolsVersion }}
key: ${{ runner.os }}-${{ runner.arch }}-wasm-tools-${{ steps.tools.outputs.wasmToolsVersion }}
- name: Build and publish development packages
run: |
./gradlew :api:build :gradle-plugin:build :api:publishToMavenLocal :gradle-plugin:publishToMavenLocal \
-Dmaven.repo.local="$PUMPKIN_CI_REPOSITORY" --build-cache --no-daemon
- name: Build and validate consumer component
env:
CONSUMER_TOOLS_CACHE_HIT: ${{ steps.consumer-tools.outputs.cache-hit }}
run: |
args=()
# The installer will download again on a fresh runner otherwise
if [[ "$CONSUMER_TOOLS_CACHE_HIT" == "true" ]]; then
args+=(--exclude-task installWasmTools)
fi
./gradlew -p "$PUMPKIN_CI_CONSUMER" build --build-cache --no-daemon "${args[@]}"
- name: Build and validate example component
run: ./example/gradlew -p example build --build-cache --no-daemon

2
.gitignore vendored
View File

@ -25,6 +25,8 @@
/api/build/
/api/tools/
/gradle-plugin/build/
/example/build/
/example/.gradle/
.idea
.kotlin

View File

@ -126,7 +126,15 @@ git submodule update --init --recursive
To test these local packages in a consumer, add `mavenLocal()` before the other repositories in both `settings.gradle.kts`'s `pluginManagement.repositories` and `build.gradle.kts`'s `repositories`. Set both version placeholders to `0.1.0-dev`. After republishing changes under that version, run `./gradlew build --refresh-dependencies` in the consumer.
The repository's legacy root template still uses the earlier `main()` registration approach. Use the separate consumer setup above for the bootstrap integration.
The [example](example/) is a standalone consumer with its own Gradle wrapper. After publishing locally, run `./example/gradlew -p example build --refresh-dependencies`, or open `example/` as a Gradle project in IntelliJ. The output is `example/build/pumpkin-example.wasm`.
The root build checks `api` and `gradle-plugin` automatically.
Build tool pins live in `gradle/tool-versions.properties`.
Gradle checks restored executables before reusing them.
- Binding generation tracks the WIT directory, generator executable, and generation options as task inputs.
- Binaryen and Gradle dependencies remain managed by the Gradle setup action.
### Updating the WIT

View File

@ -1,6 +1,8 @@
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import java.util.Properties
import java.util.concurrent.TimeUnit
plugins {
alias(libs.plugins.kotlinMultiplatform)
@ -40,7 +42,10 @@ val executableSuffix = if (System.getProperty("os.name").startsWith("Windows", i
val cargoExecutable = cargoHome.map { "$it/bin/cargo$executableSuffix" }
val witDirectory = rootProject.layout.projectDirectory.dir("wit/v0.1")
val witBindgenRevision = "700f2db5e1d01f7bee8d756750c6f631171f520e"
val toolVersions = Properties().apply {
load(providers.fileContents(rootProject.layout.projectDirectory.file("gradle/tool-versions.properties")).asText.get().reader())
}
val witBindgenRevision = toolVersions.getProperty("witBindgenRevision")
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")
@ -52,10 +57,27 @@ val installWitBindgen by tasks.registering(Exec::class) {
inputs.property("revision", witBindgenRevision)
outputs.dir(witBindgenDirectory)
onlyIf("the pinned binding generator is not already installed") {
val manifest = witBindgenDirectory.file(".crates.toml").asFile
val matchesRevision = manifest.isFile &&
manifest.readText().contains("git+https://github.com/Kotlin/wit-bindgen?rev=$witBindgenRevision#$witBindgenRevision)")
val usable = matchesRevision && witBindgen.asFile.isFile && runCatching {
val process = ProcessBuilder(witBindgen.asFile.absolutePath, "--version")
.redirectErrorStream(true).redirectOutput(ProcessBuilder.Redirect.DISCARD).start()
try {
process.waitFor(10, TimeUnit.SECONDS) && process.exitValue() == 0
} finally {
if (process.isAlive) process.destroyForcibly()
}
}.getOrDefault(false)
!usable
}
commandLine(
cargoExecutable.get(),
"install",
"wit-bindgen-cli",
"--force",
"--git", "https://github.com/Kotlin/wit-bindgen",
"--rev", witBindgenRevision,
"--locked",

View File

@ -1,202 +1,12 @@
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
import java.net.URI
plugins {
alias(libs.plugins.kotlinMultiplatform)
base
alias(libs.plugins.kotlinMultiplatform) apply false
}
evaluationDependsOn(":api")
repositories {
mavenCentral()
tasks.named("build") {
dependsOn(":api:build", ":gradle-plugin:build")
}
kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmWasi {
nodejs()
binaries.executable()
}
}
val executableSuffix = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) ".exe" else ""
val wasmToolsVersion = "1.258.0"
val wasmToolsDirectory = layout.projectDirectory.dir("tools/wasm-tools/$wasmToolsVersion")
val wasmTools = wasmToolsDirectory.file("bin/wasm-tools$executableSuffix")
val wasmToolsTarget = when {
System.getProperty("os.name").startsWith("Mac", ignoreCase = true) &&
System.getProperty("os.arch") in setOf("aarch64", "arm64") -> "aarch64-macos"
System.getProperty("os.name").startsWith("Mac", ignoreCase = true) -> "x86_64-macos"
System.getProperty("os.name").startsWith("Windows", ignoreCase = true) &&
System.getProperty("os.arch") in setOf("aarch64", "arm64") -> "aarch64-windows"
System.getProperty("os.name").startsWith("Windows", ignoreCase = true) -> "x86_64-windows"
System.getProperty("os.arch") in setOf("aarch64", "arm64") -> "aarch64-linux"
System.getProperty("os.arch") in setOf("x86_64", "amd64") -> "x86_64-linux"
else -> error("Unsupported wasm-tools host architecture: ${System.getProperty("os.arch")}")
}
val wasmToolsArchiveExtension = if (wasmToolsTarget.endsWith("windows")) "zip" else "tar.gz"
val wasmToolsUrl = "https://github.com/bytecodealliance/wasm-tools/releases/download/v$wasmToolsVersion/" +
"wasm-tools-$wasmToolsVersion-$wasmToolsTarget.$wasmToolsArchiveExtension"
val projectWasmName = rootProject.name
val embeddedReleaseComponent = layout.buildDirectory.file(
"intermediates/pumpkin-components/release/$projectWasmName-embedded.wasm"
)
val releaseComponent = layout.buildDirectory.file("$projectWasmName.wasm")
val installWasmTools by tasks.registering {
group = "build setup"
description = "Downloads the pinned wasm-tools release executable."
inputs.property("version", wasmToolsVersion)
inputs.property("target", wasmToolsTarget)
inputs.property("url", wasmToolsUrl)
outputs.file(wasmTools)
doLast {
val installationDirectory = wasmToolsDirectory.asFile
val archive = temporaryDir.resolve("wasm-tools.$wasmToolsArchiveExtension")
val installedExecutable = wasmTools.asFile
installationDirectory.deleteRecursively()
installationDirectory.mkdirs()
installedExecutable.parentFile.mkdirs()
URI(wasmToolsUrl).toURL().openStream().use { input ->
archive.outputStream().use { output -> input.copyTo(output) }
}
fun runCommand(vararg command: String) {
check(ProcessBuilder(*command).inheritIO().start().waitFor() == 0) {
"wasm-tools archive extraction failed"
}
}
if (wasmToolsTarget.endsWith("windows")) {
runCommand(
"powershell", "-NoProfile", "-Command",
"Expand-Archive -Force '$archive' '$installationDirectory'; " +
"Get-ChildItem -Path '$installationDirectory' -Recurse -Filter wasm-tools.exe | " +
"Select-Object -First 1 | Copy-Item -Destination '${wasmTools.asFile}'",
)
} else {
runCommand("tar", "-xzf", archive.absolutePath, "-C", installationDirectory.absolutePath, "--strip-components=1")
}
if (!installedExecutable.isFile) {
val downloadedExecutable = installationDirectory.walkTopDown().firstOrNull {
it.isFile && it.name == "wasm-tools$executableSuffix"
}
checkNotNull(downloadedExecutable) { "wasm-tools archive did not contain wasm-tools$executableSuffix" }
downloadedExecutable.copyTo(installedExecutable, overwrite = true)
}
installedExecutable.setExecutable(true)
}
}
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(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(unpackApiSources)
}
}
val compileReleaseWasm = tasks.named<BinaryenExec>("compileProductionExecutableKotlinWasmWasiOptimize")
val releaseCoreWasm = compileReleaseWasm.flatMap {
it.outputDirectory.file("$projectWasmName.wasm")
}
val embedComponentWitRelease by tasks.registering(Exec::class) {
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.asFile.absolutePath
argumentProviders.add(
object : CommandLineArgumentProvider {
override fun asArguments() = listOf(
"component",
"embed",
witDirectory.get().absolutePath,
releaseCoreWasm.get().asFile.absolutePath,
"-o", embeddedReleaseComponent.get().asFile.absolutePath,
)
}
)
doFirst {
val outputDirectory = embeddedReleaseComponent.get().asFile.parentFile
check(outputDirectory.mkdirs() || outputDirectory.isDirectory) {
"Could not create component directory $outputDirectory"
}
}
}
val assemblePluginRelease by tasks.registering(Exec::class) {
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)
commandLine(
wasmTools.asFile.absolutePath,
"component",
"new",
embeddedReleaseComponent.get().asFile.absolutePath,
"--adapt", "wasi_snapshot_preview1=${reactorAdapter.get().absolutePath}",
"-o", releaseComponent.get().asFile.absolutePath,
)
}
val validatePluginRelease by tasks.registering(Exec::class) {
group = "verification"
description = "Validates the release WebAssembly component."
dependsOn(assemblePluginRelease)
inputs.file(releaseComponent)
inputs.file(wasmTools)
commandLine(
wasmTools.asFile.absolutePath,
"validate",
releaseComponent.get().asFile.absolutePath,
)
}
tasks.named("assemble") {
dependsOn(assemblePluginRelease)
}
tasks.named("check") {
dependsOn(validatePluginRelease)
}
tasks.register("cleanProject") {
group = "build"
description = "Removes generated build output."
dependsOn(tasks.named("clean"))
tasks.named("clean") {
dependsOn(":api:clean", ":gradle-plugin:clean")
}

19
example/README.md Normal file
View File

@ -0,0 +1,19 @@
# Example Kotlin plugin
This standalone Gradle project serves as an example to the published Pumpkin API and build plugin. CI builds this same example against a temporary Maven repository.
Before importing it locally, publish the development packages from the repository root:
```sh
./gradlew :api:publishToMavenLocal :gradle-plugin:publishToMavenLocal
```
Then open this directory as a Gradle project in IntelliJ, or build it here:
```sh
./gradlew build --refresh-dependencies
```
Copy `build/pumpkin-example.wasm` into the matching Pumpkin server's `plugins` directory. The plugin logs its initialization, load, and unload callbacks.
`PUMPKIN_CI_REPOSITORY` optionally selects an isolated Maven repository for CI. With no override, the example uses Maven local and the public repositories. It is intentionally a separate build, so it tests package use without substituting the repository's projects.

29
example/build.gradle.kts Normal file
View File

@ -0,0 +1,29 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "2.4.0"
id("io.github.pumpkin-mc.plugin") version "0.1.0-dev"
}
repositories {
val ciRepository = providers.environmentVariable("PUMPKIN_CI_REPOSITORY").orNull
if (ciRepository != null) {
maven { url = uri(ciRepository) }
} else {
mavenLocal()
}
mavenCentral()
}
kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmWasi {
nodejs()
binaries.executable()
}
}
pumpkin {
apiVersion.set("0.1.0-dev")
pluginClass.set("example.ExamplePlugin")
}

View File

@ -0,0 +1,2 @@
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2g

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
example/gradlew vendored Executable file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
example/gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,14 @@
pluginManagement {
repositories {
val ciRepository = providers.environmentVariable("PUMPKIN_CI_REPOSITORY").orNull
if (ciRepository != null) {
maven { url = uri(ciRepository) }
} else {
mavenLocal()
}
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = "pumpkin-example"

View File

@ -1,4 +1,7 @@
package plugin
package example
import plugin.PluginMetadata
import plugin.PumpkinPlugin
import pumpkin.Context
import pumpkin.Logging
@ -27,7 +30,3 @@ class ExamplePlugin : PumpkinPlugin() {
return Result.success(Unit)
}
}
fun main() {
registerPlugin(ExamplePlugin())
}

View File

@ -16,6 +16,12 @@ repositories {
mavenCentral()
}
tasks.processResources {
from(rootProject.layout.projectDirectory.file("gradle/tool-versions.properties")) {
into("io/github/pumpkinmc/gradle")
}
}
dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0")
}

View File

@ -20,13 +20,24 @@ import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenEnvSpec
import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec
import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenPlugin
import java.net.URI
import java.io.File
import java.util.Properties
import java.util.concurrent.TimeUnit
private val toolVersions: Properties by lazy {
Properties().apply {
checkNotNull(PumpkinPlugin::class.java.getResourceAsStream("tool-versions.properties")) {
"Packaged tool versions are missing."
}.use { load(it) }
}
}
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")
val wasmToolsVersion: Property<String> = objects.property(String::class.java).convention("1.258.0")
val binaryenVersion: Property<String> = objects.property(String::class.java).convention("130")
val wasmToolsVersion: Property<String> = objects.property(String::class.java).convention(toolVersions.getProperty("wasmToolsVersion"))
val binaryenVersion: Property<String> = objects.property(String::class.java).convention(toolVersions.getProperty("binaryenVersion"))
val pluginClass: Property<String> = objects.property(String::class.java)
}
@ -138,6 +149,10 @@ class PumpkinPlugin : Plugin<Project> {
inputs.property("url", wasmToolsUrl)
outputs.file(wasmTools)
onlyIf("the requested wasm-tools version is not already installed") {
!installedWasmToolsMatches(wasmTools.get().asFile, extension.wasmToolsVersion.get())
}
doLast {
val installationDirectory = wasmToolsDirectory.get().asFile
val archive = temporaryDir.resolve("wasm-tools.$wasmToolsArchiveExtension")
@ -174,6 +189,9 @@ class PumpkinPlugin : Plugin<Project> {
downloadedExecutable.copyTo(installedExecutable, overwrite = true)
}
installedExecutable.setExecutable(true)
check(installedWasmToolsMatches(installedExecutable, extension.wasmToolsVersion.get())) {
"The downloaded wasm-tools executable does not report the requested version."
}
}
}
@ -231,6 +249,20 @@ class PumpkinPlugin : Plugin<Project> {
project.tasks.named("check") { dependsOn(validatePluginRelease) }
}
private fun installedWasmToolsMatches(executable: File, version: String): Boolean {
if (!executable.isFile) return false
return runCatching {
val process = ProcessBuilder(executable.absolutePath, "--version").redirectErrorStream(true).start()
try {
process.waitFor(10, TimeUnit.SECONDS) && process.exitValue() == 0 &&
Regex("^wasm-tools ${Regex.escape(version)}(?:\\s|$)")
.containsMatchIn(process.inputStream.bufferedReader().use { it.readText() })
} finally {
if (process.isAlive) process.destroyForcibly()
}
}.getOrDefault(false)
}
private fun componentEmbedArguments(
witDirectory: org.gradle.api.provider.Provider<java.io.File>,
releaseCoreWasm: org.gradle.api.provider.Provider<RegularFile>,

View File

@ -0,0 +1,3 @@
witBindgenRevision=700f2db5e1d01f7bee8d756750c6f631171f520e
wasmToolsVersion=1.258.0
binaryenVersion=130

View File

@ -1,4 +1,4 @@
rootProject.name = "pumpkin-plugin-template-kt"
rootProject.name = "pumpkin-api-kt"
pluginManagement {
resolutionStrategy {