From b9da57d8dc707e763932a076227c0365ab7c595e Mon Sep 17 00:00:00 2001 From: Max Weber Date: Wed, 22 Apr 2026 08:37:37 -0600 Subject: [PATCH] package: add standard builds --- README.md | 9 +- create_new_plugin.py | 2 +- .../pluginhub/packager/BuildType.java | 31 ++++ .../runelite/pluginhub/packager/Plugin.java | 144 +++++++++++++----- .../pluginhub/packager/standard-build.gradle | 26 ++++ .../packager/standard-settings.gradle | 1 + .../pluginhub/packager/PluginTest.java | 30 ++-- templateplugin/build.gradle | 1 - templateplugin/runelite-plugin.properties | 4 +- 9 files changed, 194 insertions(+), 54 deletions(-) create mode 100644 package/package/src/main/java/net/runelite/pluginhub/packager/BuildType.java create mode 100644 package/package/src/main/resources/net/runelite/pluginhub/packager/standard-build.gradle create mode 100644 package/package/src/main/resources/net/runelite/pluginhub/packager/standard-settings.gradle diff --git a/README.md b/README.md index 4f636a54b..44bb1ada3 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,10 @@ You may contribute to existing plugins by selecting the plugin from https://rune description=Alerts you when you have nothing equipped in your head slot tags=hint,gear,head plugins=com.helmetcheck.HelmetCheckPlugin + version= + build=standard ``` - `tags` will make it easier to find your plugin when searching for related words. If you want to add multiple plugin files, the `plugins` field allows for comma separated values, but this is not usually needed. + `tags` will make it easier to find your plugin when searching for related words. `version` is optional, if missing the commit will be used. If you want to add multiple plugin files, the `plugins` field allows for comma separated values, but this is not usually needed. 10. Optionally, you can add an icon to be displayed alongside with your plugin. Place a file with the name `icon.png` no larger than 48x72 px at the root of the repository. @@ -120,6 +122,11 @@ To add a new dependency, add it to the `thirdParty` configuration in [`package/v then run `../gradlew --write-verification-metadata sha256` to update the metadata file. A maintainer must then verify the dependencies manually before your pull request will be merged. This process generally adds significantly to the amount of time it takes for a plugin submission or update to be reviewed, so we recommend avoiding adding any new dependencies unless absolutely necessary. +## Build type +`runelite-plugin.properties` contains a `build` property, which may be set to either `standard` or `gradle`. In `standard` +mode, your`build.gradle` and `settings.gradle` get replaced when built during plugin submission. This allows for expedited review +if you don't need to specify any dependencies or other custom build steps. + ## My client version is outdated If your client version is outdated or your plugin suddenly stopped working after RuneLite has been updated, make sure that your `runeLiteVersion` is set to `'latest.release'` in `build.gradle`. If this is set correctly, refresh the Gradle dependencies by doing the following: 1. Open the Gradle tool window. diff --git a/create_new_plugin.py b/create_new_plugin.py index 8af395a99..536f1d503 100755 --- a/create_new_plugin.py +++ b/create_new_plugin.py @@ -86,7 +86,7 @@ subs = OrderedDict([ }), ("version", { "desc": "The initial version number of the plugin", - "value": "1.0-SNAPSHOT", + "value": "", }), ("plugin_prefix", { "desc": "The name of the your plugin's main class, without the 'Plugin' suffix", diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/BuildType.java b/package/package/src/main/java/net/runelite/pluginhub/packager/BuildType.java new file mode 100644 index 000000000..bb6ad316f --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/BuildType.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.pluginhub.packager; + +public enum BuildType +{ + STANDARD, + GRADLE, +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java b/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java index f526a80e4..85d9cc7dc 100644 --- a/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java @@ -189,6 +189,14 @@ public class Plugin implements Closeable @Setter private long buildTimeMS; + @VisibleForTesting + final File propFile; + + private Properties rlPluginProperties; + + private BuildType buildType; + private boolean buildPropMissing; + private int jarSizeLimitMiB = 10; public Plugin(File pluginCommitDescriptor) throws IOException, DisabledPluginException, PluginBuildException @@ -281,6 +289,7 @@ public class Plugin implements Closeable apiFile = new File(buildDirectory, "api"); srcZipFile = new File(buildDirectory, "source.zip"); iconFile = new File(repositoryDirectory, "icon.png"); + propFile = new File(repositoryDirectory, "runelite-plugin.properties"); } @SneakyThrows @@ -396,31 +405,32 @@ public class Plugin implements Closeable public void build(String runeliteVersion, boolean disallowedIsFatal) throws IOException, PluginBuildException { - try (DirectoryStream ds = Files.newDirectoryStream(repositoryDirectory.toPath(), "**.{gradle,gradle.kts}")) + if (!propFile.exists()) { - for (Path path : ds) + throw PluginBuildException.of(this, "runelite-plugin.properties must exist in the root of your repo"); + } + rlPluginProperties = loadProperties(propFile); + + { + String buildTypeStr = (String) rlPluginProperties.remove("build"); + if (Strings.isNullOrEmpty(buildTypeStr)) { - String badLine = MoreFiles.asCharSource(path, StandardCharsets.UTF_8) - .lines() - .filter(l -> l.codePoints().map(cp -> - { - if (cp == '\t') - { - return 8; - } - else if (cp > 127) - { - // any special char is counted as 4 because there are some very wide special characters - return 4; - } - return 1; - }).sum() > 120) - .findAny() - .orElse(null); - if (badLine != null) + buildPropMissing = true; + buildType = BuildType.GRADLE; + } + else + { + switch (buildTypeStr) { - throw PluginBuildException.of(this, "All gradle files must wrap at 120 characters or less") - .withFileLine(path.toFile(), badLine); + case "gradle": + buildType = BuildType.GRADLE; + break; + case "standard": + buildType = BuildType.STANDARD; + break; + default: + throw PluginBuildException.of(this, "build must be one of [gradle, standard], not \"{}\"", buildTypeStr) + .withFileLine(propFile, "build=" + buildTypeStr); } } } @@ -486,6 +496,55 @@ public class Plugin implements Closeable } } + try (DirectoryStream ds = Files.newDirectoryStream(repositoryDirectory.toPath(), "**.{gradle,gradle.kts}")) + { + for (Path path : ds) + { + if (buildType == BuildType.GRADLE) + { + String badLine = MoreFiles.asCharSource(path, StandardCharsets.UTF_8) + .lines() + .filter(l -> l.codePoints().map(cp -> + { + if (cp == '\t') + { + return 8; + } + else if (cp > 127) + { + // any special char is counted as 4 because there are some very wide special characters + return 4; + } + return 1; + }).sum() > 120) + .findAny() + .orElse(null); + if (badLine != null) + { + throw PluginBuildException.of(this, "All gradle files must wrap at 120 characters or less") + .withFileLine(path.toFile(), badLine); + } + } + else + { + Files.delete(path); + } + } + } + + if (buildType != BuildType.GRADLE) + { + try (InputStream is = Plugin.class.getResourceAsStream("standard-build.gradle")) + { + Files.copy(is, new File(repositoryDirectory, "build.gradle").toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + try (InputStream is = Plugin.class.getResourceAsStream("standard-settings.gradle")) + { + Files.copy(is, new File(repositoryDirectory, "settings.gradle").toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + try (InputStream is = Plugin.class.getResourceAsStream("verification-metadata.xml")) { File metadataFile = new File(repositoryDirectory, "gradle/verification-metadata.xml"); @@ -611,15 +670,16 @@ public class Plugin implements Closeable displayData.setWarning(warning); { - Properties chunk = loadProperties(new File(buildDirectory, "chunk.properties")); + String version = (String) rlPluginProperties.remove("version"); - String version = chunk.getProperty("version"); - if (Strings.isNullOrEmpty(version)) + if (Strings.isNullOrEmpty(version) && buildType == BuildType.GRADLE) { - throw new IllegalStateException("version in empty"); + Properties chunk = loadProperties(new File(buildDirectory, "chunk.properties")); + + version = chunk.getProperty("version"); } - if (version.endsWith("SNAPSHOT")) + if (Strings.isNullOrEmpty(version) || version.endsWith("SNAPSHOT")) { version = commit.substring(0, 8); } @@ -781,15 +841,8 @@ public class Plugin implements Closeable } { - File propFile = new File(repositoryDirectory, "runelite-plugin.properties"); - if (!propFile.exists()) { - throw PluginBuildException.of(this, "runelite-plugin.properties must exist in the root of your repo"); - } - Properties props = loadProperties(propFile); - - { - String displayName = (String) props.remove("displayName"); + String displayName = (String) rlPluginProperties.remove("displayName"); if (Strings.isNullOrEmpty(displayName) || disallowedIsFatal && "Example".equals(displayName)) { throw PluginBuildException.of(this, "\"displayName\" must be set") @@ -799,7 +852,7 @@ public class Plugin implements Closeable } { - String author = (String) props.remove("author"); + String author = (String) rlPluginProperties.remove("author"); if (Strings.isNullOrEmpty(author) || disallowedIsFatal && "Nobody".equals(author)) { throw PluginBuildException.of(this, "\"author\" must be set") @@ -809,7 +862,7 @@ public class Plugin implements Closeable } { - String description = (String) props.remove("description"); + String description = (String) rlPluginProperties.remove("description"); if (disallowedIsFatal && "An example greeter plugin".equals(description)) { throw PluginBuildException.of(this, "\"description\" must be set") @@ -819,7 +872,7 @@ public class Plugin implements Closeable } { - String tagsStr = (String) props.remove("tags"); + String tagsStr = (String) rlPluginProperties.remove("tags"); if (!Strings.isNullOrEmpty(tagsStr)) { displayData.setTags(Splitter.on(",") @@ -831,7 +884,7 @@ public class Plugin implements Closeable } { - String pluginsStr = (String) props.remove("plugins"); + String pluginsStr = (String) rlPluginProperties.remove("plugins"); if (pluginsStr == null) { throw PluginBuildException.of(this, "\"plugins\" must be set") @@ -886,12 +939,21 @@ public class Plugin implements Closeable } } - if (props.size() != 0) + if (rlPluginProperties.size() != 0) { - writeLog("warning: unused props in runelite-plugin.properties: {}\n", props.keySet()); + writeLog("warning: unused props in runelite-plugin.properties: {}\n", rlPluginProperties.keySet()); } } + if (disallowedIsFatal && buildPropMissing) + { + throw PluginBuildException.of(this, "\"build\" must be set") + .withHelp("You must add a build=standard or build=gradle entry.\n" + + "build=standard is recommended unless you have dependencies or other changes to your build.gradle.\n" + + "See https://github.com/runelite/plugin-hub#build-type for more info.") + .withFile(propFile); + } + realPluginChecks(); } diff --git a/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-build.gradle b/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-build.gradle new file mode 100644 index 000000000..5fe499c1f --- /dev/null +++ b/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' +} + +repositories { + mavenLocal() + maven { + url = 'https://repo.runelite.net' + content { + includeGroupByRegex("net\\.runelite.*") + } + } + mavenCentral() +} + +dependencies { + compileOnly "net.runelite:client" + + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release.set(11) +} \ No newline at end of file diff --git a/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-settings.gradle b/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-settings.gradle new file mode 100644 index 000000000..fc29554d4 --- /dev/null +++ b/package/package/src/main/resources/net/runelite/pluginhub/packager/standard-settings.gradle @@ -0,0 +1 @@ +rootProject.name = "standard-plugin-build" \ No newline at end of file diff --git a/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java b/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java index 3aa33c51f..a2c0bd31c 100644 --- a/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java +++ b/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java @@ -71,7 +71,7 @@ public class PluginTest } @Test - public void testExamplePluginCompiles() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + public void testExamplePluginCompilesStandard() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException { try (Plugin p = createExamplePlugin("example")) { @@ -79,15 +79,22 @@ public class PluginTest } } + @Test + public void testExamplePluginCompilesGradle() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + { + try (Plugin p = createExamplePlugin("example")) + { + setProp(p, "build", "gradle"); + p.build(Util.readRLVersion(), true); + } + } + @Test public void testMissingPlugin() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException { try (Plugin p = createExamplePlugin("missing-plugin")) { - File propFile = new File(p.repositoryDirectory, "runelite-plugin.properties"); - Properties props = Plugin.loadProperties(propFile); - props.setProperty("plugins", "com.nonexistent"); - writeProperties(props, propFile); + setProp(p, "plugins", "com.nonexistent"); p.build(Util.readRLVersion(), true); Assert.fail(); } @@ -103,10 +110,7 @@ public class PluginTest { try (Plugin p = createExamplePlugin("empty-plugins")) { - File propFile = new File(p.repositoryDirectory, "runelite-plugin.properties"); - Properties props = Plugin.loadProperties(propFile); - props.setProperty("plugins", ""); - writeProperties(props, propFile); + setProp(p, "plugins", ""); p.build(Util.readRLVersion(), true); Assert.fail(); } @@ -122,6 +126,7 @@ public class PluginTest { try (Plugin p = createExamplePlugin("unverified-dependency")) { + setProp(p, "build", "gradle"); File buildFile = new File(p.repositoryDirectory, "build.gradle"); String buildSrc = Files.asCharSource(buildFile, StandardCharsets.UTF_8).read(); buildSrc = buildSrc.replace("dependencies {", "dependencies {\n" + @@ -220,4 +225,11 @@ public class PluginTest { Assert.assertTrue(haystack, haystack.contains(needle)); } + + private void setProp(Plugin p, String key, String value) throws IOException + { + var props = Plugin.loadProperties(p.propFile); + props.setProperty(key, value); + writeProperties(props, p.propFile); + } } diff --git a/templateplugin/build.gradle b/templateplugin/build.gradle index 98ee43897..3f7a4570c 100644 --- a/templateplugin/build.gradle +++ b/templateplugin/build.gradle @@ -28,7 +28,6 @@ dependencies { } group = '${group_id}' -version = '${version}' tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' diff --git a/templateplugin/runelite-plugin.properties b/templateplugin/runelite-plugin.properties index 0de7f716e..65657e706 100644 --- a/templateplugin/runelite-plugin.properties +++ b/templateplugin/runelite-plugin.properties @@ -2,4 +2,6 @@ displayName=${name} author=${author} description=${description} tags= -plugins=${package}.${plugin_prefix}Plugin \ No newline at end of file +version=${version} +plugins=${package}.${plugin_prefix}Plugin +build=standard \ No newline at end of file