diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..013cc3305 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,58 @@ +on: + workflow_dispatch: + inputs: + FORCE_BUILD: + description: "List of plugins to build, or 'ALL'" + required: false + COMMIT_RANGE: + description: "Commit range to build 1234abc..5689def" + required: false + push: + pull_request: +jobs: + execute: + # 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-20.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches/ + ~/.gradle/wrapper/ + key: package-2.0.0 + - name: prepare + run: | + pushd package + ./gradlew --build-cache prep + popd + - name: build + env: + REPO_CREDS: ${{ secrets.REPO_CREDS }} + REPO_ROOT: ${{ secrets.REPO_ROOT }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + # workflow_dispatch + FORCE_BUILD: ${{ github.event.inputs.FORCE_BUILD }} + COMMIT_RANGE: ${{ github.event.inputs.COMMIT_RANGE }} + # push + COMMIT_BEFORE: ${{ github.event.before }} + COMMIT_AFTER: ${{ github.event.after }} + # pull_request + PR_BEFORE: ${{ github.event.pull_request.base.sha }} + PR_AFTER: ${{ github.event.pull_request.head.sha }} + PACKAGE_IS_PR: ${{ github.event_name == 'pull_request' }} + run: | + if $PACKAGE_IS_PR; then + export PACKAGE_COMMIT_RANGE="$PR_BEFORE..$PR_AFTER" + else + export PACKAGE_COMMIT_RANGE="${COMMIT_RANGE:-${COMMIT_BEFORE:+$COMMIT_BEFORE..$COMMIT_AFTER}}" + fi + java -XX:+UseParallelGC -jar package/package/build/libs/package.jar \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6fa6790a4..f2a313c27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ -language: java os: linux -dist: xenial +virt: lxd +dist: focal +language: generic cache: directories: - - $HOME/.m2 + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ jdk: - - openjdk8 + - openjdk11 install: true -script: ./travis.sh \ No newline at end of file +script: ./package/travis.sh \ No newline at end of file diff --git a/build_manifest.sh b/build_manifest.sh deleted file mode 100755 index a9b487533..000000000 --- a/build_manifest.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash - -# 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. - -source repo_config.sh - -set -e -x - -[[ "${TRAVIS_PULL_REQUEST:-false}" == "false" ]] || exit 0 - -RUNELITE_VERSION="$(cat "runelite.version")" - -MANIFEST="$(mktemp /tmp/manifest.XXXXXXXX)" -trap "rm -rf ""$MANIFEST*""" EXIT -MANIFEST_DIR="$MANIFEST.sub/" -mkdir "$MANIFEST_DIR" - -MANIFEST_CHUNK_DOWNLOAD=() - -for PLUGINFILE in plugins/*; do - # read in the plugin descriptor - disabled= - # shellcheck disable=SC2162 - while read LINE || [[ -n "$LINE" ]]; do - [[ $LINE =~ ^(repository|commit|disabled|warning)=(.*)$ ]] - eval "${BASH_REMATCH[1]}=\"${BASH_REMATCH[2]}\"" - done < "$PLUGINFILE" - [ -z "$disabled" ] || continue - - PLUGIN_ID=$(basename "$PLUGINFILE") - LOCATION="$REPO_ROOT/$RUNELITE_VERSION/$PLUGIN_ID/$commit" - MANIFEST_CHUNK_DOWNLOAD+=('--output' "$MANIFEST_DIR/$PLUGIN_ID" "$LOCATION.manifest") -done - -curl --fail --retry 5 \ - "${MANIFEST_CHUNK_DOWNLOAD[@]}" || true - -IS_FIRST=true -echo "[" > "$MANIFEST" -for MANIFEST_CHUNK in "$MANIFEST_DIR"/*; do - if [[ "$IS_FIRST" != true ]]; then - echo "," >> "$MANIFEST" - fi - IS_FIRST= - cat "$MANIFEST_CHUNK" >> "$MANIFEST" -done -echo "]" >> "$MANIFEST" - -# shellcheck disable=SC2059 -openssl dgst -sha256 -sign <(set +x; printf -- "$SIGNING_KEY") -out "$MANIFEST.sig" "$MANIFEST" - -perl -e "print pack('N', -s \"$MANIFEST.sig\")" > "$MANIFEST.out" -cat "$MANIFEST.sig" >> "$MANIFEST.out" -cat "$MANIFEST" >> "$MANIFEST.out" - -curl --fail --retry 5 \ - --user "$REPO_CREDS" \ - --upload-file "$MANIFEST.out" "$REPO_ROOT/$RUNELITE_VERSION/manifest.js" - -echo "Build Success" \ No newline at end of file diff --git a/build_plugin.sh b/build_plugin.sh deleted file mode 100755 index 73c7729f2..000000000 --- a/build_plugin.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/bash - -# 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. - -source repo_config.sh - -set -e -x - -PLUGINFILE="$1" -[ -s "$PLUGINFILE" ] -PLUGIN_ID=$(basename "$PLUGINFILE") - -# check valid plugin id -[[ $PLUGIN_ID =~ ^[a-z0-9-]+$ ]] - -SCRIPT_HOME="$(cd "$(dirname "$0")" ; pwd -P)" - -RUNELITE_VERSION="$(cat "$SCRIPT_HOME/runelite.version")" - -# read in the plugin descriptor -disabled= -# shellcheck disable=SC2162 -while read LINE || [[ -n "$LINE" ]]; do - [[ $LINE =~ ^(repository|commit|disabled|warning)=(.*)$ ]] - eval "${BASH_REMATCH[1]}=\"${BASH_REMATCH[2]}\"" -done < "$PLUGINFILE" -[ -z "$disabled" ] || exit 0 - -# must be a https github repo -[[ $repository =~ ^https://github.com/.*\.git$ ]] - -# we must have a full 40 char sha1sum -[[ $commit =~ ^[a-fA-F0-9]{40}+$ ]] - -# we need gradle 6.2+ for dependency verification -GRADLE_VER=gradle-6.6.1 -if [[ ! -e "/tmp/$GRADLE_VER/bin/gradle" ]]; then - wget -q -O/tmp/gradle.zip "https://services.gradle.org/distributions/$GRADLE_VER-bin.zip" - echo '7873ed5287f47ca03549ab8dcb6dc877ac7f0e3d7b1eb12685161d10080910ac */tmp/gradle.zip' | shasum -a256 -c - unzip -q /tmp/gradle.zip -d /tmp/ - [[ -e "/tmp/$GRADLE_VER/bin/gradle" ]] -fi -export GRADLE_HOME="/tmp/$GRADLE_VER/" -export PATH="$GRADLE_HOME/bin:$PATH" - -BUILDDIR="$(mktemp -d /tmp/external-plugin.XXXXXXXX)" -trap "rm -rf ""$BUILDDIR""" EXIT -pushd "$BUILDDIR" - -git clone -c 'advice.detachedHead=false' "$repository" "repo" -pushd "repo" -git checkout "$commit^{commit}" - -SIGNING_KEY="" REPO_CREDS="" gradle \ - --no-build-cache \ - --parallel \ - --console=plain \ - --init-script="$SCRIPT_HOME/package.gradle" \ - -DrlpluginRuneLiteVersion="$RUNELITE_VERSION" \ - -DrlpluginOutputDirectory="$BUILDDIR" \ - -DrlpluginPluginID="$PLUGIN_ID" \ - -DrlpluginCommit="$commit" \ - -DrlpluginWarning="$warning" \ - rlpluginPackageJar rlpluginEmitManifest - -[ -s "$BUILDDIR/plugin.jar" ] -[ -s "$BUILDDIR/plugin.manifest" ] - -cat "$BUILDDIR/plugin.manifest" - -[[ "${TRAVIS_PULL_REQUEST:-false}" == "false" ]] || exit 0 - -LOCATION="$REPO_ROOT/$RUNELITE_VERSION/$PLUGIN_ID/$commit" - -ICON_UPLOAD=() -if [ -e "icon.png" ]; then - ICON_UPLOAD=("--upload-file" "icon.png" "$LOCATION.png") -fi - -curl --fail --retry 5 \ - --user "$REPO_CREDS" \ - --upload-file "$BUILDDIR/plugin.manifest" "$LOCATION.manifest" \ - --upload-file "$BUILDDIR/plugin.jar" "$LOCATION.jar" \ - "${ICON_UPLOAD[@]}" - -echo "Build Success" diff --git a/package.gradle b/package.gradle deleted file mode 100644 index 6b5bfc350..000000000 --- a/package.gradle +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import com.google.common.hash.Hashing -import com.google.common.io.Files -import com.google.gson.Gson -import java.util.jar.JarFile - -initscript { - repositories { - jcenter() - mavenCentral() - } - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:6.0.0" - classpath "com.google.code.gson:gson:2.8.5" - classpath "com.google.guava:guava:23.2-jre" - } - configurations.classpath.resolutionStrategy { - // We don't have a way to add our direct deps to the metadata file, - // so we disable it for the initscript's configuration - disableDependencyVerification() - } -} - -allprojects { - apply plugin: "java"; - apply plugin: com.github.jengelman.gradle.plugins.shadow.ShadowPlugin - - tasks.withType(AbstractArchiveTask) { - preserveFileTimestamps = false - reproducibleFileOrder = true - } - - task rlpluginPackageJar(type: ShadowJar) { - destinationDir = new File(System.properties["rlpluginOutputDirectory"]) - archiveName = "plugin.jar" - configurations = [project.configurations.runtimeClasspath] - from sourceSets.main.output - } - - task rlpluginEmitManifest { - // this doesn't have up-to-date stuff because we always do --no-build-cache - - doLast { - def manifest = new ExternalPluginManifest() - manifest.internalName = System.properties["rlpluginPluginID"] - manifest.commit = System.properties["rlpluginCommit"] - if (System.properties["rlpluginWarning"]) { - manifest.warning = System.properties["rlpluginWarning"]; - } - - def pluginJar = new File(System.properties["rlpluginOutputDirectory"], "plugin.jar"); - manifest.hash = Files.asByteSource(pluginJar) - .hash(Hashing.sha256()) - .toString() - manifest.size = pluginJar.length() - - if (manifest.size > 10 * 1024 * 1024) { - throw new RuntimeException("The output jar is ${manifest.size.intdiv(1024 * 1024)}MiB, which is above our limit of 10MiB") - } - - def props = new Properties() - new FileInputStream(file("runelite-plugin.properties")).withCloseable { is -> - props.load(is) - } - - manifest.plugins = props["plugins"].split(/[,:;]/)*.trim() - new JarFile(pluginJar).withCloseable{ jf -> - manifest.plugins.each { plugin -> - if (jf.getEntry(plugin.replaceAll(~/\./, "/") + ".class") == null) { - throw new RuntimeException("Plugin class \"" + plugin + "\" is not in the output jar") - } - } - } - - manifest.displayName = props["displayName"] - if (!manifest.displayName) { - throw new RuntimeException("Plugin must have a display name") - } - manifest.author = props["author"] - if (!manifest.author) { - throw new RuntimeException("Plugin must have an author") - } - if (props["support"]) { - manifest.support = new URL(props["support"]) - } - manifest.description = props["description"] ?: null - if (props["tags"]) { - manifest.tags = props["tags"].split(",")*.trim() - } - - manifest.version = project.version - if (!(manifest.version ==~ /^[a-zA-Z0-9.-]+$/)) { - throw new RuntimeException("Plugin version \"${manifest.version}\" is invalid"); - } - - manifest.hasIcon = file("icon.png").exists(); - - new File(System.properties["rlpluginOutputDirectory"], "plugin.manifest") - .text = new Gson().toJson(manifest); - } - } - - rlpluginEmitManifest.dependsOn rlpluginPackageJar - - task configured { - def runeLiteDeps = [ - "client", - "runelite-api", - "http-api", - ] - def version = System.properties["rlpluginRuneLiteVersion"] - configurations.all { - resolutionStrategy.eachDependency { DependencyResolveDetails details -> - if (details.requested.group == "net.runelite" && details.requested.name in runeLiteDeps) { - details.useVersion version - } - } - } - } -} - -class ExternalPluginManifest { - String internalName; - String commit; - String hash; - int size; - String[] plugins; - - String displayName; - String version; - String author; - String description; - String warning; - String[] tags; - URL support; - boolean hasIcon; -} \ No newline at end of file diff --git a/package/.gitignore b/package/.gitignore new file mode 100644 index 000000000..33ebf499f --- /dev/null +++ b/package/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +.idea/ +build +profile.svg \ No newline at end of file diff --git a/package/build.gradle b/package/build.gradle new file mode 100644 index 000000000..d53ee0b11 --- /dev/null +++ b/package/build.gradle @@ -0,0 +1,59 @@ +/* + * 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. + */ +plugins { + id "com.github.johnrengelman.shadow" version "6.1.0" +} + +allprojects { + version "2.0-SNAPSHOT" + group "net.runelite.pluginhub" + + tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true + } +} + +subprojects { + apply plugin: "java" + apply plugin: "com.github.johnrengelman.shadow" + + sourceCompatibility = 1.8 + + shadowJar { + archiveFileName.set archiveBaseName.get() + "." + archiveExtension.get() + } +} + +// 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 ":initLib:shadowJar" + doLast { + file("build").mkdirs() + file("build/gradleHome").write(gradle.gradleHomeDir.absolutePath) + } +} \ No newline at end of file diff --git a/package/gradle.properties b/package/gradle.properties new file mode 100644 index 000000000..364404348 --- /dev/null +++ b/package/gradle.properties @@ -0,0 +1,2 @@ +# this should match what Plugin uses to build with +org.gradle.jvmargs=-Xmx768M -XX:+UseParallelGC \ No newline at end of file diff --git a/package/gradle/verification-metadata.xml b/package/gradle/verification-metadata.xml new file mode 100644 index 000000000..a47840a93 --- /dev/null +++ b/package/gradle/verification-metadata.xml @@ -0,0 +1,373 @@ + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/gradle/wrapper/gradle-wrapper.jar b/package/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..87b738cbd Binary files /dev/null and b/package/gradle/wrapper/gradle-wrapper.jar differ diff --git a/package/gradle/wrapper/gradle-wrapper.properties b/package/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2035183c5 --- /dev/null +++ b/package/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5 +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/package/gradlew b/package/gradlew new file mode 100755 index 000000000..af6708ff2 --- /dev/null +++ b/package/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/package/gradlew.bat b/package/gradlew.bat new file mode 100644 index 000000000..0f8d5937c --- /dev/null +++ b/package/gradlew.bat @@ -0,0 +1,84 @@ +@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=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/package/initLib/build.gradle b/package/initLib/build.gradle new file mode 100644 index 000000000..8dc58782b --- /dev/null +++ b/package/initLib/build.gradle @@ -0,0 +1,32 @@ +/* + * 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() + gradlePluginPortal() +} + +dependencies { + implementation "com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:6.1.0" +} \ No newline at end of file diff --git a/package/package/build.gradle b/package/package/build.gradle new file mode 100644 index 000000000..5a37ad911 --- /dev/null +++ b/package/package/build.gradle @@ -0,0 +1,59 @@ +/* + * 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-local/" + } + mavenCentral() +} + +dependencies { + implementation "org.gradle:gradle-tooling-api:6.6.1" + 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.squareup.okhttp3:okhttp:3.14.9" + 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 + + testImplementation "junit:junit:4.12" + testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9" +} + +jar { + manifest { + attributes "Main-Class": "net.runelite.pluginhub.packager.Packager" + } +} + +test { + workingDir new File(project.rootDir, "../") +} \ No newline at end of file diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/DisabledPluginException.java b/package/package/src/main/java/net/runelite/pluginhub/packager/DisabledPluginException.java new file mode 100644 index 000000000..f2692d4a9 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/DisabledPluginException.java @@ -0,0 +1,39 @@ +/* + * 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; + + public DisabledPluginException(String internalName, String cause) + { + super("Plugin \"" + internalName + "\" is disabled: " + cause); + this.internalName = internalName; + } +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/ExternalPluginManifest.java b/package/package/src/main/java/net/runelite/pluginhub/packager/ExternalPluginManifest.java new file mode 100644 index 000000000..959721d19 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/ExternalPluginManifest.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +package net.runelite.pluginhub.packager; + +import java.net.URL; +import javax.annotation.Nullable; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +public class ExternalPluginManifest +{ + private String internalName; + private String commit; + private String hash; + private int size; + private String[] plugins; + + private String displayName; + private String version; + private String author; + @Nullable + private String description; + @Nullable + private String warning; + @Nullable + private String[] tags; + @EqualsAndHashCode.Exclude + private URL support; + private boolean hasIcon; +} \ No newline at end of file diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/Packager.java b/package/package/src/main/java/net/runelite/pluginhub/packager/Packager.java new file mode 100644 index 000000000..bc341ef63 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/Packager.java @@ -0,0 +1,419 @@ +/* + * 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.Queues; +import com.google.common.collect.Sets; +import com.google.common.io.Files; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +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 okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okio.BufferedSource; + +@Slf4j +public class Packager +{ + private static final File PLUGIN_ROOT = new File("./plugins"); + public static final File PACKAGE_ROOT = new File("./package/").getAbsoluteFile(); + + private Semaphore downloadSemaphore = new Semaphore(2); + private Semaphore buildSemaphore = new Semaphore(Runtime.getRuntime().availableProcessors()); + private Semaphore uploadSemaphore = new Semaphore(2); + + private final List buildList; + + @Getter + private final String runeliteVersion; + + @Getter + private final UploadConfiguration uploadConfig = new UploadConfiguration(); + + private final AtomicInteger numDone = new AtomicInteger(0); + private final int numTotal; + + @Setter + private boolean ignoreOldManifest; + + @Setter + private boolean alwaysPrintLog; + + public Packager(List buildList) throws IOException + { + this.buildList = buildList; + this.numTotal = buildList.size(); + this.runeliteVersion = readRLVersion(); + } + + Set newManifests = Sets.newConcurrentHashSet(); + Set remove = Sets.newConcurrentHashSet(); + + public void buildPlugins() + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException + { + Queue buildQueue = Queues.synchronizedQueue(new ArrayDeque<>(buildList)); + List 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); + } + } + + if (uploadConfig.isComplete()) + { + Gson gson = new Gson(); + HttpUrl manifestURL = uploadConfig.getUploadRepoRoot().newBuilder() + .addPathSegment("manifest.js") + .build(); + + List manifests = new ArrayList<>(); + if (!ignoreOldManifest) + { + try (Response res = uploadConfig.getClient().newCall(new Request.Builder() + .url(manifestURL) + .get() + .build()) + .execute()) + { + if (res.code() != 404) + { + Util.check(res); + + BufferedSource src = res.body().source(); + + byte[] signature = new byte[src.readInt()]; + src.readFully(signature); + + byte[] data = src.readByteArray(); + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(uploadConfig.getCert()); + s.update(data); + + if (!s.verify(signature)) + { + throw new RuntimeException("Unable to verify external plugin manifest"); + } + + manifests = gson.fromJson(new String(data, StandardCharsets.UTF_8), + new TypeToken>() + { + }.getType()); + } + } + } + + manifests.removeIf(m -> remove.contains(m.getInternalName())); + manifests.addAll(newManifests); + manifests.sort(Comparator.comparing(ExternalPluginManifest::getInternalName)); + + { + byte[] data = gson.toJson(manifests).getBytes(StandardCharsets.UTF_8); + Signature s = Signature.getInstance("SHA256withRSA"); + s.initSign(uploadConfig.getKey()); + s.update(data); + byte[] sig = s.sign(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new DataOutputStream(out).writeInt(sig.length); + out.write(sig); + out.write(data); + byte[] manifest = out.toByteArray(); + + try (Response res = uploadConfig.getClient().newCall(new Request.Builder() + .url(manifestURL) + .put(RequestBody.create(null, manifest)) + .build()) + .execute()) + { + Util.check(res); + } + } + + uploadConfig.getClient().connectionPool().evictAll(); + } + } + + + private void buildPlugin(File plugin) + { + remove.add(plugin.getName()); + + if (!plugin.exists()) + { + return; + } + + try (Plugin p = new Plugin(plugin)) + { + try + { + try (Closeable ignored = acquireDownload(p)) + { + p.download(); + } + try (Closeable ignored = acquireBuild(p)) + { + p.build(runeliteVersion); + p.assembleManifest(); + } + if (uploadConfig.isComplete()) + { + try (Closeable ignored = acquireUpload(p)) + { + p.upload(uploadConfig); + } + + // outside the semaphore so the timing gets uploaded too + p.uploadLog(uploadConfig); + } + + newManifests.add(p.getManifest()); + log.info("{}: done in {}ms [{}/{}]", p.getInternalName(), p.getBuildTimeMS(), numDone.get() + 1, numTotal); + } + catch (PluginBuildException e) + { + p.writeLog("package failed\n", e); + if (!alwaysPrintLog) + { + Files.asCharSource(p.getLogFile(), StandardCharsets.UTF_8).copyTo(System.out); + } + + if (uploadConfig.isComplete()) + { + p.uploadLog(uploadConfig); + } + } + finally + { + if (alwaysPrintLog) + { + Files.asCharSource(p.getLogFile(), StandardCharsets.UTF_8).copyTo(System.out); + } + } + } + catch (DisabledPluginException e) + { + log.info("{}", e.getMessage()); + } + catch (PluginBuildException e) + { + log.info("", e); + } + catch (Exception e) + { + log.warn("{}: crashed the build script: ", plugin.getName(), e); + } + finally + { + numDone.addAndGet(1); + } + } + + 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 static String readRLVersion() throws IOException + { + return Files.asCharSource(new File("./runelite.version"), StandardCharsets.UTF_8).read().trim(); + } + + + public static void main(String... args) throws Exception + { + boolean isBuildingAll = false; + List 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(); + } + 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(System.getenv("PACKAGE_COMMIT_RANGE"))) + { + Process gitdiff = new ProcessBuilder("git", "diff", "--name-only", System.getenv("PACKAGE_COMMIT_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) + { + new ProcessBuilder(new File(PACKAGE_ROOT, "gradlew").getAbsolutePath(), "--console=plain", "test") + .directory(PACKAGE_ROOT) + .inheritIO() + .start() + .waitFor(); + } + + 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"); + } + + Packager pkg = new Packager(buildList); + pkg.getUploadConfig().fromEnvironment(pkg.getRuneliteVersion()); + pkg.setAlwaysPrintLog(!pkg.getUploadConfig().isComplete()); + pkg.setIgnoreOldManifest(isBuildingAll); + pkg.buildPlugins(); + } + + static List listAllPlugins() + { + return Arrays.asList(PLUGIN_ROOT.listFiles()); + } +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java b/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java new file mode 100644 index 000000000..be102dbd8 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/Plugin.java @@ -0,0 +1,574 @@ +/* + * 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.annotations.VisibleForTesting; +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import okhttp3.HttpUrl; +import org.gradle.tooling.CancellationTokenSource; +import org.gradle.tooling.GradleConnectionException; +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ProjectConnection; +import org.gradle.tooling.ResultHandler; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; + +public class Plugin implements Closeable +{ + private static final Pattern PLUGIN_INTERNAL_NAME_TEST = Pattern.compile("^[a-z0-9-]+$"); + 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 File TMP_ROOT; + private static final File GRADLE_HOME; + + static + { + ImageIO.setUseCache(false); + + try + { + TMP_ROOT = Files.createTempDirectory("pluginhub-package").toFile(); + TMP_ROOT.deleteOnExit(); + + GRADLE_HOME = new File(com.google.common.io.Files.asCharSource(new File(Packager.PACKAGE_ROOT, "build/gradleHome"), StandardCharsets.UTF_8).read().trim()); + if (!GRADLE_HOME.exists()) + { + throw new RuntimeException("gradle home has moved"); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Getter + private final String internalName; + + private final File buildDirectory; + + @VisibleForTesting + final File repositoryDirectory; + + private final File jarFile; + private final File iconFile; + + @Getter + private final File logFile; + + @Getter + private FileOutputStream log; + + @Nullable + private final String warning; + + private final String repositoryURL; + private final String commit; + + @Getter + private final ExternalPluginManifest manifest = new ExternalPluginManifest(); + + @Getter + @Setter + private long buildTimeMS; + + public Plugin(File pluginCommitDescriptor) throws IOException, DisabledPluginException, PluginBuildException + { + internalName = pluginCommitDescriptor.getName(); + if (!PLUGIN_INTERNAL_NAME_TEST.matcher(internalName).matches()) + { + throw PluginBuildException.of(internalName, "invalid plugin file name \"{}\"", internalName) + .withHelp("plugin file names must be lowercase alphanumeric + dashes. try: \"" + + internalName.toLowerCase().replaceAll("[^a-z0-9]+", "-") + "\"") + .withFile(pluginCommitDescriptor); + } + + Properties cd = loadProperties(pluginCommitDescriptor); + + String disabled = cd.getProperty("disabled"); + if (!Strings.isNullOrEmpty(disabled)) + { + throw new DisabledPluginException(internalName, disabled); + } + + repositoryURL = (String) cd.remove("repository"); + if (repositoryURL == null) + { + throw PluginBuildException.of(internalName, "repository is missing from {}", pluginCommitDescriptor) + .withFile(pluginCommitDescriptor); + } + + if (!REPOSITORY_TEST.matcher(repositoryURL).matches()) + { + throw PluginBuildException.of(internalName, "repository is not an accepted url") + .withFileLine(pluginCommitDescriptor, "repository=" + repositoryURL) + .withHelp(() -> + { + if (!repositoryURL.startsWith("https")) + { + return "repositories must be https clone urls, not git:"; + } + if (!repositoryURL.contains("github")) + { + return "repositories must be hosted on GitHub.com"; + } + if (!repositoryURL.endsWith(".git")) + { + return "repository must be a clone url ~ it should end with .git"; + } + return null; + }); + } + + commit = (String) cd.remove("commit"); + if (!COMMIT_TEST.matcher(commit).matches()) + { + throw PluginBuildException.of(internalName, "commit must be a full 40 character sha1sum") + .withFileLine(pluginCommitDescriptor, "commit=" + commit); + } + + warning = (String) cd.remove("warning"); + + for (Map.Entry extra : cd.entrySet()) + { + throw PluginBuildException.of(internalName, "unexpected key in commit descriptor") + .withFileLine(pluginCommitDescriptor, extra.getKey() + "=" + extra.getValue()); + } + + buildDirectory = new File(TMP_ROOT, internalName); + if (!buildDirectory.mkdirs()) + { + throw new RuntimeException("Unable to create temp directory"); + } + repositoryDirectory = new File(buildDirectory, "repo"); + logFile = new File(buildDirectory, "log"); + log = new FileOutputStream(logFile, true); + jarFile = new File(buildDirectory, "plugin.jar"); + iconFile = new File(repositoryDirectory, "icon.png"); + } + + public void download() throws IOException, PluginBuildException + { + Process gitclone = new ProcessBuilder("git", "clone", "--config", "advice.detachedHead=false", this.repositoryURL, repositoryDirectory.getAbsolutePath()) + .redirectOutput(ProcessBuilder.Redirect.appendTo(logFile)) + .redirectError(ProcessBuilder.Redirect.appendTo(logFile)) + .start(); + Util.waitAndCheck(this, gitclone, "git clone", 2, TimeUnit.MINUTES); + + + Process gitcheckout = new ProcessBuilder("git", "checkout", commit + "^{commit}") + .redirectOutput(ProcessBuilder.Redirect.appendTo(logFile)) + .redirectError(ProcessBuilder.Redirect.appendTo(logFile)) + .directory(repositoryDirectory) + .start(); + Util.waitAndCheck(this, gitcheckout, "git checkout", 2, TimeUnit.MINUTES); + } + + public void build(String runeliteVersion) throws IOException, PluginBuildException + { + try (ProjectConnection con = GradleConnector.newConnector() + .forProjectDirectory(repositoryDirectory) + .useInstallation(GRADLE_HOME) + .connect()) + { + CancellationTokenSource cancel = GradleConnector.newCancellationTokenSource(); + BlockingQueue queue = new ArrayBlockingQueue<>(1); + String buildSuccess = "success"; + + con.newBuild() + .withArguments( + "--no-build-cache", + "--console=plain", + "--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.buildDir", buildDirectory.getAbsolutePath(), + "runelite.pluginhub.package.runeliteVersion", runeliteVersion)) + .setJvmArguments("-Xmx768M", "-XX:+UseParallelGC") + .setStandardOutput(log) + .setStandardError(log) + .forTasks("runelitePluginHubPackage", "runelitePluginHubManifest") + .withCancellationToken(cancel.token()) + .run(new ResultHandler() + { + @Override + public void onComplete(Void result) + { + queue.add(buildSuccess); + } + + @Override + public void onFailure(GradleConnectionException failure) + { + queue.add(failure); + } + }); + log.flush(); + + Object output = queue.poll(5, TimeUnit.MINUTES); + if (output == null) + { + cancel.cancel(); + throw PluginBuildException.of(this, "build did not complete within 5 minutes"); + } + if (output == buildSuccess) + { + return; + } + else if (output instanceof GradleConnectionException) + { + throw PluginBuildException.of(this, "build failed", output); + } + throw new IllegalStateException(output.toString()); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + + public void assembleManifest() throws IOException, PluginBuildException + { + manifest.setInternalName(internalName); + manifest.setCommit(commit); + manifest.setWarning(warning); + + { + Properties chunk = loadProperties(new File(buildDirectory, "chunk.properties")); + + manifest.setVersion(chunk.getProperty("version")); + if (Strings.isNullOrEmpty(manifest.getVersion())) + { + throw new IllegalStateException("version in empty"); + } + } + + { + long size = jarFile.length(); + if (size > 10 * 1024 * 1024) + { + throw PluginBuildException.of(this, "the output jar is {}MiB, which is above our limit of 10MiB", size / (1024 * 1024)); + } + manifest.setSize((int) size); + } + + manifest.setHash(com.google.common.io.Files.asByteSource(jarFile) + .hash(Hashing.sha256()) + .toString()); + + if (iconFile.exists()) + { + long size = iconFile.length(); + if (size > 256 * 1024) + { + throw PluginBuildException.of(this, "icon.png is {}KiB, which is above our limit of 256KiB", size / 1024) + .withFile(iconFile); + } + + synchronized (ImageIO.class) + { + try + { + Objects.requireNonNull(ImageIO.read(iconFile)); + } + catch (Exception e) + { + throw PluginBuildException.of(this, "icon is invalid", e) + .withFile(iconFile); + } + } + + manifest.setHasIcon(true); + } + + Set pluginClasses = new HashSet<>(); + Set jarClasses = new HashSet<>(); + { + try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile))) + { + for (JarEntry je; (je = jis.getNextJarEntry()) != null; ) + { + String fileName = je.getName(); + if (!fileName.endsWith(".class")) + { + continue; + } + + byte[] classData = ByteStreams.toByteArray(jis); + new ClassReader(classData).accept(new ClassVisitor(Opcodes.ASM7) + { + boolean extendsPlugin; + String name; + + @SneakyThrows + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) + { + if (version > Opcodes.V1_8 && !fileName.startsWith("META-INF/versions")) + { + throw PluginBuildException.of(Plugin.this, "plugins must be Java 1.8 compatible") + .withFile(fileName); + } + + jarClasses.add(name.replace('/', '.')); + + extendsPlugin = "net/runelite/client/plugins/Plugin".equals(superName); + this.name = name; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) + { + if ("Lnet/runelite/client/plugins/PluginDescriptor;".equals(descriptor) && extendsPlugin) + { + pluginClasses.add(name.replace('/', '.')); + } + + return null; + } + }, ClassReader.SKIP_FRAMES); + } + } + } + + { + File propFile = new File(repositoryDirectory, "runelite-plugin.properties"); + if (!propFile.exists()) + { + throw PluginBuildException.of(this, "runelite-plugin.properties must exist in the root of your repo"); + } + Properties props = loadProperties(propFile); + + { + String displayName = (String) props.remove("displayName"); + if (Strings.isNullOrEmpty(displayName)) + { + throw PluginBuildException.of(this, "\"displayName\" must be set") + .withFile(propFile); + } + manifest.setDisplayName(displayName); + } + + { + String author = (String) props.remove("author"); + if (Strings.isNullOrEmpty(author)) + { + throw PluginBuildException.of(this, "\"author\" must be set") + .withFile(propFile); + } + manifest.setAuthor(author); + } + + { + String supportStr = (String) props.remove("support"); + if (!Strings.isNullOrEmpty(supportStr)) + { + try + { + manifest.setSupport(new URL(supportStr)); + } + catch (MalformedURLException e) + { + throw PluginBuildException.of(this, "support url is malformed", e) + .withFileLine(propFile, "support=" + supportStr); + } + } + } + + manifest.setDescription((String) props.remove("description")); + + { + String tagsStr = (String) props.remove("tags"); + if (!Strings.isNullOrEmpty(tagsStr)) + { + manifest.setTags(Splitter.on(",") + .omitEmptyStrings() + .trimResults() + .splitToList(tagsStr) + .toArray(new String[0])); + } + } + + { + String pluginsStr = (String) props.remove("plugins"); + if (pluginsStr == null) + { + throw PluginBuildException.of(this, "\"plugins\" must be set") + .withFile(propFile); + } + + List plugins = Splitter.on(CharMatcher.anyOf(",:;")) + .omitEmptyStrings() + .trimResults() + .splitToList(pluginsStr); + + manifest.setPlugins(plugins.toArray(new String[0])); + + for (String className : plugins) + { + if (pluginClasses.contains(className)) + { + continue; + } + + if (jarClasses.contains(className)) + { + throw PluginBuildException.of(this, "Plugin class \"{}\" is not a valid Plugin", className) + .withHelp("All plugins must extend Plugin an have an @PluginDescriptor") + .withFileLine(propFile, "plugins=" + pluginsStr); + } + + Set unusedPlugins = new HashSet<>(pluginClasses); + unusedPlugins.removeAll(plugins); + + throw PluginBuildException.of(this, + "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))) + .withFileLine(propFile, "plugins=" + pluginsStr); + } + } + + if (props.size() != 0) + { + writeLog("warning: unused props in runelite-plugin.properties: {}\n", props.keySet()); + } + } + } + + public void upload(UploadConfiguration uploadConfig) throws IOException + { + HttpUrl pluginRoot = uploadConfig.getUploadRepoRoot().newBuilder() + .addPathSegment(internalName) + .build(); + + uploadConfig.put( + pluginRoot.newBuilder().addPathSegment(commit + ".jar").build(), + jarFile); + + if (manifest.isHasIcon()) + { + uploadConfig.put( + pluginRoot.newBuilder().addPathSegment(commit + ".png").build(), + iconFile); + } + } + + public void uploadLog(UploadConfiguration uploadConfig) throws IOException + { + try + { + log.close(); + log = null; + } + catch (IOException ignored) + { + } + + uploadConfig.put(uploadConfig.getUploadRepoRoot() + .newBuilder() + .addPathSegment(internalName) + .addPathSegment(commit + ".log") + .build(), + logFile); + } + + public void writeLog(String format, Object... args) throws IOException + { + FormattingTuple fmt = MessageFormatter.arrayFormat(format, args); + log.write(fmt.getMessage().getBytes(StandardCharsets.UTF_8)); + Throwable t = fmt.getThrowable(); + if (t != null) + { + PrintWriter pw = new PrintWriter(new OutputStreamWriter(log, StandardCharsets.UTF_8)); + pw.println(t.getMessage()); + t.printStackTrace(pw); + pw.flush(); + } + log.flush(); + } + + static Properties loadProperties(File path) throws IOException + { + Properties props = new Properties(); + try (FileInputStream fis = new FileInputStream(path)) + { + props.load(fis); + } + return props; + } + + @Override + public void close() throws IOException + { + if (log != null) + { + log.close(); + } + MoreFiles.deleteRecursively(buildDirectory.toPath(), RecursiveDeleteOption.ALLOW_INSECURE); + } +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/PluginBuildException.java b/package/package/src/main/java/net/runelite/pluginhub/packager/PluginBuildException.java new file mode 100644 index 000000000..b7ae83ab5 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/PluginBuildException.java @@ -0,0 +1,130 @@ +/* + * 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 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()); + } +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/UploadConfiguration.java b/package/package/src/main/java/net/runelite/pluginhub/packager/UploadConfiguration.java new file mode 100644 index 000000000..1bad84e1b --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/UploadConfiguration.java @@ -0,0 +1,157 @@ +/* + * 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.Strings; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +@Getter +@Accessors(chain = true) +public class UploadConfiguration +{ + private RSAPrivateCrtKey key; + private PublicKey cert; + private OkHttpClient client; + + @Setter + private HttpUrl uploadRepoRoot; + + public UploadConfiguration fromEnvironment(String runeliteVersion) + { + String prNo = System.getenv("PACKAGE_IS_PR"); + if (prNo != null && !prNo.isEmpty() && !"false".equalsIgnoreCase(prNo)) + { + return this; + } + + setKey(System.getenv("SIGNING_KEY")); + setClient(System.getenv("REPO_CREDS")); + + String uploadRepoRootStr = System.getenv("REPO_ROOT"); + if (!Strings.isNullOrEmpty(uploadRepoRootStr)) + { + uploadRepoRoot = HttpUrl.parse(uploadRepoRootStr) + .newBuilder() + .addPathSegment(runeliteVersion) + .build(); + } + + return this; + } + + public boolean isComplete() + { + return key != null && cert != null && client != null && uploadRepoRoot != null; + } + + public UploadConfiguration setKey(String keyStr) + { + if (keyStr == null) + { + key = null; + cert = null; + return this; + } + + 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); + } + + return this; + } + + 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 < 2; attempts++) + { + res = chain.proceed(userAgentRequest); + 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); + } + } +} diff --git a/package/package/src/main/java/net/runelite/pluginhub/packager/Util.java b/package/package/src/main/java/net/runelite/pluginhub/packager/Util.java new file mode 100644 index 000000000..a81ad2894 --- /dev/null +++ b/package/package/src/main/java/net/runelite/pluginhub/packager/Util.java @@ -0,0 +1,65 @@ +/* + * 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.IOException; +import java.util.concurrent.TimeUnit; +import okhttp3.Response; + +public class Util +{ + private Util() + { + } + + public static void waitAndCheck(Plugin plugin, Process process, String name, long timeout, TimeUnit timeoutUnit) throws PluginBuildException + { + try + { + if (!process.waitFor(timeout, timeoutUnit)) + { + process.destroy(); + throw PluginBuildException.of(plugin, name + " failed to complete in a reasonable time"); + } + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + + if (process.exitValue() != 0) + { + throw PluginBuildException.of(plugin, name + " exited with " + process.exitValue()); + } + } + + public static void check(Response res) throws IOException + { + if ((res.code() / 100) != 2) + { + throw new IOException(res.request().url() + ": " + res.code() + " " + res.message()); + } + } +} diff --git a/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java b/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java new file mode 100644 index 000000000..e42e4003d --- /dev/null +++ b/package/package/src/test/java/net/runelite/pluginhub/packager/PluginTest.java @@ -0,0 +1,151 @@ +/* + * 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 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 testExamplePluginCompiles() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + { + try (Plugin p = createExamplePlugin("example")) + { + p.build(Packager.readRLVersion()); + p.assembleManifest(); + } + } + + @Test + public void testMissingPlugin() throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + { + try (Plugin p = createExamplePlugin("missing-plugin")) + { + File propFile = new File(p.repositoryDirectory, "runelite-plugin.properties"); + Properties props = Plugin.loadProperties(propFile); + props.setProperty("plugins", "com.nonexistent"); + writeProperties(props, propFile); + p.build(Packager.readRLVersion()); + p.assembleManifest(); + Assert.fail(); + } + catch (PluginBuildException e) + { + log.info("ok: ", e); + assertContains(e.getHelpText(), "com.example.ExamplePlugin"); + } + } + + 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); + } + finally + { + f.delete(); + tmp.delete(); + } + } + + private static Plugin createExamplePlugin(String name) throws DisabledPluginException, PluginBuildException, IOException, InterruptedException + { + Plugin p = newPlugin(name, "" + + "repository=https://github.com/runelite/example-plugin.git\n" + + "commit=0000000000000000000000000000000000000000"); + + Assert.assertEquals(new ProcessBuilder( + new File("./create_new_plugin.py").getAbsolutePath(), + "--noninteractive", + "--output_directory", p.repositoryDirectory.getAbsolutePath(), + "--name", "Example", + "--package", "com.example", + "--author", "Nobody", + "--description", "An example greeter plugin") + .inheritIO() + .start() + .waitFor(), 0); + + return p; + } + + private void assertContains(String haystack, String needle) + { + Assert.assertTrue(haystack, haystack.contains(needle)); + } +} \ No newline at end of file diff --git a/package/package/src/test/java/net/runelite/pluginhub/packager/UploadConfigurationTest.java b/package/package/src/test/java/net/runelite/pluginhub/packager/UploadConfigurationTest.java new file mode 100644 index 000000000..6021cfee1 --- /dev/null +++ b/package/package/src/test/java/net/runelite/pluginhub/packager/UploadConfigurationTest.java @@ -0,0 +1,129 @@ +/* + * 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.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +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 +{ + 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 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(res.code(), 200); + Assert.assertEquals(res.body().string(), "ok"); + } + + RecordedRequest r2 = server.takeRequest(); + Assert.assertEquals(r2.getHeader("Authorization"), "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + } + + @Test + public void testSigning() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException + { + UploadConfiguration cfg = new UploadConfiguration() + .setKey(TEST_SIGNING_KEY); + + byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] sig; + { + Signature s = Signature.getInstance("SHA256withRSA"); + s.initSign(cfg.getKey()); + s.update(data); + sig = s.sign(); + } + + { + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(cfg.getCert()); + s.update(data); + Assert.assertTrue(s.verify(sig)); + } + + { + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(cfg.getCert()); + s.update("moo".getBytes(StandardCharsets.UTF_8)); + Assert.assertFalse(s.verify(sig)); + } + } +} \ No newline at end of file diff --git a/package/settings.gradle b/package/settings.gradle new file mode 100644 index 000000000..6b436e59a --- /dev/null +++ b/package/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = "package-root" + +include "initLib" +include "package" \ No newline at end of file diff --git a/package/target_init.gradle b/package/target_init.gradle new file mode 100644 index 000000000..1341afdb4 --- /dev/null +++ b/package/target_init.gradle @@ -0,0 +1,81 @@ +/* + * 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. + */ +import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +initscript { + dependencies { + classpath files(System.getenv("runelite.pluginhub.package.lib")) + } +} + +allprojects { + apply plugin: "java" + apply plugin: ShadowPlugin + + compileJava { + options.release.set(8) + } + + tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true + } + + def buildDir = new File(System.getenv("runelite.pluginhub.package.buildDir")); + + task runelitePluginHubPackage(type: ShadowJar) { + destinationDir = buildDir + archiveName = "plugin.jar" + configurations = [project.configurations.runtimeClasspath] + from sourceSets.main.output + } + + 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 + } + } + } + } +} \ No newline at end of file diff --git a/rebuild_all.sh b/package/travis.sh similarity index 70% rename from rebuild_all.sh rename to package/travis.sh index 40937152b..85764fcc4 100755 --- a/rebuild_all.sh +++ b/package/travis.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2019 Abex +# Copyright (c) 2020 Abex # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -23,23 +23,16 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -[[ "${TRAVIS_PULL_REQUEST:-false}" == "false" ]] || exit 0 +set -e -x -SECONDS=0 +: ' +env: + -FORCE_BUILD: example-external-plugin +' +pushd "$(dirname "$0")" +./gradlew --console=plain --build-cache prep +popd -for PLUGIN in plugins/* ; do - if [ $SECONDS -gt 60 ]; then - echo "travis_fold:start:intermediate_manifest" - ./build_manifest.sh - SECONDS=0 - echo "travis_fold:end:intermediate_manifest" - fi - PLUGIN_ID=$(basename "$PLUGIN") - echo "travis_fold:start:$PLUGIN_ID" - ./build_plugin.sh "$PLUGIN" || echo "Build failed for $PLUGIN_ID" - echo "travis_fold:end:$PLUGIN_ID" -done - -echo "travis_fold:start:final_manifest" -./build_manifest.sh -echo "travis_fold:end:final_manifest" \ No newline at end of file +PACKAGE_IS_PR="$TRAVIS_PULL_REQUEST" \ +PACKAGE_COMMIT_RANGE="$TRAVIS_COMMIT_RANGE" \ + java -XX:+UseParallelGC -jar package/package/build/libs/package.jar \ No newline at end of file diff --git a/repo_config.sh b/repo_config.sh deleted file mode 100755 index cf3e57481..000000000 --- a/repo_config.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -# export REPO_CREDS="user:password" -# export REPO_ROOT="https://your/webdav/server" -# export SIGNING_KEY=" -# -----BEGIN PRIVATE KEY----- -# ... -# -----END PRIVATE KEY----- -# " diff --git a/travis.sh b/travis.sh deleted file mode 100755 index 43a51b802..000000000 --- a/travis.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -# 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. - -set -e -x - -: ' -env: - -FORCE_BUILD: example-external-plugin -' - -if [[ "$FORCE_BUILD" == "ALL" ]]; then - ./rebuild_all.sh - exit -elif [[ -n "${FORCE_BUILD+x}" ]]; then - for FI in $(echo "$FORCE_BUILD" | tr ',' '\n'); do - ./build_plugin.sh "plugins/$FI" - done - ./build_manifest.sh - exit -fi - -PLUGIN_CHANGE= -while read -r FI ; do - if [[ $FI =~ ^plugins/.*$ ]]; then - [ -e "$FI" ] && ./build_plugin.sh "$FI" < /dev/null - PLUGIN_CHANGE=true - elif [[ "$FI" == "runelite.version" ]]; then - ./rebuild_all.sh < /dev/null - fi -done < <(git diff --name-only "$TRAVIS_COMMIT_RANGE") - -if [[ "$PLUGIN_CHANGE" == true ]]; then - ./build_manifest.sh -fi \ No newline at end of file