package: add standard builds

This commit is contained in:
Max Weber
2026-04-22 08:37:37 -06:00
parent ccb97463ef
commit b9da57d8dc
9 changed files with 194 additions and 54 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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,
}

View File

@@ -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<Path> 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<Path> 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();
}

View File

@@ -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)
}

View File

@@ -0,0 +1 @@
rootProject.name = "standard-plugin-build"

View File

@@ -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);
}
}

View File

@@ -28,7 +28,6 @@ dependencies {
}
group = '${group_id}'
version = '${version}'
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'

View File

@@ -2,4 +2,6 @@ displayName=${name}
author=${author}
description=${description}
tags=
plugins=${package}.${plugin_prefix}Plugin
version=${version}
plugins=${package}.${plugin_prefix}Plugin
build=standard