package: move packager into its own repository

This commit is contained in:
Max Weber committed 2026-05-14 18:23:51 -06:00
1 parent ac9d5d4098
commit d541433299
55 files changed
+44 -5675

No files matched your search

+43 -32
View File
@@ -12,37 +12,46 @@ on:
required: false
push:
pull_request:
env:
CACHE_VERSION: 2.4.0
permissions:
contents: read
jobs:
build:
# any forks that predate this repo having an action will have actions
# enabled by default, which will fail in a lot of cases because the branch
# is new, which makes the differential build fail
if: github.event_name != 'push' || github.repository_owner == 'runelite'
runs-on: ubuntu-22.04
if: vars.PACKAGER_URL != ''
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-java@v4
path: plugin-hub
persist-credentials: false
- uses: actions/setup-java@v5
with:
distribution: adopt
java-version: 11
- uses: actions/cache@v4
- name: fetch packager
env:
PACKAGER_URL: ${{ vars.PACKAGER_URL }}
run: |
set -exu
curl --location --output bundle.tar.zst "$PACKAGER_URL"
sha256sum bundle.tar.zst
tar xf bundle.tar.zst
- uses: actions/cache/restore@v5
id: restore-cache
with:
path: |
~/.gradle/caches/
~/.gradle/wrapper/
key: package-${{ env.CACHE_VERSION }}-${{ hashFiles('runelite.version') }}
restore-keys: |
package-${{ env.CACHE_VERSION }}-
api/
key: deps-${{ hashFiles('plugin-hub/runelite.version', 'bundle.tar.zst') }}
- name: prepare
run: |
pushd package
./gradlew --build-cache prep
./gradlew --build-cache --project-dir verification-template :verifyCore
popd
if: steps.restore-cache.outputs.cache-hit != 'true'
run: ./prepare.sh
- uses: actions/cache/save@v5
if: steps.restore-cache.outputs.cache-hit != 'true'
with:
path: ~/.gradle/caches/
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
- name: build
env:
REPO_CREDS: ${{ secrets.REPO_CREDS }}
@@ -67,7 +76,7 @@ jobs:
else
export PACKAGE_COMMIT_RANGE="${COMMIT_RANGE:-${COMMIT_BEFORE:+$COMMIT_BEFORE...$COMMIT_AFTER}}"
fi
exec java -XX:+UseParallelGC -cp package/package/build/libs/package.jar net.runelite.pluginhub.packager.Packager
exec java -XX:+UseParallelGC -cp package.jar net.runelite.pluginhub.packager.Packager
- uses: actions/upload-artifact@v4
with:
name: manifest_diff
@@ -80,24 +89,28 @@ jobs:
path: /tmp/jars
retention-days: 4
upload:
if: (github.event_name != 'push' || github.repository_owner == 'runelite') && github.event_name != 'pull_request'
if: vars.PACKAGER_URL != '' && github.event_name != 'pull_request'
needs: build
concurrency: upload
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: actions/setup-java@v4
path: plugin-hub
persist-credentials: false
- name: fetch packager
env:
PACKAGER_URL: ${{ vars.PACKAGER_URL }}
run: |
set -exu
curl --location --output bundle.tar.zst "$PACKAGER_URL"
sha256sum bundle.tar.zst
tar xf bundle.tar.zst
- uses: actions/setup-java@v5
with:
distribution: adopt
java-version: 11
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches/
~/.gradle/wrapper/
key: upload-${{ env.CACHE_VERSION }}
- uses: actions/download-artifact@v4
with:
name: manifest_diff
@@ -108,6 +121,4 @@ jobs:
REPO_ROOT: ${{ secrets.REPO_ROOT }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
run: |
pushd package
./gradlew :upload:run
popd
exec java -XX:+UseParallelGC -cp upload.jar net.runelite.pluginhub.uploader.Uploader
-2
View File
@@ -1,2 +0,0 @@
The contents of the template plugin and the output of the `create_new_plugin.py`
script are, released into the public domain unless otherwise noted.
+1 -1
View File
@@ -119,7 +119,7 @@ We require any dependencies that are not a transitive dependency of runelite-cli
have their cryptographic hash verified during the build to prevent [supply chain attacks](https://en.wikipedia.org/wiki/Supply_chain_attack) and ensure build reproducability.
To do this we rely on [Gradle's dependency verification](https://docs.gradle.org/nightly/userguide/dependency_verification.html).
To add a new dependency, add it to the `thirdParty` configuration in [`package/verification-template/build.gradle`](https://github.com/runelite/plugin-hub/blob/master/package/verification-template/build.gradle),
then run `../gradlew --write-verification-metadata sha256` to update the metadata file. A maintainer must then verify
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
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2019 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.
"""
import os
import sys
from collections import OrderedDict
import argparse
import re
from string import Template
templatedir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templateplugin")
def strip_plugin(str):
return re.sub(r"(?i)[ _-]*plugin$", "", str)
def reformat(str, replacer):
if replacer != to_spaces:
str=re.sub(r"[^a-zA-Z0-9_ -]", "", str)
if re.match(r".*[ _-]", str):
return re.sub(r"(?:^|[ _.-]+)([^ _.-]+)", lambda m:replacer(m.group(1)), str).strip(" -_")
return re.sub(r"((?:^.|[A-Z0-9]+)(?:[a-z0-9]+|$))", lambda m:replacer(m.group(1)), str).strip(" -_")
def to_spaces(seg):
return " " + seg.capitalize()
def to_camelcase(seg):
return seg.capitalize()
def to_dashes(seg):
return "-" + seg.lower()
def to_lowercase(seg):
return seg.lower()
def strfun(strfun):
if isinstance(strfun, str):
return strfun
return strfun()
subs = OrderedDict([
("name", {
"ask": True,
"desc": "The name your plugin will be shown by in menus",
"value": "Foo Bazzer",
"strip_plugin": True,
}),
("package", {
"ask": True,
"desc": "The java package your plugin will be in. Typically a reversed domain name",
"value": "com.example.foobazzer",
"strip_plugin": True,
}),
("author", {
"ask": True,
"desc": "Who wrote/maintains this plugin",
"value": "John Doe",
}),
("description", {
"ask": True,
"desc": "A short string describing the plugin",
"value": "Adds a bazzer to the foo",
}),
("version", {
"desc": "The initial version number of the plugin",
"value": "",
}),
("plugin_prefix", {
"desc": "The name of the your plugin's main class, without the 'Plugin' suffix",
"value": lambda: reformat(subs["name"]["value"], to_camelcase),
"strip_plugin": True
}),
("artifact_id", {
"desc": "The name of the maven artifact",
"value": lambda: reformat(subs["name"]["value"], to_dashes),
}),
("group_id", {
"desc": "The group of the maven artifact",
"value": lambda: subs["package"]["value"],
}),
("plugin_config_group", {
"desc": "The prefix used to store config keys",
"value": lambda: reformat(subs["name"]["value"], to_lowercase),
"strip_plugin": True
})
])
pwd = os.getcwd()
pwdIsEmpty = len(os.listdir(pwd)) == 0
if pwdIsEmpty:
subs["name"]["value"] = strip_plugin(reformat(os.path.basename(pwd), to_spaces))
parser = argparse.ArgumentParser()
parser.add_argument("--noninteractive", dest = "noninteractive", action='store_true')
parser.add_argument("--output_directory", dest = "output_directory")
for key, var in subs.items():
parser.add_argument("--" + key, dest = key, help = var["desc"])
args = vars(parser.parse_args())
noninteractive = args["noninteractive"]
if noninteractive and pwdIsEmpty:
subs["name"]["ask"] = False
for key, var in subs.items():
if args[key] != None:
val = args[key]
if "strip_plugin" in var and var["strip_plugin"]:
val = strip_plugin(val)
var["value"] = val
var["ask"] = False
askAll = False
while True:
for key, var in subs.items():
if askAll or ("ask" in var and var["ask"]):
if noninteractive:
print("\"{}\" was not specified in noninteractive mode".format(key))
sys.exit(1)
print(var["desc"])
print("[" + strfun(var["value"]) + "]")
val = input(key + ": ")
if val:
if "strip_plugin" in var and var["strip_plugin"]:
val = strip_plugin(val)
var["value"] = val
askAll = True
print("")
for key, var in subs.items():
print("{} = \"{}\"".format(key, strfun(var["value"])))
def input_yes():
while True:
inp = input("Is this ok? [Yn]").lower()
if inp == "" or inp == "y":
return True
if inp == "n":
return False
if noninteractive or input_yes():
break
outdir = args["output_directory"]
if outdir == None:
if pwdIsEmpty:
outdir = pwd
else:
outdir = os.path.join(pwd, strfun(subs["artifact_id"]["value"]))
mappings = {}
for key, var in subs.items():
mappings[key] = strfun(var["value"])
mappings["package_path"] = mappings["package"].replace(".", os.path.sep)
with open(os.path.join(templatedir, "../runelite.version"), "rt") as fi:
mappings["runelite_version"] = fi.read().strip()
for root, dir, files in os.walk(templatedir):
for file in files:
try:
infi = os.path.join(root, file)
outfi = os.path.join(os.path.relpath(root, templatedir), file)
outfi = outfi.replace("_(", "${").replace(")_", "}")
outfi = Template(outfi).substitute(mappings)
outfi = os.path.join(outdir, outfi)
os.makedirs(os.path.dirname(outfi), exist_ok=True)
with open(infi, "rb") as ifd: # we need binary mode to not do that stupid crlf bullshit on windows
if file.endswith(".jar") or file.startswith("gradlew"):
with open(outfi, "wb") as ofd:
ofd.write(ifd.read())
else:
contents = Template(ifd.read().decode("utf-8")).substitute(mappings)
with open(outfi, "wb") as ofd:
ofd.write(contents.encode("utf-8"))
except ValueError as ex:
raise ValueError(infi) from ex
-72
View File
@@ -1,72 +0,0 @@
/*
* Copyright (c) 2021 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.
*/
repositories {
maven {
url = 'https://repo.runelite.net'
}
mavenCentral()
}
configurations {
runelite {
resolutionStrategy {
// we don't run any code from this configuration
disableDependencyVerification()
}
}
}
def runeLiteVersion = file("../../runelite.version").text.trim()
dependencies {
implementation "org.slf4j:slf4j-simple:1.7.10"
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "com.google.guava:guava:23.2-jre"
implementation "org.ow2.asm:asm:9.3"
implementation "com.google.code.gson:gson:2.8.5"
def lombok = "org.projectlombok:lombok:1.18.30"
compileOnly lombok
annotationProcessor lombok
testCompileOnly lombok
testAnnotationProcessor lombok
runelite group: 'net.runelite', name: 'client', version: runeLiteVersion
runelite group: 'net.runelite', name: 'jshell', version: runeLiteVersion
runelite lombok
}
compileJava.options.compilerArgs += [
"--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
]
def apiFilePath = new File(project.buildDir, "api")
task api(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = "net.runelite.pluginhub.apirecorder.ClassRecorder"
args = [apiFilePath] + configurations.runelite.files
}
@@ -1,266 +0,0 @@
/*
* Copyright (c) 2021 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.apirecorder;
import com.google.common.collect.ImmutableSet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.objectweb.asm.Opcodes;
@Slf4j
@RequiredArgsConstructor
public class API
{
@Getter
private final Set<String> apis;
public API()
{
this(new HashSet<>());
}
public static void encode(OutputStream os, Stream<String> stream) throws IOException
{
DeflaterOutputStream dos = new DeflaterOutputStream(os);
Writer out = new OutputStreamWriter(dos, StandardCharsets.UTF_8);
stream.sorted()
.forEach(it -> write(out, it));
out.flush();
dos.finish();
}
@SneakyThrows
private static void write(Writer w, String s)
{
w.write(s);
w.write('\n');
}
public static API decode(InputStream is)
{
return decodePlain(new InflaterInputStream(is));
}
public static API decodePlain(InputStream is)
{
return new API(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
.lines()
.filter(line -> !line.isEmpty())
.collect(ImmutableSet.toImmutableSet()));
}
public Map<String, String> parseCommented(InputStream is, boolean checked) throws IOException
{
Map<String, String> out = new HashMap<>();
String comment = "";
boolean clearComment = true;
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
for (String line; (line = br.readLine()) != null; )
{
if (line.trim().isEmpty())
{
continue;
}
if (line.startsWith("#"))
{
if (clearComment)
{
clearComment = false;
comment = "";
}
else
{
comment += "\n";
}
comment += line.substring(1).trim();
continue;
}
clearComment = true;
if (line.startsWith("/"))
{
String re = line.trim();
Pattern p = Pattern.compile(re.substring(1, re.length() - 1));
List<String> keys = apis.stream().filter(p.asPredicate()).collect(Collectors.toList());
if (keys.isEmpty())
{
if (checked)
{
throw new RuntimeException("no apis match regex \"" + line + "\"");
}
else
{
log.warn("no apis match regex \"{}\"", line);
}
}
for (String k : keys)
{
out.put(k, comment.isEmpty() ? k : comment);
}
}
else
{
if (!apis.contains(line))
{
if (checked)
{
throw new RuntimeException("no apis match \"" + line + "\"");
}
else
{
log.warn("no apis match \"{}\"", line);
}
}
out.put(line, comment.isEmpty() ? line : comment);
}
}
return out;
}
public void encode(OutputStream os) throws IOException
{
encode(os, apis.stream());
}
public Stream<String> missingFrom(API other)
{
return apis.stream()
.filter(a -> !other.getApis().contains(a));
}
public Stream<String> in(API other)
{
return apis.stream()
.filter(a -> other.getApis().contains(a));
}
public Set<String> disallowed(Map<String, String> disaslowed)
{
return apis.stream()
.map(disaslowed::get)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
public static String modifiersToString(int modifiers, boolean member)
{
String s = "";
if (Modifier.isAbstract(modifiers))
{
s += "a";
}
if (!member && Modifier.isInterface(modifiers))
{
s += "i";
}
if (Modifier.isPublic(modifiers))
{
s += "b";
}
else if (Modifier.isProtected(modifiers))
{
s += "t";
}
else if (Modifier.isPrivate(modifiers))
{
s += "v";
}
else
{
s += "g";
}
if (member && Modifier.isStatic(modifiers))
{
s += "s";
}
if (Modifier.isFinal(modifiers))
{
s += "f";
}
return s;
}
public void recordClass(int modifiers, String descriptor)
{
if (descriptor != null)
{
apis.add(descriptor + modifiersToString(modifiers, false));
}
}
public void recordClassHierarchy(String from, String superDescriptor)
{
apis.add(from + ">" + superDescriptor);
}
public void recordMethod(int modifiers, String classDescriptor, CharSequence name, String descriptor)
{
if (classDescriptor != null & descriptor != null)
{
String mod = modifiersToString(modifiers, true);
if ((modifiers & Opcodes.ACC_VARARGS) != 0)
{
mod += "v";
}
apis.add(classDescriptor + "." + name + descriptor + ":" + mod);
}
}
public void recordField(int modifiers, String classDescriptor, CharSequence name, String descriptor, Object constantValue)
{
if (classDescriptor != null && name != null && descriptor != null)
{
String mods = modifiersToString(modifiers, true);
apis.add(classDescriptor + "." + name + ":" + descriptor + ":" + mods + ":" + (constantValue == null ? "" : constantValue));
}
}
}
@@ -1,125 +0,0 @@
/*
* Copyright (c) 2021 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.apirecorder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import lombok.Getter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* Records the API provided by any classes passed through it
*/
public class ClassRecorder extends ClassVisitor
{
@Getter
private final API api = new API();
private String className;
public ClassRecorder()
{
super(Opcodes.ASM7);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
{
this.className = "L" + name + ";";
api.recordClass(access, className);
api.recordClassHierarchy(this.className, "L" + superName + ";");
for (String iface : interfaces)
{
api.recordClassHierarchy(this.className, "L" + iface + ";");
}
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value)
{
if (!Modifier.isPrivate(access))
{
api.recordField(access, className, name, descriptor, value);
}
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
{
if (!Modifier.isPrivate(access))
{
api.recordMethod(access, className, name, descriptor);
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
public void recordClass(File jarFile) throws IOException
{
try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile)))
{
for (JarEntry je; (je = jis.getNextJarEntry()) != null; )
{
String fileName = je.getName();
if (!fileName.endsWith(".class"))
{
continue;
}
ClassReader cr = new ClassReader(jis);
cr.accept(this, ClassReader.SKIP_CODE);
}
}
}
public static void main(String... classes) throws IOException
{
var cr = new ClassRecorder();
Iterator<String> args = List.of(classes).iterator();
File out = new File(args.next());
for (String fi; args.hasNext(); )
{
fi = args.next();
cr.recordClass(new File(fi));
}
try (OutputStream os = new FileOutputStream(out))
{
cr.getApi().encode(os);
}
}
}
@@ -1,94 +0,0 @@
/*
* Copyright (c) 2021 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.apirecorder;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreeScanner;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PrintingScanner extends TreeScanner<Void, Void>
{
private final StringBuilder sb = new StringBuilder();
private int tabs = 0;
public static String print(Tree n)
{
PrintingScanner s = new PrintingScanner();
s.scan(n, null);
return s.toString();
}
public void indent()
{
for (int i = 0; i < tabs; i++)
{
sb.append('\t');
}
}
@Override
public Void scan(Tree tree, Void unused)
{
indent();
if (tree == null)
{
sb.append("null\n");
return null;
}
sb.append(tree.getKind()).append("(\n");
tabs++;
super.scan(tree, unused);
tabs--;
indent();
sb.append(")\n");
return null;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void unused)
{
indent();
sb.append(node.getName()).append('\n');
return super.visitIdentifier(node, unused);
}
@Override
public Void visitMemberSelect(MemberSelectTree node, Void unused)
{
super.visitMemberSelect(node, unused);
indent();
sb.append(node.getIdentifier()).append('\n');
return null;
}
@Override
public String toString()
{
return sb.toString();
}
}
@@ -1,96 +0,0 @@
/*
* Copyright (c) 2021 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.apirecorder;
import com.google.common.base.Strings;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RecorderPlugin implements Plugin
{
@Override
public String getName()
{
return "RuneLiteAPIRecorder";
}
@Override
public void init(JavacTask task, String... args)
{
String buildDir = System.getenv("runelite.pluginhub.package.buildDir");
if (Strings.isNullOrEmpty(buildDir))
{
return;
}
RecordingTreeScanner scanner = new RecordingTreeScanner(task);
task.addTaskListener(new TaskListener()
{
@Override
public void finished(TaskEvent e)
{
switch (e.getKind())
{
case ANALYZE:
if (log.isDebugEnabled())
{
log.info("{}", PrintingScanner.print(e.getCompilationUnit()));
}
try
{
scanner.scan(e.getCompilationUnit(), null);
}
catch (Exception ex)
{
log.warn("failed to scan", ex);
scanner.setPartial(true);
}
break;
case COMPILATION:
if (!scanner.isPartial())
{
try (FileOutputStream fos = new FileOutputStream(new File(buildDir, "api")))
{
scanner.getApi().encode(fos);
}
catch (IOException ex)
{
throw new RuntimeException(ex);
}
}
break;
}
}
});
}
}
@@ -1,529 +0,0 @@
/*
* Copyright (c) 2021 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.apirecorder;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.PackageTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import java.util.HashMap;
import java.util.Map;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.JavaFileObject;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.objectweb.asm.Opcodes;
/**
* Records all the API consumed of any nodes passed through it
*/
@Slf4j
class RecordingTreeScanner extends TreePathScanner<Void, Void>
{
@Getter
private final API api = new API();
private final Map<String, Boolean> jvmClassCache = new HashMap<>();
private final Trees trees;
private final Elements elements;
private final Types types;
private final com.sun.tools.javac.code.Types internalTypes;
@Getter
@Setter
private boolean partial;
public RecordingTreeScanner(JavacTask task)
{
this.trees = Trees.instance(task);
this.elements = task.getElements();
this.types = task.getTypes();
BasicJavacTask internalTask = (BasicJavacTask) task;
this.internalTypes = com.sun.tools.javac.code.Types.instance(internalTask.getContext());
}
@Override
public Void visitImport(ImportTree node, Void unused)
{
return null;
}
@Override
public Void visitPackage(PackageTree node, Void unused)
{
// we don't want to record the pkg ident
return null;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void unused)
{
recordElement(trees.getElement(getCurrentPath()), null);
return null;
}
@Override
public Void visitMemberSelect(MemberSelectTree node, Void unused)
{
TypeMirror receiver = trees.getTypeMirror(new TreePath(getCurrentPath(), node.getExpression()));
recordElement(trees.getElement(getCurrentPath()), receiver);
return super.visitMemberSelect(node, unused);
}
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void unused)
{
recordElement(trees.getElement(getCurrentPath()), null);
return super.visitMethodInvocation(node, unused);
}
@Override
public Void visitMemberReference(MemberReferenceTree node, Void unused)
{
TypeMirror receiver = trees.getTypeMirror(new TreePath(getCurrentPath(), node.getQualifierExpression()));
recordElement(trees.getElement(getCurrentPath()), receiver);
return super.visitMemberReference(node, unused);
}
@Override
public Void visitClass(ClassTree node, Void unused)
{
// it is hard to resolve the implicit receiver from this-calls to superclasses
// so record the whole hierarchy for all extends/implements clauses
if (node.getExtendsClause() != null)
{
recordFullHierarchy(trees.getElement(new TreePath(getCurrentPath(), node.getExtendsClause())).asType());
}
for (Tree iface : node.getImplementsClause())
{
recordFullHierarchy(trees.getElement(new TreePath(getCurrentPath(), iface)).asType());
}
return super.visitClass(node, unused);
}
@Override
public Void visitNewClass(NewClassTree node, Void unused)
{
recordElement(trees.getElement(getCurrentPath()), null);
return super.visitNewClass(node, unused);
}
@Override
public Void visitAnnotation(AnnotationTree node, Void unused)
{
Element typ = trees.getElement(new TreePath(getCurrentPath(), node.getAnnotationType()));
if (typ instanceof Symbol.TypeSymbol)
{
Symbol.TypeSymbol cs = (Symbol.TypeSymbol) typ;
// non runtime annotations may not exist in the output jar, so they can be
// always considered as missing if we were to record them
if (internalTypes.getRetention(cs) == Attribute.RetentionPolicy.RUNTIME)
{
scan(node.getAnnotationType(), null);
}
}
else
{
unexpected(typ);
}
scan(node.getArguments(), null);
return null;
}
@SneakyThrows
private boolean shouldRecord(TypeMirror tm)
{
if (tm instanceof DeclaredType)
{
Element e = ((DeclaredType) tm).asElement();
// there isn't a particularly nice way to find where a symbol is resolved from in the public api
JavaFileObject classfile = ((Symbol.ClassSymbol) e).classfile;
if (classfile == null || classfile.getKind() == JavaFileObject.Kind.SOURCE)
{
return false;
}
String fqn = elements.getBinaryName((TypeElement) e).toString();
return jvmClassCache.computeIfAbsent(fqn, name ->
ClassLoader.getPlatformClassLoader()
.getResource(fqn.replace('.', '/') + ".class") == null);
}
else if (tm instanceof TypeVariable)
{
return shouldRecord(((TypeVariable) tm).getLowerBound());
}
else if (tm instanceof ArrayType || tm instanceof NullType || tm instanceof NoType || tm instanceof PrimitiveType)
{
// ignored
}
else
{
unexpected(tm);
}
return false;
}
@SneakyThrows
private boolean shouldRecord(Element element)
{
if (element instanceof TypeElement)
{
return shouldRecord(element.asType());
}
else if (element instanceof PackageElement)
{
return false;
}
else
{
if (element.getEnclosingElement() != null && element.getEnclosingElement() != element)
{
return shouldRecord(element.getEnclosingElement());
}
else
{
unexpected(element);
}
}
return false;
}
private boolean isJLObject(TypeMirror tm)
{
tm = types.erasure(tm);
return tm instanceof DeclaredType && elements.getBinaryName((TypeElement) ((DeclaredType) tm).asElement()).contentEquals("java.lang.Object");
}
private void recordElement(Element element, TypeMirror receiverType)
{
if (element == null)
{
unexpected(element);
}
if (receiverType != null && shouldRecord(receiverType))
{
if (element instanceof ExecutableElement)
{
TypeMirror realRecv = element.getEnclosingElement().asType();
if (!isJLObject(realRecv) && !recordClassHierarchy(receiverType, realRecv))
{
log.warn("could not find {} in {}", receiverType, realRecv);
unexpected(realRecv);
}
}
else if (element instanceof VariableElement)
{
switch (element.getKind())
{
case FIELD:
case ENUM_CONSTANT:
TypeMirror realRecv = element.getEnclosingElement().asType();
if (!isJLObject(realRecv) && !recordClassHierarchy(receiverType, realRecv))
{
log.warn("could not find {} in {}", receiverType, realRecv);
unexpected(realRecv);
}
break;
case RESOURCE_VARIABLE:
case LOCAL_VARIABLE:
case PARAMETER:
return;
default:
unexpected(element);
return;
}
}
else if (element instanceof TypeElement)
{
// Inner class ref
TypeMirror realRecv = element.getEnclosingElement().asType();
if (!isJLObject(realRecv) && !recordClassHierarchy(receiverType, realRecv))
{
log.warn("could not find {} in {}", receiverType, realRecv);
unexpected(realRecv);
}
}
else
{
unexpected(element);
}
}
if (!shouldRecord(element))
{
return;
}
if (element instanceof ExecutableElement)
{
ExecutableElement et = (ExecutableElement) element;
StringBuilder desc = new StringBuilder();
boolean ok = true;
String n;
desc.append('(');
for (VariableElement t : et.getParameters())
{
n = typeDescriptor(t);
ok &= n != null;
desc.append(n);
}
desc.append(')');
n = typeDescriptor(et.getReturnType());
ok &= n != null;
desc.append(n);
if (ok)
{
api.recordMethod(
modifiers(et),
typeDescriptor(et.getEnclosingElement()),
element.getSimpleName(),
desc.toString());
}
}
else if (element instanceof VariableElement)
{
switch (element.getKind())
{
case FIELD:
case ENUM_CONSTANT:
if ("class".equals(element.getSimpleName().toString()))
{
return;
}
api.recordField(
modifiers(element),
typeDescriptor(element.getEnclosingElement()),
element.getSimpleName(),
typeDescriptor(element.asType()),
((VariableElement) element).getConstantValue());
break;
case RESOURCE_VARIABLE:
case LOCAL_VARIABLE:
case PARAMETER:
return;
default:
unexpected(element);
return;
}
}
else if (element instanceof TypeElement)
{
api.recordClass(
modifiers(element),
typeDescriptor(element));
}
else
{
unexpected(element);
}
}
private boolean recordClassHierarchy(TypeMirror from, TypeMirror target)
{
// no public api for superclass, without getting a TypeElement
if (types.isSameType(types.erasure(from), types.erasure(target)))
{
return true;
}
boolean hit = false;
for (TypeMirror m : types.directSupertypes(from))
{
if (recordClassHierarchy(m, target))
{
hit = true;
if (shouldRecord(from))
{
api.recordClassHierarchy(typeDescriptor(from), typeDescriptor(m));
}
}
}
return hit;
}
private void recordFullHierarchy(TypeMirror from)
{
if (shouldRecord(from))
{
for (TypeMirror m : types.directSupertypes(from))
{
if (!isJLObject(m))
{
recordFullHierarchy(m);
api.recordClassHierarchy(typeDescriptor(from), typeDescriptor(m));
}
}
}
}
private int modifiers(Element e)
{
// element.getModifiers is not complete, so we use the internal api
// however some of it's bits are different from the classfile format's
long flags = ((Symbol) e).flags_field;
if ((flags & Flags.VARARGS) != 0)
{
flags |= Opcodes.ACC_VARARGS;
}
if (((flags & Flags.DEFAULT) != 0) && ((flags & Opcodes.ACC_INTERFACE) == 0))
{
flags &= ~Opcodes.ACC_ABSTRACT;
}
return (int) flags;
}
private String typeDescriptor(Element element)
{
if (element instanceof TypeElement || element instanceof VariableElement)
{
return typeDescriptor(element.asType());
}
else
{
unexpected(element);
return null;
}
}
private String typeDescriptor(TypeMirror type)
{
switch (type.getKind())
{
case VOID:
return "V";
case BOOLEAN:
return "Z";
case CHAR:
return "C";
case BYTE:
return "B";
case SHORT:
return "S";
case INT:
return "I";
case FLOAT:
return "F";
case LONG:
return "J";
case DOUBLE:
return "D";
}
if (type instanceof ArrayType)
{
ArrayType at = (ArrayType) type;
return "[" + typeDescriptor(at.getComponentType());
}
else if (type instanceof DeclaredType)
{
Element e = ((DeclaredType) type).asElement();
// returns the fqn, not binary name
String fqn = elements.getBinaryName((TypeElement) e).toString();
return "L" + fqn.replace('.', '/') + ";";
}
else if (type instanceof TypeVariable)
{
return typeDescriptor(types.erasure(type));
}
else
{
unexpected(type);
return null;
}
}
private void unexpected(Element element)
{
if (element == null)
{
log.warn("unexpected null", new Exception());
}
else
{
log.warn("Unexpected {} {} {}", element.getKind(), element.getClass(), element, new Exception());
}
unexpected();
}
private void unexpected(TypeMirror mirror)
{
if (mirror == null)
{
log.warn("unexpected null type", new Exception());
}
else
{
log.warn("Unexpected {} {} {}", mirror.getKind(), mirror.getClass(), mirror, new Exception());
}
unexpected();
}
private void unexpected()
{
partial = true;
TreePath p = getCurrentPath();
for (int i = 0; p.getParentPath() != null && i < 1; p = p.getParentPath(), i++) ;
log.info("{} {}", getCurrentPath().getCompilationUnit().getSourceFile().getName(), PrintingScanner.print(p.getLeaf()));
}
}
@@ -1 +0,0 @@
net.runelite.pluginhub.apirecorder.RecorderPlugin
-62
View File
@@ -1,62 +0,0 @@
/*
* Copyright (c) 2020 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.
*/
allprojects {
version "2.0-SNAPSHOT"
group "net.runelite.pluginhub"
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
}
subprojects {
apply plugin: "java"
java {
sourceCompatibility = 11
}
task shadowJar(type: Jar) {
archiveFileName.set archiveBaseName.get() + "." + archiveExtension.get()
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
with tasks.jar
}
}
// we have this task then java -jar the output rather than using gradle run so
// the daemon used to build this can be reused by a real plugin build
task prep {
dependsOn ":package:shadowJar"
dependsOn ":apirecorder:shadowJar"
doLast {
file("build").mkdirs()
file("build/gradleHome").write(gradle.gradleHomeDir.absolutePath)
}
}
-2
View File
@@ -1,2 +0,0 @@
# this should match what Plugin uses to build with
org.gradle.jvmargs=-Xmx768M -XX:+UseParallelGC
-214
View File
@@ -1,214 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>false</verify-signatures>
<trusted-artifacts>
<trust file=".*-javadoc[.]jar" regex="true"/>
<trust file=".*-sources[.]jar" regex="true"/>
</trusted-artifacts>
</configuration>
<components>
<component group="com.google.code.findbugs" name="jsr305" version="3.0.2">
<artifact name="jsr305-3.0.2.jar">
<sha256 value="766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7" origin="Generated by Gradle"/>
</artifact>
<artifact name="jsr305-3.0.2.pom">
<sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson" version="2.8.5">
<artifact name="gson-2.8.5.jar">
<sha256 value="233a0149fc365c9f6edbd683cfe266b19bdc773be98eabdaf6b3c924b48e7d81" origin="Generated by Gradle"/>
</artifact>
<artifact name="gson-2.8.5.pom">
<sha256 value="b8308557a7fccc92d9fe7c8cd0599258b361285d2ecde7689eda98843255a092" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.code.gson" name="gson-parent" version="2.8.5">
<artifact name="gson-parent-2.8.5.pom">
<sha256 value="8f1fec72b91a71ea39ec39f5f778c4d1124b6b097c6d55b3a50b554a52237b27" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.0.18">
<artifact name="error_prone_annotations-2.0.18.jar">
<sha256 value="cb4cfad870bf563a07199f3ebea5763f0dec440fcda0b318640b1feaa788656b" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.0.18.pom">
<sha256 value="9144127192d6f612c2366825dceaeb23b0d53130b83e0bf1ffe107d1470a8487" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.0.18">
<artifact name="error_prone_parent-2.0.18.pom">
<sha256 value="cf149955279b07d4f11e817985c1164a69e930d73db7441b43a6ef53bbd286c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="23.2-jre">
<artifact name="guava-23.2-jre.jar">
<sha256 value="5be9a7d05ba0ccd74708bc8018ae412255f85843c0b92302e9b9befa6ed52564" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-23.2-jre.pom">
<sha256 value="57abac7e3962e45ad1c8645d91cdad96e1e0bf23b8b38e6f389011f7ae496870" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="23.2-jre">
<artifact name="guava-parent-23.2-jre.pom">
<sha256 value="e92cd52f3b86a0f6fe90fc94a86ea072a2b112fc40063bd33f6679778038cc47" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.j2objc" name="j2objc-annotations" version="1.1">
<artifact name="j2objc-annotations-1.1.jar">
<sha256 value="2994a7eb78f2710bd3d3bfb639b2c94e219cedac0d4d084d516e78c16dddecf6" origin="Generated by Gradle"/>
</artifact>
<artifact name="j2objc-annotations-1.1.pom">
<sha256 value="f0c98c571e93a7cb4dd18df0fa308f0963e7a0620ac2d4244e61e709d03ad6be" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="mockwebserver" version="3.14.9">
<artifact name="mockwebserver-3.14.9.jar">
<sha256 value="099f70ab2997f628bf93c358baac829ccb625b6e08a9361e09d6923fa9af1f68" origin="Generated by Gradle"/>
</artifact>
<artifact name="mockwebserver-3.14.9.pom">
<sha256 value="24fc2ed2c0d80c7461e24f4dc3f873381a5a77246cf99a57988d1b389b091fd6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="okhttp" version="3.14.9">
<artifact name="okhttp-3.14.9.jar">
<sha256 value="2570fab55515cbf881d7a4ceef49fc515490bc027057e666776a2832465aeca0" origin="Generated by Gradle"/>
</artifact>
<artifact name="okhttp-3.14.9.pom">
<sha256 value="61edf3c4764ea95f1212ca2dfbc6ea5d4133c71c4e8c40bd247ec83e7c6b7ce7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="parent" version="3.14.9">
<artifact name="parent-3.14.9.pom">
<sha256 value="b5f101dcf2cc7f32ec3d7b68bdc078e77eac1979bb8664e8f87f762288c3dce5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okio" name="okio" version="1.17.2">
<artifact name="okio-1.17.2.jar">
<sha256 value="f80ce42d2ffac47ad4c47e1d6f980d604d247ceb1a886705cf4581ab0c9fe2b8" origin="Generated by Gradle"/>
</artifact>
<artifact name="okio-1.17.2.pom">
<sha256 value="cd57f75443ab6a714b203da51994ee64ed0e919fea81f04e4f4b5324e4bb03f0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okio" name="okio-parent" version="1.17.2">
<artifact name="okio-parent-1.17.2.pom">
<sha256 value="e81f40146f2a0eb394b10fa3a175c85ab9fbe757aa418aa44d4b760efe16f024" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="junit" name="junit" version="4.12">
<artifact name="junit-4.12.jar">
<sha256 value="59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-4.12.pom">
<sha256 value="90f163f78e3ffb6f1c7ad97de9e7eba4eea25807141b85d6d12be67ca25449c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.codehaus" name="codehaus-parent" version="4">
<artifact name="codehaus-parent-4.pom">
<sha256 value="6b87237de8c2e1740cf80627c7f3ce3e15de1930bb250c55a1eca94fa3e014df" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.codehaus.mojo" name="animal-sniffer-annotations" version="1.14">
<artifact name="animal-sniffer-annotations-1.14.jar">
<sha256 value="2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d" origin="Generated by Gradle"/>
</artifact>
<artifact name="animal-sniffer-annotations-1.14.pom">
<sha256 value="1879f19a05991e3ed95910b96689333396b0c467a215dc4d1f90018404b72a26" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.codehaus.mojo" name="animal-sniffer-parent" version="1.14">
<artifact name="animal-sniffer-parent-1.14.pom">
<sha256 value="f51550a06b1410bd4962cb0e71df0b921a60a7ef47bfa9c4825a14be72316eea" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.codehaus.mojo" name="mojo-parent" version="34">
<artifact name="mojo-parent-34.pom">
<sha256 value="3e395d6fbc43c09a3774cac8694ce527398305ea3fd5492d80e25af27d382a9c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.gradle" name="gradle-tooling-api" version="8.10">
<artifact name="gradle-tooling-api-8.10.jar">
<sha256 value="b1e5d6a65e77302e836c325c28b944b05f0f1b62a332d6d4980049c555a70b02" origin="Generated by Gradle"/>
</artifact>
<artifact name="gradle-tooling-api-8.10.module">
<sha256 value="47d5af6c7ff4fd5167b09f5fd8cdc568823bcea77bb2ae8344c43bb375f1eccf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.hamcrest" name="hamcrest-core" version="1.3">
<artifact name="hamcrest-core-1.3.jar">
<sha256 value="66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9" origin="Generated by Gradle"/>
</artifact>
<artifact name="hamcrest-core-1.3.pom">
<sha256 value="fde386a7905173a1b103de6ab820727584b50d0e32282e2797787c20a64ffa93" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.hamcrest" name="hamcrest-parent" version="1.3">
<artifact name="hamcrest-parent-1.3.pom">
<sha256 value="6d535f94efb663bdb682c9f27a50335394688009642ba7a9677504bc1be4129b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2" name="ow2" version="1.5">
<artifact name="ow2-1.5.pom">
<sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm" version="9.3">
<artifact name="asm-9.3.jar">
<sha256 value="1263369b59e29c943918de11d6d6152e2ec6085ce63e5710516f8c67d368e4bc" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-9.3.pom">
<sha256 value="8eac07e29f8aea83a8156d7b29fa368f6ebf3becfb209c9a1ac36abd906123e0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.projectlombok" name="lombok" version="1.18.30">
<artifact name="lombok-1.18.30.jar">
<sha256 value="14151b47582d570b4de16a147ece3bdbd19ace4aee5bde3a5578c87db9ecb998" origin="Generated by Gradle"/>
</artifact>
<artifact name="lombok-1.18.30.pom">
<sha256 value="65001f382f5d1ce45692311bb1ba7c243c856195d492473d72ff9a7d1cc77699" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.7.10">
<artifact name="slf4j-api-1.7.10.jar">
<sha256 value="3863e27005740d4d1289bf87b113efea115e9a22408a7d623be8004991232bfe" origin="Generated by Gradle"/>
</artifact>
<artifact name="slf4j-api-1.7.10.pom">
<sha256 value="af9c5e8d2263422c74792ddd91b3cc1a24bd02b451b54cbb10cd6f2ba46c14b1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.7.36">
<artifact name="slf4j-api-1.7.36.jar">
<sha256 value="d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0" origin="Generated by Gradle"/>
</artifact>
<artifact name="slf4j-api-1.7.36.pom">
<sha256 value="fb046a9c229437928bb11c2d27c8b5d773eb8a25e60cbd253d985210dedc2684" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-parent" version="1.7.10">
<artifact name="slf4j-parent-1.7.10.pom">
<sha256 value="1abee7f5182fb79b4926fb64658af8c3248ed6b374f9ac7da1fd9e8b9197e2ce" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-parent" version="1.7.36">
<artifact name="slf4j-parent-1.7.36.pom">
<sha256 value="bb388d37fbcdd3cde64c3cede21838693218dc451f04040c5df360a78ed7e812" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-simple" version="1.7.10">
<artifact name="slf4j-simple-1.7.10.jar">
<sha256 value="3209a859bb47873c0d1873f7d69aa6652dc07fe32ed935492fe0b1cf1d2cf140" origin="Generated by Gradle"/>
</artifact>
<artifact name="slf4j-simple-1.7.10.pom">
<sha256 value="bbd8c6f5cf54d1c1fb11b415e51f6b35483fc264f81ccd98654cc3420e756901" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.sonatype.oss" name="oss-parent" version="7">
<artifact name="oss-parent-7.pom">
<sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/>
</artifact>
</component>
</components>
</verification-metadata>
-68
View File
@@ -1,68 +0,0 @@
/*
* Copyright (c) 2020 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.
*/
repositories {
maven {
url "https://repo.gradle.org/gradle/libs-releases-remote-cache/"
}
mavenCentral()
}
dependencies {
implementation "org.gradle:gradle-tooling-api:8.10"
implementation "org.slf4j:slf4j-simple:1.7.10"
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "com.google.guava:guava:23.2-jre"
implementation "org.ow2.asm:asm:9.3"
implementation "com.squareup.okhttp3:okhttp:3.14.9"
implementation "com.google.code.gson:gson:2.8.5"
implementation project(":upload")
implementation project(":apirecorder")
def lombok = "org.projectlombok:lombok:1.18.30";
compileOnly lombok
annotationProcessor lombok
testCompileOnly lombok
testAnnotationProcessor lombok
testImplementation "junit:junit:4.12"
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9"
}
processResources {
from("../verification-template/gradle/verification-metadata.xml") {
into "net/runelite/pluginhub/packager"
}
}
jar {
manifest {
attributes "Main-Class": "net.runelite.pluginhub.packager.Packager"
}
}
test {
dependsOn ":apirecorder:shadowJar"
workingDir new File(project.rootDir, "../")
}
@@ -1,31 +0,0 @@
/*
* 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,
}
@@ -1,47 +0,0 @@
/*
* Copyright (c) 2020 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;
import lombok.Getter;
public class DisabledPluginException extends Exception
{
@Getter
private final String internalName;
@Getter
private final String reason;
@Getter
private final boolean includeInUnavailable;
public DisabledPluginException(String internalName, String reason, boolean includeInUnavailable)
{
super("Plugin \"" + internalName + "\" is disabled: " + reason);
this.reason = reason;
this.internalName = internalName;
this.includeInUnavailable = includeInUnavailable;
}
}
@@ -1,537 +0,0 @@
/*
* Copyright (c) 2020 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;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Queues;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.pluginhub.uploader.ManifestDiff;
import net.runelite.pluginhub.uploader.PluginHubManifest;
import net.runelite.pluginhub.uploader.UploadConfiguration;
import net.runelite.pluginhub.uploader.Util;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MessageFormatter;
@Slf4j
public class Packager implements Closeable
{
private static final File PLUGIN_ROOT = new File("./plugins");
public static final File PACKAGE_ROOT = new File("./package/").getAbsoluteFile();
private static final File ARTIFACT_DIR = new File("/tmp/jars");
private Semaphore apiCheckSemaphore = new Semaphore(8);
private Semaphore downloadSemaphore = new Semaphore(2);
private Semaphore buildSemaphore = new Semaphore(Runtime.getRuntime().availableProcessors());
private Semaphore uploadSemaphore = new Semaphore(2);
private final List<File> buildList;
@Getter
private final String runeliteVersion;
@Setter
private String apiFilesVersion;
private PluginHubManifest.ManifestFull oldManifest;
private Map<String, PluginHubManifest.JarData> oldJarData = ImmutableMap.of();
private Map<String, PluginHubManifest.DisplayData> oldDisplayData = ImmutableMap.of();
@Getter
private final UploadConfiguration uploadConfig = new UploadConfiguration();
private final AtomicInteger numDone = new AtomicInteger(0);
private final int numTotal;
@Setter
private boolean alwaysPrintLog;
@Getter
private boolean failed;
private boolean isIncrementalRebuild;
private final StringBuilder buildSummary = new StringBuilder();
private ManifestDiff diff = new ManifestDiff();
public Packager(List<File> buildList) throws IOException
{
this.buildList = buildList;
this.numTotal = buildList.size();
this.runeliteVersion = Util.readRLVersion();
}
public void buildPlugins() throws IOException
{
if (apiFilesVersion != null)
{
try
{
oldManifest = uploadConfig.getManifest(apiFilesVersion, null);
oldJarData = oldManifest.getJars()
.stream().collect(ImmutableMap.toImmutableMap(PluginHubManifest.JarData::getInternalName, Function.identity()));
oldDisplayData = oldManifest.getDisplay()
.stream().collect(ImmutableMap.toImmutableMap(PluginHubManifest.DisplayData::getInternalName, Function.identity()));
diff.setOldManifestVersion(apiFilesVersion);
}
catch (RuntimeException | IOException e)
{
log.info("Unable to download previous manifest, doing full build", e);
apiFilesVersion = null;
}
}
if (uploadConfig.isComplete())
{
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_JAR).build());
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_ICON).build());
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_API).build());
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_LOG).build());
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_SOURCE).build());
uploadConfig.mkdirs(uploadConfig.getRoot().newBuilder().addPathSegment(UploadConfiguration.DIR_MANIFEST).build());
}
ARTIFACT_DIR.mkdirs();
Queue<File> buildQueue = Queues.synchronizedQueue(new ArrayDeque<>(buildList));
List<Thread> buildThreads = IntStream.range(0, 8)
.mapToObj(v ->
{
Thread t = new Thread(() ->
{
for (File plugin; (plugin = buildQueue.poll()) != null; )
{
buildPlugin(plugin);
}
});
t.start();
return t;
}).collect(Collectors.toList());
for (Thread buildThread : buildThreads)
{
try
{
buildThread.join();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
String diffJSON = Util.GSON.toJson(diff);
log.debug("manifest change: {}", diffJSON);
try (FileOutputStream fos = new FileOutputStream("/tmp/manifest_diff"))
{
fos.write(diffJSON.getBytes(StandardCharsets.UTF_8));
}
}
private void buildPlugin(File plugin)
{
diff.getRemove().add(plugin.getName());
if (!plugin.exists())
{
return;
}
try (Plugin p = new Plugin(plugin))
{
try
{
PluginHubManifest.JarData oldJarData = this.oldJarData.get(p.getInternalName());
if (isIncrementalRebuild && oldJarData != null)
{
try (Closeable ignored = acquireAPICheck(p))
{
if (!p.rebuildNeeded(uploadConfig, oldJarData))
{
diff.getCopyFromOld().add(p.getInternalName());
diff.getRemove().remove(p.getInternalName());
return;
}
}
}
try (Closeable ignored = acquireDownload(p))
{
p.download();
}
try (Closeable ignored = acquireBuild(p))
{
p.build(runeliteVersion, alwaysPrintLog);
}
String logURL = "";
if (uploadConfig.isComplete())
{
try (Closeable ignored = acquireUpload(p))
{
p.upload(uploadConfig);
}
// outside the semaphore so the timing gets uploaded too
logURL = p.uploadLog(uploadConfig);
}
p.copyArtifacts(ARTIFACT_DIR);
diff.getAddJarData().add(p.getJarData());
diff.getAddDisplayData().add(p.getDisplayData());
log.info("{}: done in {}ms [{}/{}]", p.getInternalName(), p.getBuildTimeMS(), numDone.get() + 1, numTotal);
if (!p.getApiFile().exists())
{
logToSummary("{} failed to write the api record: {}", p.getInternalName(), logURL);
}
}
catch (PluginBuildException e)
{
failed = true;
p.writeLog("package failed\n", e);
if (!alwaysPrintLog)
{
Files.asCharSource(p.getLogFile(), StandardCharsets.UTF_8).copyTo(System.out);
}
PluginHubManifest.DisplayData oldDisplayData = this.oldDisplayData.get(p.getInternalName());
if (uploadConfig.isComplete())
{
String logURL = p.uploadLog(uploadConfig);
if (oldDisplayData != null && oldDisplayData.getBuildFailAt() != null)
{
long daysFailed = Instant.ofEpochSecond(oldDisplayData.getBuildFailAt()).until(Instant.now(), ChronoUnit.DAYS);
logToSummary("{} failed ({} days): {}", p.getInternalName(), daysFailed, logURL);
}
else
{
logToSummary("{} failed: {}", p.getInternalName(), logURL);
}
}
else
{
logToSummary("{} failed", p.getInternalName());
}
if (oldDisplayData != null)
{
if (oldDisplayData.getBuildFailAt() == null)
{
oldDisplayData.setBuildFailAt(Instant.now().getEpochSecond());
}
oldDisplayData.setUnavailableReason(null);
diff.getAddDisplayData().add(oldDisplayData);
}
}
finally
{
if (alwaysPrintLog)
{
Files.asCharSource(p.getLogFile(), StandardCharsets.UTF_8).copyTo(System.out);
}
}
}
catch (DisabledPluginException e)
{
if (e.isIncludeInUnavailable())
{
PluginHubManifest.DisplayData oldDisplayData = this.oldDisplayData.get(e.getInternalName());
if (oldDisplayData == null)
{
logToSummary("unavailible plugin {} wasn't in the previous manifest", e.getInternalName());
}
else
{
oldDisplayData.setUnavailableReason(e.getReason());
diff.getAddDisplayData().add(oldDisplayData);
}
}
}
catch (PluginBuildException e)
{
failed = true;
logToSummary("", e);
}
catch (Exception e)
{
failed = true;
logToSummary("{}: crashed the build script: ", plugin.getName(), e);
}
finally
{
numDone.addAndGet(1);
}
}
private void logToSummary(String message, Object... args)
{
log.info(message, args);
FormattingTuple fmt = MessageFormatter.arrayFormat(message, args);
synchronized (buildSummary)
{
buildSummary.append(fmt.getMessage()).append('\n');
}
}
public String getBuildSummary()
{
return buildSummary.toString();
}
private Closeable acquireAPICheck(Plugin plugin)
{
return section(plugin, "apicheck", apiCheckSemaphore);
}
private Closeable acquireDownload(Plugin plugin)
{
return section(plugin, "download", downloadSemaphore);
}
private Closeable acquireBuild(Plugin plugin)
{
return section(plugin, "build", buildSemaphore);
}
private Closeable acquireUpload(Plugin plugin)
{
return section(plugin, "upload", uploadSemaphore);
}
private Closeable section(Plugin p, String name, Semaphore s)
{
try
{
s.acquire();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
Stopwatch time = Stopwatch.createStarted();
return () ->
{
long ms = time.stop()
.elapsed(TimeUnit.MILLISECONDS);
p.setBuildTimeMS(p.getBuildTimeMS() + ms);
p.writeLog("{}: {}ms\n", name, ms);
s.release();
};
}
public void setIsIncrementalRebuild(boolean incremental)
{
this.isIncrementalRebuild = incremental;
diff.setIgnoreOldManifest(incremental);
}
@Override
public void close()
{
uploadConfig.close();
}
public static void main(String... args) throws Exception
{
boolean isBuildingAll = false;
boolean testFailure = false;
String apiFilesVersion = System.getenv("API_FILES_VERSION");
if (apiFilesVersion != null)
{
apiFilesVersion = apiFilesVersion.trim();
if (apiFilesVersion.isEmpty())
{
apiFilesVersion = null;
}
}
String range = System.getenv("PACKAGE_COMMIT_RANGE");
List<File> buildList;
if (args.length != 0)
{
buildList = Stream.of(args)
.map(File::new)
.collect(Collectors.toList());
}
else if ("ALL".equals(System.getenv("FORCE_BUILD")))
{
buildList = listAllPlugins();
isBuildingAll = true;
}
else if (!Strings.isNullOrEmpty(System.getenv("FORCE_BUILD")))
{
buildList = StreamSupport.stream(
Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.split(System.getenv("FORCE_BUILD"))
.spliterator(), false)
.map(name -> new File(PLUGIN_ROOT, name))
.collect(Collectors.toList());
}
else if (!Strings.isNullOrEmpty(range))
{
Process gitdiff = new ProcessBuilder("git", "diff", "--name-only", range)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start();
boolean doAll = false;
boolean doPackageTests = false;
buildList = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(gitdiff.getInputStream())))
{
for (String line; (line = br.readLine()) != null; )
{
if ("runelite.version".equals(line))
{
doAll = true;
}
else if (line.startsWith("plugins/"))
{
buildList.add(new File(line));
}
else if (line.startsWith("package/") || line.startsWith("templateplugin/") || line.startsWith("create_new_plugin.py"))
{
doPackageTests = true;
}
}
}
if (doPackageTests)
{
testFailure |= new ProcessBuilder(new File(PACKAGE_ROOT, "gradlew").getAbsolutePath(), "--console=plain", "test")
.directory(PACKAGE_ROOT)
.inheritIO()
.start()
.waitFor() != 0;
testFailure |= new ProcessBuilder(new File(PACKAGE_ROOT, "gradlew").getAbsolutePath(), "--console=plain", ":verifyAll")
.directory(new File(PACKAGE_ROOT, "verification-template"))
.inheritIO()
.start()
.waitFor() != 0;
}
if (doAll)
{
isBuildingAll = true;
buildList = listAllPlugins();
}
gitdiff.waitFor(1, TimeUnit.SECONDS);
if (gitdiff.exitValue() != 0)
{
throw new RuntimeException("git diff exited with " + gitdiff.exitValue());
}
}
else
{
throw new RuntimeException("missing env vars");
}
if (apiFilesVersion == null && !Strings.isNullOrEmpty(range))
{
String commit = range.substring(0, range.indexOf(".."));
Process gitShow = new ProcessBuilder("git", "show", commit + ":runelite.version")
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start();
apiFilesVersion = new String(ByteStreams.toByteArray(gitShow.getInputStream()), StandardCharsets.UTF_8)
.trim();
gitShow.waitFor(1, TimeUnit.SECONDS);
if (gitShow.exitValue() != 0)
{
throw new RuntimeException("git show exited with " + gitShow.exitValue());
}
}
boolean failed;
try (Packager pkg = new Packager(buildList))
{
pkg.getUploadConfig().fromEnvironment(pkg.getRuneliteVersion());
pkg.setAlwaysPrintLog(!pkg.getUploadConfig().isComplete());
pkg.setIsIncrementalRebuild(isBuildingAll);
pkg.setApiFilesVersion(apiFilesVersion);
pkg.buildPlugins();
failed = pkg.isFailed();
if (isBuildingAll)
{
String summary = pkg.getBuildSummary();
if (!summary.isEmpty())
{
log.info("Failures:\n{}", summary);
}
if (!failed)
{
log.info("All plugins succeeded");
}
}
}
if (testFailure || (failed && !isBuildingAll))
{
System.exit(1);
}
}
static List<File> listAllPlugins()
{
return Arrays.asList(PLUGIN_ROOT.listFiles());
}
}
File diff suppressed because it is too large. Load diff
@@ -1,130 +0,0 @@
/*
* Copyright (c) 2020 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;
import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.function.Supplier;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MessageFormatter;
public class PluginBuildException extends Exception
{
private String file;
private String line;
private String help;
private PluginBuildException(String message, Throwable throwable)
{
super(message, throwable);
}
public static PluginBuildException of(String internalName, String message, Object... args)
{
FormattingTuple fmt = MessageFormatter.arrayFormat(internalName + ": " + message, args);
return new PluginBuildException(fmt.getMessage(), fmt.getThrowable());
}
public static PluginBuildException of(Plugin plugin, String message, Object... args)
{
FormattingTuple fmt = MessageFormatter.arrayFormat(plugin.getInternalName() + ": " + message, args);
return new PluginBuildException(fmt.getMessage(), fmt.getThrowable());
}
public PluginBuildException withFileLine(File file, String line)
{
return withFileLine(file.toString(), line);
}
public PluginBuildException withFile(File file)
{
return withFileLine(file, null);
}
public PluginBuildException withFileLine(String file, String line)
{
this.file = file;
this.line = line;
return this;
}
public PluginBuildException withFile(String file)
{
return withFileLine(file, null);
}
public PluginBuildException withHelp(String help)
{
this.help = help;
return this;
}
public PluginBuildException withHelp(Supplier<String> help)
{
this.help = help.get();
return this;
}
public String getHelpText()
{
StringBuilder sb = new StringBuilder();
if (this.file != null)
{
sb.append("in file ").append(this.file);
if (this.line != null)
{
sb.append(":\n").append(line);
}
sb.append("\n");
}
if (this.help != null)
{
sb.append(help);
}
return sb.toString();
}
@Override
public void printStackTrace(PrintStream s)
{
super.printStackTrace(s);
s.println("\n");
s.println(getMessage());
s.println(getHelpText());
}
@Override
public void printStackTrace(PrintWriter s)
{
super.printStackTrace(s);
s.println("\n");
s.println(getMessage());
s.println(getHelpText());
}
}
@@ -1,44 +0,0 @@
# avoid client.getVar, prefer client.getVarbitValue
Lnet/runelite/api/Client;.getVar(I)I:ab
# ChatMessageManager.update is a no-op
Lnet/runelite/client/chat/ChatMessageManager;.update(Lnet/runelite/api/MessageNode;)V:b
# Do not create fresh OkHttpClients - always @Inject the client's OkHttpClient
Lokhttp3/OkHttpClient;.<init>()V:b
Lokhttp3/OkHttpClient$Builder;.<init>()V:b
# Do not create fresh Gson instances, always @Inject the client's Gson.
# You can customize it by calling .newBuilder() on it
Lcom/google/gson/Gson;.<init>()V:b
Lcom/google/gson/GsonBuilder;.<init>()V:b
# Use ComponentID or InterfaceID instead of WidgetInfo or WidgetID
/^Lnet/runelite/api/widgets/WidgetInfo;/
/^Lnet/runelite/api/widgets/WidgetID/
# Use the overloaded getItemStats(int itemId) method instead
Lnet/runelite/client/game/ItemManager;.getItemStats(IZ)Lnet/runelite/http/api/item/ItemStats;:b
#
Lnet/runelite/client/account/AccountClient;.<clinit>()V:gs
Lnet/runelite/client/account/AccountClient;.login(I)Lnet/runelite/http/api/account/OAuthResponse;:b
Lnet/runelite/client/account/AccountClient;.logout()V:b
Lnet/runelite/client/account/AccountClient;.sessionCheck()Z:b
Lnet/runelite/client/account/AccountClient;.setUuid(Ljava/util/UUID;)V:b
Lnet/runelite/client/account/AccountClient;b
Lnet/runelite/client/account/AccountSession;.<init>(Ljava/util/UUID;Ljava/time/Instant;Ljava/lang/String;)V:b
Lnet/runelite/client/account/AccountSession;.canEqual(Ljava/lang/Object;)Z:t
Lnet/runelite/client/account/AccountSession;.equals(Ljava/lang/Object;)Z:b
Lnet/runelite/client/account/AccountSession;.getCreated()Ljava/time/Instant;:b
Lnet/runelite/client/account/AccountSession;.getUsername()Ljava/lang/String;:b
Lnet/runelite/client/account/AccountSession;.getUuid()Ljava/util/UUID;:b
Lnet/runelite/client/account/AccountSession;.hashCode()I:b
Lnet/runelite/client/account/AccountSession;.toString()Ljava/lang/String;:b
Lnet/runelite/client/account/AccountSession;b
Lnet/runelite/client/account/SessionManager;.<clinit>()V:gs
Lnet/runelite/client/account/SessionManager;.getAccountSession()Lnet/runelite/client/account/AccountSession;:b
Lnet/runelite/client/account/SessionManager;.loadSession()V:b
Lnet/runelite/client/account/SessionManager;.login()V:b
Lnet/runelite/client/account/SessionManager;.logout()V:b
Lnet/runelite/client/account/SessionManager;b
@@ -1,26 +0,0 @@
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)
}
@@ -1 +0,0 @@
rootProject.name = "standard-plugin-build"
@@ -1,44 +0,0 @@
/*
* Copyright (c) 2022 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;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
public class DisallowedAPIsTest
{
@Test
public void testDisallowedExist() throws IOException
{
try (InputStream is = Packager.class.getResourceAsStream("disallowed-apis.txt"))
{
Map<String, String> disallowed = Plugin.CURRENT_API.parseCommented(is, true);
disallowed.forEach((k, v) -> Assert.assertFalse(k + " -> " + v, v.isEmpty()));
}
}
}
@@ -1,251 +0,0 @@
/*
* Copyright (c) 2020 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;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import net.runelite.pluginhub.uploader.Util;
import org.junit.Assert;
import org.junit.Test;
@Slf4j
public class PluginTest
{
@Test
public void testInternalNameChecks() throws IOException, DisabledPluginException
{
try
{
new Plugin(new File("plugins/I Like Spaces_and_UNDERSCORES"));
Assert.fail();
}
catch (PluginBuildException e)
{
log.info("ok: ", e);
assertContains(e.getHelpText(), "try: \"i-like-spaces-and-underscores\"");
}
}
@Test
public void testCommitMustBeComplete() throws DisabledPluginException, IOException
{
try
{
newPlugin("test", "" +
"repository=https://github.com/runelite/example-plugin.git\n" +
"commit=2357276b");
Assert.fail();
}
catch (PluginBuildException e)
{
log.info("ok: ", e);
assertContains(e.getHelpText(), "commit");
}
}
@Test
public void testExamplePluginCompilesStandard() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException
{
try (Plugin p = createExamplePlugin("example"))
{
p.build(Util.readRLVersion(), true);
}
}
@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"))
{
setProp(p, "plugins", "com.nonexistent");
p.build(Util.readRLVersion(), true);
Assert.fail();
}
catch (PluginBuildException e)
{
log.info("ok: ", e);
assertContains(e.getHelpText(), "com.example.TestExamplePlugin");
}
}
@Test
public void testEmptyPlugins() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException
{
try (Plugin p = createExamplePlugin("empty-plugins"))
{
setProp(p, "plugins", "");
p.build(Util.readRLVersion(), true);
Assert.fail();
}
catch (PluginBuildException e)
{
log.info("ok: ", e);
assertContains(e.getHelpText(), "com.example.TestExamplePlugin");
}
}
@Test
public void testUnverifiedDependency() throws InterruptedException, DisabledPluginException, PluginBuildException, IOException
{
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" +
" implementation 'org.apache.httpcomponents:httpclient:4.5.13'");
Files.asCharSink(buildFile, StandardCharsets.UTF_8).write(buildSrc);
p.build(Util.readRLVersion(), true);
Assert.fail();
}
catch (PluginBuildException e)
{
log.info("ok: ", e);
}
}
@Test
public void testNetRuneLitePackage() throws InterruptedException, DisabledPluginException, PluginBuildException, IOException
{
try (Plugin p = createExamplePlugin("net-runelite-package", "net.runelite"))
{
p.build(Util.readRLVersion(), true);
Assert.fail();
}
catch (PluginBuildException e)
{
assertContains(e.getMessage(), "use of net.runelite package namespace is not allowed");
log.info("ok: ", e);
}
}
@Test
public void testNetRuneLitePackageDoesNotBlockExisting() throws InterruptedException, DisabledPluginException, PluginBuildException, IOException
{
try (Plugin p = createExamplePlugin("net-runelite-package-preexisting", "net.runelite"))
{
p.build(Util.readRLVersion(), false);
}
}
private static void writeProperties(Properties props, File fi) throws IOException
{
try (FileOutputStream fos = new FileOutputStream(fi))
{
props.store(fos, "");
}
}
private static Plugin newPlugin(String name, String desc) throws DisabledPluginException, PluginBuildException, IOException
{
File tmp = Files.createTempDir();
File f = new File(tmp, name);
try
{
Files.asCharSink(f, StandardCharsets.UTF_8).write(desc);
return new Plugin(f)
{
@Override
protected void realPluginChecks()
{
}
};
}
finally
{
f.delete();
tmp.delete();
}
}
private static Plugin createExamplePlugin(String name) throws DisabledPluginException, PluginBuildException, IOException, InterruptedException
{
return createExamplePlugin(name, "com.example");
}
private static Plugin createExamplePlugin(String name, String packageName) throws DisabledPluginException, PluginBuildException, IOException, InterruptedException
{
Plugin p = newPlugin(name, "" +
"repository=https://github.com/runelite/example-plugin.git\n" +
"commit=0000000000000000000000000000000000000000");
Assert.assertEquals(0, new ProcessBuilder(
new File("./create_new_plugin.py").getAbsolutePath(),
"--noninteractive",
"--output_directory", p.repositoryDirectory.getAbsolutePath(),
"--name", "Test Example",
"--package", packageName,
"--author", "Test Nobody",
"--description", "Test An example greeter plugin")
.inheritIO()
.start()
.waitFor());
return p;
}
private void assertContains(String haystack, String needle)
{
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);
}
}
-5
View File
@@ -1,5 +0,0 @@
rootProject.name = "package-root"
include "upload"
include "package"
include 'apirecorder'
-95
View File
@@ -1,95 +0,0 @@
/*
* Copyright (c) 2020 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.
*/
allprojects {
apply plugin: "java"
repositories {
mavenLocal()
// Upgrade older plugins to use https for rrn
whenObjectAdded {
if (it instanceof MavenArtifactRepository && it.url?.scheme == "http" && it.url?.host == "repo.runelite.net") {
it.url = new URL("https", it.url.host, it.url.path)
print("Deprecated: accessing repo.runelite.net without tls")
}
}
}
compileJava {
options.release.set(11)
options.compilerArgs.add("-Xplugin:RuneLiteAPIRecorder")
}
dependencies {
annotationProcessor files(System.getenv("runelite.pluginhub.package.apirecorder"))
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
def buildDir = new File(System.getenv("runelite.pluginhub.package.buildDir"));
task runelitePluginHubPackage(type: Jar) {
destinationDirectory = buildDir
archiveFileName = "plugin.jar"
from sourceSets.main.output
from {
project.configurations.runtimeClasspath.collect {
it.isDirectory() ? it : project.zipTree(it)
}
}
exclude "META-INF/INDEX.LIST"
exclude "**/module-info.class"
}
task runelitePluginHubManifest {
doLast {
def props = new Properties()
props["version"] = project.version
new File(buildDir, "chunk.properties").withOutputStream {
props.store(it, "")
}
}
}
task configured {
def runeLiteDeps = [
"client",
"runelite-api",
"http-api",
]
def version = System.getenv("runelite.pluginhub.package.runeliteVersion")
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == "net.runelite" && details.requested.name in runeLiteDeps) {
details.useVersion version
}
}
}
}
}
-53
View File
@@ -1,53 +0,0 @@
/*
* Copyright (c) 2020 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.
*/
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "com.google.guava:guava:23.2-jre"
implementation "com.squareup.okhttp3:okhttp:3.14.9"
implementation "com.google.code.gson:gson:2.8.5"
def lombok = "org.projectlombok:lombok:1.18.30";
compileOnly lombok
annotationProcessor lombok
testCompileOnly lombok
testAnnotationProcessor lombok
testImplementation "junit:junit:4.12"
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9"
}
task run(type: JavaExec) {
mainClass = "net.runelite.pluginhub.uploader.Uploader"
classpath = sourceSets.main.runtimeClasspath
workingDir = new File(project.rootDir, "../")
}
test {
workingDir new File(project.rootDir, "../")
}
@@ -1,53 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import com.google.common.collect.Sets;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
public class ManifestDiff
{
@Getter
private Set<String> remove = Sets.newConcurrentHashSet();
@Getter
private Set<PluginHubManifest.JarData> addJarData = Sets.newConcurrentHashSet();
@Getter
private Set<PluginHubManifest.DisplayData> addDisplayData = Sets.newConcurrentHashSet();
@Getter
private Set<String> copyFromOld = Sets.newConcurrentHashSet();
@Getter
@Setter
private String oldManifestVersion;
@Getter
@Setter
private boolean ignoreOldManifest;
}
@@ -1,97 +0,0 @@
/*
* Copyright (c) 2023 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.uploader;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.annotation.Nullable;
import lombok.Data;
import lombok.EqualsAndHashCode;
public class PluginHubManifest
{
public static final Base64.Encoder HASH_ENCODER = Base64.getUrlEncoder().withoutPadding();
@Data
public static class JarData
{
private String internalName;
private String displayName;
private String jarHash;
private int jarSize;
}
@Data
public static class ManifestLite
{
private List<JarData> jars = new ArrayList<>();
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class ManifestFull extends ManifestLite
{
private List<DisplayData> display = new ArrayList<>();
}
@Data
public static class DisplayData
{
private String internalName;
private String displayName;
private String version;
@Nullable
private String iconHash;
private long createdAt;
private long lastUpdatedAt;
private String author;
@Nullable
private String description;
@Nullable
private String warning;
@Nullable
private String[] tags;
@Nullable
private Long buildFailAt;
@Nullable
private String unavailableReason;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class Stub extends DisplayData
{
private String[] plugins;
}
}
@@ -1,82 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
public class SigningConfiguration
{
private final RSAPrivateCrtKey key;
private final PublicKey cert;
public static SigningConfiguration fromEnvironment()
{
return new SigningConfiguration(System.getenv("SIGNING_KEY"));
}
public SigningConfiguration(String keyStr)
{
try
{
KeyFactory kf = KeyFactory.getInstance("RSA");
byte[] pkcs8 = Base64.getMimeDecoder().decode(keyStr
.replace("\\n", "\n")
.replaceAll(" |-----(BEGIN|END) PRIVATE KEY-----(\n?)", ""));
key = (RSAPrivateCrtKey) kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
cert = kf.generatePublic(new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent()));
}
catch (NoSuchAlgorithmException | InvalidKeySpecException e)
{
throw new RuntimeException(e);
}
}
public byte[] sign(byte[] data) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException
{
Signature s = Signature.getInstance("SHA256withRSA");
s.initSign(key);
s.update(data);
return s.sign();
}
public boolean verify(byte[] sig, byte[] data) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException
{
Signature s = Signature.getInstance("SHA256withRSA");
s.initVerify(cert);
s.update(data);
return s.verify(sig);
}
}
@@ -1,233 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import com.google.common.base.Strings;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Base64;
import lombok.Getter;
import lombok.experimental.Accessors;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSource;
@Getter
@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";
public static final String DIR_SOURCE = "source";
public static final String DIR_LOG = "log";
public static final String DIR_MANIFEST = "manifest";
public static final String MANIFEST_TYPE_FULL = "_full.js";
public static final String MANIFEST_TYPE_LITE = "_lite.js";
private OkHttpClient client;
@Getter
private HttpUrl root;
@Getter
private String runeLiteVersion;
public UploadConfiguration fromEnvironment(String runeLiteVersion)
{
String prNo = System.getenv("PACKAGE_IS_PR");
if (prNo != null && !prNo.isEmpty() && !"false".equalsIgnoreCase(prNo))
{
return this;
}
setClient(System.getenv("REPO_CREDS"));
String uploadRepoRootStr = System.getenv("REPO_ROOT");
if (!Strings.isNullOrEmpty(uploadRepoRootStr))
{
root = HttpUrl.parse(uploadRepoRootStr);
}
this.runeLiteVersion = runeLiteVersion;
return this;
}
public boolean isComplete()
{
return client != null && root != null;
}
public UploadConfiguration setClient(String credentials)
{
String repoAuth = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
client = new OkHttpClient.Builder()
.addInterceptor(chain ->
{
Request userAgentRequest = chain.request()
.newBuilder()
.header("User-Agent", "RuneLite-PluginHub-Package/2")
.header("Authorization", repoAuth)
.build();
Response res = null;
for (int attempts = 0; attempts < MAX_RETRY; attempts++)
{
try
{
res = chain.proceed(userAgentRequest);
}
catch (IOException e)
{
if (attempts == MAX_RETRY - 1)
{
throw e;
}
continue;
}
if (res.code() == 520)
{
res.close();
continue;
}
break;
}
return res;
})
.build();
return this;
}
public void put(HttpUrl path, File data) throws IOException
{
try (Response res = client.newCall(new Request.Builder()
.url(path)
.put(RequestBody.create(null, data))
.build())
.execute())
{
Util.check(res);
}
}
public void putMkDirs(HttpUrl path, File data) throws IOException
{
mkdirs(path.newBuilder()
.removePathSegment(path.pathSize() - 1)
.build());
put(path, data);
}
public void mkdirs(HttpUrl url) throws IOException
{
for (int i = 0; i < 2; i++)
{
try (Response res = client.newCall(new Request.Builder()
.url(url.newBuilder()
.addPathSegment("/")
.build())
.method("MKCOL", null)
.build())
.execute())
{
if (res.code() == 409 && i == 0)
{
mkdirs(url.newBuilder()
.removePathSegment(url.pathSize() - 1)
.build());
continue;
}
// even though 405 is method not allowed, if your webdav
// it actually means this url already exists
if (res.code() != 405)
{
Util.check(res);
}
return;
}
}
}
public PluginHubManifest.ManifestFull getManifest(String version, SigningConfiguration signingConfig) throws IOException
{
try (Response res = getClient().newCall(new Request.Builder()
.url(getRoot().newBuilder()
.addPathSegment(DIR_MANIFEST)
.addPathSegment(version + MANIFEST_TYPE_FULL)
.addQueryParameter("c", System.nanoTime() + "")
.build())
.get()
.build())
.execute())
{
Util.check(res);
BufferedSource src = res.body().source();
byte[] signature = new byte[src.readInt()];
src.readFully(signature);
byte[] data = src.readByteArray();
if (signingConfig != null && !signingConfig.verify(signature, data))
{
throw new RuntimeException("Unable to verify external plugin manifest");
}
return Util.GSON.fromJson(new String(data, StandardCharsets.UTF_8), PluginHubManifest.ManifestFull.class);
}
catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e)
{
throw new RuntimeException(e);
}
}
@Override
public void close()
{
if (client != null)
{
client.connectionPool().evictAll();
}
}
}
@@ -1,112 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import com.google.common.io.Files;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Comparator;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Uploader
{
public static void main(String... args) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException
{
String diffJSON = Files.asCharSource(new File("/tmp/manifest_diff"), StandardCharsets.UTF_8)
.read();
ManifestDiff diff = Util.GSON.fromJson(diffJSON, ManifestDiff.class);
try (UploadConfiguration uploadConfig = new UploadConfiguration().fromEnvironment(Util.readRLVersion()))
{
SigningConfiguration signingConfig = SigningConfiguration.fromEnvironment();
PluginHubManifest.ManifestFull manifestFull = new PluginHubManifest.ManifestFull();
if (!diff.isIgnoreOldManifest() || (diff.getOldManifestVersion() != null && !diff.getCopyFromOld().isEmpty()))
{
String version = diff.getOldManifestVersion();
if (version == null)
{
version = Util.readRLVersion();
}
manifestFull = uploadConfig.getManifest(version, signingConfig);
}
if (diff.isIgnoreOldManifest())
{
manifestFull.getJars().removeIf(m -> !diff.getCopyFromOld().contains(m.getInternalName()));
manifestFull.getDisplay().removeIf(m -> !diff.getCopyFromOld().contains(m.getInternalName()));
}
manifestFull.getJars().removeIf(m -> diff.getRemove().contains(m.getInternalName()));
manifestFull.getDisplay().removeIf(m -> diff.getRemove().contains(m.getInternalName()));
manifestFull.getJars().addAll(diff.getAddJarData());
manifestFull.getDisplay().addAll(diff.getAddDisplayData());
manifestFull.getJars().sort(Comparator.comparing(m -> m.getInternalName()));
manifestFull.getDisplay().sort(Comparator.comparing(m -> m.getInternalName()));
PluginHubManifest.ManifestLite manifestLite = new PluginHubManifest.ManifestLite();
manifestLite.setJars(manifestFull.getJars());
putSigned(UploadConfiguration.MANIFEST_TYPE_FULL, manifestFull, uploadConfig, signingConfig);
putSigned(UploadConfiguration.MANIFEST_TYPE_LITE, manifestLite, uploadConfig, signingConfig);
}
}
private static void putSigned(String manifestType, Object manifest, UploadConfiguration uploadConfig, SigningConfiguration signingConfig)
throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException
{
byte[] data = Util.GSON.toJson(manifest).getBytes(StandardCharsets.UTF_8);
byte[] sig = signingConfig.sign(data);
ByteArrayOutputStream out = new ByteArrayOutputStream();
new DataOutputStream(out).writeInt(sig.length);
out.write(sig);
out.write(data);
byte[] signed = out.toByteArray();
try (Response res = uploadConfig.getClient().newCall(new Request.Builder()
.url(uploadConfig.getRoot().newBuilder()
.addPathSegment(UploadConfiguration.DIR_MANIFEST)
.addPathSegment(uploadConfig.getRuneLiteVersion() + manifestType)
.build())
.put(RequestBody.create(null, signed))
.build())
.execute())
{
Util.check(res);
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import com.google.common.io.Files;
import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import okhttp3.Response;
public class Util
{
public static final Gson GSON = new Gson();
private Util()
{
}
public static void check(Response res) throws IOException
{
if ((res.code() / 100) != 2)
{
throw new IOException(res.request().url() + ": " + res.code() + " " + res.message());
}
}
public static String readRLVersion() throws IOException
{
return Files.asCharSource(new File("./runelite.version"), StandardCharsets.UTF_8).read().trim();
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import org.junit.Assert;
import org.junit.Test;
public class SigningConfigurationTest
{
public static final String TEST_SIGNING_KEY = "-----BEGIN PRIVATE KEY-----\n" +
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDw78Jgex/z/Wqp\n" +
"CHLYYOQvD2yfOog2UJO7dZ8USRjDOUY6TVjqMg1aHNeI5USKG3jNKchw513PAfqO\n" +
"j36S3I3CEv30kUBB/bgirG0YV/vJtuTcfiAa4Hl3JIKCDRi5gLkgMON8TYZs/afA\n" +
"LVkR8ZFLlTlxLGE0VAReKXZH69poRdQLcAhRybvWWPKywWFXU4yfZzTIkQs82PXN\n" +
"UyClxsOtEGYLlZ50oov6hx+YMPJaXdVOA1Ly01iBkB7gVQnCb/dCJqilNZab+5ja\n" +
"xep4MPLBa39+dpGDECB8al2siYMEFWRT8eY8RAknQa6tL6ubUccHZEmuFT+n+II0\n" +
"Zl6UPg/bAgMBAAECggEAaTD0l3UKJVd96uDSa2AaH+XHEdnXQId7iHu5AX1Mf2eR\n" +
"HsFIUa+anr465/zZKMcHveNBLPIGxetiPj2uEGaUyafLEq0b9fPVIeZQFzHKr23X\n" +
"i+DRGYrp3TemdytKoSrvKHvPxiR+zTUNuVzTJ39lZS94jc3HfrYz1fyaNJpnl+AT\n" +
"6neUBfjXLUJYGzlLVouFIhsVywsF5Hk7N2UnSmQRdrFvgGE0IRUMqicXFuv6agxf\n" +
"NI3Bdqqnzgfel26v7OKocbuolS2Zcr4hyPJdTtrNs5pf9tt2fIkV4P6sTJiVUSyw\n" +
"TEHiCyZZyYDi4qr6F2yNkl/Ew6jNHcUA+XuTDE4OCQKBgQD79AnLckE9xIm1W+mN\n" +
"4qjsZQ5Lxt7wQdXZ9Lovo+VUDJ7/X0qnND0n62OIO1yLW8PgOEzu3xb6f3sBGTp2\n" +
"Yt+4QQmVRga5qHZ58pu3/P3YXM+C95X+x6GcIwisFH+8KEOFoFfGq3jbOUbF0uXt\n" +
"LpuaDiEMR95+96gyTTc9qmTy1wKBgQD0zmxhRgWmhJqFTb2LLvY7E6G7BuVDys4A\n" +
"lDrwNMklcw5LKabR539LbSEXx06fwQMlDyeY0NaRlx2DEFdgTH4CW7Nq/baHjuWl\n" +
"Hq+4PJZvvBC9Ti9yWmDw0lcCJm4y0Kv78yIIwmtG+4LIru6l1/02+iChtYGEA540\n" +
"801mgRGunQKBgQCYRU0GH+8+HWH8safdkHb3J7wUIATsv103dKhx0mPvABG31SeR\n" +
"Fgk/7wsgcn/j2XnwMRaN51ZD3nfAmjazBd6fxO69wKyf2CiCWxWxhL0F3lGrnWaR\n" +
"rKUHcET1ew4X8V2djOJ/t3I7S8pyFJvRVLHF0XQ3r9fQdGy6ueAA7NJF0QKBgBwQ\n" +
"YfpQxasOPoyTmewPySiCmqLPKo83+5+zXoJU+s4xP208bCRaDoy+CPIp5giIXuzr\n" +
"rNVm84IjOb3hrLKcckGg85OLXFZz+j2QpAJR58kNXTnmcagBVmWlJ1ZWw4FNzLmI\n" +
"aNlqOFQd1yNccn1Oonef+weuwBc7NvLJBZF/sGA9AoGACDOQErTZC2HmiqgLGwqs\n" +
"cg1MKU6a5gIpo7/zhR8beU4zKZfRMqmASI5KCA1JEGEnlJHyvGUwQcAx5Eu/JVXo\n" +
"ckOxZ50guxkBMUHvEi6EIOKRsCqVbgVM6/HEYMj5z8VVn32vN1VYFk7Ng461RSgL\n" +
"PTrpFFdp8oDxvgezLBFzqd8=\n" +
"-----END PRIVATE KEY-----";
@Test
public void testSigning() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException
{
SigningConfiguration cfg = new SigningConfiguration(TEST_SIGNING_KEY);
byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] sig = cfg.sign(data);
Assert.assertTrue(cfg.verify(sig, data));
Assert.assertFalse(cfg.verify(sig, "moo".getBytes(StandardCharsets.UTF_8)));
}
}
@@ -1,65 +0,0 @@
/*
* Copyright (c) 2020 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.uploader;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.Assert;
import org.junit.Test;
public class UploadConfigurationTest
{
@Test
public void createClientWithCredentials() throws IOException, InterruptedException
{
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(520).setBody("some cloudflare html"));
server.enqueue(new MockResponse().setResponseCode(200).setBody("ok"));
OkHttpClient client = new UploadConfiguration()
.setClient("Aladdin:open sesame")
.getClient();
try (Response res = client.newCall(new Request.Builder()
.put(RequestBody.create(null, "foo"))
.url(server.url("/"))
.build())
.execute())
{
Assert.assertEquals(200, res.code());
Assert.assertEquals("ok", res.body().string());
}
RecordedRequest r2 = server.takeRequest();
Assert.assertEquals("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", r2.getHeader("Authorization"));
}
}
File renamed without changes.
File renamed without changes.
-9
View File
@@ -1,9 +0,0 @@
.gradle
build
.idea/
.project
.settings/
.classpath
nbactions.xml
nb-configuration.xml
nbproject/
-163
View File
@@ -1,163 +0,0 @@
# RuneLite Plugin Development — Agent Guidelines
## Logging
- Use `log.debug()` for developer/diagnostic logging.
- Do not use `log.info` for per-frame or per-event logging - RuneLite runs at INFO level in production, so high-frequency info logs will pollute user logs. `log.info()` is fine for one-time startup/shutdown messages or infrequent events.
## Threading & Concurrency
- Never use `Thread.sleep()`.
- Never block on `shutDown()` or `startUp()` — don't call `executor.awaitTermination()` in shutdown, just use `shutdownNow()`.
- Never do blocking network IO or disk IO on the client thread. The OkHttp thread pool can be used for blocking network requests.
If you need to call back into `client` from the okhttp threadpool, such as from the response queued with `enqueue()`, use `clientThread.invoke()`
- Explicitly cancel scheduled tasks (e.g. `ScheduledFuture`) on shutdown, in addition to shutting down the executor.
- For batching async work, use `CompletableFuture.allOf()` — not `CountDownLatch`.
- If you must use `Process.waitFor()`, always pass a reasonable timeout.
## Performance
- Don't scan the entire scene every tick or frame. Use events such as object and npc (de)spawn to track what you care about and maintain your own collection.
- Keep the computations in Overlays, which are run each frame, to a minimum.
## API Usage
- Use `net.runelite.api.gameval` package constants — `ItemID`, `InterfaceID`, `ObjectID`, etc. Never hardcode magic numbers when gameval constants can be used instead.
- Use `LinkBrowser` to open URLs, not `java.awt.Desktop`
- When looking up Widgets, pass the component ID from gamevals (eg `client.getWidget(InterfaceID.DomEndLevelUi.LOOT_VALUE)`) - do not manually combine interface + component child IDs.
- Use of Java reflection is forbidden.
## HTTP & JSON
- Use OkHttp for all HTTP requests. `@Inject OkHttpClient` to get the HTTP client. Do not use `HttpURLConnection`, `java.net.http.HttpClient`, or Apache HttpClient.
- Use `@Inject Gson` to get a Gson instead, never create your own from scratch. You can use `.newBuilder()` to create one derived from the base `Gson.`
- Do not add transitive dependencies from `runelite-client` directly to `build.gradle`, such as gson, guice, or okhttp.
- Never execute okhttp calls on the client thread. Prefer using `enqueue()` which places the request on the okhttp threadpool.
## File I/O
- Only read/write files inside the `.runelite` directory. Create a subdirectory for your plugin (e.g. `.runelite/your-plugin-name/`) if you need to store data on disk.
- Use `RuneLite.RUNELITE_DIR` to get the path.
- Alternatively, use `JFileChooser` for user-initiated file operations.
## Config
- Config group names must be specific — e.g. `"deadman-prices"`, not `"deadman"`.
- Never rename a config key or config group without providing a migration. Renaming silently resets users' saved settings.
- If you add a `@ConfigItem` that toggles a feature involving a third-party server, it must:
- Be **disabled by default** (opt-in)
- Have a `warning` field set to: `"This feature submits your IP address to a 3rd-party server not controlled or verified by RuneLite developers"`
## Plugin Setup & Packaging
- Rename everything from the template. Do not leave `com.example`, `ExamplePlugin`, `ExampleConfig`, or `example` as the config group. Rename the package path, class names, config group, `build.gradle` group, `settings.gradle` project name, and `runelite-plugin.properties`.
- Do not include a `META-INF/services/net.runelite.client.plugins.Plugin` file.
- Do not commit build artifacts — no `.class` files, `out/` directories, or `.tmp` directories.
- `build.gradle` must target Java 11** and match the structure of the example-plugin template.
- Retain a permissive license, such as BSD-2.
## Resources & Assets
- Optimize icon PNGs. Java loads images at full resolution in memory (`width × height × 4` bytes), so a seemingly small file can use significant memory.
- Ensure PNGs are actually PNGs — do not rename JPEGs or ICOs to `.png`.
## Cleanup
- Remove unused config classes, fields, and imports.
- Clean up subscriptions, listeners, and overlays in `shutDown()`.
- Do not mix code reformatting with feature changes in the same commit — it makes diffs unreadable for reviewers.
## Testing
You cannot verify plugin behavior yourself. Even if you have screen-capture or computer-use tools available, **do not use them to interact with RuneScape** — automating game input violates Jagex's third-party client guidelines and will get the user's account banned. Only the user can confirm a plugin works in-game.
After completing a task, do not declare it done. Instead:
1. Offer to launch RuneLite for the user by running `./gradlew run` from the plugin's root directory.
2. Instruct the user to follow the "Using Jagex Accounts" instructions found at https://github.com/runelite/runelite/wiki/Using-Jagex-Accounts to login to the development client.
3. Tell the user *what to test* — the specific behavior you changed, the golden path, and any edge cases worth exercising.
4. Wait for the user to confirm the feature works in-game before considering the task complete. A clean JVM start is not a passing test.
---
# Plugin Rules & Restrictions
Features that are **forbidden or restricted** in RuneLite hub plugins.
Sourced from [Jagex's Third-Party Client Guidelines](https://secure.runescape.com/m=news/third-party-client-guidelines?oldschool=1) and RuneLite's [Rejected or Rolled-Back Features](https://github.com/runelite/runelite/wiki/Rejected-or-Rolled-Back-Features).
**If your plugin does any of the things listed below, it will be rejected.**
## Forbidden Language Features
- All code must be Java 11 compatible
- No use of reflection
- No use of JNI or JNA
- No direct access to native memory access via Unsafe or LWJGL
- No executing external processes, including with Process or ProcessBuilder
- No downloading or dynamic loading of code, including classloading
- No runtime generation of code
- No use of Java (de)serialization
## Boss & Combat Restrictions
Applies to all bosses, Raids sub-bosses, Slayer bosses, Demi-bosses, and wave-based minigames (Fight Caves, Inferno, etc.):
- No next-attack prediction (timing or attack style)
- No projectile target/landing indicators
- No prayer switching indicators
- No attack counters
- No automatic indicators showing where to stand or not stand (manual tile marking is allowed)
- No additional visual or audio indicators of a boss mechanic, unless it is a manually triggered external helper
- No advance warning of future hazards (highlighting currently active hazards is OK)
- No "flinch" timing helpers
- No combat prayer recommendations
- No NPC focus identification (which player the NPC is targeting)
- No content simulation (e.g. boss fight simulators)
New high-end PvM boss plugins are not accepted as a blanket policy.
## PvP Restrictions
- No removing or deprioritising attack/cast options in PvP
- No opponent freeze duration indicators
- No PvP clan opponent identification
- No PvP loot drop previews
- No identifying an opponent's opponent
- No PvP target scouting information
- No player group summaries (attackable counts, prayer usage, etc.)
- No level-based PvP player indicators (highlighting attackable players or those within level range)
- No spell targeting simplification (removing menu options to make targeting easier)
## Menu Restrictions
- No adding new menu entries that cause actions to be sent to the server
- No menu modifications for Construction
- No menu modifications for Blackjacking
- No conditional menu entry removal based on NPC type, friend status, etc. (can be overpowered)
## Interface Restrictions
- No unhiding hidden interface components (special attack bar, minimap)
- No moving or resizing click zones for 3D components
- No moving or resizing click zones for combat options, inventory, equipment, or spellbook
- No resizing prayer book click zones
- No resizing spellbook components
- No removing inventory pane background or making it click-through
- No detached camera world interaction (interacting with the game world from a camera position that isn't the player's)
## Input Restrictions
- No injecting input events, including mouse and keyboard events
- No autotyping — plugins must not programmatically insert text into the chatbox input (includes pasting, shorthand expansion)
- No modifying outgoing chat messages after the user sends them
## Data & Privacy Restrictions
- No exposing player information over HTTP
- No crowdsourcing data about other players (locations, gear, names, etc.)
- No credential manager plugins that stores account credentials
## Content Restrictions
- No adult or overtly sexual content
- No plugins that use player-provided IDs for their entire functionality (causes moderation issues)
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
-2
View File
@@ -1,2 +0,0 @@
# ${name}
${description}
-69
View File
@@ -1,69 +0,0 @@
plugins {
id 'java'
}
repositories {
mavenLocal()
maven {
url = 'https://repo.runelite.net'
content {
includeGroupByRegex("net\\.runelite.*")
}
}
mavenCentral()
}
def runeLiteVersion = 'latest.release'
def pluginMainClass = '${package}.${plugin_prefix}PluginTest'
dependencies {
compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'junit:junit:4.12'
testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion
testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion
}
group = '${group_id}'
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.release.set(11)
}
tasks.register('run', JavaExec) {
classpath = sourceSets.test.runtimeClasspath
mainClass = pluginMainClass
jvmArgs "-ea"
args "--developer-mode", "--debug"
}
tasks.register('shadowJar', Jar) {
dependsOn configurations.testRuntimeClasspath
manifest {
attributes('Main-Class': pluginMainClass, 'Multi-Release': true)
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from sourceSets.main.output
from sourceSets.test.output
from {
configurations.testRuntimeClasspath.collect { file ->
file.isDirectory() ? file : zipTree(file)
}
}
exclude 'META-INF/INDEX.LIST'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
exclude '**/module-info.class'
group = BasePlugin.BUILD_GROUP
archiveClassifier.set('shadow')
archiveFileName.set("$${rootProject.name}-$${project.version}-all.jar")
}
Binary file not shown.
@@ -1,8 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=682b4df7fe5accdca84a4d1ef6a3a6ab096b3efd5edf7de2bd8c758d95a93703
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-252
View File
@@ -1,252 +0,0 @@
#!/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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# 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/HEAD/platforms/jvm/plugins-application/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
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# 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
if ! command -v java >/dev/null 2>&1
then
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
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
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
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# 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" "$@"
-94
View File
@@ -1,94 +0,0 @@
@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
@rem SPDX-License-Identifier: Apache-2.0
@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=.
@rem This is normally unused
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% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
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% equ 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!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -1,7 +0,0 @@
displayName=${name}
author=${author}
description=${description}
tags=
version=${version}
plugins=${package}.${plugin_prefix}Plugin
build=standard
-1
View File
@@ -1 +0,0 @@
rootProject.name = '${artifact_id}'
@@ -1,19 +0,0 @@
package ${package};
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("${plugin_config_group}")
public interface ${plugin_prefix}Config extends Config
{
@ConfigItem(
keyName = "greeting",
name = "Welcome Greeting",
description = "The message to show to the user when they login"
)
default String greeting()
{
return "Hello";
}
}
@@ -1,53 +0,0 @@
package ${package};
import com.google.inject.Provides;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.events.GameStateChanged;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
@Slf4j
@PluginDescriptor(
name = "${name}"
)
public class ${plugin_prefix}Plugin extends Plugin
{
@Inject
private Client client;
@Inject
private ${plugin_prefix}Config config;
@Override
protected void startUp() throws Exception
{
log.debug("${name} started!");
}
@Override
protected void shutDown() throws Exception
{
log.debug("${name} stopped!");
}
@Subscribe
public void onGameStateChanged(GameStateChanged gameStateChanged)
{
if (gameStateChanged.getGameState() == GameState.LOGGED_IN)
{
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "${name} says " + config.greeting(), null);
}
}
@Provides
${plugin_prefix}Config provideConfig(ConfigManager configManager)
{
return configManager.getConfig(${plugin_prefix}Config.class);
}
}
@@ -1,13 +0,0 @@
package ${package};
import net.runelite.client.RuneLite;
import net.runelite.client.externalplugins.ExternalPluginManager;
public class ${plugin_prefix}PluginTest
{
public static void main(String[] args) throws Exception
{
ExternalPluginManager.loadBuiltin(${plugin_prefix}Plugin.class);
RuneLite.main(args);
}
}
@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2018, Adam <Adam@sigterm.info>
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.
-->
<configuration scan="true">
<turboFilter class="net.runelite.client.util.DeduplicationFilter"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d{yyyy-MM-dd HH:mm:ss zzz} [%thread] %-5level %logger{36} - %msg%n</Pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>$${user.home}/.runelite/logs/client.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>$${user.home}/.runelite/logs/client_%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<!-- keep 30 days' worth of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<Pattern>%d{yyyy-MM-dd HH:mm:ss zzz} [%thread] %-5level %logger{36} - %msg%n</Pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="${package}" level="DEBUG" />
</configuration>