diff --git a/jps/jps-plugin/jps-plugin.iml b/jps/jps-plugin/jps-plugin.iml index 6878e4f6683..23016075fe8 100644 --- a/jps/jps-plugin/jps-plugin.iml +++ b/jps/jps-plugin/jps-plugin.iml @@ -12,7 +12,7 @@ - + diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java new file mode 100644 index 00000000000..f2329867d68 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ComparatorUtil; +import com.sampullara.cli.Argument; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +public class ArgumentUtils { + private ArgumentUtils() {} + + @NotNull + public static List convertArgumentsToStringList(@NotNull CommonCompilerArguments arguments) + throws InstantiationException, IllegalAccessException { + List result = new ArrayList(); + convertArgumentsToStringList(arguments, arguments.getClass().newInstance(), arguments.getClass(), result); + result.addAll(arguments.freeArgs); + return result; + } + + private static void convertArgumentsToStringList( + @NotNull CommonCompilerArguments arguments, + @NotNull CommonCompilerArguments defaultArguments, + @NotNull Class clazz, + @NotNull List result + ) throws IllegalAccessException, InstantiationException { + for (Field field : clazz.getDeclaredFields()) { + Argument argument = field.getAnnotation(Argument.class); + if (argument == null) continue; + + Object value; + Object defaultValue; + try { + value = field.get(arguments); + defaultValue = field.get(defaultArguments); + } + catch (IllegalAccessException ignored) { + // skip this field + continue; + } + + if (ComparatorUtil.equalsNullable(value, defaultValue)) continue; + + String name = getAlias(argument); + if (name == null) { + name = getName(argument, field); + } + + Class fieldType = field.getType(); + + if (fieldType.isArray()) { + Object[] values = (Object[]) value; + if (values.length == 0) continue; + //noinspection unchecked + value = StringUtil.join(values, Function.TO_STRING, argument.delimiter()); + } + + result.add(argument.prefix() + name); + + if (fieldType == boolean.class || fieldType == Boolean.class) continue; + + result.add(value.toString()); + } + + Class superClazz = clazz.getSuperclass(); + if (superClazz != null) { + convertArgumentsToStringList(arguments, defaultArguments, superClazz, result); + } + } + + private static String getAlias(Argument argument) { + String alias = argument.alias(); + return alias.isEmpty() ? null : alias; + } + + private static String getName(Argument argument, Field field) { + String name = argument.value(); + return name.isEmpty() ? field.getName() : name; + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerEnvironment.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerEnvironment.java new file mode 100644 index 00000000000..c4df720924d --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerEnvironment.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.cli.common.messages.MessageCollector; +import org.jetbrains.kotlin.config.Services; +import org.jetbrains.kotlin.preloading.ClassCondition; +import org.jetbrains.kotlin.utils.KotlinPaths; +import org.jetbrains.kotlin.utils.PathUtil; + +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation.NO_LOCATION; +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR; + +public class CompilerEnvironment { + @NotNull + public static CompilerEnvironment getEnvironmentFor( + @NotNull KotlinPaths kotlinPaths, + @NotNull ClassCondition classesToLoadByParent, + @NotNull Services compilerServices + ) { + return new CompilerEnvironment(kotlinPaths, classesToLoadByParent, compilerServices); + } + + private final KotlinPaths kotlinPaths; + private final ClassCondition classesToLoadByParent; + private final Services services; + + private CompilerEnvironment( + @NotNull KotlinPaths kotlinPaths, + @NotNull ClassCondition classesToLoadByParent, + @NotNull Services services + ) { + this.kotlinPaths = kotlinPaths; + this.classesToLoadByParent = classesToLoadByParent; + this.services = services; + } + + public boolean success() { + return kotlinPaths.getHomePath().exists(); + } + + @NotNull + public KotlinPaths getKotlinPaths() { + return kotlinPaths; + } + + @NotNull + public ClassCondition getClassesToLoadByParent() { + return classesToLoadByParent; + } + + public void reportErrorsTo(@NotNull MessageCollector messageCollector) { + if (!kotlinPaths.getHomePath().exists()) { + messageCollector.report(ERROR, "Cannot find kotlinc home: " + kotlinPaths.getHomePath() + ". Make sure plugin is properly installed, " + + "or specify " + PathUtil.JPS_KOTLIN_HOME_PROPERTY + " system property", NO_LOCATION); + } + } + + @NotNull + public Services getServices() { + return services; + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerOutputParser.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerOutputParser.java new file mode 100644 index 00000000000..e5bb3d4ea3d --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerOutputParser.java @@ -0,0 +1,188 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Stack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +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.common.messages.OutputMessageUtil; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.IOException; +import java.io.Reader; +import java.util.Map; + +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation.NO_LOCATION; +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*; +import static org.jetbrains.kotlin.cli.common.messages.MessageCollectorUtil.reportException; + +public class CompilerOutputParser { + public static void parseCompilerMessagesFromReader(MessageCollector messageCollector, final Reader reader, OutputItemsCollector collector) { + // Sometimes the compiler doesn't output valid XML. + // Example: error in command line arguments passed to the compiler. + // The compiler will print the usage and the SAX parser will break. + // In this case, we want to read everything from this stream and report it as an IDE error. + final StringBuilder stringBuilder = new StringBuilder(); + Reader wrappingReader = new Reader() { + @Override + public int read(@NotNull char[] cbuf, int off, int len) throws IOException { + int read = reader.read(cbuf, off, len); + stringBuilder.append(cbuf, off, len); + return read; + } + + @Override + public void close() throws IOException { + // Do nothing: + // If the SAX parser sees a syntax error, it throws an exception + // and calls close() on the reader. + // We prevent hte reader from being closed here, and close it later, + // when all the text is read from it + } + }; + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser parser = factory.newSAXParser(); + parser.parse(new InputSource(wrappingReader), new CompilerOutputSAXHandler(messageCollector, collector)); + } + catch (Throwable e) { + // Load all the text into the stringBuilder + try { + // This will not close the reader (see the wrapper above) + FileUtil.loadTextAndClose(wrappingReader); + } + catch (IOException ioException) { + reportException(messageCollector, ioException); + } + String message = stringBuilder.toString(); + reportException(messageCollector, new IllegalStateException(message, e)); + messageCollector.report(ERROR, message, NO_LOCATION); + } + finally { + try { + reader.close(); + } + catch (IOException e) { + reportException(messageCollector, e); + } + } + } + + private static class CompilerOutputSAXHandler extends DefaultHandler { + private static final Map CATEGORIES = new ContainerUtil.ImmutableMapBuilder() + .put("error", ERROR) + .put("warning", WARNING) + .put("logging", LOGGING) + .put("output", OUTPUT) + .put("exception", EXCEPTION) + .put("info", INFO) + .put("messages", INFO) // Root XML element + .build(); + + private final MessageCollector messageCollector; + private final OutputItemsCollector collector; + + private final StringBuilder message = new StringBuilder(); + private final Stack tags = new Stack(); + private String path; + private int line; + private int column; + + public CompilerOutputSAXHandler(MessageCollector messageCollector, OutputItemsCollector collector) { + this.messageCollector = messageCollector; + this.collector = collector; + } + + @Override + public void startElement(@NotNull String uri, @NotNull String localName, @NotNull String qName, @NotNull Attributes attributes) + throws SAXException { + tags.push(qName); + + message.setLength(0); + + path = attributes.getValue("path"); + line = safeParseInt(attributes.getValue("line"), -1); + column = safeParseInt(attributes.getValue("column"), -1); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (tags.size() == 1) { + // We're directly inside the root tag: + String message = new String(ch, start, length); + if (!message.trim().isEmpty()) { + messageCollector.report(ERROR, "Unhandled compiler output: " + message, NO_LOCATION); + } + } + else { + message.append(ch, start, length); + } + } + + @Override + public void endElement(String uri, @NotNull String localName, @NotNull String qName) throws SAXException { + if (tags.size() == 1) { + // We're directly inside the root tag: + return; + } + String qNameLowerCase = qName.toLowerCase(); + CompilerMessageSeverity category = CATEGORIES.get(qNameLowerCase); + if (category == null) { + messageCollector.report(ERROR, "Unknown compiler message tag: " + qName, NO_LOCATION); + category = INFO; + } + String text = message.toString(); + + if (category == OUTPUT) { + reportToCollector(text); + } + else { + messageCollector.report(category, text, CompilerMessageLocation.create(path, line, column)); + } + tags.pop(); + } + + private void reportToCollector(String text) { + OutputMessageUtil.Output output = OutputMessageUtil.parseOutputMessage(text); + if (output != null) { + collector.add(output.sourceFiles, output.outputFile); + } + } + + private static int safeParseInt(@Nullable String value, int defaultValue) { + if (value == null) { + return defaultValue; + } + try { + return Integer.parseInt(value.trim()); + } + catch (NumberFormatException e) { + return defaultValue; + } + } + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerRunnerUtil.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerRunnerUtil.java new file mode 100644 index 00000000000..75036a84842 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/CompilerRunnerUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.cli.common.messages.MessageCollector; +import org.jetbrains.kotlin.preloading.ClassPreloadingUtils; +import org.jetbrains.kotlin.utils.KotlinPaths; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.ref.SoftReference; +import java.lang.reflect.Method; +import java.util.Collections; + +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation.NO_LOCATION; +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR; + +public class CompilerRunnerUtil { + + private static SoftReference ourClassLoaderRef = new SoftReference(null); + + @NotNull + private static synchronized ClassLoader getOrCreateClassLoader( + @NotNull CompilerEnvironment environment, + @NotNull File libPath + ) throws IOException { + ClassLoader classLoader = ourClassLoaderRef.get(); + if (classLoader == null) { + classLoader = ClassPreloadingUtils.preloadClasses( + Collections.singletonList(new File(libPath, "kotlin-compiler.jar")), + /* estimatedClassNumber = */ 4096, + CompilerRunnerUtil.class.getClassLoader(), + environment.getClassesToLoadByParent() + ); + ourClassLoaderRef = new SoftReference(classLoader); + } + return classLoader; + } + + @Nullable + private static File getLibPath(@NotNull KotlinPaths paths, @NotNull MessageCollector messageCollector) { + File libs = paths.getLibPath(); + if (libs.exists() && !libs.isFile()) return libs; + + messageCollector.report( + ERROR, + "Broken compiler at '" + libs.getAbsolutePath() + "'. Make sure plugin is properly installed", + NO_LOCATION + ); + + return null; + } + + @Nullable + public static Object invokeExecMethod( + @NotNull String compilerClassName, + @NotNull String[] arguments, + @NotNull CompilerEnvironment environment, + @NotNull MessageCollector messageCollector, + @NotNull PrintStream out + ) throws Exception { + File libPath = getLibPath(environment.getKotlinPaths(), messageCollector); + if (libPath == null) return null; + + ClassLoader classLoader = getOrCreateClassLoader(environment, libPath); + + Class kompiler = Class.forName(compilerClassName, true, classLoader); + Method exec = kompiler.getMethod( + "execAndOutputXml", + PrintStream.class, + Class.forName("org.jetbrains.kotlin.config.Services", true, classLoader), + String[].class + ); + + return exec.invoke(kompiler.newInstance(), out, environment.getServices(), arguments); + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.java new file mode 100644 index 00000000000..22515bce97f --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/KotlinCompilerRunner.java @@ -0,0 +1,176 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.Accessor; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.cli.common.ExitCode; +import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments; +import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments; +import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; +import org.jetbrains.kotlin.cli.common.messages.MessageCollector; +import org.jetbrains.kotlin.cli.common.messages.MessageCollectorUtil; +import org.jetbrains.kotlin.config.CompilerSettings; + +import java.io.*; +import java.util.Collection; +import java.util.List; + +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation.NO_LOCATION; +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR; +import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.INFO; + +public class KotlinCompilerRunner { + private static final String K2JVM_COMPILER = "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler"; + private static final String K2JS_COMPILER = "org.jetbrains.kotlin.cli.js.K2JSCompiler"; + private static final String INTERNAL_ERROR = ExitCode.INTERNAL_ERROR.toString(); + + public static void runK2JvmCompiler( + CommonCompilerArguments commonArguments, + K2JVMCompilerArguments k2jvmArguments, + CompilerSettings compilerSettings, + MessageCollector messageCollector, + CompilerEnvironment environment, + File moduleFile, + OutputItemsCollector collector + ) { + K2JVMCompilerArguments arguments = mergeBeans(commonArguments, k2jvmArguments); + setupK2JvmArguments(moduleFile, arguments); + + runCompiler(K2JVM_COMPILER, arguments, compilerSettings.getAdditionalArguments(), messageCollector, collector, environment); + } + + public static void runK2JsCompiler( + CommonCompilerArguments commonArguments, + K2JSCompilerArguments k2jsArguments, + CompilerSettings compilerSettings, + MessageCollector messageCollector, + CompilerEnvironment environment, + OutputItemsCollector collector, + Collection sourceFiles, + List libraryFiles, + File outputFile + ) { + K2JSCompilerArguments arguments = mergeBeans(commonArguments, k2jsArguments); + setupK2JsArguments(outputFile, sourceFiles, libraryFiles, arguments); + + runCompiler(K2JS_COMPILER, arguments, compilerSettings.getAdditionalArguments(), messageCollector, collector, environment); + } + + private static void runCompiler( + String compilerClassName, + CommonCompilerArguments arguments, + String additionalArguments, + MessageCollector messageCollector, + OutputItemsCollector collector, + CompilerEnvironment environment + ) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(stream); + + String exitCode = execCompiler(compilerClassName, arguments, additionalArguments, environment, out, messageCollector); + + BufferedReader reader = new BufferedReader(new StringReader(stream.toString())); + CompilerOutputParser.parseCompilerMessagesFromReader(messageCollector, reader, collector); + + if (INTERNAL_ERROR.equals(exitCode)) { + messageCollector.report(ERROR, "Compiler terminated with internal error", NO_LOCATION); + } + } + + @NotNull + private static String execCompiler( + String compilerClassName, + CommonCompilerArguments arguments, + String additionalArguments, + CompilerEnvironment environment, + PrintStream out, + MessageCollector messageCollector + ) { + try { + messageCollector.report(INFO, "Using kotlin-home = " + environment.getKotlinPaths().getHomePath(), NO_LOCATION); + + List argumentsList = ArgumentUtils.convertArgumentsToStringList(arguments); + argumentsList.addAll(StringUtil.split(additionalArguments, " ")); + + Object rc = CompilerRunnerUtil.invokeExecMethod( + compilerClassName, ArrayUtil.toStringArray(argumentsList), environment, messageCollector, out + ); + + // exec() returns an ExitCode object, class of which is loaded with a different class loader, + // so we take it's contents through reflection + return getReturnCodeFromObject(rc); + } + catch (Throwable e) { + MessageCollectorUtil.reportException(messageCollector, e); + return INTERNAL_ERROR; + } + } + + @NotNull + private static String getReturnCodeFromObject(@Nullable Object rc) throws Exception { + if (rc == null) { + return INTERNAL_ERROR; + } + else if (ExitCode.class.getName().equals(rc.getClass().getName())) { + return rc.toString(); + } + else { + throw new IllegalStateException("Unexpected return: " + rc); + } + } + + private static T mergeBeans(F from, T to) { + T copy = XmlSerializerUtil.createCopy(to); + + for (Accessor accessor : XmlSerializerUtil.getAccessors(from.getClass())) { + accessor.write(copy, accessor.read(from)); + } + + return copy; + } + + private static void setupK2JvmArguments(File moduleFile, K2JVMCompilerArguments settings) { + settings.module = moduleFile.getAbsolutePath(); + settings.noStdlib = true; + settings.noJdkAnnotations = true; + settings.noJdk = true; + } + + private static void setupK2JsArguments( + File outputFile, + Collection sourceFiles, + List libraryFiles, + K2JSCompilerArguments settings + ) { + settings.noStdlib = true; + settings.freeArgs = ContainerUtil.map(sourceFiles, new Function() { + @Override + public String fun(File file) { + return file.getPath(); + } + }); + settings.outputFile = outputFile.getPath(); + settings.libraryFiles = ArrayUtil.toStringArray(libraryFiles); + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollector.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollector.java new file mode 100644 index 00000000000..a05fe540e47 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollector.java @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import java.io.File; +import java.util.Collection; + +public interface OutputItemsCollector { + void add(Collection sourceFiles, File outputFile); +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollectorImpl.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollectorImpl.java new file mode 100644 index 00000000000..b2b40cd1811 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/OutputItemsCollectorImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +public class OutputItemsCollectorImpl implements OutputItemsCollector { + private final List outputs = ContainerUtil.newArrayList(); + + @Override + public void add(Collection sourceFiles, File outputFile) { + outputs.add(new SimpleOutputItem(sourceFiles, outputFile)); + } + + @NotNull + public List getOutputs() { + return outputs; + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/SimpleOutputItem.java b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/SimpleOutputItem.java new file mode 100644 index 00000000000..ed9c716f7cb --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/compilerRunner/SimpleOutputItem.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.compilerRunner; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Collection; + +public class SimpleOutputItem { + private final Collection sourceFiles; + private final File outputFile; + + public SimpleOutputItem(@NotNull Collection sourceFiles, @NotNull File outputFile) { + this.sourceFiles = sourceFiles; + this.outputFile = outputFile; + } + + @NotNull + public Collection getSourceFiles() { + return sourceFiles; + } + + @NotNull + public File getOutputFile() { + return outputFile; + } + + @Override + public String toString() { + return sourceFiles + " -> " + outputFile; + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt b/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt index 1989aa73f36..ac2a1951f6a 100644 --- a/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinBuilder.kt @@ -24,7 +24,7 @@ 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.compilerRunner.CompilerEnvironment -import org.jetbrains.kotlin.compilerRunner.CompilerRunnerConstants +import org.jetbrains.kotlin.config.CompilerRunnerConstants import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.config.IncrementalCompilation @@ -43,7 +43,7 @@ import java.io.File import java.util.* import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation.NO_LOCATION import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.* -import org.jetbrains.kotlin.compilerRunner.CompilerRunnerConstants.INTERNAL_ERROR_PREFIX +import org.jetbrains.kotlin.config.CompilerRunnerConstants.INTERNAL_ERROR_PREFIX import org.jetbrains.kotlin.compilerRunner.KotlinCompilerRunner.runK2JsCompiler import org.jetbrains.kotlin.compilerRunner.KotlinCompilerRunner.runK2JvmCompiler import org.jetbrains.kotlin.utils.keysToMap diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilder.java b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilder.java new file mode 100644 index 00000000000..43722755430 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.modules; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface KotlinModuleDescriptionBuilder { + KotlinModuleDescriptionBuilder addModule( + String moduleName, + String outputDir, + List sourceFiles, + List javaSourceRoots, + Collection classpathRoots, + List annotationRoots, + boolean tests, + Set directoriesToFilterOut + ); + + CharSequence asText(); +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilderFactory.java b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilderFactory.java new file mode 100644 index 00000000000..322c75dfbd0 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleDescriptionBuilderFactory.java @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.modules; + +public interface KotlinModuleDescriptionBuilderFactory { + KotlinModuleDescriptionBuilder create(); + String getFileExtension(); +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleScriptBuilderFactory.java b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleScriptBuilderFactory.java new file mode 100644 index 00000000000..83b5ef59715 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleScriptBuilderFactory.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.modules; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static com.intellij.openapi.util.io.FileUtil.toSystemIndependentName; + +public class KotlinModuleScriptBuilderFactory implements KotlinModuleDescriptionBuilderFactory { + + public static final KotlinModuleScriptBuilderFactory INSTANCE = new KotlinModuleScriptBuilderFactory(); + + private KotlinModuleScriptBuilderFactory() {} + + @Override + public KotlinModuleDescriptionBuilder create() { + return new Builder(); + } + + @Override + public String getFileExtension() { + return "kts"; + } + + private static class Builder implements KotlinModuleDescriptionBuilder { + private final StringBuilder script = new StringBuilder(); + private boolean done = false; + + { + script.append("import kotlin.modules.*\n"); + script.append("fun project() {\n"); + } + + @Override + public KotlinModuleDescriptionBuilder addModule( + String moduleName, + String outputDir, + List sourceFiles, + List javaSourceRoots, + Collection classpathRoots, + List annotationRoots, + boolean tests, + Set directoriesToFilterOut + ) { + assert !done : "Already done"; + + if (tests) { + script.append("// Module script for tests\n"); + } + else { + script.append("// Module script for production\n"); + } + + script.append(" module(\"" + moduleName + "\", outputDir = \"" + toSystemIndependentName(outputDir) + "\") {\n"); + + for (File sourceFile : sourceFiles) { + script.append(" sources += \"" + toSystemIndependentName(sourceFile.getPath()) + "\"\n"); + } + + if (!javaSourceRoots.isEmpty()) { + processClassPathSection("Java source roots", javaSourceRoots, directoriesToFilterOut); + } + + processClassPathSection("Classpath", classpathRoots, directoriesToFilterOut); + processAnnotationRoots(annotationRoots); + + script.append(" }\n"); + return this; + } + + private void processClassPathSection( + @NotNull String sectionDescription, + @NotNull Collection files, + @NotNull Set directoriesToFilterOut + ) { + script.append(" // " + sectionDescription + "\n"); + for (File file : files) { + if (directoriesToFilterOut.contains(file)) { + // For IDEA's make (incremental compilation) purposes, output directories of the current module and its dependencies + // appear on the class path, so we are at risk of seeing the results of the previous build, i.e. if some class was + // removed in the sources, it may still be there in binaries. Thus, we delete these entries from the classpath. + script.append(" // Output directory, commented out\n"); + script.append(" // "); + } + script.append(" classpath += \"" + toSystemIndependentName(file.getPath()) + "\"\n"); + } + } + + private void processAnnotationRoots(@NotNull List files) { + script.append(" // External annotations\n"); + for (File file : files) { + script.append(" annotationsPath += \"").append(toSystemIndependentName(file.getPath())).append("\"\n"); + } + } + + @Override + public CharSequence asText() { + if (!done) { + script.append("}\n"); + done = true; + } + + return script; + } + } +} diff --git a/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleXmlBuilderFactory.java b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleXmlBuilderFactory.java new file mode 100644 index 00000000000..8ecbca258c3 --- /dev/null +++ b/jps/jps-plugin/src/org/jetbrains/kotlin/modules/KotlinModuleXmlBuilderFactory.java @@ -0,0 +1,154 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.modules; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.config.IncrementalCompilation; +import org.jetbrains.kotlin.utils.Printer; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static com.intellij.openapi.util.io.FileUtil.toSystemIndependentName; +import static com.intellij.openapi.util.text.StringUtil.escapeXml; +import static org.jetbrains.kotlin.cli.common.modules.ModuleXmlParser.*; + +public class KotlinModuleXmlBuilderFactory implements KotlinModuleDescriptionBuilderFactory { + + public static final KotlinModuleXmlBuilderFactory INSTANCE = new KotlinModuleXmlBuilderFactory(); + + private KotlinModuleXmlBuilderFactory() {} + + @Override + public KotlinModuleDescriptionBuilder create() { + return new Builder(); + } + + @Override + public String getFileExtension() { + return "xml"; + } + + private static class Builder implements KotlinModuleDescriptionBuilder { + private final StringBuilder xml = new StringBuilder(); + private final Printer p = new Printer(xml); + private boolean done = false; + + public Builder() { + openTag(p, MODULES); + } + + @Override + public KotlinModuleDescriptionBuilder addModule( + String moduleName, + String outputDir, + List sourceFiles, + List javaSourceRoots, + Collection classpathRoots, + List annotationRoots, + boolean tests, + Set directoriesToFilterOut + ) { + assert !done : "Already done"; + + if (tests) { + p.println(""); + } + else { + p.println(""); + } + + p.println("<", MODULE, " ", + NAME, "=\"", escapeXml(moduleName), "\" ", + OUTPUT_DIR, "=\"", getEscapedPath(new File(outputDir)), "\">" + ); + p.pushIndent(); + + for (File sourceFile : sourceFiles) { + p.println("<", SOURCES, " ", PATH, "=\"", getEscapedPath(sourceFile), "\"/>"); + } + + if (!javaSourceRoots.isEmpty()) { + processClassPathSection("Java source roots", javaSourceRoots, directoriesToFilterOut); + } + + processClassPathSection("Classpath", classpathRoots, directoriesToFilterOut); + processAnnotationRoots(annotationRoots); + + closeTag(p, MODULE); + return this; + } + + private void processClassPathSection( + @NotNull String sectionDescription, + @NotNull Collection files, + @NotNull Set directoriesToFilterOut + ) { + p.println(""); + for (File file : files) { + boolean isOutput = directoriesToFilterOut.contains(file) && !IncrementalCompilation.ENABLED; + if (isOutput) { + // For IDEA's make (incremental compilation) purposes, output directories of the current module and its dependencies + // appear on the class path, so we are at risk of seeing the results of the previous build, i.e. if some class was + // removed in the sources, it may still be there in binaries. Thus, we delete these entries from the classpath. + p.println(""); + p.println(""); + } + } + } + + private void processAnnotationRoots(@NotNull List files) { + p.println(""); + for (File file : files) { + p.println("<", EXTERNAL_ANNOTATIONS, " ", PATH, "=\"", getEscapedPath(file), "\"/>"); + } + } + + @Override + public CharSequence asText() { + if (!done) { + closeTag(p, MODULES); + done = true; + } + return xml; + } + } + + private static void openTag(Printer p, String tag) { + p.println("<" + tag + ">"); + p.pushIndent(); + } + + private static void closeTag(Printer p, String tag) { + p.popIndent(); + p.println(""); + } + + private static String getEscapedPath(File sourceFile) { + return escapeXml(toSystemIndependentName(sourceFile.getPath())); + } +} diff --git a/jps/jps-plugin/test/org/jetbrains/kotlin/jvm/compiler/ClasspathOrderTest.kt b/jps/jps-plugin/test/org/jetbrains/kotlin/jvm/compiler/ClasspathOrderTest.kt new file mode 100644 index 00000000000..7652295c2f9 --- /dev/null +++ b/jps/jps-plugin/test/org/jetbrains/kotlin/jvm/compiler/ClasspathOrderTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.jvm.compiler + +import org.jetbrains.kotlin.test.TestCaseWithTmpdir +import org.jetbrains.kotlin.test.JetTestUtils +import org.jetbrains.kotlin.test.MockLibraryUtil +import org.jetbrains.kotlin.modules.KotlinModuleXmlBuilderFactory +import java.io.File +import org.jetbrains.kotlin.utils.PathUtil + +/** + * This test checks that Java classes from sources have higher priority in Kotlin resolution process than classes from binaries. + * To test this, we compile a Kotlin+Java module (in two modes: CLI and module-based) where a runtime Java class was replaced + * with a "newer" version in sources, and check that this class resolves to the one from sources by calling a method absent in the runtime + */ +public class ClasspathOrderTest : TestCaseWithTmpdir() { + class object { + val sourceDir = File(JetTestUtils.getTestDataPathBase() + "/classpathOrder").getAbsoluteFile() + } + + public fun testClasspathOrderForCLI() { + MockLibraryUtil.compileKotlin(sourceDir.getPath(), tmpdir) + } + + public fun testClasspathOrderForModuleScriptBuild() { + val xmlContent = KotlinModuleXmlBuilderFactory.INSTANCE.create().addModule( + "name", + File(tmpdir, "output").getAbsolutePath(), + listOf(sourceDir), + listOf(sourceDir), + listOf(PathUtil.getKotlinPathsForDistDirectory().getRuntimePath()), + listOf(), + false, + setOf() + ).asText().toString() + + val xml = File(tmpdir, "module.xml") + xml.writeText(xmlContent) + + MockLibraryUtil.compileKotlinModule(xml.getAbsolutePath()) + } +} \ No newline at end of file diff --git a/jps/jps-plugin/test/org/jetbrains/kotlin/modules/KotlinModuleXmlGeneratorTest.java b/jps/jps-plugin/test/org/jetbrains/kotlin/modules/KotlinModuleXmlGeneratorTest.java new file mode 100644 index 00000000000..e024e504827 --- /dev/null +++ b/jps/jps-plugin/test/org/jetbrains/kotlin/modules/KotlinModuleXmlGeneratorTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * 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.jetbrains.kotlin.modules; + +import junit.framework.TestCase; +import org.jetbrains.kotlin.test.JetTestUtils; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; + +public class KotlinModuleXmlGeneratorTest extends TestCase { + public void testBasic() throws Exception { + String actual = KotlinModuleXmlBuilderFactory.INSTANCE.create().addModule( + "name", + "output", + Arrays.asList(new File("s1"), new File("s2")), + Collections.singletonList(new File("java")), + Arrays.asList(new File("cp1"), new File("cp2")), + Arrays.asList(new File("a1/f1"), new File("a2")), + false, + Collections.emptySet() + ).asText().toString(); + JetTestUtils.assertEqualsToFile(new File("idea/testData/modules.xml/basic.xml"), actual); + } + + public void testFiltered() throws Exception { + String actual = KotlinModuleXmlBuilderFactory.INSTANCE.create().addModule( + "name", + "output", + Arrays.asList(new File("s1"), new File("s2")), + Collections.emptyList(), + Arrays.asList(new File("cp1"), new File("cp2")), + Arrays.asList(new File("a1/f1"), new File("a2")), + false, + Collections.singleton(new File("cp1")) + ).asText().toString(); + JetTestUtils.assertEqualsToFile(new File("idea/testData/modules.xml/filtered.xml"), actual); + } + + public void testMultiple() throws Exception { + KotlinModuleDescriptionBuilder builder = KotlinModuleXmlBuilderFactory.INSTANCE.create(); + builder.addModule( + "name", + "output", + Arrays.asList(new File("s1"), new File("s2")), + Collections.emptyList(), + Arrays.asList(new File("cp1"), new File("cp2")), + Arrays.asList(new File("a1/f1"), new File("a2")), + false, + Collections.singleton(new File("cp1")) + ); + builder.addModule( + "name2", + "output2", + Arrays.asList(new File("s12"), new File("s22")), + Collections.emptyList(), + Arrays.asList(new File("cp12"), new File("cp22")), + Arrays.asList(new File("a12/f12"), new File("a22")), + true, + Collections.singleton(new File("cp12")) + ); + String actual = builder.asText().toString(); + JetTestUtils.assertEqualsToFile(new File("idea/testData/modules.xml/multiple.xml"), actual); + } +}