package liveplugin.pluginrunner; import clojure.lang.*; import clojure.lang.Compiler; import com.intellij.util.Function; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import static liveplugin.MyFileUtil.*; import static liveplugin.pluginrunner.PluginRunner.ClasspathAddition.createClassLoaderWithDependencies; import static liveplugin.pluginrunner.PluginRunner.ClasspathAddition.findClasspathAdditions; import static liveplugin.pluginrunner.PluginRunner.ClasspathAddition.findPluginDependencies; /** * This class should not be loaded unless clojure libs are on classpath. */ public class ClojurePluginRunner implements PluginRunner { public static final String MAIN_SCRIPT = "plugin.clj"; private static final String CLOJURE_ADD_TO_CLASSPATH_KEYWORD = "; " + ADD_TO_CLASSPATH_KEYWORD; private static final String CLOJURE_DEPENDS_ON_PLUGIN_KEYWORD = "; " + DEPENDS_ON_PLUGIN_KEYWORD; private static boolean initialized; private final ErrorReporter errorReporter; private final Map<String, String> environment; public ClojurePluginRunner(ErrorReporter errorReporter, Map<String, String> environment) { this.errorReporter = errorReporter; this.environment = environment; } @Override public boolean canRunPlugin(String pathToPluginFolder) { return findScriptFileIn(pathToPluginFolder, MAIN_SCRIPT) != null; } @Override public void runPlugin(final String pathToPluginFolder, final String pluginId, final Map<String, ?> binding, Function<Runnable, Void> runOnEDTCallback) { if (!initialized) { // need this to avoid "java.lang.IllegalStateException: Attempting to call unbound fn: #'clojure.core/refer" // use classloader of RunPluginAction assuming that clojure was first initialized from it // (see https://groups.google.com/forum/#!topic/clojure/F3ERon6Fye0) Thread.currentThread().setContextClassLoader(RunPluginAction.class.getClassLoader()); // need to initialize RT before Compiler, otherwise Compiler initialization fails with NPE RT.init(); initialized = true; } final File scriptFile = findScriptFileIn(pathToPluginFolder, MAIN_SCRIPT); assert scriptFile != null; final List<String> dependentPlugins = new ArrayList<>(); final List<String> additionalPaths = new ArrayList<>(); try { environment.put("PLUGIN_PATH", pathToPluginFolder); dependentPlugins.addAll(findPluginDependencies(readLines(asUrl(scriptFile)), CLOJURE_DEPENDS_ON_PLUGIN_KEYWORD)); additionalPaths.addAll(findClasspathAdditions(readLines(asUrl(scriptFile)), CLOJURE_ADD_TO_CLASSPATH_KEYWORD, environment, path -> { errorReporter.addLoadingError(pluginId, "Couldn't find dependency '" + path + "'"); return null; })); } catch (IOException e) { errorReporter.addLoadingError(pluginId, "Error reading script file: " + scriptFile); return; } final ClassLoader classLoader = createClassLoaderWithDependencies(additionalPaths, dependentPlugins, asUrl(scriptFile), pluginId, errorReporter); runOnEDTCallback.fun(() -> { try { Associative bindings = Var.getThreadBindings(); for (Map.Entry<String, ?> entry : binding.entrySet()) { Var key = createKey("*" + entry.getKey() + "*"); bindings = bindings.assoc(key, entry.getValue()); } bindings = bindings.assoc(Compiler.LOADER, classLoader); Var.pushThreadBindings(bindings); // assume that clojure Compile is thread-safe Compiler.loadFile(scriptFile.getAbsolutePath()); } catch (IOException e) { errorReporter.addLoadingError(pluginId, "Error reading script file: " + scriptFile); } catch (LinkageError e) { errorReporter.addLoadingError(pluginId, "Error linking script file: " + scriptFile); } catch (Error e) { errorReporter.addLoadingError(pluginId, e); } catch (Exception e) { errorReporter.addRunningError(pluginId, e); } finally { Var.popThreadBindings(); } }); } @Override public String scriptName() { return MAIN_SCRIPT; } private static Var createKey(String name) { return Var.intern(Namespace.findOrCreate(Symbol.intern("clojure.core")), Symbol.intern(name), "no_" + name).setDynamic(); } }