package liveplugin.pluginrunner; import com.intellij.ide.ui.laf.IntelliJLaf; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.project.Project; import com.intellij.util.Function; import kotlin.jvm.internal.Reflection; import liveplugin.toolwindow.settingsmenu.languages.DownloadKotlinCompilerLib; import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; import org.jetbrains.kotlin.cli.common.messages.MessageCollector; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler; import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot; import org.jetbrains.kotlin.cli.jvm.config.JvmContentRootsKt; import org.jetbrains.kotlin.codegen.CompilationException; import org.jetbrains.kotlin.codegen.GeneratedClassLoader; import org.jetbrains.kotlin.codegen.state.GenerationState; import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer; import org.jetbrains.kotlin.config.CompilerConfiguration; import org.jetbrains.kotlin.config.KotlinSourceRoot; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.psi.KtScript; import org.jetbrains.kotlin.script.KotlinScriptDefinition; import org.jetbrains.kotlin.utils.PathUtil; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import static java.util.Arrays.asList; import static liveplugin.LivePluginAppComponent.LIVEPLUGIN_LIBS_PATH; import static liveplugin.MyFileUtil.*; import static liveplugin.pluginrunner.PluginRunner.ClasspathAddition.*; import static org.jetbrains.kotlin.cli.common.CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY; import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR; import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.EXCEPTION; import static org.jetbrains.kotlin.cli.common.messages.MessageRenderer.PLAIN_FULL_PATHS; import static org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles.JVM_CONFIG_FILES; import static org.jetbrains.kotlin.config.CommonConfigurationKeys.MODULE_NAME; import static org.jetbrains.kotlin.config.JVMConfigurationKeys.*; public class KotlinPluginRunner implements PluginRunner { public static final String MAIN_SCRIPT = "plugin.kts"; private static final String KOTLIN_ADD_TO_CLASSPATH_KEYWORD = "// " + ADD_TO_CLASSPATH_KEYWORD; private static final String KOTLIN_DEPENDS_ON_PLUGIN_KEYWORD = "// " + DEPENDS_ON_PLUGIN_KEYWORD; private final ErrorReporter errorReporter; private final Map<String, String> environment; public KotlinPluginRunner(ErrorReporter errorReporter, Map<String, String> environment) { this.errorReporter = errorReporter; this.environment = environment; } @Override public String scriptName() { return MAIN_SCRIPT; } @Override public boolean canRunPlugin(String pathToPluginFolder) { return findScriptFileIn(pathToPluginFolder, MAIN_SCRIPT) != null; } @Override public void runPlugin(String pathToPluginFolder, String pluginId, Map<String, ?> binding, Function<Runnable, Void> runOnEDTCallback) { org.jetbrains.kotlin.com.intellij.openapi.Disposable rootDisposable = Disposer.newDisposable(); try { String mainScriptUrl = asUrl(findScriptFileIn(pathToPluginFolder, MAIN_SCRIPT)); List<String> dependentPlugins = findPluginDependencies(readLines(mainScriptUrl), KOTLIN_DEPENDS_ON_PLUGIN_KEYWORD); List<String> pathsToAdd = findClasspathAdditions(readLines(mainScriptUrl), KOTLIN_ADD_TO_CLASSPATH_KEYWORD, environment, path -> { errorReporter.addLoadingError(pluginId, "Couldn't find dependency '" + path + "'"); return null; }); String pluginFolderUrl = "file:///" + pathToPluginFolder + "/"; // prefix with "file:///" so that unix-like path works on windows pathsToAdd.add(pluginFolderUrl); CompilerConfiguration configuration = createConfiguration(pathToPluginFolder, pluginId, errorReporter); // TODO if (saveClassesDir != null) { // configuration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, saveClassesDir) // } environment.put("PLUGIN_PATH", pathToPluginFolder); KotlinCoreEnvironment environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, JVM_CONFIG_FILES); KotlinToJVMBytecodeCompiler compiler = KotlinToJVMBytecodeCompiler.INSTANCE; GenerationState state = compiler.analyzeAndGenerate(environment); if (state == null) return; ClassLoader classLoader = createClassLoaderWithDependencies(pathsToAdd, dependentPlugins, mainScriptUrl, pluginId, errorReporter); GeneratedClassLoader generatedClassLoader = new GeneratedClassLoader(state.getFactory(), classLoader); for (KtFile ktFile : environment.getSourceFiles()) { if (ktFile.getName().equals(MAIN_SCRIPT)) { KtScript ktScript = ktFile.getScript(); assert ktScript != null; Class<?> aClass = generatedClassLoader.loadClass(ktScript.getFqName().asString()); runOnEDTCallback.fun(() -> { try { // Arguments below must match constructor of KotlinScriptTemplate class. // There doesn't seem to be a way to add binding as Map, therefore, hardcoding them. aClass.getConstructors()[0].newInstance( (Project) binding.get("project"), (Boolean) binding.get("isIdeStartup"), (String) binding.get("pluginPath"), (Disposable) binding.get("pluginDisposable") ); } catch (Exception e) { errorReporter.addRunningError(pluginId, e); } }); } } } catch (IOException e) { errorReporter.addLoadingError(pluginId, "Error creating scripting engine. " + e.getMessage()); } catch (CompilationException | ClassNotFoundException e) { errorReporter.addLoadingError(pluginId, "Error compiling script. " + e.getMessage()); } catch (Throwable e) { errorReporter.addLoadingError(pluginId, "Internal error compiling script. " + e.getMessage()); } finally { rootDisposable.dispose(); } } @NotNull private static CompilerConfiguration createConfiguration(String pathToPluginFolder, String pluginId, final ErrorReporter errorReporter) { MessageCollector messageCollector = new MessageCollector() { boolean hasErrors = false; @Override public void report(@NotNull CompilerMessageSeverity severity, @NotNull String message, @NotNull CompilerMessageLocation location) { if (severity == ERROR || severity == EXCEPTION) { errorReporter.addLoadingError(pluginId, PLAIN_FULL_PATHS.render(severity, message, location)); hasErrors = true; } } @Override public boolean hasErrors() { return hasErrors; } @Override public void clear() {} }; CompilerConfiguration configuration = new CompilerConfiguration(); configuration.put(MESSAGE_COLLECTOR_KEY, messageCollector); configuration.put(MODULE_NAME, "LivePluginScript"); JvmContentRootsKt.addJvmClasspathRoots(configuration, PathUtil.getJdkClassesRoots()); configuration.add(CONTENT_ROOTS, new KotlinSourceRoot(pathToPluginFolder)); configuration.add(SCRIPT_DEFINITIONS, new KotlinScriptDefinition(Reflection.createKotlinClass(KotlinScriptTemplate.class))); configuration.put(RETAIN_OUTPUT_IN_MEMORY, false); String ideaJarPath = PathManager.getJarPathForClass(IntelliJLaf.class); assert ideaJarPath != null; File[] ijLibFiles = new File(ideaJarPath).getParentFile().listFiles(); assert ijLibFiles != null; asList(ijLibFiles).forEach(file -> configuration.add(CONTENT_ROOTS, new JvmClasspathRoot(file))); for (String fileName : fileNamesMatching(DownloadKotlinCompilerLib.LIB_FILES_PATTERN, LIVEPLUGIN_LIBS_PATH)) { configuration.add(CONTENT_ROOTS, new JvmClasspathRoot(new File(LIVEPLUGIN_LIBS_PATH + "/" + fileName))); } String ideLibFolderPath = new File(ideaJarPath).getParentFile().getAbsolutePath(); for (String fileName : fileNamesMatching("kotlin-.*jar", ideLibFolderPath)) { configuration.add(CONTENT_ROOTS, new JvmClasspathRoot(new File(ideLibFolderPath + "/" + fileName))); } configuration.add(CONTENT_ROOTS, new JvmClasspathRoot(new File(LIVEPLUGIN_LIBS_PATH))); configuration.add(CONTENT_ROOTS, new JvmClasspathRoot(new File(PathManager.getPluginsPath() + "/LivePlugin/classes"))); // TODO add other plugins jars? return configuration; } }