diff --git a/.github/scripts/prepare-consumer.sh b/.github/scripts/prepare-consumer.sh deleted file mode 100644 index 43372f2..0000000 --- a/.github/scripts/prepare-consumer.sh +++ /dev/null @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ed9b1b..8f4a3c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.gitignore b/.gitignore index 2621d4a..1364167 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ /api/build/ /api/tools/ /gradle-plugin/build/ +/example/build/ +/example/.gradle/ .idea .kotlin diff --git a/README.md b/README.md index 2d39c50..8f90add 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/build.gradle.kts b/api/build.gradle.kts index e452733..c4a3bd2 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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", diff --git a/build.gradle.kts b/build.gradle.kts index 858a95f..1e1ed61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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("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("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") } diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..7a63c98 --- /dev/null +++ b/example/README.md @@ -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. diff --git a/example/build.gradle.kts b/example/build.gradle.kts new file mode 100644 index 0000000..2639d58 --- /dev/null +++ b/example/build.gradle.kts @@ -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") +} diff --git a/example/gradle.properties b/example/gradle.properties new file mode 100644 index 0000000..5848304 --- /dev/null +++ b/example/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx2g diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/gradle/wrapper/gradle-wrapper.properties b/example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..221c4f9 --- /dev/null +++ b/example/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/example/gradlew b/example/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/example/gradlew @@ -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" "$@" diff --git a/example/gradlew.bat b/example/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/example/gradlew.bat @@ -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 diff --git a/example/settings.gradle.kts b/example/settings.gradle.kts new file mode 100644 index 0000000..ac67417 --- /dev/null +++ b/example/settings.gradle.kts @@ -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" diff --git a/src/wasmWasiMain/kotlin/plugin/Plugin.kt b/example/src/wasmWasiMain/kotlin/example/ExamplePlugin.kt similarity index 91% rename from src/wasmWasiMain/kotlin/plugin/Plugin.kt rename to example/src/wasmWasiMain/kotlin/example/ExamplePlugin.kt index 3c48744..f5cc488 100644 --- a/src/wasmWasiMain/kotlin/plugin/Plugin.kt +++ b/example/src/wasmWasiMain/kotlin/example/ExamplePlugin.kt @@ -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()) -} diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 67edbba..b4b4d4f 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -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") } diff --git a/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt b/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt index 8cd3ca5..c8eff8d 100644 --- a/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt +++ b/gradle-plugin/src/main/kotlin/io/github/pumpkinmc/gradle/PumpkinPlugin.kt @@ -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 = objects.property(String::class.java).convention("io.github.pumpkin-mc") val apiArtifact: Property = objects.property(String::class.java).convention("pumpkin-api-kt") val apiVersion: Property = objects.property(String::class.java).convention("0.1.0-dev") - val wasmToolsVersion: Property = objects.property(String::class.java).convention("1.258.0") - val binaryenVersion: Property = objects.property(String::class.java).convention("130") + val wasmToolsVersion: Property = objects.property(String::class.java).convention(toolVersions.getProperty("wasmToolsVersion")) + val binaryenVersion: Property = objects.property(String::class.java).convention(toolVersions.getProperty("binaryenVersion")) val pluginClass: Property = objects.property(String::class.java) } @@ -138,6 +149,10 @@ class PumpkinPlugin : Plugin { 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 { 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.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, releaseCoreWasm: org.gradle.api.provider.Provider, diff --git a/gradle/tool-versions.properties b/gradle/tool-versions.properties new file mode 100644 index 0000000..6eaaf3e --- /dev/null +++ b/gradle/tool-versions.properties @@ -0,0 +1,3 @@ +witBindgenRevision=700f2db5e1d01f7bee8d756750c6f631171f520e +wasmToolsVersion=1.258.0 +binaryenVersion=130 diff --git a/settings.gradle.kts b/settings.gradle.kts index 67f3936..457db07 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "pumpkin-plugin-template-kt" +rootProject.name = "pumpkin-api-kt" pluginManagement { resolutionStrategy {