Skip to content

Commit

Permalink
Implement plugin dependencies, loaded in dependency first order (#1701)
Browse files Browse the repository at this point in the history
* Implement plugin dependencies, loaded in dependency first order

# Conflicts:
#	chunky/src/java/se/llbit/chunky/main/Chunky.java

* Add additional documentation

* Move plugin loader api out into a separate class.

This also makes PluginManager much easier to test.

* Add tests to verify load order of plugins with dependencies.

* Apply review suggestions

* Add maven-artifact:3.9.9 dependency.

---------

Co-authored-by: Maik Marschner <[email protected]>
  • Loading branch information
NotStirred and leMaik authored Sep 29, 2024
1 parent 6f3fe7a commit 0bc1302
Show file tree
Hide file tree
Showing 14 changed files with 613 additions and 140 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ Chunky uses the following 3rd party libraries:
The library is covered by the Apache License, version 2.0.
See the file `licenses/Apache-2.0.txt` for the full license text.
See the file `licenses/commons-math.txt` for the copyright notices.
- **Apache Maven Artifact by the Apache Software Foundation**
The library is covered by the Apache License, version 2.0.
See the file `licenses/Apache-2.0.txt` for the full license text.
- **FastUtil by Sebastiano Vigna**
FastUtil is covered by Apache License, version 2.0.
See the file `licenses/Apache-2.0.txt` for the full license text.
Expand Down
1 change: 1 addition & 0 deletions chunky/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.2'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.lz4:lz4-java:1.8.0'
implementation 'org.apache.maven:maven-artifact:3.9.9'
implementation project(':lib')
}

Expand Down
Binary file added chunky/lib/maven-artifact-3.9.9.jar
Binary file not shown.
68 changes: 31 additions & 37 deletions chunky/src/java/se/llbit/chunky/main/Chunky.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
import se.llbit.chunky.block.MinecraftBlockProvider;
import se.llbit.chunky.block.legacy.LegacyMinecraftBlockProvider;
import se.llbit.chunky.main.CommandLineOptions.Mode;
import se.llbit.chunky.plugin.ContextMenuTransformer;
import se.llbit.chunky.plugin.PluginApi;
import se.llbit.chunky.plugin.ChunkyPlugin;
import se.llbit.chunky.plugin.TabTransformer;
import se.llbit.chunky.plugin.*;
import se.llbit.chunky.plugin.loader.PluginManager;
import se.llbit.chunky.plugin.loader.JarPluginLoader;
import se.llbit.chunky.plugin.manifest.PluginManifest;
import se.llbit.chunky.renderer.*;
import se.llbit.chunky.renderer.RenderManager;
import se.llbit.chunky.renderer.export.PictureExportFormat;
Expand All @@ -43,7 +43,6 @@
import se.llbit.chunky.ui.render.RenderControlsTabTransformer;
import se.llbit.chunky.world.MaterialStore;
import se.llbit.json.JsonArray;
import se.llbit.json.JsonValue;
import se.llbit.log.ConsoleReceiver;
import se.llbit.log.Level;
import se.llbit.log.Log;
Expand All @@ -55,11 +54,10 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Chunky is a Minecraft mapping and rendering tool created byJesper Öqvist ([email protected]).
Expand Down Expand Up @@ -265,35 +263,31 @@ private void loadPlugins() {
}
Path pluginsPath = pluginsDirectory.toPath();
JsonArray plugins = PersistentSettings.getPlugins();
Set<String> loadedPlugins = new HashSet<>();
for (JsonValue value : plugins) {
String jarName = value.asString("");
if (!jarName.isEmpty()) {
Log.info("Loading plugin: " + value);
try {
ChunkyPlugin
.load(pluginsPath.resolve(jarName).toRealPath().toFile(), (plugin, manifest) -> {
CreditsController.addPlugin(manifest);
String pluginName = manifest.get("name").asString("");
if (loadedPlugins.contains(pluginName)) {
Log.warnf(
"Multiple plugins with the same name (\"%s\") are enabled. Loading multiple versions of the same plugin can lead to strange behavior.",
pluginName);
}
loadedPlugins.add(pluginName);
try {
plugin.attach(this);
} catch (Throwable t) {
Log.error("Plugin " + jarName + " failed to load.", t);
}
Log.infof("Plugin loaded: %s %s", manifest.get("name").asString(""),
manifest.get("version").asString(""));
});
} catch (Throwable t) {
Log.error("Plugin " + jarName + " failed to load.", t);
}
// TODO: allow plugins to implement a custom plugin loader.
PluginManager pluginManager = new PluginManager(new JarPluginLoader());

// Parse plugin manifests
Set<PluginManifest> pluginManifests = plugins.elements.stream()
.map(value -> value.asString(""))
.filter(jarName -> !jarName.isEmpty())
.map(jarName -> pluginsPath.resolve(jarName).toAbsolutePath().toFile())
.map(PluginManager::parsePluginManifest)
.flatMap(Optional::stream)
.collect(Collectors.toSet());

// Load plugins
pluginManager.load(pluginManifests, (plugin, manifest) -> {
String jarName = manifest.pluginJar.getName();
Log.infof("Loading plugin: %s", jarName);
CreditsController.addPlugin(manifest.name, manifest.version.toString(), manifest.author, manifest.description);

try {
plugin.attach(this);
} catch (Throwable t) {
Log.error("Plugin " + jarName + " failed to load.", t);
}
}
Log.infof("Plugin loaded: %s %s", manifest.name, manifest.version);
});
}

/**
Expand Down
102 changes: 0 additions & 102 deletions chunky/src/java/se/llbit/chunky/plugin/ChunkyPlugin.java

This file was deleted.

45 changes: 45 additions & 0 deletions chunky/src/java/se/llbit/chunky/plugin/loader/JarPluginLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package se.llbit.chunky.plugin.loader;

import se.llbit.chunky.Plugin;
import se.llbit.chunky.plugin.PluginApi;
import se.llbit.chunky.plugin.manifest.PluginManifest;
import se.llbit.log.Log;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.function.BiConsumer;

@PluginApi
public class JarPluginLoader implements PluginLoader {
public void load(BiConsumer<Plugin, PluginManifest> onLoad, PluginManifest pluginManifest) {
try {
Class<?> pluginClass = loadPluginClass(pluginManifest.main, pluginManifest.pluginJar);
Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
onLoad.accept(plugin, pluginManifest);
} catch (IOException | ClassNotFoundException e) {
Log.error("Could not load the plugin", e);
} catch (ClassCastException e) {
Log.error("Plugin main class has wrong type (must implement se.llbit.chunky.Plugin)", e);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
Log.error("Could not create plugin instance", e);
}
}

/**
* This method is {@link PluginApi} to allow plugins to override only classloading functionality of the default plugin loader.
*
* @param pluginMainClass The plugin's main class to load.
* @param pluginJarFile The jar file to load classes from.
* @return The loaded plugin's main class
* @throws ClassNotFoundException If the main class doesn't exist
* @throws MalformedURLException If the jar file cannot be converted to a URL
*/
@PluginApi
protected Class<?> loadPluginClass(String pluginMainClass, File pluginJarFile) throws ClassNotFoundException, MalformedURLException {
return new URLClassLoader(new URL[] { pluginJarFile.toURI().toURL() }).loadClass(pluginMainClass);
}
}
18 changes: 18 additions & 0 deletions chunky/src/java/se/llbit/chunky/plugin/loader/PluginLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package se.llbit.chunky.plugin.loader;

import se.llbit.chunky.Plugin;
import se.llbit.chunky.plugin.PluginApi;
import se.llbit.chunky.plugin.manifest.PluginManifest;

import java.util.function.BiConsumer;

@PluginApi
public interface PluginLoader {
/**
* Load the plugin specified in the manifest
* @param onLoad The consumer to call with the loaded plugin
* @param pluginManifest The plugin to load.
*/
@PluginApi
void load(BiConsumer<Plugin, PluginManifest> onLoad, PluginManifest pluginManifest);
}
Loading

0 comments on commit 0bc1302

Please sign in to comment.