Converted KotlinBuilder from Java to Kotlin

This commit is contained in:
Evgeny Gerashchenko
2014-08-20 14:31:02 +04:00
parent ccc4e1c767
commit 75ffc0f5ff
9 changed files with 377 additions and 406 deletions
@@ -3,7 +3,12 @@
name='com.intellij.openapi.util.Conditions com.intellij.openapi.util.Condition<T> or(com.intellij.openapi.util.Condition<T>, com.intellij.openapi.util.Condition<T>)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.openapi.util.Key com.intellij.openapi.util.Key&lt;T&gt; create(java.lang.String)'>
<item name='com.intellij.openapi.util.Key T get(com.intellij.openapi.util.UserDataHolder, T)'>
<annotation name='kotlin.jvm.KotlinSignature'>
<val name="value" val="&quot;fun get(holder: UserDataHolder?, defaultValue: T): T&quot;"/>
</annotation>
</item>
<item name='com.intellij.openapi.util.Key com.intellij.openapi.util.Key&lt;T&gt; create(java.lang.String)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.openapi.util.Pair A getFirst()'>
@@ -0,0 +1,11 @@
<root>
<item name='org.jetbrains.jps.ModuleChunk java.util.Set&lt;org.jetbrains.jps.incremental.ModuleBuildTarget&gt; getTargets()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.jetbrains.jps.ModuleChunk java.util.Set&lt;org.jetbrains.jps.model.module.JpsModule&gt; getModules()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='org.jetbrains.jps.ModuleChunk org.jetbrains.jps.incremental.ModuleBuildTarget representativeTarget()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -0,0 +1,5 @@
<root>
<item name='org.jetbrains.jps.cmdline.ProjectDescriptor dataManager'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -0,0 +1,25 @@
<root>
<item name='org.jetbrains.jps.incremental.CompileContext org.jetbrains.jps.cmdline.ProjectDescriptor getProjectDescriptor()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='org.jetbrains.jps.incremental.ModuleLevelBuilder org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode build(org.jetbrains.jps.incremental.CompileContext, org.jetbrains.jps.ModuleChunk, org.jetbrains.jps.builders.DirtyFilesHolder&lt;org.jetbrains.jps.builders.java.JavaSourceRootDescriptor,org.jetbrains.jps.incremental.ModuleBuildTarget&gt;, org.jetbrains.jps.incremental.ModuleLevelBuilder.OutputConsumer)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='org.jetbrains.jps.incremental.ModuleLevelBuilder org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode build(org.jetbrains.jps.incremental.CompileContext, org.jetbrains.jps.ModuleChunk, org.jetbrains.jps.builders.DirtyFilesHolder&lt;org.jetbrains.jps.builders.java.JavaSourceRootDescriptor,org.jetbrains.jps.incremental.ModuleBuildTarget&gt;, org.jetbrains.jps.incremental.ModuleLevelBuilder.OutputConsumer) 0'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='org.jetbrains.jps.incremental.ModuleLevelBuilder org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode build(org.jetbrains.jps.incremental.CompileContext, org.jetbrains.jps.ModuleChunk, org.jetbrains.jps.builders.DirtyFilesHolder&lt;org.jetbrains.jps.builders.java.JavaSourceRootDescriptor,org.jetbrains.jps.incremental.ModuleBuildTarget&gt;, org.jetbrains.jps.incremental.ModuleLevelBuilder.OutputConsumer) 1'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='org.jetbrains.jps.incremental.ModuleLevelBuilder org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode build(org.jetbrains.jps.incremental.CompileContext, org.jetbrains.jps.ModuleChunk, org.jetbrains.jps.builders.DirtyFilesHolder&lt;org.jetbrains.jps.builders.java.JavaSourceRootDescriptor,org.jetbrains.jps.incremental.ModuleBuildTarget&gt;, org.jetbrains.jps.incremental.ModuleLevelBuilder.OutputConsumer) 2'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='org.jetbrains.jps.incremental.ModuleLevelBuilder org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode build(org.jetbrains.jps.incremental.CompileContext, org.jetbrains.jps.ModuleChunk, org.jetbrains.jps.builders.DirtyFilesHolder&lt;org.jetbrains.jps.builders.java.JavaSourceRootDescriptor,org.jetbrains.jps.incremental.ModuleBuildTarget&gt;, org.jetbrains.jps.incremental.ModuleLevelBuilder.OutputConsumer) 3'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -31,6 +31,7 @@ import static org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.ERRO
public final class CompilerEnvironment {
@NotNull
public static CompilerEnvironment getEnvironmentFor(
@NotNull KotlinPaths kotlinPaths,
@Nullable File outputDir,
@@ -16,6 +16,8 @@
package org.jetbrains.jet.compiler.runner;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.Collection;
@@ -23,15 +25,17 @@ public class SimpleOutputItem {
private final Collection<File> sourceFiles;
private final File outputFile;
public SimpleOutputItem(Collection<File> sourceFiles, File outputFile) {
public SimpleOutputItem(@NotNull Collection<File> sourceFiles, @NotNull File outputFile) {
this.sourceFiles = sourceFiles;
this.outputFile = outputFile;
}
@NotNull
public Collection<File> getSourceFiles() {
return sourceFiles;
}
@NotNull
public File getOutputFile() {
return outputFile;
}
@@ -1,404 +0,0 @@
/*
* Copyright 2010-2013 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.jet.jps.build;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.cli.common.KotlinVersion;
import org.jetbrains.jet.cli.common.arguments.CommonCompilerArguments;
import org.jetbrains.jet.cli.common.arguments.K2JSCompilerArguments;
import org.jetbrains.jet.cli.common.arguments.K2JVMCompilerArguments;
import org.jetbrains.jet.cli.common.messages.CompilerMessageLocation;
import org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity;
import org.jetbrains.jet.cli.common.messages.MessageCollector;
import org.jetbrains.jet.compiler.CompilerSettings;
import org.jetbrains.jet.compiler.runner.CompilerEnvironment;
import org.jetbrains.jet.compiler.runner.CompilerRunnerConstants;
import org.jetbrains.jet.compiler.runner.OutputItemsCollectorImpl;
import org.jetbrains.jet.compiler.runner.SimpleOutputItem;
import org.jetbrains.jet.config.IncrementalCompilation;
import org.jetbrains.jet.config.Services;
import org.jetbrains.jet.jps.JpsKotlinCompilerSettings;
import org.jetbrains.jet.jps.incremental.IncrementalCacheImpl;
import org.jetbrains.jet.jps.incremental.IncrementalCacheProviderImpl;
import org.jetbrains.jet.jps.incremental.IncrementalCacheStorageProvider;
import org.jetbrains.jet.lang.resolve.kotlin.incremental.cache.IncrementalCacheProvider;
import org.jetbrains.jet.preloading.ClassCondition;
import org.jetbrains.jet.utils.PathUtil;
import org.jetbrains.jet.utils.UtilsPackage;
import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
import org.jetbrains.jps.incremental.*;
import org.jetbrains.jps.incremental.java.JavaBuilder;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.storage.BuildDataManager;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.module.JpsModule;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import static org.jetbrains.jet.cli.common.messages.CompilerMessageLocation.NO_LOCATION;
import static org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.*;
import static org.jetbrains.jet.compiler.runner.CompilerRunnerConstants.INTERNAL_ERROR_PREFIX;
import static org.jetbrains.jet.compiler.runner.KotlinCompilerRunner.runK2JsCompiler;
import static org.jetbrains.jet.compiler.runner.KotlinCompilerRunner.runK2JvmCompiler;
public class KotlinBuilder extends ModuleLevelBuilder {
private static final Key<Set<File>> ALL_COMPILED_FILES_KEY = Key.create("_all_kotlin_compiled_files_");
private static final Key<Set<ModuleBuildTarget>> PROCESSED_TARGETS_WITH_REMOVED_FILES = Key.create("_processed_targets_with_removed_files_");
public static final String KOTLIN_BUILDER_NAME = "Kotlin Builder";
private static final List<String> COMPILABLE_FILE_EXTENSIONS = Collections.singletonList("kt");
private static final Function<JpsModule,String> MODULE_NAME = new Function<JpsModule, String>() {
@Override
public String fun(JpsModule module) {
return module.getName();
}
};
protected KotlinBuilder() {
super(BuilderCategory.SOURCE_PROCESSOR);
}
@NotNull
@Override
public String getPresentableName() {
return KOTLIN_BUILDER_NAME;
}
@Override
public ExitCode build(
CompileContext context,
ModuleChunk chunk,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
OutputConsumer outputConsumer
) throws ProjectBuildException, IOException {
MessageCollector messageCollector = new MessageCollectorAdapter(context);
// Workaround for Android Studio
if (!isJavaPluginEnabled(context)) {
messageCollector.report(INFO, "Kotlin JPS plugin is disabled", NO_LOCATION);
return ExitCode.NOTHING_DONE;
}
ModuleBuildTarget representativeTarget = chunk.representativeTarget();
// For non-incremental build: take all sources
if (!dirtyFilesHolder.hasDirtyFiles() && !dirtyFilesHolder.hasRemovedFiles()) {
return ExitCode.NOTHING_DONE;
}
boolean hasKotlinFiles = hasKotlinDirtyOrRemovedFiles(dirtyFilesHolder, chunk);
if (!hasKotlinFiles) {
return ExitCode.NOTHING_DONE;
}
messageCollector.report(INFO, "Kotlin JPS plugin version " + KotlinVersion.VERSION, NO_LOCATION);
File outputDir = representativeTarget.getOutputDir();
BuildDataManager dataManager = context.getProjectDescriptor().dataManager;
Map<ModuleBuildTarget, IncrementalCacheImpl> incrementalCaches = UtilsPackage.newHashMapWithExpectedSize(chunk.getTargets().size());
for (ModuleBuildTarget target : chunk.getTargets()) {
incrementalCaches.put(target, dataManager.getStorage(target, IncrementalCacheStorageProvider.INSTANCE$));
}
Services compilerServices = new Services.Builder()
.register(IncrementalCacheProvider.class, new IncrementalCacheProviderImpl(incrementalCaches))
.build();
CompilerEnvironment environment = CompilerEnvironment.getEnvironmentFor(
PathUtil.getKotlinPathsForJpsPluginOrJpsTests(), outputDir, getClass().getClassLoader(), new ClassCondition() {
@Override
public boolean accept(String className) {
return className.startsWith("org.jetbrains.jet.lang.resolve.kotlin.incremental.cache.") ||
className.equals("org.jetbrains.jet.config.Services");
}
}, compilerServices);
if (!environment.success()) {
environment.reportErrorsTo(messageCollector);
return ExitCode.ABORT;
}
assert outputDir != null : "CompilerEnvironment must have checked for outputDir to be not null, but it didn't";
OutputItemsCollectorImpl outputItemCollector = new OutputItemsCollectorImpl();
JpsProject project = representativeTarget.getModule().getProject();
CommonCompilerArguments commonArguments = JpsKotlinCompilerSettings.getCommonCompilerArguments(project);
commonArguments.verbose = true; // Make compiler report source to output files mapping
CompilerSettings compilerSettings = JpsKotlinCompilerSettings.getCompilerSettings(project);
final Set<File> allCompiledFiles = getAllCompiledFilesContainer(context);
if (JpsUtils.isJsKotlinModule(representativeTarget)) {
if (chunk.getModules().size() > 1) {
// We do not support circular dependencies, but if they are present, we do our best should not break the build,
// so we simply yield a warning and report NOTHING_DONE
messageCollector.report(
WARNING, "Circular dependencies are not supported. " +
"The following JS modules depend on each other: " + StringUtil.join(chunk.getModules(), MODULE_NAME, ", ") + ". " +
"Kotlin is not compiled for these modules",
NO_LOCATION);
return ExitCode.NOTHING_DONE;
}
Collection<File> sourceFiles = KotlinSourceFileCollector.getAllKotlinSourceFiles(representativeTarget);
//List<File> sourceFiles = KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder);
if (sourceFiles.isEmpty()) {
return ExitCode.NOTHING_DONE;
}
File outputFile = new File(outputDir, representativeTarget.getModule().getName() + ".js");
List<String> libraryFiles = JpsJsModuleUtils.getLibraryFilesAndDependencies(representativeTarget);
K2JSCompilerArguments k2JsArguments = JpsKotlinCompilerSettings.getK2JsCompilerArguments(project);
runK2JsCompiler(commonArguments, k2JsArguments, compilerSettings, messageCollector, environment,
outputItemCollector, sourceFiles, libraryFiles, outputFile);
}
else {
if (chunk.getModules().size() > 1) {
messageCollector.report(
WARNING, "Circular dependencies are only partially supported. " +
"The following modules depend on each other: " + StringUtil.join(chunk.getModules(), MODULE_NAME, ", ") + ". " +
"Kotlin will compile them, but some strange effect may happen",
NO_LOCATION);
}
MultiMap<ModuleBuildTarget, File> filesToCompile = KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder);
for (ModuleBuildTarget target : filesToCompile.keySet()) {
filesToCompile.getModifiable(target).removeAll(allCompiledFiles);
}
allCompiledFiles.addAll(filesToCompile.values());
Set<ModuleBuildTarget> processedTargetsWithRemoved = getProcessedTargetsWithRemovedFilesContainer(context);
boolean haveRemovedFiles = false;
for (ModuleBuildTarget target : chunk.getTargets()) {
if (!KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, target).isEmpty()) {
if (processedTargetsWithRemoved.add(target)) {
haveRemovedFiles = true;
}
}
}
File moduleFile = KotlinBuilderModuleScriptGenerator
.generateModuleDescription(context, chunk, filesToCompile, haveRemovedFiles);
if (moduleFile == null) {
// No Kotlin sources found
return ExitCode.NOTHING_DONE;
}
K2JVMCompilerArguments k2JvmArguments = JpsKotlinCompilerSettings.getK2JvmCompilerArguments(project);
runK2JvmCompiler(commonArguments, k2JvmArguments, compilerSettings, messageCollector, environment,
moduleFile, outputItemCollector);
moduleFile.delete();
}
// If there's only one target, this map is empty: get() always returns null, and the representativeTarget will be used below
Map<File, ModuleBuildTarget> sourceToTarget = new HashMap<File, ModuleBuildTarget>();
if (chunk.getTargets().size() > 1) {
for (ModuleBuildTarget target : chunk.getTargets()) {
for (File file : KotlinSourceFileCollector.getAllKotlinSourceFiles(target)) {
sourceToTarget.put(file, target);
}
}
}
for (ModuleBuildTarget target : chunk.getTargets()) {
incrementalCaches.get(target).clearCacheForRemovedFiles(
KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, target), target.getOutputDir());
}
IncrementalCacheImpl.RecompilationDecision recompilationDecision = IncrementalCacheImpl.RecompilationDecision.DO_NOTHING;
for (SimpleOutputItem outputItem : outputItemCollector.getOutputs()) {
ModuleBuildTarget target = null;
Collection<File> sourceFiles = outputItem.getSourceFiles();
if (!sourceFiles.isEmpty()) {
target = sourceToTarget.get(sourceFiles.iterator().next());
}
if (target == null) {
target = representativeTarget;
}
File outputFile = outputItem.getOutputFile();
if (IncrementalCompilation.ENABLED) {
IncrementalCacheImpl.RecompilationDecision newDecision = incrementalCaches.get(target).saveFileToCache(sourceFiles, outputFile);
recompilationDecision = recompilationDecision.merge(newDecision);
}
outputConsumer.registerOutputFile(target, outputFile, paths(sourceFiles));
}
if (IncrementalCompilation.ENABLED) {
if (recompilationDecision == IncrementalCacheImpl.RecompilationDecision.RECOMPILE_ALL) {
allCompiledFiles.clear();
return ExitCode.CHUNK_REBUILD_REQUIRED;
}
if (recompilationDecision == IncrementalCacheImpl.RecompilationDecision.COMPILE_OTHERS) {
// TODO should mark dependencies as dirty, as well
FSOperations.markDirty(context, chunk, new FileFilter() {
@Override
public boolean accept(@NotNull File file) {
return !allCompiledFiles.contains(file);
}
});
}
return ExitCode.ADDITIONAL_PASS_REQUIRED;
}
else {
return ExitCode.OK;
}
}
private static Set<File> getAllCompiledFilesContainer(CompileContext context) {
Set<File> allCompiledFiles = ALL_COMPILED_FILES_KEY.get(context);
if (allCompiledFiles == null) {
allCompiledFiles = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
ALL_COMPILED_FILES_KEY.set(context, allCompiledFiles);
}
return allCompiledFiles;
}
private static Set<ModuleBuildTarget> getProcessedTargetsWithRemovedFilesContainer(CompileContext context) {
Set<ModuleBuildTarget> set = PROCESSED_TARGETS_WITH_REMOVED_FILES.get(context);
if (set == null) {
set = new HashSet<ModuleBuildTarget>();
PROCESSED_TARGETS_WITH_REMOVED_FILES.set(context, set);
}
return set;
}
private static boolean hasKotlinDirtyOrRemovedFiles(
@NotNull DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
@NotNull ModuleChunk chunk
)
throws IOException {
if (!KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder).isEmpty()) {
return true;
}
for (ModuleBuildTarget target : chunk.getTargets()) {
if (!KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, target).isEmpty()) {
return true;
}
}
return false;
}
private static boolean isJavaPluginEnabled(@NotNull CompileContext context) {
try {
// Using reflection for backward compatibility with IDEA 12
Field javaPluginIsEnabledField = JavaBuilder.class.getDeclaredField("IS_ENABLED");
return Modifier.isPublic(javaPluginIsEnabledField.getModifiers()) ? JavaBuilder.IS_ENABLED.get(context, Boolean.TRUE) : true;
}
catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Cannot check if Java Jps Plugin is enabled", e);
}
}
private static Collection<String> paths(Collection<File> files) {
Collection<String> result = ContainerUtil.newArrayList();
for (File file : files) {
result.add(file.getPath());
}
return result;
}
public static class MessageCollectorAdapter implements MessageCollector {
private final CompileContext context;
public MessageCollectorAdapter(@NotNull CompileContext context) {
this.context = context;
}
@Override
public void report(
@NotNull CompilerMessageSeverity severity,
@NotNull String message,
@NotNull CompilerMessageLocation location
) {
String prefix = "";
if (severity == EXCEPTION) {
prefix = INTERNAL_ERROR_PREFIX;
}
context.processMessage(new CompilerMessage(
CompilerRunnerConstants.KOTLIN_COMPILER_NAME,
kind(severity),
prefix + message + renderLocationIfNeeded(location),
location.getPath(),
-1, -1, -1,
location.getLine(),
location.getColumn()
));
}
private static String renderLocationIfNeeded(@NotNull CompilerMessageLocation location) {
if (location == NO_LOCATION) return "";
// Sometimes we report errors in JavaScript library stubs, i.e. files like core/javautil.kt
// IDEA can't find these files, and does not display paths in Messages View, so we add the position information
// to the error message itself:
String pathname = String.valueOf(location.getPath());
return new File(pathname).exists() ? "" : " (" + location + ")";
}
@NotNull
private static BuildMessage.Kind kind(@NotNull CompilerMessageSeverity severity) {
switch (severity) {
case INFO:
return BuildMessage.Kind.INFO;
case ERROR:
case EXCEPTION:
return BuildMessage.Kind.ERROR;
case WARNING:
return BuildMessage.Kind.WARNING;
case LOGGING:
return BuildMessage.Kind.PROGRESS;
default:
throw new IllegalArgumentException("Unsupported severity: " + severity);
}
}
}
@Override
public List<String> getCompilableFileExtensions() {
return COMPILABLE_FILE_EXTENSIONS;
}
}
@@ -0,0 +1,323 @@
/*
* Copyright 2010-2014 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.jet.jps.build
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.io.FileUtil
import gnu.trove.THashSet
import org.jetbrains.jet.cli.common.KotlinVersion
import org.jetbrains.jet.cli.common.messages.CompilerMessageLocation
import org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.jet.cli.common.messages.MessageCollector
import org.jetbrains.jet.compiler.runner.CompilerEnvironment
import org.jetbrains.jet.compiler.runner.CompilerRunnerConstants
import org.jetbrains.jet.compiler.runner.OutputItemsCollectorImpl
import org.jetbrains.jet.config.Services
import org.jetbrains.jet.config.IncrementalCompilation
import org.jetbrains.jet.jps.JpsKotlinCompilerSettings
import org.jetbrains.jet.jps.incremental.*
import org.jetbrains.jet.lang.resolve.kotlin.incremental.cache.IncrementalCacheProvider
import org.jetbrains.jet.utils.PathUtil
import org.jetbrains.jps.ModuleChunk
import org.jetbrains.jps.builders.DirtyFilesHolder
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor
import org.jetbrains.jps.incremental.*
import org.jetbrains.jps.incremental.java.JavaBuilder
import org.jetbrains.jps.incremental.messages.BuildMessage
import org.jetbrains.jps.incremental.messages.CompilerMessage
import java.io.File
import java.io.IOException
import java.lang.reflect.Modifier
import java.util.*
import org.jetbrains.jet.cli.common.messages.CompilerMessageLocation.NO_LOCATION
import org.jetbrains.jet.cli.common.messages.CompilerMessageSeverity.*
import org.jetbrains.jet.compiler.runner.CompilerRunnerConstants.INTERNAL_ERROR_PREFIX
import org.jetbrains.jet.compiler.runner.KotlinCompilerRunner.runK2JsCompiler
import org.jetbrains.jet.compiler.runner.KotlinCompilerRunner.runK2JvmCompiler
import org.jetbrains.jet.utils.keysToMap
import org.jetbrains.jps.incremental.ModuleLevelBuilder.ExitCode.*
public class KotlinBuilder : ModuleLevelBuilder(BuilderCategory.SOURCE_PROCESSOR) {
override fun getPresentableName() = "Kotlin Builder"
override fun getCompilableFileExtensions() = arrayListOf("kt")
override fun build(
context: CompileContext,
chunk: ModuleChunk,
dirtyFilesHolder: DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget>,
outputConsumer: ModuleLevelBuilder.OutputConsumer
): ModuleLevelBuilder.ExitCode {
val messageCollector = MessageCollectorAdapter(context)
// Workaround for Android Studio
if (!isJavaPluginEnabled(context)) {
messageCollector.report(INFO, "Kotlin JPS plugin is disabled", NO_LOCATION)
return NOTHING_DONE
}
val representativeTarget = chunk.representativeTarget()
// For non-incremental build: take all sources
if (!dirtyFilesHolder.hasDirtyFiles() && !dirtyFilesHolder.hasRemovedFiles()) {
return NOTHING_DONE
}
if (!hasKotlinDirtyOrRemovedFiles(dirtyFilesHolder, chunk)) {
return NOTHING_DONE
}
messageCollector.report(INFO, "Kotlin JPS plugin version " + KotlinVersion.VERSION, NO_LOCATION)
val outputDir = representativeTarget.getOutputDir()
val dataManager = context.getProjectDescriptor().dataManager
val incrementalCaches = chunk.getTargets().keysToMap { dataManager.getStorage(it, IncrementalCacheStorageProvider) }
val compilerServices = Services.Builder()
.register(javaClass<IncrementalCacheProvider>(), IncrementalCacheProviderImpl(incrementalCaches))
.build()
val environment = CompilerEnvironment.getEnvironmentFor(
PathUtil.getKotlinPathsForJpsPluginOrJpsTests(),
outputDir,
javaClass.getClassLoader(),
{ className ->
className!!.startsWith("org.jetbrains.jet.lang.resolve.kotlin.incremental.cache.")
|| className == "org.jetbrains.jet.config.Services"
},
compilerServices
)
if (!environment.success()) {
environment.reportErrorsTo(messageCollector)
return ABORT
}
assert(outputDir != null, "CompilerEnvironment must have checked for outputDir to be not null, but it didn't")
val outputItemCollector = OutputItemsCollectorImpl()
val project = representativeTarget.getModule().getProject()!!
val commonArguments = JpsKotlinCompilerSettings.getCommonCompilerArguments(project)
commonArguments.verbose = true // Make compiler report source to output files mapping
val compilerSettings = JpsKotlinCompilerSettings.getCompilerSettings(project)
val allCompiledFiles = getAllCompiledFilesContainer(context)
if (JpsUtils.isJsKotlinModule(representativeTarget)) {
if (chunk.getModules().size() > 1) {
// We do not support circular dependencies, but if they are present, we do our best should not break the build,
// so we simply yield a warning and report NOTHING_DONE
messageCollector.report(WARNING, "Circular dependencies are not supported. "
+ "The following JS modules depend on each other: "
+ chunk.getModules().map { it.getName() }.joinToString(", ")
+ ". "
+ "Kotlin is not compiled for these modules", NO_LOCATION)
return NOTHING_DONE
}
val sourceFiles = KotlinSourceFileCollector.getAllKotlinSourceFiles(representativeTarget)
//List<File> sourceFiles = KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder);
if (sourceFiles.isEmpty()) {
return NOTHING_DONE
}
val outputFile = File(outputDir, representativeTarget.getModule().getName() + ".js")
val libraryFiles = JpsJsModuleUtils.getLibraryFilesAndDependencies(representativeTarget)
val k2JsArguments = JpsKotlinCompilerSettings.getK2JsCompilerArguments(project)
runK2JsCompiler(commonArguments, k2JsArguments, compilerSettings, messageCollector, environment, outputItemCollector, sourceFiles, libraryFiles, outputFile)
}
else {
if (chunk.getModules().size() > 1) {
messageCollector.report(WARNING, "Circular dependencies are only partially supported. "
+ "The following modules depend on each other: "
+ chunk.getModules().map { it.getName() }.joinToString(", ")
+ ". "
+ "Kotlin will compile them, but some strange effect may happen", NO_LOCATION)
}
val filesToCompile = KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder)
for (target in filesToCompile.keySet()) {
filesToCompile.getModifiable(target).removeAll(allCompiledFiles)
}
allCompiledFiles.addAll(filesToCompile.values())
val processedTargetsWithRemoved = getProcessedTargetsWithRemovedFilesContainer(context)
var haveRemovedFiles = false
for (target in chunk.getTargets()) {
if (!KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, target).isEmpty()) {
if (processedTargetsWithRemoved.add(target)) {
haveRemovedFiles = true
}
}
}
val moduleFile = KotlinBuilderModuleScriptGenerator.generateModuleDescription(context, chunk, filesToCompile, haveRemovedFiles)
if (moduleFile == null) {
// No Kotlin sources found
return NOTHING_DONE
}
val k2JvmArguments = JpsKotlinCompilerSettings.getK2JvmCompilerArguments(project)
runK2JvmCompiler(commonArguments, k2JvmArguments, compilerSettings, messageCollector, environment, moduleFile, outputItemCollector)
moduleFile.delete()
}
// If there's only one target, this map is empty: get() always returns null, and the representativeTarget will be used below
val sourceToTarget = HashMap<File, ModuleBuildTarget>()
if (chunk.getTargets().size() > 1) {
for (target in chunk.getTargets()) {
for (file in KotlinSourceFileCollector.getAllKotlinSourceFiles(target)) {
sourceToTarget.put(file, target)
}
}
}
for ((target, cache) in incrementalCaches) {
cache.clearCacheForRemovedFiles(
KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, target),
target.getOutputDir()!!
)
}
var recompilationDecision = IncrementalCacheImpl.RecompilationDecision.DO_NOTHING
for (outputItem in outputItemCollector.getOutputs()) {
var target: ModuleBuildTarget? = null
val sourceFiles = outputItem.getSourceFiles()
if (!sourceFiles.isEmpty()) {
target = sourceToTarget[sourceFiles.iterator().next()]
}
if (target == null) {
target = representativeTarget
}
val outputFile = outputItem.getOutputFile()
if (IncrementalCompilation.ENABLED) {
val newDecision = incrementalCaches[target]!!.saveFileToCache(sourceFiles, outputFile)
recompilationDecision = recompilationDecision.merge(newDecision)
}
outputConsumer.registerOutputFile(target, outputFile, sourceFiles.map { it.getPath() })
}
if (IncrementalCompilation.ENABLED) {
if (recompilationDecision == IncrementalCacheImpl.RecompilationDecision.RECOMPILE_ALL) {
allCompiledFiles.clear()
return CHUNK_REBUILD_REQUIRED
}
if (recompilationDecision == IncrementalCacheImpl.RecompilationDecision.COMPILE_OTHERS) {
// TODO should mark dependencies as dirty, as well
FSOperations.markDirty(context, chunk, { file -> !allCompiledFiles.contains(file) })
}
return ADDITIONAL_PASS_REQUIRED
}
return OK
}
public class MessageCollectorAdapter(private val context: CompileContext) : MessageCollector {
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) {
var prefix = ""
if (severity == EXCEPTION) {
prefix = INTERNAL_ERROR_PREFIX
}
context.processMessage(CompilerMessage(
CompilerRunnerConstants.KOTLIN_COMPILER_NAME,
kind(severity),
prefix + message + renderLocationIfNeeded(location),
location.getPath(),
-1, -1, -1,
location.getLine().toLong(), location.getColumn().toLong()
))
}
private fun renderLocationIfNeeded(location: CompilerMessageLocation): String {
if (location == NO_LOCATION) return ""
// Sometimes we report errors in JavaScript library stubs, i.e. files like core/javautil.kt
// IDEA can't find these files, and does not display paths in Messages View, so we add the position information
// to the error message itself:
val pathname = "" + location.getPath()
return if (File(pathname).exists()) "" else " (" + location + ")"
}
private fun kind(severity: CompilerMessageSeverity): BuildMessage.Kind {
return when (severity) {
INFO -> BuildMessage.Kind.INFO
ERROR, EXCEPTION -> BuildMessage.Kind.ERROR
WARNING -> BuildMessage.Kind.WARNING
LOGGING -> BuildMessage.Kind.PROGRESS
else -> throw IllegalArgumentException("Unsupported severity: " + severity)
}
}
}
}
private val ALL_COMPILED_FILES_KEY = Key.create<MutableSet<File>>("_all_kotlin_compiled_files_")
private fun getAllCompiledFilesContainer(context: CompileContext): MutableSet<File> {
var allCompiledFiles = ALL_COMPILED_FILES_KEY.get(context)
if (allCompiledFiles == null) {
allCompiledFiles = THashSet(FileUtil.FILE_HASHING_STRATEGY)
ALL_COMPILED_FILES_KEY.set(context, allCompiledFiles)
}
return allCompiledFiles!!
}
private val PROCESSED_TARGETS_WITH_REMOVED_FILES = Key.create<MutableSet<ModuleBuildTarget>>("_processed_targets_with_removed_files_")
private fun getProcessedTargetsWithRemovedFilesContainer(context: CompileContext): MutableSet<ModuleBuildTarget> {
var set = PROCESSED_TARGETS_WITH_REMOVED_FILES.get(context)
if (set == null) {
set = HashSet<ModuleBuildTarget>()
PROCESSED_TARGETS_WITH_REMOVED_FILES.set(context, set)
}
return set!!
}
private fun hasKotlinDirtyOrRemovedFiles(
dirtyFilesHolder: DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget>,
chunk: ModuleChunk
): Boolean {
if (!KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder).isEmpty()) {
return true
}
return chunk.getTargets().any { !KotlinSourceFileCollector.getRemovedKotlinFiles(dirtyFilesHolder, it).isEmpty() }
}
private fun isJavaPluginEnabled(context: CompileContext): Boolean {
try {
// Using reflection for backward compatibility with IDEA 12
val javaPluginIsEnabledField = javaClass<JavaBuilder>().getDeclaredField("IS_ENABLED")
return if (Modifier.isPublic(javaPluginIsEnabledField.getModifiers())) JavaBuilder.IS_ENABLED[context, true] else true
}
catch (e: NoSuchFieldException) {
throw IllegalArgumentException("Cannot check if Java Jps Plugin is enabled", e)
}
}
@@ -40,6 +40,7 @@ import java.util.List;
public class KotlinSourceFileCollector {
// For incremental compilation
@NotNull
public static MultiMap<ModuleBuildTarget, File> getDirtySourceFiles(DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder)
throws IOException
{