diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index 3d6ff691b9..b88f2b5729 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -24,6 +24,7 @@ */ package net.runelite.client; +import com.google.common.base.Strings; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -43,6 +44,7 @@ public class RuneLiteProperties private static final String PATREON_LINK = "runelite.patreon.link"; private static final String LAUNCHER_VERSION_PROPERTY = "launcher.version"; private static final String PLUGIN_PATH = "plugin.path"; + private static final String PLUGIN_DEVELOPMENT_PATH = "plugin.development.path"; private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link"; private static final String BUILDING_LINK = "runelite.wiki.building.link"; private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; @@ -146,6 +148,20 @@ public class RuneLiteProperties return pluginPath.equals("") ? null : pluginPath; } + public static String[] getPluginDevelopmentPath() + { + // First check if property supplied as environment variable PLUGIN_DEVELOPMENT_PATHS + String developmentPluginPaths = System.getenv(PLUGIN_DEVELOPMENT_PATH.replace('.', '_').toUpperCase()); + + if (Strings.isNullOrEmpty(developmentPluginPaths)) + { + // Otherwise check the property file + developmentPluginPaths = properties.getProperty(PLUGIN_DEVELOPMENT_PATH); + } + + return Strings.isNullOrEmpty(developmentPluginPaths) ? new String[0] : developmentPluginPaths.split(";"); + } + public static String getImgurClientId() { return properties.getProperty(IMGUR_CLIENT_ID); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java index 99c7ceef9a..a4666b4e81 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java @@ -32,6 +32,7 @@ import com.google.inject.Key; import com.google.inject.Module; import java.io.Closeable; import java.io.File; +import java.io.FileFilter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; @@ -40,6 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -80,8 +82,13 @@ import net.runelite.client.util.Groups; import net.runelite.client.util.MiscUtils; import net.runelite.client.util.SwingUtil; import org.jgroups.Message; +import org.pf4j.BasePluginLoader; +import org.pf4j.CompoundPluginLoader; +import org.pf4j.CompoundPluginRepository; import org.pf4j.DefaultPluginManager; import org.pf4j.DependencyResolver; +import org.pf4j.DevelopmentPluginClasspath; +import org.pf4j.DevelopmentPluginRepository; import org.pf4j.JarPluginLoader; import org.pf4j.JarPluginRepository; import org.pf4j.ManifestPluginDescriptorFinder; @@ -114,6 +121,8 @@ public class ExternalPluginManager private final EventBus eventBus; private final ConfigManager configManager; private final Map pluginsMap = new HashMap<>(); + @Getter + private final boolean developmentMode = RuneLiteProperties.getPluginDevelopmentPath().length > 0; @Getter(AccessLevel.PUBLIC) private final Map> pluginsInfoMap = new HashMap<>(); private final Groups groups; @@ -153,45 +162,182 @@ public class ExternalPluginManager @Override protected PluginDescriptorFinder createPluginDescriptorFinder() { - return new ManifestPluginDescriptorFinder(); - } - - @Override - protected PluginRepository createPluginRepository() - { - return new JarPluginRepository(getPluginsRoot()) + return new ManifestPluginDescriptorFinder() { - @Override - public List getPluginPaths() + protected Path getManifestPath(Path pluginPath) { - File[] files = pluginsRoot.toFile().listFiles(filter); - - if ((files == null) || files.length == 0) + if (isDevelopment()) { - return Collections.emptyList(); + // The superclass performs a find, which is slow in development mode since we're pointing + // at a sources directory, which can have a lot of files. The external plugin template + // will always output the manifest at the following location, so we can hardcode this path. + return pluginPath.resolve("build/tmp/jar/MANIFEST.MF"); } - List paths = new ArrayList<>(files.length); - for (File file : files) - { - paths.add(file.toPath()); - } - - return paths; + return super.getManifestPath(pluginPath); } }; } @Override - protected PluginLoader createPluginLoader() + protected PluginRepository createPluginRepository() { - return new JarPluginLoader(this); + CompoundPluginRepository compoundPluginRepository = new CompoundPluginRepository(); + + if (isNotDevelopment()) + { + JarPluginRepository jarPluginRepository = new JarPluginRepository(getPluginsRoot()) + { + @Override + public List getPluginPaths() + { + File[] files = pluginsRoot.toFile().listFiles(filter); + + if ((files == null) || files.length == 0) + { + return Collections.emptyList(); + } + + List paths = new ArrayList<>(files.length); + for (File file : files) + { + paths.add(file.toPath()); + } + + return paths; + } + }; + + compoundPluginRepository.add(jarPluginRepository); + } + + if (isDevelopment()) + { + FileFilter pluginsFilter = new FileFilter() + { + private final List blacklist = Arrays.asList( + ".git", + "build", + "target" + ); + + private final List buildFiles = Arrays.asList( + "%s.gradle.kts", + "%s.gradle" + ); + + @Override + public boolean accept(File pathName) + { + // Check if this path looks like a plugin development directory + if (!pathName.isDirectory()) + { + return false; + } + + String dirName = pathName.getName(); + if (blacklist.contains(dirName)) + { + return false; + } + + boolean isPlugin = false; + + // By convention plugins their directory is $name and they have a $name.gradle.kts or $name.gradle file in their root + for (String buildFile : buildFiles) + { + if (new File(pathName, String.format(buildFile, dirName)).exists()) + { + isPlugin = true; + break; + } + } + + // It is a plugin directory, but we should also check if it can actually be loaded + if (!new File(pathName, "build/tmp/jar/MANIFEST.MF").exists()) + { + return false; + } + + return isPlugin; + } + }; + + for (String developmentPluginPath : RuneLiteProperties.getPluginDevelopmentPath()) + { + DevelopmentPluginRepository developmentPluginRepository = new DevelopmentPluginRepository(Paths.get(developmentPluginPath)) + { + @Override + public boolean deletePluginPath(Path pluginPath) + { + // Do nothing, because we'd be deleting our sources! + return filter.accept(pluginPath.toFile()); + } + }; + + developmentPluginRepository.setFilter(pluginsFilter); + compoundPluginRepository.add(developmentPluginRepository); + } + } + + return compoundPluginRepository; } @Override - public RuntimeMode getRuntimeMode() + protected PluginLoader createPluginLoader() { - return RuneLiteProperties.getLauncherVersion() == null ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; + return new CompoundPluginLoader() + .add(new BasePluginLoader(this, new DevelopmentPluginClasspath().addJarsDirectories("build/deps")), this::isDevelopment) + .add(new JarPluginLoader(this), this::isNotDevelopment); + } + + @Override + public void loadPlugins() + { + if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot)) + { + log.warn("No '{}' root", pluginsRoot); + return; + } + + List pluginPaths = pluginRepository.getPluginPaths(); + + if (pluginPaths.isEmpty()) + { + log.warn("No plugins"); + return; + } + + log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths); + + for (Path pluginPath : pluginPaths) + { + try + { + loadPluginFromPath(pluginPath); + } + catch (PluginRuntimeException e) + { + if (!(e instanceof PluginAlreadyLoadedException)) + { + log.error("Could not load plugin {}", pluginPath, e); + } + } + } + + try + { + resolvePlugins(); + } + catch (PluginRuntimeException e) + { + if (e instanceof DependencyResolver.DependenciesNotFoundException) + { + throw e; + } + + log.error(e.getMessage(), e); + } } @Override @@ -258,52 +404,9 @@ public class ExternalPluginManager } @Override - public void loadPlugins() + public RuntimeMode getRuntimeMode() { - if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot)) - { - log.warn("No '{}' root", pluginsRoot); - return; - } - - List pluginPaths = pluginRepository.getPluginPaths(); - - if (pluginPaths.isEmpty()) - { - log.warn("No plugins"); - return; - } - - log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths); - - for (Path pluginPath : pluginPaths) - { - try - { - loadPluginFromPath(pluginPath); - } - catch (PluginRuntimeException e) - { - if (!(e instanceof PluginAlreadyLoadedException)) - { - log.error(e.getMessage(), e); - } - } - } - - try - { - resolvePlugins(); - } - catch (PluginRuntimeException e) - { - if (e instanceof DependencyResolver.DependenciesNotFoundException) - { - throw e; - } - - log.error(e.getMessage(), e); - } + return developmentMode ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; } @Override @@ -1038,30 +1141,39 @@ public class ExternalPluginManager return true; } - // Null version returns the last release version of this plugin for given system version try { - PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId); - - if (latest == null) + if (!developmentMode) { - try + PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId); + + // Null version returns the last release version of this plugin for given system version + if (latest == null) { - SwingUtil.syncExec(() -> - JOptionPane.showMessageDialog(ClientUI.getFrame(), - pluginId + " is outdated and cannot be installed", - "Installation error", - JOptionPane.ERROR_MESSAGE)); - } - catch (InvocationTargetException | InterruptedException ignored) - { - return false; + try + { + SwingUtil.syncExec(() -> + JOptionPane.showMessageDialog(ClientUI.getFrame(), + pluginId + " is outdated and cannot be installed", + "Installation error", + JOptionPane.ERROR_MESSAGE)); + } + catch (InvocationTargetException | InterruptedException ignored) + { + return false; + } + + return true; } - return true; + updateManager.installPlugin(pluginId, null); + } + else + { + // In development mode our plugin will already be present in a repository, so we can just load it + externalPluginManager.loadPlugins(); + externalPluginManager.startPlugin(pluginId); } - - updateManager.installPlugin(pluginId, null); scanAndInstantiate(loadPlugin(pluginId), true, true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java index b744861106..b3143c1f38 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -147,32 +147,37 @@ public class PluginListItem extends JPanel String pluginId = pluginInfo.get("id"); hotSwapButton.setIcon(REFRESH_ICON); - externalPluginManager.uninstall(pluginId); - SwingWorker worker = new SwingWorker<>() + new SwingWorker<>() { @Override protected Boolean doInBackground() { return externalPluginManager.uninstall(pluginId); } - }; - worker.execute(); - JOptionPane.showMessageDialog(ClientUI.getFrame(), - pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`", - "Hotswap " + pluginId, - JOptionPane.INFORMATION_MESSAGE); - - worker = new SwingWorker<>() - { @Override - protected Boolean doInBackground() + protected void done() { - return externalPluginManager.reloadStart(pluginId); + // In development mode our plugins will be loaded directly from sources, so we don't need to prompt + if (!externalPluginManager.isDevelopmentMode()) + { + JOptionPane.showMessageDialog(ClientUI.getFrame(), + pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`", + "Hotswap " + pluginId, + JOptionPane.INFORMATION_MESSAGE); + } + + new SwingWorker<>() + { + @Override + protected Boolean doInBackground() + { + return externalPluginManager.reloadStart(pluginId); + } + }.execute(); } - }; - worker.execute(); + }.execute(); }); hotSwapButton.setVisible(true);