JPS plugin runs only when one of significant classes change

This commit is contained in:
Alexey Andreev 2016-04-15 00:21:24 +03:00
parent e557a25a04
commit da8382271f
6 changed files with 427 additions and 175 deletions

View File

@ -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<String> classPathEntries = new ArrayList<>();
private List<String> 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<TeaVMStorage.Entry> 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<String> resources = Stream.concat(tool.getClasses().stream().map(cls -> cls.replace('.', '/') + ".class"),
tool.getUsedResources().stream())
.sorted()
.collect(toSet());
List<TeaVMStorage.Entry> 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<JpsModule> 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()));
}
}
}
}

View File

@ -15,14 +15,7 @@
*/ */
package org.teavm.idea.jps; package org.teavm.idea.jps;
import java.io.File;
import java.io.IOException; 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.annotations.NotNull;
import org.jetbrains.jps.ModuleChunk; import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.builders.DirtyFilesHolder; 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.ModuleBuildTarget;
import org.jetbrains.jps.incremental.ModuleLevelBuilder; import org.jetbrains.jps.incremental.ModuleLevelBuilder;
import org.jetbrains.jps.incremental.ProjectBuildException; 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.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 { public class TeaVMBuilder extends ModuleLevelBuilder {
private TeaVMStorageProvider storageProvider = new TeaVMStorageProvider();
public TeaVMBuilder() { public TeaVMBuilder() {
super(BuilderCategory.CLASS_POST_PROCESSOR); super(BuilderCategory.CLASS_POST_PROCESSOR);
} }
@ -60,8 +39,10 @@ public class TeaVMBuilder extends ModuleLevelBuilder {
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
OutputConsumer outputConsumer) throws ProjectBuildException, IOException { OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
boolean doneSomething = false; boolean doneSomething = false;
TeaVMBuild build = new TeaVMBuild(context);
for (JpsModule module : chunk.getModules()) { for (JpsModule module : chunk.getModules()) {
doneSomething |= buildModule(module, context); doneSomething |= build.perform(module, chunk.representativeTarget());
if (context.getCancelStatus().isCanceled()) { if (context.getCancelStatus().isCanceled()) {
return ExitCode.ABORT; return ExitCode.ABORT;
} }
@ -70,155 +51,6 @@ public class TeaVMBuilder extends ModuleLevelBuilder {
return doneSomething ? ExitCode.OK : ExitCode.NOTHING_DONE; 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<String> 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<JpsModule> visited, Set<String> 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 @NotNull
@Override @Override

View File

@ -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<Entry> 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<Entry> participatingFiles) {
if (participatingFiles == null) {
this.participatingFiles = null;
} else {
this.participatingFiles = new ArrayList<>(participatingFiles);
}
dirty = true;
}
public List<Entry> 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;
}
}
}

View File

@ -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<TeaVMStorage> {
@NotNull
@Override
public TeaVMStorage createStorage(File file) throws IOException {
return new TeaVMStorage(file);
}
}

View File

@ -9,7 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="jdk" jdkName="IntelliJ IDEA IU-144.3600.7" jdkType="IDEA JDK" /> <orderEntry type="jdk" jdkName="IntelliJ IDEA IU-145.844.1" jdkType="IDEA JDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.ow2.asm:asm-debug-all:5.0.4" level="project" /> <orderEntry type="library" name="Maven: org.ow2.asm:asm-debug-all:5.0.4" level="project" />
<orderEntry type="library" name="teavm-all" level="project" /> <orderEntry type="library" name="teavm-all" level="project" />

View File

@ -10,7 +10,7 @@
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="jdk" jdkName="IntelliJ IDEA IU-144.3600.7" jdkType="IDEA JDK" /> <orderEntry type="jdk" jdkName="IntelliJ IDEA IU-145.844.1" jdkType="IDEA JDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="teavm-jps-plugin" /> <orderEntry type="module" module-name="teavm-jps-plugin" />
<orderEntry type="library" name="teavm-all" level="project" /> <orderEntry type="library" name="teavm-all" level="project" />