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 44e629910..85b6abc9c 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); } } } @@ -455,7 +465,7 @@ public class Plugin implements Closeable public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { String zipPath = repositoryDirectory.toPath().relativize(path).toString().replace('\\', '/'); - (zipPath.contains(".gradle") || zipPath.startsWith("src/main/") ? core : extras) + (zipPath.contains(".gradle") || zipPath.startsWith("src/main/") || zipPath.equals("runelite-plugin.properties") ? core : extras) .add(new Entry(path, zipPath, path.toFile().length())); return FileVisitResult.CONTINUE; } @@ -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,30 @@ public class Plugin implements Closeable } } - if (props.size() != 0) + if (rlPluginProperties.size() != 0) { - writeLog("warning: unused props in runelite-plugin.properties: {}\n", props.keySet()); + if (disallowedIsFatal) + { + var entry = rlPluginProperties.entrySet().iterator().next(); + throw PluginBuildException.of(this, "unknown {} key(s) in runelite-plugin.properties", rlPluginProperties.keySet()) + .withFileLine(propFile, entry.getKey() + "=" + entry.getValue()); + } + else + { + 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(); } @@ -998,6 +1069,7 @@ public class Plugin implements Closeable { Files.copy(jarFile.toPath(), new File(artifactDir, getInternalName() + ".jar").toPath()); Files.copy(logFile.toPath(), new File(artifactDir, getInternalName() + ".log").toPath()); + Files.copy(srcZipFile.toPath(), new File(artifactDir, getInternalName() + ".zip").toPath()); } public void writeLog(String format, Object... args) throws IOException 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..8f546fb40 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,38 @@ 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 testPropGarbage() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + { + try (Plugin p = createExamplePlugin("example")) + { + setProp(p, "bui1d", "gradle"); + p.build(Util.readRLVersion(), true); + Assert.fail(); + } + catch (PluginBuildException e) + { + log.info("ok: ", e); + assertContains(e.getHelpText(), "bui1d"); + } + } + @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 +126,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 +142,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 +241,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/package/upload/src/main/java/net/runelite/pluginhub/uploader/UploadConfiguration.java b/package/upload/src/main/java/net/runelite/pluginhub/uploader/UploadConfiguration.java index 281776e51..0414df460 100644 --- a/package/upload/src/main/java/net/runelite/pluginhub/uploader/UploadConfiguration.java +++ b/package/upload/src/main/java/net/runelite/pluginhub/uploader/UploadConfiguration.java @@ -46,6 +46,8 @@ import okio.BufferedSource; @Accessors(chain = true) public class UploadConfiguration implements Closeable { + private static final int MAX_RETRY = 2; + public static final String DIR_JAR = "jar"; public static final String DIR_API = "api"; public static final String DIR_ICON = "icon"; @@ -102,9 +104,22 @@ public class UploadConfiguration implements Closeable .build(); Response res = null; - for (int attempts = 0; attempts < 2; attempts++) + for (int attempts = 0; attempts < MAX_RETRY; attempts++) { - res = chain.proceed(userAgentRequest); + try + { + res = chain.proceed(userAgentRequest); + } + catch (IOException e) + { + if (attempts == MAX_RETRY - 1) + { + throw e; + } + + continue; + } + if (res.code() == 520) { res.close(); 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