diff --git a/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuild.java b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuild.java new file mode 100644 index 000000000..92ef91ff2 --- /dev/null +++ b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuild.java @@ -0,0 +1,269 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * 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 org.teavm.idea.jps; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.jetbrains.jps.incremental.CompileContext; +import org.jetbrains.jps.incremental.ModuleBuildTarget; +import org.jetbrains.jps.incremental.messages.BuildMessage; +import org.jetbrains.jps.incremental.messages.CompilerMessage; +import org.jetbrains.jps.incremental.messages.ProgressMessage; +import org.jetbrains.jps.model.java.JpsJavaExtensionService; +import org.jetbrains.jps.model.library.JpsLibrary; +import org.jetbrains.jps.model.library.JpsOrderRootType; +import org.jetbrains.jps.model.module.JpsDependencyElement; +import org.jetbrains.jps.model.module.JpsLibraryDependency; +import org.jetbrains.jps.model.module.JpsModule; +import org.jetbrains.jps.model.module.JpsModuleDependency; +import org.teavm.idea.jps.model.TeaVMJpsConfiguration; +import org.teavm.tooling.TeaVMTool; +import org.teavm.tooling.TeaVMToolException; +import org.teavm.tooling.TeaVMToolLog; +import org.teavm.vm.TeaVMPhase; +import org.teavm.vm.TeaVMProgressFeedback; +import org.teavm.vm.TeaVMProgressListener; + +public class TeaVMBuild { + private CompileContext context; + private TeaVMStorageProvider storageProvider = new TeaVMStorageProvider(); + private List classPathEntries = new ArrayList<>(); + private List directoryClassPathEntries; + private TeaVMStorage storage; + + public TeaVMBuild(CompileContext context) { + this.context = context; + } + + public boolean perform(JpsModule module, ModuleBuildTarget target) throws IOException { + storage = context.getProjectDescriptor().dataManager.getStorage(target, storageProvider); + + TeaVMJpsConfiguration config = TeaVMJpsConfiguration.get(module); + if (config == null || !config.isEnabled()) { + return false; + } + + classPathEntries.clear(); + buildClassPath(module, new HashSet<>()); + directoryClassPathEntries = classPathEntries.stream().filter(name -> new File(name).isDirectory()) + .collect(toList()); + + if (!hasChanges(target)) { + return false; + } + + TeaVMTool tool = new TeaVMTool(); + tool.setProgressListener(createProgressListener(context)); + tool.setLog(createLog(context)); + tool.setMainClass(config.getMainClass()); + tool.setSourceMapsFileGenerated(true); + tool.setTargetDirectory(new File(config.getTargetDirectory())); + tool.setClassLoader(buildClassLoader()); + tool.setMinifying(false); + + boolean errorOccurred = false; + try { + tool.generate(); + } catch (TeaVMToolException | RuntimeException | Error e) { + e.printStackTrace(System.err); + context.processMessage(new CompilerMessage("TeaVM", e)); + errorOccurred = true; + } + + if (!errorOccurred && tool.getProblemProvider().getSevereProblems().isEmpty()) { + updateStorage(tool); + } + + return true; + } + + private boolean hasChanges(ModuleBuildTarget target) { + if (!context.getScope().isBuildIncrementally(target.getTargetType()) + || context.getScope().isBuildForced(target)) { + return true; + } + List filesToWatch = storage.getParticipatingFiles(); + if (filesToWatch == null) { + return true; + } + + for (TeaVMStorage.Entry fileToWatch : filesToWatch) { + Long actualTimestamp = getTimestamp(fileToWatch.path); + if (actualTimestamp == null || actualTimestamp > fileToWatch.timestamp) { + return true; + } + } + return false; + } + + private void updateStorage(TeaVMTool tool) { + Set resources = Stream.concat(tool.getClasses().stream().map(cls -> cls.replace('.', '/') + ".class"), + tool.getUsedResources().stream()) + .sorted() + .collect(toSet()); + List participatingFiles = resources.stream() + .map(path -> { + Long timestamp = getTimestamp(path); + return timestamp != null ? new TeaVMStorage.Entry(path, timestamp) : null; + }) + .filter(Objects::nonNull) + .collect(toList()); + storage.setParticipatingFiles(participatingFiles); + } + + private Long getTimestamp(String path) { + for (String classPathEntry : directoryClassPathEntries) { + File file = new File(classPathEntry, path); + if (file.exists()) { + return file.lastModified(); + } + } + return null; + } + + private TeaVMToolLog createLog(CompileContext context) { + return new TeaVMToolLog() { + @Override + public void info(String text) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text)); + } + + @Override + public void debug(String text) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text)); + } + + @Override + public void warning(String text) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.WARNING, text)); + } + + @Override + public void error(String text) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.ERROR, text)); + } + + @Override + public void info(String text, Throwable e) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text + "\n" + + CompilerMessage.getTextFromThrowable(e))); + } + + @Override + public void debug(String text, Throwable e) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text + "\n" + + CompilerMessage.getTextFromThrowable(e))); + } + + @Override + public void warning(String text, Throwable e) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.WARNING, text + "\n" + + CompilerMessage.getTextFromThrowable(e))); + } + + @Override + public void error(String text, Throwable e) { + context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.ERROR, text + "\n" + + CompilerMessage.getTextFromThrowable(e))); + } + }; + } + + private TeaVMProgressListener createProgressListener(CompileContext context) { + return new TeaVMProgressListener() { + private TeaVMPhase currentPhase; + int expectedCount; + + @Override + public TeaVMProgressFeedback phaseStarted(TeaVMPhase phase, int count) { + expectedCount = count; + context.processMessage(new ProgressMessage(phaseName(phase), 0)); + currentPhase = phase; + return context.getCancelStatus().isCanceled() ? TeaVMProgressFeedback.CANCEL + : TeaVMProgressFeedback.CONTINUE; + } + + @Override + public TeaVMProgressFeedback progressReached(int progress) { + context.processMessage(new ProgressMessage(phaseName(currentPhase), (float) progress / expectedCount)); + return context.getCancelStatus().isCanceled() ? TeaVMProgressFeedback.CANCEL + : TeaVMProgressFeedback.CONTINUE; + } + }; + } + + private static String phaseName(TeaVMPhase phase) { + switch (phase) { + case DEPENDENCY_CHECKING: + return "Discovering classes to compile"; + case LINKING: + return "Resolving method invocations"; + case DEVIRTUALIZATION: + return "Eliminating virtual calls"; + case DECOMPILATION: + return "Compiling classes"; + case RENDERING: + return "Building JS file"; + default: + throw new AssertionError(); + } + } + + private ClassLoader buildClassLoader() { + URL[] urls = classPathEntries.stream().map(entry -> { + try { + return new File(entry).toURI().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(entry); + } + }).toArray(URL[]::new); + + return new URLClassLoader(urls, TeaVMBuilder.class.getClassLoader()); + } + + private void buildClassPath(JpsModule module, Set visited) { + if (!visited.add(module)) { + return; + } + File output = JpsJavaExtensionService.getInstance().getOutputDirectory(module, false); + if (output != null) { + classPathEntries.add(output.getPath()); + } + for (JpsDependencyElement dependency : module.getDependenciesList().getDependencies()) { + if (dependency instanceof JpsModuleDependency) { + buildClassPath(((JpsModuleDependency) dependency).getModule(), visited); + } else if (dependency instanceof JpsLibraryDependency) { + JpsLibrary library = ((JpsLibraryDependency) dependency).getLibrary(); + if (library == null) { + continue; + } + classPathEntries.addAll(library.getFiles(JpsOrderRootType.COMPILED).stream().map(File::getPath) + .collect(toList())); + } + } + } +} diff --git a/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuilder.java b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuilder.java index c93b008cb..47c86abaa 100644 --- a/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuilder.java +++ b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMBuilder.java @@ -15,14 +15,7 @@ */ package org.teavm.idea.jps; -import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.jps.ModuleChunk; import org.jetbrains.jps.builders.DirtyFilesHolder; @@ -32,25 +25,11 @@ import org.jetbrains.jps.incremental.CompileContext; import org.jetbrains.jps.incremental.ModuleBuildTarget; import org.jetbrains.jps.incremental.ModuleLevelBuilder; import org.jetbrains.jps.incremental.ProjectBuildException; -import org.jetbrains.jps.incremental.messages.BuildMessage; -import org.jetbrains.jps.incremental.messages.CompilerMessage; -import org.jetbrains.jps.incremental.messages.ProgressMessage; -import org.jetbrains.jps.model.java.JpsJavaExtensionService; -import org.jetbrains.jps.model.library.JpsLibrary; -import org.jetbrains.jps.model.library.JpsOrderRootType; -import org.jetbrains.jps.model.module.JpsDependencyElement; -import org.jetbrains.jps.model.module.JpsLibraryDependency; import org.jetbrains.jps.model.module.JpsModule; -import org.jetbrains.jps.model.module.JpsModuleDependency; -import org.teavm.idea.jps.model.TeaVMJpsConfiguration; -import org.teavm.tooling.TeaVMTool; -import org.teavm.tooling.TeaVMToolException; -import org.teavm.tooling.TeaVMToolLog; -import org.teavm.vm.TeaVMPhase; -import org.teavm.vm.TeaVMProgressFeedback; -import org.teavm.vm.TeaVMProgressListener; public class TeaVMBuilder extends ModuleLevelBuilder { + private TeaVMStorageProvider storageProvider = new TeaVMStorageProvider(); + public TeaVMBuilder() { super(BuilderCategory.CLASS_POST_PROCESSOR); } @@ -60,8 +39,10 @@ public class TeaVMBuilder extends ModuleLevelBuilder { DirtyFilesHolder dirtyFilesHolder, OutputConsumer outputConsumer) throws ProjectBuildException, IOException { boolean doneSomething = false; + + TeaVMBuild build = new TeaVMBuild(context); for (JpsModule module : chunk.getModules()) { - doneSomething |= buildModule(module, context); + doneSomething |= build.perform(module, chunk.representativeTarget()); if (context.getCancelStatus().isCanceled()) { return ExitCode.ABORT; } @@ -70,155 +51,6 @@ public class TeaVMBuilder extends ModuleLevelBuilder { return doneSomething ? ExitCode.OK : ExitCode.NOTHING_DONE; } - private boolean buildModule(JpsModule module, CompileContext context) { - TeaVMJpsConfiguration config = TeaVMJpsConfiguration.get(module); - if (config == null || !config.isEnabled()) { - return false; - } - - TeaVMTool tool = new TeaVMTool(); - tool.setProgressListener(createProgressListener(context)); - tool.setLog(createLog(context)); - tool.setMainClass(config.getMainClass()); - tool.setSourceMapsFileGenerated(true); - tool.setTargetDirectory(new File(config.getTargetDirectory())); - tool.setClassLoader(buildClassLoader(module)); - tool.setMinifying(false); - - try { - tool.generate(); - } catch (TeaVMToolException | RuntimeException | Error e) { - e.printStackTrace(System.err); - context.processMessage(new CompilerMessage("TeaVM", e)); - } - - return true; - } - - private TeaVMToolLog createLog(CompileContext context) { - return new TeaVMToolLog() { - @Override - public void info(String text) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text)); - } - - @Override - public void debug(String text) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text)); - } - - @Override - public void warning(String text) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.WARNING, text)); - } - - @Override - public void error(String text) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.ERROR, text)); - } - - @Override - public void info(String text, Throwable e) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text + "\n" - + CompilerMessage.getTextFromThrowable(e))); - } - - @Override - public void debug(String text, Throwable e) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.INFO, text + "\n" - + CompilerMessage.getTextFromThrowable(e))); - } - - @Override - public void warning(String text, Throwable e) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.WARNING, text + "\n" - + CompilerMessage.getTextFromThrowable(e))); - } - - @Override - public void error(String text, Throwable e) { - context.processMessage(new CompilerMessage("TeaVM", BuildMessage.Kind.ERROR, text + "\n" - + CompilerMessage.getTextFromThrowable(e))); - } - }; - } - - private TeaVMProgressListener createProgressListener(CompileContext context) { - return new TeaVMProgressListener() { - private TeaVMPhase currentPhase; - int expectedCount; - - @Override - public TeaVMProgressFeedback phaseStarted(TeaVMPhase phase, int count) { - expectedCount = count; - context.processMessage(new ProgressMessage(phaseName(phase), 0)); - currentPhase = phase; - return context.getCancelStatus().isCanceled() ? TeaVMProgressFeedback.CANCEL - : TeaVMProgressFeedback.CONTINUE; - } - - @Override - public TeaVMProgressFeedback progressReached(int progress) { - context.processMessage(new ProgressMessage(phaseName(currentPhase), (float) progress / expectedCount)); - return context.getCancelStatus().isCanceled() ? TeaVMProgressFeedback.CANCEL - : TeaVMProgressFeedback.CONTINUE; - } - }; - } - - private static String phaseName(TeaVMPhase phase) { - switch (phase) { - case DEPENDENCY_CHECKING: - return "Discovering classes to compile"; - case LINKING: - return "Resolving method invocations"; - case DEVIRTUALIZATION: - return "Eliminating virtual calls"; - case DECOMPILATION: - return "Compiling classes"; - case RENDERING: - return "Building JS file"; - default: - throw new AssertionError(); - } - } - - private ClassLoader buildClassLoader(JpsModule module) { - Set classPathEntries = new HashSet<>(); - buildClassPath(module, new HashSet<>(), classPathEntries); - - URL[] urls = classPathEntries.stream().map(entry -> { - try { - return new File(entry).toURI().toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(entry); - } - }).toArray(URL[]::new); - - return new URLClassLoader(urls, TeaVMBuilder.class.getClassLoader()); - } - - private void buildClassPath(JpsModule module, Set visited, Set classPathEntries) { - if (!visited.add(module)) { - return; - } - File output = JpsJavaExtensionService.getInstance().getOutputDirectory(module, false); - if (output != null) { - classPathEntries.add(output.getPath()); - } - for (JpsDependencyElement dependency : module.getDependenciesList().getDependencies()) { - if (dependency instanceof JpsModuleDependency) { - buildClassPath(((JpsModuleDependency) dependency).getModule(), visited, classPathEntries); - } else if (dependency instanceof JpsLibraryDependency) { - JpsLibrary library = ((JpsLibraryDependency) dependency).getLibrary(); - if (library == null) { - continue; - } - classPathEntries.addAll(library.getFiles(JpsOrderRootType.COMPILED).stream().map(File::getPath) - .collect(Collectors.toList())); - } - } - } @NotNull @Override diff --git a/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorage.java b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorage.java new file mode 100644 index 000000000..65acbffbb --- /dev/null +++ b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorage.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * 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 org.teavm.idea.jps; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.jps.incremental.storage.StorageOwner; + +public class TeaVMStorage implements StorageOwner { + private File file; + private List participatingFiles; + private boolean dirty; + + TeaVMStorage(File file) throws IOException { + file = new File(file, "teavm.storage"); + this.file = file; + if (file.exists()) { + participatingFiles = new ArrayList<>(); + try (Reader innerReader = new InputStreamReader(new FileInputStream(file), "UTF-8"); + BufferedReader reader = new BufferedReader(innerReader)) { + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + int index = line.lastIndexOf(':'); + if (index < 0) { + participatingFiles = null; + file.delete(); + break; + } + participatingFiles.add(new Entry(line.substring(0, index), + Long.parseLong(line.substring(index + 1)))); + } + } + } + } + + public void setParticipatingFiles(List participatingFiles) { + if (participatingFiles == null) { + this.participatingFiles = null; + } else { + this.participatingFiles = new ArrayList<>(participatingFiles); + } + dirty = true; + } + + public List getParticipatingFiles() { + return participatingFiles != null ? new ArrayList<>(participatingFiles) : null; + } + + @Override + public void flush(boolean b) { + } + + @Override + public void clean() throws IOException { + file.delete(); + participatingFiles = null; + } + + @Override + public void close() throws IOException { + if (dirty) { + if (participatingFiles == null) { + if (file.exists()) { + file.delete(); + } + } else { + try (Writer innerWriter = new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); + BufferedWriter writer = new BufferedWriter(innerWriter)) { + for (Entry participatingFile : participatingFiles) { + writer.append(participatingFile.path + ":" + participatingFile.timestamp); + writer.newLine(); + } + } + } + } + } + + public boolean causesBuild(String file) { + return participatingFiles == null || participatingFiles.contains(file); + } + + public static class Entry { + public final String path; + public final long timestamp; + + public Entry(String path, long timestamp) { + this.path = path; + this.timestamp = timestamp; + } + } +} diff --git a/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorageProvider.java b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorageProvider.java new file mode 100644 index 000000000..df9848bac --- /dev/null +++ b/tools/idea/jps-plugin/src/main/java/org/teavm/idea/jps/TeaVMStorageProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * 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 org.teavm.idea.jps; + +import java.io.File; +import java.io.IOException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.builders.storage.StorageProvider; + +public class TeaVMStorageProvider extends StorageProvider { + @NotNull + @Override + public TeaVMStorage createStorage(File file) throws IOException { + return new TeaVMStorage(file); + } +} diff --git a/tools/idea/jps-plugin/teavm-jps-plugin.iml b/tools/idea/jps-plugin/teavm-jps-plugin.iml index 4aa591fa7..29b643292 100644 --- a/tools/idea/jps-plugin/teavm-jps-plugin.iml +++ b/tools/idea/jps-plugin/teavm-jps-plugin.iml @@ -9,7 +9,7 @@ - + diff --git a/tools/idea/teavm-idea-plugin.iml b/tools/idea/teavm-idea-plugin.iml index 33bdc0471..656bd4099 100644 --- a/tools/idea/teavm-idea-plugin.iml +++ b/tools/idea/teavm-idea-plugin.iml @@ -10,7 +10,7 @@ - +