mirror of
https://github.com/runelite/plugin-hub.git
synced 2025-12-23 22:48:49 -05:00
package: avoid rebuilding plugins with compatible apis
we have to do this as part of a compiler plugin, and not by just looking at the classes' bytecode, because jls§13.1 requires inlining of certain constant values, so references to these will not be present in the bytecode.
This commit is contained in:
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -7,8 +7,13 @@ on:
|
||||
COMMIT_RANGE:
|
||||
description: "Commit range to build 1234abc..5689def"
|
||||
required: false
|
||||
API_FILES_VERSION:
|
||||
description: "RuneLite version to use .api files from"
|
||||
required: false
|
||||
push:
|
||||
pull_request:
|
||||
env:
|
||||
CACHE_VERSION: 2.1.0
|
||||
jobs:
|
||||
build:
|
||||
# any forks that predate this repo having an action will have actions
|
||||
@@ -29,7 +34,9 @@ jobs:
|
||||
path: |
|
||||
~/.gradle/caches/
|
||||
~/.gradle/wrapper/
|
||||
key: package-2.0.10
|
||||
key: package-${{ env.CACHE_VERSION }}-${{ hashFiles('runelite.version') }}
|
||||
restore-keys: |
|
||||
package-${{ env.CACHE_VERSION }}-
|
||||
- name: prepare
|
||||
run: |
|
||||
pushd package
|
||||
@@ -43,6 +50,7 @@ jobs:
|
||||
# workflow_dispatch
|
||||
FORCE_BUILD: ${{ github.event.inputs.FORCE_BUILD }}
|
||||
COMMIT_RANGE: ${{ github.event.inputs.COMMIT_RANGE }}
|
||||
API_FILES_VERSION: ${{ github.event.inputs.API_FILES_VERSION }}
|
||||
# push
|
||||
COMMIT_BEFORE: ${{ github.event.before }}
|
||||
COMMIT_AFTER: ${{ github.event.after }}
|
||||
@@ -68,6 +76,7 @@ jobs:
|
||||
upload:
|
||||
if: (github.event_name != 'push' || github.repository_owner == 'runelite') && github.event_name != 'pull_request'
|
||||
needs: build
|
||||
concurrency: upload
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -82,7 +91,7 @@ jobs:
|
||||
path: |
|
||||
~/.gradle/caches/
|
||||
~/.gradle/wrapper/
|
||||
key: upload-2.0.10
|
||||
key: upload-${{ env.CACHE_VERSION }}
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: manifest_diff
|
||||
|
||||
69
package/apirecorder/build.gradle
Normal file
69
package/apirecorder/build.gradle
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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:7.0"
|
||||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
|
||||
def lombok = "org.projectlombok:lombok:1.18.4"
|
||||
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"]
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.HashSet;
|
||||
import java.util.Set;
|
||||
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 org.objectweb.asm.Opcodes;
|
||||
|
||||
@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 new API(new BufferedReader(new InputStreamReader(new InflaterInputStream(is), StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.filter(line -> !line.isEmpty())
|
||||
.collect(ImmutableSet.toImmutableSet()));
|
||||
}
|
||||
|
||||
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 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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* 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.ImportTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.PackageTree;
|
||||
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.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.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
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;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean partial;
|
||||
|
||||
public RecordingTreeScanner(JavacTask task)
|
||||
{
|
||||
this.trees = Trees.instance(task);
|
||||
this.elements = task.getElements();
|
||||
this.types = task.getTypes();
|
||||
}
|
||||
|
||||
@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()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitMemberSelect(MemberSelectTree node, Void unused)
|
||||
{
|
||||
recordElement(trees.getElement(getCurrentPath()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitMethodInvocation(MethodInvocationTree node, Void unused)
|
||||
{
|
||||
recordElement(trees.getElement(getCurrentPath()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private boolean shouldRecord(Element element)
|
||||
{
|
||||
if (element instanceof TypeElement)
|
||||
{
|
||||
// there isn't a particularly nice way to find where a symbol is resolved from in the public api
|
||||
JavaFileObject classfile = ((Symbol.ClassSymbol) element).classfile;
|
||||
if (classfile == null || classfile.getKind() == JavaFileObject.Kind.SOURCE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TypeMirror tm = element.asType();
|
||||
if (tm instanceof DeclaredType)
|
||||
{
|
||||
Element e = ((DeclaredType) tm).asElement();
|
||||
String fqn = elements.getBinaryName((TypeElement) e).toString();
|
||||
return jvmClassCache.computeIfAbsent(fqn, name ->
|
||||
ClassLoader.getPlatformClassLoader()
|
||||
.getResource(fqn.replace('.', '/') + ".class") == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
unexpected(tm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (element.getEnclosingElement() != null && element.getEnclosingElement() != element)
|
||||
{
|
||||
return shouldRecord(element.getEnclosingElement());
|
||||
}
|
||||
else
|
||||
{
|
||||
unexpected(element);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void recordElement(Element element)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
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 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("{}", PrintingScanner.print(p.getLeaf()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
net.runelite.pluginhub.apirecorder.RecorderPlugin
|
||||
@@ -40,7 +40,7 @@ subprojects {
|
||||
apply plugin: "java"
|
||||
apply plugin: "com.github.johnrengelman.shadow"
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
sourceCompatibility = 11
|
||||
|
||||
shadowJar {
|
||||
archiveFileName.set archiveBaseName.get() + "." + archiveExtension.get()
|
||||
@@ -52,6 +52,7 @@ subprojects {
|
||||
task prep {
|
||||
dependsOn ":package:shadowJar"
|
||||
dependsOn ":initLib:shadowJar"
|
||||
dependsOn ":apirecorder:shadowJar"
|
||||
doLast {
|
||||
file("build").mkdirs()
|
||||
file("build/gradleHome").write(gradle.gradleHomeDir.absolutePath)
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies {
|
||||
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.4";
|
||||
compileOnly lombok
|
||||
@@ -63,5 +64,6 @@ jar {
|
||||
|
||||
test {
|
||||
dependsOn ":initLib:shadowJar"
|
||||
dependsOn ":apirecorder:shadowJar"
|
||||
workingDir new File(project.rootDir, "../")
|
||||
}
|
||||
@@ -28,13 +28,16 @@ import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Queues;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.Gson;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayDeque;
|
||||
@@ -51,7 +54,9 @@ import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.runelite.pluginhub.apirecorder.API;
|
||||
import net.runelite.pluginhub.uploader.ManifestDiff;
|
||||
import net.runelite.pluginhub.uploader.UploadConfiguration;
|
||||
import net.runelite.pluginhub.uploader.Util;
|
||||
@@ -64,6 +69,7 @@ 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 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);
|
||||
@@ -73,6 +79,11 @@ public class Packager implements Closeable
|
||||
@Getter
|
||||
private final String runeliteVersion;
|
||||
|
||||
@Setter
|
||||
private String apiFilesVersion;
|
||||
|
||||
private API previousApi;
|
||||
|
||||
@Getter
|
||||
private final UploadConfiguration uploadConfig = new UploadConfiguration();
|
||||
|
||||
@@ -98,6 +109,11 @@ public class Packager implements Closeable
|
||||
|
||||
public void buildPlugins() throws IOException
|
||||
{
|
||||
if (apiFilesVersion != null)
|
||||
{
|
||||
loadApi();
|
||||
}
|
||||
|
||||
Queue<File> buildQueue = Queues.synchronizedQueue(new ArrayDeque<>(buildList));
|
||||
List<Thread> buildThreads = IntStream.range(0, 8)
|
||||
.mapToObj(v ->
|
||||
@@ -148,6 +164,18 @@ public class Packager implements Closeable
|
||||
{
|
||||
try
|
||||
{
|
||||
if (apiFilesVersion != null && previousApi != null)
|
||||
{
|
||||
try (Closeable ignored = acquireAPICheck(p))
|
||||
{
|
||||
if (!p.rebuildNeeded(uploadConfig, apiFilesVersion, previousApi))
|
||||
{
|
||||
diff.getCopyFromOld().add(p.getInternalName());
|
||||
diff.getRemove().remove(p.getInternalName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
try (Closeable ignored = acquireDownload(p))
|
||||
{
|
||||
p.download();
|
||||
@@ -157,6 +185,7 @@ public class Packager implements Closeable
|
||||
p.build(runeliteVersion);
|
||||
p.assembleManifest();
|
||||
}
|
||||
String logURL = "";
|
||||
if (uploadConfig.isComplete())
|
||||
{
|
||||
try (Closeable ignored = acquireUpload(p))
|
||||
@@ -165,11 +194,16 @@ public class Packager implements Closeable
|
||||
}
|
||||
|
||||
// outside the semaphore so the timing gets uploaded too
|
||||
p.uploadLog(uploadConfig);
|
||||
logURL = p.uploadLog(uploadConfig);
|
||||
}
|
||||
|
||||
diff.getAdd().add(p.getManifest());
|
||||
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)
|
||||
{
|
||||
@@ -228,11 +262,37 @@ public class Packager implements Closeable
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void loadApi() throws IOException
|
||||
{
|
||||
diff.setOldManifestVersion(apiFilesVersion);
|
||||
|
||||
Process gradleApi = new ProcessBuilder(new File(PACKAGE_ROOT, "gradlew").getAbsolutePath(), "--console=plain", ":apirecorder:api")
|
||||
.directory(PACKAGE_ROOT)
|
||||
.inheritIO()
|
||||
.start();
|
||||
gradleApi.waitFor(2, TimeUnit.MINUTES);
|
||||
if (gradleApi.exitValue() != 0)
|
||||
{
|
||||
throw new RuntimeException("gradle :apirecorder:api exited with " + gradleApi.exitValue());
|
||||
}
|
||||
|
||||
try (InputStream is = new FileInputStream(new File(PACKAGE_ROOT, "apirecorder/build/api")))
|
||||
{
|
||||
previousApi = API.decode(is);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -284,6 +344,19 @@ public class Packager implements Closeable
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -299,17 +372,17 @@ public class Packager implements Closeable
|
||||
else if (!Strings.isNullOrEmpty(System.getenv("FORCE_BUILD")))
|
||||
{
|
||||
buildList = StreamSupport.stream(
|
||||
Splitter.on(',')
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.split(System.getenv("FORCE_BUILD"))
|
||||
.spliterator(), false)
|
||||
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(System.getenv("PACKAGE_COMMIT_RANGE")))
|
||||
else if (!Strings.isNullOrEmpty(range))
|
||||
{
|
||||
Process gitdiff = new ProcessBuilder("git", "diff", "--name-only", System.getenv("PACKAGE_COMMIT_RANGE"))
|
||||
Process gitdiff = new ProcessBuilder("git", "diff", "--name-only", range)
|
||||
.redirectError(ProcessBuilder.Redirect.INHERIT)
|
||||
.start();
|
||||
|
||||
@@ -353,6 +426,20 @@ public class Packager implements Closeable
|
||||
{
|
||||
isBuildingAll = true;
|
||||
buildList = listAllPlugins();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
gitdiff.waitFor(1, TimeUnit.SECONDS);
|
||||
@@ -372,6 +459,10 @@ public class Packager implements Closeable
|
||||
pkg.getUploadConfig().fromEnvironment(pkg.getRuneliteVersion());
|
||||
pkg.setAlwaysPrintLog(!pkg.getUploadConfig().isComplete());
|
||||
pkg.setIgnoreOldManifest(isBuildingAll);
|
||||
if (!pkg.getRuneliteVersion().equals(apiFilesVersion))
|
||||
{
|
||||
pkg.setApiFilesVersion(apiFilesVersion);
|
||||
}
|
||||
pkg.buildPlugins();
|
||||
failed = pkg.isFailed();
|
||||
if (isBuildingAll)
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.google.common.io.CountingOutputStream;
|
||||
import com.google.common.io.MoreFiles;
|
||||
import com.google.common.io.RecursiveDeleteOption;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.CharArrayWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
@@ -45,6 +46,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@@ -71,6 +73,7 @@ import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -79,9 +82,14 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
import net.runelite.pluginhub.apirecorder.API;
|
||||
import net.runelite.pluginhub.apirecorder.ClassRecorder;
|
||||
import net.runelite.pluginhub.uploader.ExternalPluginManifest;
|
||||
import net.runelite.pluginhub.uploader.UploadConfiguration;
|
||||
import net.runelite.pluginhub.uploader.Util;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.gradle.tooling.CancellationTokenSource;
|
||||
import org.gradle.tooling.GradleConnectionException;
|
||||
import org.gradle.tooling.GradleConnector;
|
||||
@@ -104,6 +112,11 @@ public class Plugin implements Closeable
|
||||
private static final Pattern REPOSITORY_TEST = Pattern.compile("^(https://github\\.com/.*)\\.git$");
|
||||
private static final Pattern COMMIT_TEST = Pattern.compile("^[a-fA-F0-9]{40}$");
|
||||
|
||||
private static final String SUFFIX_JAR = ".jar";
|
||||
private static final String SUFFIX_SOURCES = "-sources.zip";
|
||||
private static final String SUFFIX_API = ".api";
|
||||
private static final String SUFFIX_ICON = ".png";
|
||||
|
||||
private static final File TMP_ROOT;
|
||||
private static final File GRADLE_HOME;
|
||||
|
||||
@@ -142,6 +155,9 @@ public class Plugin implements Closeable
|
||||
private final File srcZipFile;
|
||||
private final File iconFile;
|
||||
|
||||
@Getter
|
||||
private final File apiFile;
|
||||
|
||||
@Getter
|
||||
private final File logFile;
|
||||
|
||||
@@ -239,6 +255,7 @@ public class Plugin implements Closeable
|
||||
logFile = new File(buildDirectory, "log");
|
||||
log = new FileOutputStream(logFile, true);
|
||||
jarFile = new File(buildDirectory, "plugin.jar");
|
||||
apiFile = new File(buildDirectory, "api");
|
||||
srcZipFile = new File(buildDirectory, "source.zip");
|
||||
iconFile = new File(repositoryDirectory, "icon.png");
|
||||
}
|
||||
@@ -264,6 +281,60 @@ public class Plugin implements Closeable
|
||||
}
|
||||
}
|
||||
|
||||
public boolean rebuildNeeded(UploadConfiguration uploadConfig, String previousVersion, API currentApi) throws IOException
|
||||
{
|
||||
if (previousVersion == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpUrl oldPluginRoot = uploadConfig.getVersionlessRoot().newBuilder()
|
||||
.addPathSegment(previousVersion)
|
||||
.addPathSegment(internalName)
|
||||
.build();
|
||||
|
||||
try (Response res = uploadConfig.getClient().newCall(new Request.Builder()
|
||||
.url(oldPluginRoot.newBuilder()
|
||||
.addPathSegment(commit + SUFFIX_API)
|
||||
.build())
|
||||
.get()
|
||||
.build()).execute())
|
||||
{
|
||||
if (res.code() == 404)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Util.check(res);
|
||||
|
||||
String missing = API.decode(res.body().byteStream())
|
||||
.missingFrom(currentApi)
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
if (!missing.isEmpty())
|
||||
{
|
||||
writeLog("API changed; rebuild needed. changed:\n{}\n", missing);
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpUrl pluginRoot = uploadConfig.getUploadRepoRoot().newBuilder()
|
||||
.addPathSegment(internalName)
|
||||
.build();
|
||||
|
||||
uploadConfig.mkdirs(pluginRoot);
|
||||
uploadConfig.copy(oldPluginRoot, pluginRoot, commit + SUFFIX_JAR, true);
|
||||
uploadConfig.copy(oldPluginRoot, pluginRoot, commit + SUFFIX_API, true);
|
||||
uploadConfig.copy(oldPluginRoot, pluginRoot, commit + SUFFIX_SOURCES, true);
|
||||
uploadConfig.copy(oldPluginRoot, pluginRoot, commit + SUFFIX_ICON, false);
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (UncheckedIOException | IOException e)
|
||||
{
|
||||
writeLog("failed to check api compatibility\n", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void download() throws IOException, PluginBuildException
|
||||
{
|
||||
Process gitclone = new ProcessBuilder("git", "clone",
|
||||
@@ -399,6 +470,7 @@ public class Plugin implements Closeable
|
||||
"--init-script", new File("./package/target_init.gradle").getAbsolutePath())
|
||||
.setEnvironmentVariables(ImmutableMap.of(
|
||||
"runelite.pluginhub.package.lib", new File(Packager.PACKAGE_ROOT, "initLib/build/libs/initLib.jar").toString(),
|
||||
"runelite.pluginhub.package.apirecorder", new File(Packager.PACKAGE_ROOT, "apirecorder/build/libs/apirecorder.jar").toString(),
|
||||
"runelite.pluginhub.package.buildDir", buildDirectory.getAbsolutePath(),
|
||||
"runelite.pluginhub.package.runeliteVersion", runeliteVersion))
|
||||
.setJvmArguments("-Xmx768M", "-XX:+UseParallelGC")
|
||||
@@ -406,7 +478,7 @@ public class Plugin implements Closeable
|
||||
.setStandardError(log)
|
||||
.forTasks("runelitePluginHubPackage", "runelitePluginHubManifest")
|
||||
.withCancellationToken(cancel.token())
|
||||
.run(new ResultHandler<Void>()
|
||||
.run(new ResultHandler<>()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Void result)
|
||||
@@ -520,6 +592,8 @@ public class Plugin implements Closeable
|
||||
Set<String> pluginClasses = new HashSet<>();
|
||||
Set<String> jarClasses = new HashSet<>();
|
||||
{
|
||||
ClassRecorder builtinApi = new ClassRecorder();
|
||||
|
||||
try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile)))
|
||||
{
|
||||
for (JarEntry je; (je = jis.getNextJarEntry()) != null; )
|
||||
@@ -531,7 +605,7 @@ public class Plugin implements Closeable
|
||||
}
|
||||
|
||||
byte[] classData = ByteStreams.toByteArray(jis);
|
||||
new ClassReader(classData).accept(new ClassVisitor(Opcodes.ASM7)
|
||||
new ClassReader(classData).accept(new ClassVisitor(Opcodes.ASM7, builtinApi)
|
||||
{
|
||||
boolean extendsPlugin;
|
||||
String name;
|
||||
@@ -564,9 +638,20 @@ public class Plugin implements Closeable
|
||||
|
||||
return null;
|
||||
}
|
||||
}, ClassReader.SKIP_FRAMES);
|
||||
}, ClassReader.SKIP_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
if (apiFile.exists())
|
||||
{
|
||||
// we can record api symbols from the plugin's own dependencies, we need to strip those
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (FileInputStream fis = new FileInputStream(apiFile))
|
||||
{
|
||||
API.encode(out, API.decode(fis).missingFrom(builtinApi.getApi()));
|
||||
}
|
||||
Files.write(apiFile.toPath(), out.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -677,7 +762,7 @@ public class Plugin implements Closeable
|
||||
unusedPlugins.removeAll(plugins);
|
||||
|
||||
throw PluginBuildException.of(this,
|
||||
"Plugin class \"{}\" is missing from the output jar", className)
|
||||
"Plugin class \"{}\" is missing from the output jar", className)
|
||||
.withHelp(unusedPlugins.isEmpty()
|
||||
? "All plugins must extend Plugin an have an @PluginDescriptor"
|
||||
: ("Perhaps you wanted " + String.join(", ", unusedPlugins)))
|
||||
@@ -741,17 +826,24 @@ public class Plugin implements Closeable
|
||||
.build();
|
||||
|
||||
uploadConfig.put(
|
||||
pluginRoot.newBuilder().addPathSegment(commit + ".jar").build(),
|
||||
pluginRoot.newBuilder().addPathSegment(commit + SUFFIX_JAR).build(),
|
||||
jarFile);
|
||||
|
||||
if (apiFile.exists())
|
||||
{
|
||||
uploadConfig.put(
|
||||
pluginRoot.newBuilder().addPathSegment(commit + SUFFIX_API).build(),
|
||||
apiFile);
|
||||
}
|
||||
|
||||
uploadConfig.put(
|
||||
pluginRoot.newBuilder().addPathSegment(commit + "-sources.zip").build(),
|
||||
pluginRoot.newBuilder().addPathSegment(commit + SUFFIX_SOURCES).build(),
|
||||
srcZipFile);
|
||||
|
||||
if (manifest.isHasIcon())
|
||||
{
|
||||
uploadConfig.put(
|
||||
pluginRoot.newBuilder().addPathSegment(commit + ".png").build(),
|
||||
pluginRoot.newBuilder().addPathSegment(commit + SUFFIX_ICON).build(),
|
||||
iconFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ rootProject.name = "package-root"
|
||||
|
||||
include "initLib"
|
||||
include "upload"
|
||||
include "package"
|
||||
include "package"
|
||||
include 'apirecorder'
|
||||
@@ -37,6 +37,11 @@ allprojects {
|
||||
|
||||
compileJava {
|
||||
options.release.set(8)
|
||||
options.compilerArgs.add("-Xplugin:RuneLiteAPIRecorder")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
annotationProcessor files(System.getenv("runelite.pluginhub.package.apirecorder"))
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
|
||||
@@ -37,6 +37,13 @@ public class ManifestDiff
|
||||
@Getter
|
||||
private Set<ExternalPluginManifest> add = Sets.newConcurrentHashSet();
|
||||
|
||||
@Getter
|
||||
private Set<String> copyFromOld = Sets.newConcurrentHashSet();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private String oldManifestVersion;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean ignoreOldManifest;
|
||||
|
||||
@@ -31,7 +31,6 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -45,7 +44,9 @@ public class UploadConfiguration implements Closeable
|
||||
{
|
||||
private OkHttpClient client;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private HttpUrl versionlessRoot;
|
||||
|
||||
private HttpUrl uploadRepoRoot;
|
||||
|
||||
public UploadConfiguration fromEnvironment(String runeliteVersion)
|
||||
@@ -61,7 +62,8 @@ public class UploadConfiguration implements Closeable
|
||||
String uploadRepoRootStr = System.getenv("REPO_ROOT");
|
||||
if (!Strings.isNullOrEmpty(uploadRepoRootStr))
|
||||
{
|
||||
uploadRepoRoot = HttpUrl.parse(uploadRepoRootStr)
|
||||
versionlessRoot = HttpUrl.parse(uploadRepoRootStr);
|
||||
uploadRepoRoot = versionlessRoot
|
||||
.newBuilder()
|
||||
.addPathSegment(runeliteVersion)
|
||||
.build();
|
||||
@@ -110,15 +112,71 @@ public class UploadConfiguration implements Closeable
|
||||
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())
|
||||
.url(path)
|
||||
.put(RequestBody.create(null, data))
|
||||
.build())
|
||||
.execute())
|
||||
{
|
||||
Util.check(res);
|
||||
}
|
||||
}
|
||||
|
||||
public void copy(HttpUrl from, HttpUrl to, String resource, boolean mustExist) throws IOException
|
||||
{
|
||||
if (from.equals(to))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try (Response res = client.newCall(new Request.Builder()
|
||||
.url(from.newBuilder().addPathSegment(resource).build())
|
||||
.method("COPY", null)
|
||||
.header("Destination", to.newBuilder().addPathSegment(resource).build().toString())
|
||||
.build())
|
||||
.execute())
|
||||
{
|
||||
if (!mustExist && res.code() == 404)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Util.check(res);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
@@ -46,6 +45,8 @@ import okio.BufferedSource;
|
||||
|
||||
public class Uploader
|
||||
{
|
||||
private static final String MANIFEST_NAME = "manifest.js";
|
||||
|
||||
public static void main(String... args) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException
|
||||
{
|
||||
Gson gson = new Gson();
|
||||
@@ -58,15 +59,18 @@ public class Uploader
|
||||
{
|
||||
SigningConfiguration signingConfig = SigningConfiguration.fromEnvironment();
|
||||
|
||||
HttpUrl manifestURL = uploadConfig.getUploadRepoRoot().newBuilder()
|
||||
.addPathSegment("manifest.js")
|
||||
.build();
|
||||
|
||||
List<ExternalPluginManifest> manifests = new ArrayList<>();
|
||||
if (!diff.isIgnoreOldManifest())
|
||||
if (!diff.isIgnoreOldManifest() || (diff.getOldManifestVersion() != null && !diff.getCopyFromOld().isEmpty()))
|
||||
{
|
||||
String version = diff.getOldManifestVersion();
|
||||
if (version == null)
|
||||
{
|
||||
version = Util.readRLVersion();
|
||||
}
|
||||
try (Response res = uploadConfig.getClient().newCall(new Request.Builder()
|
||||
.url(manifestURL.newBuilder()
|
||||
.url(uploadConfig.getVersionlessRoot().newBuilder()
|
||||
.addPathSegment(version)
|
||||
.addPathSegment(MANIFEST_NAME)
|
||||
.addQueryParameter("c", System.nanoTime() + "")
|
||||
.build())
|
||||
.get()
|
||||
@@ -96,6 +100,11 @@ public class Uploader
|
||||
}
|
||||
}
|
||||
|
||||
if (diff.isIgnoreOldManifest())
|
||||
{
|
||||
manifests.removeIf(m -> !diff.getCopyFromOld().contains(m.getInternalName()));
|
||||
}
|
||||
|
||||
manifests.removeIf(m -> diff.getRemove().contains(m.getInternalName()));
|
||||
manifests.addAll(diff.getAdd());
|
||||
manifests.sort(Comparator.comparing(ExternalPluginManifest::getInternalName));
|
||||
@@ -111,7 +120,9 @@ public class Uploader
|
||||
byte[] manifest = out.toByteArray();
|
||||
|
||||
try (Response res = uploadConfig.getClient().newCall(new Request.Builder()
|
||||
.url(manifestURL)
|
||||
.url(uploadConfig.getUploadRepoRoot().newBuilder()
|
||||
.addPathSegment(MANIFEST_NAME)
|
||||
.build())
|
||||
.put(RequestBody.create(null, manifest))
|
||||
.build())
|
||||
.execute())
|
||||
|
||||
Reference in New Issue
Block a user