diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..da12e14 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +dist: trusty + +language: java +jdk: + - openjdk8 + - oraclejdk8 + +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + +before_install: + - chmod +x ./gradlew + +install: + - ./gradlew --refresh-dependencies + +script: + - ./gradlew clean build + +after_success: + - ./gradlew bintrayUpload + +notifications: + email: false \ No newline at end of file diff --git a/README.md b/README.md index 17da09a..974bc52 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ # MixinBootstrap +[![Build Status](https://api.travis-ci.com/LXGaming/MixinBootstrap.svg?branch=master)](https://travis-ci.com/LXGaming/MixinBootstrap) [![License](https://lxgaming.github.io/badges/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Patreon](https://lxgaming.github.io/badges/Patreon-donate-yellow.svg)](https://www.patreon.com/lxgaming) [![Paypal](https://lxgaming.github.io/badges/Paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CZUUA6LE7YS44&item_name=MixinBootstrap+(from+GitHub.com)) +**MixinBootstrap** is a way of booting [Mixin](https://github.com/SpongePowered/Mixin) in a [MinecraftForge](https://github.com/MinecraftForge/MinecraftForge) production environment by utilizing a [ModLauncher](https://github.com/cpw/modlauncher) ITransformationService. + +## Prerequisites +[MinecraftForge](https://github.com/MinecraftForge/MinecraftForge) must be running [ModLauncher](https://github.com/cpw/modlauncher) v4.2.0 + +## Usage +Simply drop the `MixinBootstrap-.jar` into the [MinecraftForge](https://github.com/MinecraftForge/MinecraftForge) mods folder + +## Download +MixinBootstrap is available on [GitHub](https://github.com/LXGaming/MixinBootstrap/releases) + ## License MixinBootstrap is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..a2587eb --- /dev/null +++ b/build.gradle @@ -0,0 +1,101 @@ +plugins { + id "java" + id "signing" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +group = "io.github.lxgaming" +archivesBaseName = "MixinBootstrap" +version = "1.0.0" + +configurations { + provided { + compile.extendsFrom(provided) + } + + build.dependsOn("signJar") +} + +repositories { + jcenter() + maven { + name = "minecraftforge" + url = "https://files.minecraftforge.net/maven" + } + maven { + name = "spongepowered" + url = "https://repo.spongepowered.org/maven" + } +} + +dependencies { + provided("com.google.code.findbugs:jsr305:3.0.2") + provided("cpw.mods:modlauncher:4.2.0") { + exclude(module: "asm-analysis") + } + provided("net.sf.jopt-simple:jopt-simple:5.0.4") + provided("org.apache.logging.log4j:log4j-api:2.11.2") + compile("org.ow2.asm:asm-analysis:6.2") { + transitive = false + } + compile("org.ow2.asm:asm-util:6.2") { + transitive = false + } + compile("org.spongepowered:mixin:0.8-SNAPSHOT") { + transitive = false + } +} + +jar { + from ((configurations.compile - configurations.provided).findAll({ + it.isDirectory() || it.name.endsWith(".jar") + }).collect({ + it.isDirectory() ? it : zipTree(it) + })) { + + exclude("META-INF/services/cpw.mods.modlauncher.*") + exclude("META-INF/*.RSA") + exclude("META-INF/*.SF") + } + + exclude("module-info.class") +} + +processResources { + from("LICENSE") + rename("LICENSE", "LICENSE-" + archivesBaseName) +} + +task signJar { + doFirst { + if (!project.hasProperty("signing.keyStorePath") || !project.hasProperty("signing.secretKeyRingFile")) { + project.logger.warn("========== [WARNING] ==========") + project.logger.warn("") + project.logger.warn(" This build is not signed! ") + project.logger.warn("") + project.logger.warn("========== [WARNING] ==========") + throw new StopExecutionException() + } + } + + doLast { + configurations.archives.allArtifacts.files.each { + ant.signjar( + jar: it, + alias: project.property("signing.alias"), + storepass: project.property("signing.keyStorePassword"), + keystore: project.property("signing.keyStorePath"), + keypass: project.property("signing.keyStorePassword"), + preservelastmodified: project.property("signing.preserveLastModified"), + tsaurl: project.property("signing.timestampAuthority"), + digestalg: project.property("signing.digestAlgorithm") + ) + project.logger.lifecycle("JAR Signed: " + it.name) + + signing.sign(it) + project.logger.lifecycle("PGP Signed: " + it.name) + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..9aca134 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx512M \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java new file mode 100644 index 0000000..ace93ff --- /dev/null +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Alex Thomson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lxgaming.mixin.launch; + +import cpw.mods.modlauncher.TransformingClassLoader; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +public class MixinLaunchPluginService implements ILaunchPluginService { + + public static final String NAME = "mixinbootstrap"; + + private static final List SKIP_PACKAGES = Arrays.asList( + "org.objectweb.asm.", + "org.spongepowered.asm.launch.", + "org.spongepowered.asm.lib.", + "org.spongepowered.asm.mixin.", + "org.spongepowered.asm.service.", + "org.spongepowered.asm.util." + ); + + @Override + public String name() { + return MixinLaunchPluginService.NAME; + } + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty) { + throw new UnsupportedOperationException("Outdated ModLauncher"); + } + + @Override + public boolean processClass(Phase phase, ClassNode classNode, Type classType) { + throw new UnsupportedOperationException("Outdated ModLauncher"); + } + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty, String reason) { + return EnumSet.noneOf(Phase.class); + } + + @Override + public boolean processClass(Phase phase, ClassNode classNode, Type classType, String reason) { + return false; + } + + @Override + public void addResources(List> resources) { + } + + @Override + public void initializeLaunch(ITransformerLoader transformerLoader, Path[] specialPaths) { + TransformingClassLoader classLoader = (TransformingClassLoader) Thread.currentThread().getContextClassLoader(); + classLoader.addTargetPackageFilter(name -> SKIP_PACKAGES.stream().noneMatch(name::startsWith)); + } + + @Override + public T getExtension() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java new file mode 100644 index 0000000..5f99029 --- /dev/null +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java @@ -0,0 +1,245 @@ +/* + * Copyright 2019 Alex Thomson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lxgaming.mixin.launch; + +import cpw.mods.modlauncher.LaunchPluginHandler; +import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.ITransformationService; +import cpw.mods.modlauncher.api.ITransformer; +import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import joptsimple.OptionSpecBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class MixinTransformationService implements ITransformationService { + + public static final String NAME = "mixinbootstrap"; + + private static final Logger LOGGER = LogManager.getLogger("MixinBootstrap Launch"); + private final Map launchPluginServices; + private final Set transformationServices; + + public MixinTransformationService() { + if (Launcher.INSTANCE == null) { + throw new IllegalStateException("Launcher has not been initialized!"); + } + + this.launchPluginServices = getLaunchPluginServices(); + this.transformationServices = new HashSet<>(); + } + + @Nonnull + @Override + public String name() { + return MixinTransformationService.NAME; + } + + @Override + public void arguments(BiFunction argumentBuilder) { + this.transformationServices.forEach(transformationService -> transformationService.arguments(argumentBuilder)); + } + + @Override + public void argumentValues(OptionResult option) { + this.transformationServices.forEach(transformationService -> transformationService.argumentValues(option)); + } + + @Override + public void initialize(IEnvironment environment) { + this.transformationServices.forEach(transformationService -> transformationService.initialize(environment)); + } + + @Override + public void beginScanning(IEnvironment environment) { + this.transformationServices.forEach(transformationService -> transformationService.beginScanning(environment)); + } + + @Override + public List> runScan(IEnvironment environment) { + List> list = new ArrayList<>(); + this.transformationServices.forEach(transformationService -> { + list.addAll(transformationService.runScan(environment)); + }); + + return list; + } + + @Override + public void onLoad(IEnvironment env, Set otherServices) throws IncompatibleEnvironmentException { + if (this.launchPluginServices == null) { + throw new IncompatibleEnvironmentException("LaunchPluginServices is unavailable"); + } + + // Running in a dev environment + if (getClass().getClassLoader() == Launcher.class.getClassLoader()) { + return; + } + + try { + URLClassLoader classLoader = (URLClassLoader) Launcher.class.getClassLoader(); + Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURLMethod.setAccessible(true); + addURLMethod.invoke(classLoader, getClass().getProtectionDomain().getCodeSource().getLocation().toURI().toURL()); + } catch (Throwable ex) { + throw new IncompatibleEnvironmentException("Failed to invoke URLClassLoader::addURL"); + } + + // Mixin + // - Plugin Service + registerLaunchPluginService("org.spongepowered.asm.launch.MixinLaunchPlugin"); + + // - Transformation Service + // This cannot be loaded by the ServiceLoader as it will load classes under the wrong classloader + registerTransformationService("org.spongepowered.asm.launch.MixinTransformationService"); + + // MixinBootstrap + // - Plugin Service + registerLaunchPluginService("io.github.lxgaming.mixin.launch.MixinLaunchPluginService"); + + for (ITransformationService transformationService : this.transformationServices) { + transformationService.onLoad(env, otherServices); + } + } + + @Nonnull + @Override + public List transformers() { + List list = new ArrayList<>(); + this.transformationServices.forEach(transformationService -> { + list.addAll(transformationService.transformers()); + }); + + return list; + } + + @Override + public Map.Entry, Supplier>>> additionalClassesLocator() { + return null; + } + + @Override + public Map.Entry, Supplier>>> additionalResourcesLocator() { + return null; + } + + private void registerLaunchPluginService(String className) throws IncompatibleEnvironmentException { + try { + // noinspection unchecked + Class launchPluginServiceClass = (Class) Class.forName(className, true, Launcher.class.getClassLoader()); + if (isLaunchPluginServicePresent(launchPluginServiceClass)) { + LOGGER.warn("{} is already registered", launchPluginServiceClass.getSimpleName()); + return; + } + + ILaunchPluginService launchPluginService = launchPluginServiceClass.newInstance(); + String pluginName = launchPluginService.name(); + this.launchPluginServices.put(pluginName, launchPluginService); + + List> mods = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElse(null); + if (mods != null) { + Map mod = new HashMap<>(); + mod.put("name", pluginName); + mod.put("type", "PLUGINSERVICE"); + String fileName = launchPluginServiceClass.getProtectionDomain().getCodeSource().getLocation().getFile(); + mod.put("file", fileName.substring(fileName.lastIndexOf('/'))); + mods.add(mod); + } + + LOGGER.debug("Registered {} ({})", launchPluginServiceClass.getSimpleName(), pluginName); + } catch (Throwable ex) { + LOGGER.error("Encountered an error while registering {}", className, ex); + throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); + } + } + + private void registerTransformationService(String className) throws IncompatibleEnvironmentException { + try { + // noinspection unchecked + Class transformationServiceClass = (Class) Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + if (isTransformationServicePresent(transformationServiceClass)) { + LOGGER.warn("{} is already registered", transformationServiceClass.getSimpleName()); + return; + } + + ITransformationService transformationService = transformationServiceClass.newInstance(); + String name = transformationService.name(); + this.transformationServices.add(transformationService); + LOGGER.debug("Registered {} ({})", transformationServiceClass.getSimpleName(), name); + } catch (Exception ex) { + LOGGER.error("Encountered an error while registering {}", className, ex); + throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); + } + } + + private boolean isLaunchPluginServicePresent(Class launchPluginServiceClass) { + for (ILaunchPluginService launchPluginService : this.launchPluginServices.values()) { + if (launchPluginServiceClass.isInstance(launchPluginService)) { + return true; + } + } + + return false; + } + + private boolean isTransformationServicePresent(Class transformationServiceClass) { + for (ITransformationService transformationService : this.transformationServices) { + if (transformationServiceClass.isInstance(transformationService)) { + return true; + } + } + + return false; + } + + private Map getLaunchPluginServices() { + try { + // cpw.mods.modlauncher.Launcher.launchPlugins + Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins"); + launchPluginsField.setAccessible(true); + LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE); + + // cpw.mods.modlauncher.LaunchPluginHandler.plugins + Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins"); + pluginsField.setAccessible(true); + + // noinspection unchecked + return (Map) pluginsField.get(launchPluginHandler); + } catch (Exception ex) { + LOGGER.error("Encountered an error while getting LaunchPluginServices", ex); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService b/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService new file mode 100644 index 0000000..81932dd --- /dev/null +++ b/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService @@ -0,0 +1 @@ +io.github.lxgaming.mixin.launch.MixinTransformationService \ No newline at end of file