diff --git a/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt b/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt index ad255176329..db8c9744316 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/buildUtil.kt @@ -175,8 +175,8 @@ fun OutputItemsCollectorImpl.generatedFiles( } data class DirtyData( - val dirtyLookupSymbols: Iterable, - val dirtyClassesFqNames: Iterable + val dirtyLookupSymbols: Collection = emptyList(), + val dirtyClassesFqNames: Collection = emptyList() ) fun CompilationResult.getDirtyData( @@ -261,7 +261,7 @@ private fun File.isJavaFile() = extension.equals(JavaFileType.INSTANCE.defaultEx private fun findSrcDirRoot(file: File, roots: Iterable): File? = roots.firstOrNull { FileUtil.isAncestor(it, file, false) } -private fun withSubtypes( +fun withSubtypes( typeFqName: FqName, caches: Iterable> ): Set { diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ArtifactDifferenceRegistry.kt b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ArtifactDifferenceRegistry.kt new file mode 100644 index 00000000000..e027cedfe9b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ArtifactDifferenceRegistry.kt @@ -0,0 +1,12 @@ +package org.jetbrains.kotlin.gradle.tasks + +import org.jetbrains.kotlin.incremental.DirtyData +import java.io.File + +interface ArtifactDifferenceRegistry { + operator fun get(artifact: File): Iterable? + fun add(artifact: File, difference: ArtifactDifference) + fun remove(artifact: File) +} + +class ArtifactDifference(val buildTS: Long, val dirtyData: DirtyData) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/BuildInfo.kt b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/BuildInfo.kt new file mode 100644 index 00000000000..9f6b26512fd --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/BuildInfo.kt @@ -0,0 +1,23 @@ +package org.jetbrains.kotlin.gradle.tasks + +import java.io.* + +internal data class BuildInfo(val startTS: Long) : Serializable { + companion object { + fun read(file: File): BuildInfo? = + try { + ObjectInputStream(FileInputStream(file)).use { + it.readObject() as BuildInfo + } + } + catch (e: Exception) { + null + } + + fun write(buildInfo: BuildInfo, file: File) { + ObjectOutputStream(FileOutputStream(file)).use { + it.writeObject(buildInfo) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ChangesEither.kt b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ChangesEither.kt new file mode 100644 index 00000000000..a120d1dce5c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/ChangesEither.kt @@ -0,0 +1,8 @@ +package org.jetbrains.kotlin.gradle.tasks + +import org.jetbrains.kotlin.incremental.LookupSymbol + +internal sealed class ChangesEither { + internal class Known(val lookupSymbols: Set) : ChangesEither() + internal class Unknown : ChangesEither() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt index 1a4c8281f38..fbe6c99a361 100644 --- a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlin.gradle.tasks import org.apache.commons.io.FilenameUtils import org.codehaus.groovy.runtime.MethodClosure import org.gradle.api.GradleException +import org.gradle.api.Project import org.gradle.api.file.SourceDirectorySet import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging @@ -42,6 +43,7 @@ import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.incremental.* import org.jetbrains.kotlin.incremental.components.LookupTracker import org.jetbrains.kotlin.modules.TargetId +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.utils.LibraryUtils import java.io.File @@ -52,6 +54,7 @@ const val ANNOTATIONS_PLUGIN_NAME = "org.jetbrains.kotlin.kapt" const val KOTLIN_BUILD_DIR_NAME = "kotlin" const val CACHES_DIR_NAME = "caches" const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt" +const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin" const val USING_EXPERIMENTAL_INCREMENTAL_MESSAGE = "Using experimental kotlin incremental compilation" abstract class AbstractKotlinCompile() : AbstractCompile() { @@ -154,6 +157,7 @@ open class KotlinCompile() : AbstractKotlinCompile() { private val taskBuildDirectory: File by lazy { File(File(project.buildDir, KOTLIN_BUILD_DIR_NAME), name) } private val cacheDirectory: File by lazy { File(taskBuildDirectory, CACHES_DIR_NAME) } private val dirtySourcesSinceLastTimeFile: File by lazy { File(taskBuildDirectory, DIRTY_SOURCES_FILE_NAME) } + private val lastBuildInfoFile: File by lazy { File(taskBuildDirectory, LAST_BUILD_INFO_FILE_NAME) } private val cacheVersions by lazy { listOf(normalCacheVersion(taskBuildDirectory), experimentalCacheVersion(taskBuildDirectory), @@ -172,6 +176,7 @@ open class KotlinCompile() : AbstractKotlinCompile() { val kaptOptions = KaptOptions() val pluginOptions = CompilerPluginOptions() + var artifactDifferenceRegistry: ArtifactDifferenceRegistry? = null override fun populateTargetSpecificArgs(args: K2JVMCompilerArguments) { logger.kotlinDebug("args.freeArgs = ${args.freeArgs}") @@ -232,15 +237,11 @@ open class KotlinCompile() : AbstractKotlinCompile() { } override fun callCompiler(args: K2JVMCompilerArguments, sources: List, isIncrementalRequested: Boolean, modified: List, removed: List) { + val rootProjectDir = project.rootProject.projectDir - fun projectRelativePath(f: File) = f.toRelativeString(project.projectDir) - - if (incremental) { - // TODO: consider other ways to pass incremental flag to compiler/builder - System.setProperty("kotlin.incremental.compilation", "true") - // TODO: experimental should be removed as soon as it becomes standard - System.setProperty("kotlin.incremental.compilation.experimental", "true") - } + fun projectRelativePath(f: File) = f.toRelativeString(rootProjectDir) + fun filesToString(files: Iterable) = + "[" + files.map(::projectRelativePath).sorted().joinToString(separator = ", \n") + "]" val targetType = "java-production" val moduleName = args.moduleName @@ -252,6 +253,8 @@ open class KotlinCompile() : AbstractKotlinCompile() { var currentRemoved = removed.filter { it.isKotlinFile() } val allGeneratedFiles = hashSetOf>() val logAction = { logStr: String -> logger.kotlinInfo(logStr) } + val lastBuildInfo = BuildInfo.read(lastBuildInfoFile) + logger.kotlinDebug { "Last Kotlin Build info for task $path -- $lastBuildInfo" } fun getOrCreateIncrementalCache(target: TargetId): GradleIncrementalCacheImpl { val cacheDir = File(cacheDirectory, "increCache.${target.name}") @@ -261,68 +264,117 @@ open class KotlinCompile() : AbstractKotlinCompile() { fun getIncrementalCache(it: TargetId) = caches.getOrPut(it, { getOrCreateIncrementalCache(it) }) - fun PsiClass.findLookupSymbols(): Iterable { + fun PsiClass.addLookupSymbols(symbols: MutableSet) { val fqn = qualifiedName.orEmpty() - return listOf(LookupSymbol(name.orEmpty(), if (fqn == name) "" else fqn.removeSuffix("." + name!!))) + - methods.map { LookupSymbol(it.name, fqn) } + - fields.map { LookupSymbol(it.name.orEmpty(), fqn) } + - innerClasses.flatMap { it.findLookupSymbols() } + + symbols.add(LookupSymbol(name.orEmpty(), if (fqn == name) "" else fqn.removeSuffix("." + name!!))) + methods.forEach { symbols.add(LookupSymbol(it.name, fqn)) } + fields.forEach { symbols.add(LookupSymbol(it.name.orEmpty(), fqn)) } + innerClasses.forEach { it.addLookupSymbols(symbols) } } - fun dirtyLookupSymbolsFromModifiedJavaFiles(): List { + val dirtyJavaLookupSymbols: Lazy> = lazy { + val symbols = HashSet() val modifiedJavaFiles = modified.filter { it.isJavaFile() } - return (if (modifiedJavaFiles.any()) { - val rootDisposable = Disposer.newDisposable() - val configuration = CompilerConfiguration() - val environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) - val project = environment.project - val psiFileFactory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl - modifiedJavaFiles.flatMap { - val javaFile = psiFileFactory.createFileFromText(it.nameWithoutExtension, Language.findLanguageByID("JAVA")!!, it.readText()) - if (javaFile is PsiJavaFile) - javaFile.classes.flatMap { it.findLookupSymbols() } - else listOf() + if (modifiedJavaFiles.isEmpty()) return@lazy symbols + + val rootDisposable = Disposer.newDisposable() + val configuration = CompilerConfiguration() + val environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + val project = environment.project + val psiFileFactory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl + + modifiedJavaFiles.forEach { + val javaFile = psiFileFactory.createFileFromText(it.nameWithoutExtension, Language.findLanguageByID("JAVA")!!, it.readText()) + if (javaFile is PsiJavaFile) { + javaFile.classes.forEach { it.addLookupSymbols(symbols) } } - } else listOf()) + } + symbols } - fun dirtyKotlinSourcesFromGradle(): MutableSet { - // TODO: handle classpath changes similarly - compare with cashed version (likely a big change, may be costly, some heuristics could be considered) - val modifiedKotlinFiles = modified.filter { it.isKotlinFile() }.toMutableSet() - val lookupSymbols = dirtyLookupSymbolsFromModifiedJavaFiles() - // TODO: add dirty lookups from modified kotlin files to reduce number of steps needed - - if (lookupSymbols.any()) { - val kotlinModifiedFilesSet = modifiedKotlinFiles.toHashSet() - val dirtyFilesFromLookups = mapLookupSymbolsToFiles(lookupStorage, lookupSymbols, logAction, ::projectRelativePath, excludes = kotlinModifiedFilesSet) - modifiedKotlinFiles.addAll(dirtyFilesFromLookups) + fun getClasspathChanges(modifiedClasspath: List): DirtyData? { + if (modifiedClasspath.isEmpty()) { + logger.kotlinDebug { "No classpath changes" } + return DirtyData() + } + if (artifactDifferenceRegistry == null) { + logger.kotlinDebug { "No artifact history provider" } + return null } - return modifiedKotlinFiles + val lastBuildTS = lastBuildInfo?.startTS + if (lastBuildTS == null) { + logger.kotlinDebug { "Could not determine last build timestamp" } + return null + } + + val symbols = HashSet() + val fqNames = HashSet() + for (file in modifiedClasspath) { + val diffs = artifactDifferenceRegistry!![file] + if (diffs == null) { + logger.kotlinDebug { "Could not get changes for file: $file" } + return null + } + + val (beforeLastBuild, afterLastBuild) = diffs.partition { it.buildTS < lastBuildTS } + if (beforeLastBuild.isEmpty()) { + logger.kotlinDebug { "No known build preceding timestamp $lastBuildTS for file $file" } + return null + } + + afterLastBuild.forEach { + symbols.addAll(it.dirtyData.dirtyLookupSymbols) + fqNames.addAll(it.dirtyData.dirtyClassesFqNames) + } + } + + return DirtyData(symbols, fqNames) } - fun isClassPathChanged(): Boolean = - modified.any { classpath.contains(it) } - fun calculateSourcesToCompile(): Pair, Boolean> { - if (!incremental - || !isIncrementalRequested - // TODO: more precise will be not to rebuild unconditionally on classpath changes, but retrieve lookup info and try to find out which sources are affected by cp changes - || isClassPathChanged() - // so far considering it not incremental TODO: store java files in the cache and extract removed symbols from it here - || removed.any { it.isJavaFile() || it.hasClassFileExtension() } - || modified.any { it.hasClassFileExtension() } - ) { - logger.kotlinInfo(if (!isIncrementalRequested) "clean caches on rebuild" else "classpath changed, rebuilding all kotlin files") + fun rebuild(reason: String): Pair, Boolean> { + logger.kotlinInfo("Non-incremental compilation will be performed: $reason") targets.forEach { getIncrementalCache(it).clean() } lookupStorage.clean() dirtySourcesSinceLastTimeFile.delete() return Pair(sources.toSet(), false) } - val dirtyFiles = dirtyKotlinSourcesFromGradle() + if (!incremental) return rebuild("incremental compilation is not enabled") + if (!isIncrementalRequested) return rebuild("inputs' changes are unknown (first or clean build)") + + // TODO: store java files in the cache and extract removed symbols from it here + val illegalRemovedFiles = removed.filter { it.isJavaFile() || it.hasClassFileExtension() } + if (illegalRemovedFiles.any()) return rebuild("unsupported removed files: ${filesToString(illegalRemovedFiles)}") + + val illegalModifiedFiles = modified.filter(File::hasClassFileExtension) + if (illegalModifiedFiles.any()) return rebuild("unsupported modified files: ${filesToString(illegalModifiedFiles)}") + + val modifiedClasspathEntries = modified.filter { it in classpath } + val classpathChanges = getClasspathChanges(modifiedClasspathEntries) + ?: return rebuild("could not get changes from modified classpath entries: ${filesToString(modifiedClasspathEntries)}") + + val dirtyFiles = modified.filter { it.isKotlinFile() }.toMutableSet() + val lookupSymbols = HashSet() + lookupSymbols.addAll(dirtyJavaLookupSymbols.value) + lookupSymbols.addAll(classpathChanges.dirtyLookupSymbols) + + if (lookupSymbols.any()) { + val dirtyFilesFromLookups = mapLookupSymbolsToFiles(lookupStorage, lookupSymbols, logAction, ::projectRelativePath) + dirtyFiles.addAll(dirtyFilesFromLookups) + } + + val allCaches = targets.map(::getIncrementalCache) + val dirtyClassesFqNames = classpathChanges.dirtyClassesFqNames.flatMap { withSubtypes(it, allCaches) } + if (dirtyClassesFqNames.any()) { + val dirtyFilesFromFqNames = mapClassesFqNamesToFiles(allCaches, dirtyClassesFqNames, logAction, ::projectRelativePath) + dirtyFiles.addAll(dirtyFilesFromFqNames) + } + if (dirtySourcesSinceLastTimeFile.exists()) { - val files = dirtySourcesSinceLastTimeFile.readLines().map(::File).filter { it.exists() } + val files = dirtySourcesSinceLastTimeFile.readLines().map(::File).filter(File::exists) if (files.isNotEmpty()) { logger.kotlinDebug { "Source files added since last compilation: $files" } } @@ -334,9 +386,8 @@ open class KotlinCompile() : AbstractKotlinCompile() { } fun cleanupOnError() { - val filesToDelete = allGeneratedFiles.map { it.outputFile } - logger.kotlinInfo("deleting output on error: ${filesToDelete.joinToString()}") - filesToDelete.forEach { it.delete() } + logger.kotlinInfo("deleting $destinationDir on error") + destinationDir.deleteRecursively() } fun processCompilerExitCode(exitCode: ExitCode) { @@ -379,6 +430,13 @@ open class KotlinCompile() : AbstractKotlinCompile() { kaptAnnotationsFileUpdater = null } + val artifactFile = project.tryGetSingleArtifact() + logger.kotlinDebug { "Artifact to register difference for task $path: $artifactFile" } + val currentBuildInfo = BuildInfo(startTS = System.currentTimeMillis()) + BuildInfo.write(currentBuildInfo, lastBuildInfoFile) + val buildDirtyLookupSymbols = HashSet() + val buildDirtyFqNames = HashSet() + var exitCode = ExitCode.OK while (sourcesToCompile.any() || currentRemoved.any()) { val removedAndModified = (sourcesToCompile + currentRemoved).toList() @@ -419,18 +477,38 @@ open class KotlinCompile() : AbstractKotlinCompile() { lookupStorage.update(lookupTracker, sourcesToCompile, currentRemoved) - if (!isIncrementalDecided) break + if (!isIncrementalDecided) { + artifactFile?.let { artifactDifferenceRegistry?.remove(it) } + break + } val (dirtyLookupSymbols, dirtyClassFqNames) = compilationResult.getDirtyData(caches.values, logAction) sourcesToCompile = mapLookupSymbolsToFiles(lookupStorage, dirtyLookupSymbols, logAction, ::projectRelativePath, excludes = sourcesToCompile) + mapClassesFqNamesToFiles(caches.values, dirtyClassFqNames, logAction, ::projectRelativePath, excludes = sourcesToCompile) + buildDirtyLookupSymbols.addAll(dirtyLookupSymbols) + buildDirtyFqNames.addAll(dirtyClassFqNames) + if (currentRemoved.any()) { anyClassesCompiled = true currentRemoved = listOf() } } + if (exitCode == ExitCode.OK) { + buildDirtyLookupSymbols.addAll(dirtyJavaLookupSymbols.value) + } + if (artifactFile != null && artifactDifferenceRegistry != null) { + val dirtyData = DirtyData(buildDirtyLookupSymbols, buildDirtyFqNames) + val artifactDifference = ArtifactDifference(currentBuildInfo.startTS, dirtyData) + artifactDifferenceRegistry!!.add(artifactFile, artifactDifference) + logger.kotlinDebug { + val dirtySymbolsSorted = buildDirtyLookupSymbols.map { it.scope + "#" + it.name }.sorted() + "Added artifact difference for $artifactFile (ts: ${currentBuildInfo.startTS}): " + + "[\n\t${dirtySymbolsSorted.joinToString(",\n\t")}]" + } + } + anyClassesCompiled = anyClassesCompiled || allGeneratedFiles.isNotEmpty() processCompilerExitCode(exitCode) } @@ -719,6 +797,22 @@ class GradleMessageCollector(val logger: Logger, val outputCollector: OutputItem } } +fun Project.tryGetSingleArtifact(): File? { + val log = logger + log.kotlinDebug { "Trying to determine single artifact for project $path" } + + val archives = configurations.findByName("archives") + if (archives == null) { + log.kotlinDebug { "Could not find 'archives' configuration for project $path" } + return null + } + + val artifacts = archives.artifacts.files.files + log.kotlinDebug { "All artifacts for project $path: [${artifacts.joinToString()}]" } + + return if (artifacts.size == 1) artifacts.first() else null +} + internal fun Logger.kotlinInfo(message: String) { this.info("[KOTLIN] $message") } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinGradleBuildServices.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinGradleBuildServices.kt index b6845f403db..be4b2d80c09 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinGradleBuildServices.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinGradleBuildServices.kt @@ -26,6 +26,9 @@ import org.jetbrains.kotlin.com.intellij.openapi.util.io.ZipFileCache import org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.ZipHandler import org.jetbrains.kotlin.com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.ArtifactDifferenceRegistry +import org.jetbrains.kotlin.gradle.tasks.incremental.BuildCacheStorage +import java.io.File private fun comparableVersionStr(version: String) = "(\\d+)\\.(\\d+).*" @@ -37,31 +40,44 @@ private fun comparableVersionStr(version: String) = ?.let { if (it.all { (it?.value?.length ?: 0).let { it > 0 && it < 4 }}) it else null } ?.joinToString(".", transform = { it!!.value.padStart(3, '0') }) -class KotlinGradleBuildServices private constructor(): BuildAdapter() { +class KotlinGradleBuildServices private constructor(gradle: Gradle): BuildAdapter() { companion object { + private val CLASS_NAME = KotlinGradleBuildServices::class.java.simpleName const val FORCE_SYSTEM_GC_MESSAGE = "Forcing System.gc()" - const val INIT_MESSAGE = "Initialized KotlinGradleBuildServices" - const val DISPOSE_MESSAGE = "Disposed KotlinGradleBuildServices" - private var initialized = false + val INIT_MESSAGE = "Initialized $CLASS_NAME" + val DISPOSE_MESSAGE = "Disposed $CLASS_NAME" + val ALREADY_INITIALIZED_MESSAGE = "$CLASS_NAME is already initialized" + @field:Volatile + private var instance: KotlinGradleBuildServices? = null @JvmStatic @Synchronized - fun init(gradle: Gradle) { - if (initialized) return - - val listener = KotlinGradleBuildServices() - gradle.addBuildListener(listener) - initialized = true + fun getInstance(gradle: Gradle): KotlinGradleBuildServices { val log = Logging.getLogger(KotlinGradleBuildServices::class.java) + + if (instance != null) { + log.kotlinDebug(ALREADY_INITIALIZED_MESSAGE) + return instance!! + } + + val services = KotlinGradleBuildServices(gradle) + gradle.addBuildListener(services) + instance = services log.kotlinDebug(INIT_MESSAGE) - listener.buildStarted() + services.buildStarted() + return services } } private val log = Logging.getLogger(this.javaClass) private val cleanup = CompilerServicesCleanup() private var startMemory: Long? = null + private val workingDir = File(gradle.rootProject.buildDir, "kotlin-build").apply { mkdirs() } + private val buildCacheStorage = BuildCacheStorage(workingDir) + + internal val artifactDifferenceRegistry: ArtifactDifferenceRegistry + get() = buildCacheStorage.artifactDifferenceRegistry // There is function with the same name in BuildAdapter, // but it is called before any plugin can attach build listener @@ -93,8 +109,11 @@ class KotlinGradleBuildServices private constructor(): BuildAdapter() { } } + buildCacheStorage.flush(memoryCachesOnly = false) + buildCacheStorage.close() + gradle.removeListener(this) - initialized = false + instance = null log.kotlinDebug(DISPOSE_MESSAGE) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt index 6b72617a631..893941f4b3d 100755 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt @@ -25,10 +25,8 @@ import org.jetbrains.kotlin.gradle.internal.Kapt2GradleSubplugin import org.jetbrains.kotlin.gradle.internal.Kapt2KotlinGradleSubplugin import org.jetbrains.kotlin.gradle.internal.initKapt import org.jetbrains.kotlin.gradle.plugin.android.AndroidGradleWrapper -import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider -import org.jetbrains.kotlin.gradle.tasks.SyncOutputTask +import org.jetbrains.kotlin.gradle.tasks.* +import org.jetbrains.kotlin.gradle.tasks.incremental.configureMultiProjectIncrementalCompilation import java.io.File import java.net.URL import java.util.* @@ -98,18 +96,14 @@ class Kotlin2JvmSourceSetProcessor( sourceSet: SourceSet, tasksProvider: KotlinTasksProvider, kotlinSourceSetProvider: KotlinSourceSetProvider, - private val kotlinPluginVersion: String + private val kotlinPluginVersion: String, + private val artifactDifferenceRegistry: ArtifactDifferenceRegistry ) : KotlinSourceSetProcessor( project, javaBasePlugin, sourceSet, tasksProvider, kotlinSourceSetProvider, dslExtensionName = KOTLIN_DSL_NAME, compileTaskNameSuffix = "kotlin", taskDescription = "Compiles the $sourceSet.kotlin." ) { - - private companion object { - private var cachedKotlinAnnotationProcessingDep: String? = null - } - override val defaultKotlinDestinationDir: File get() = File(project.buildDir, "kotlin-classes/$sourceSetName") @@ -147,6 +141,7 @@ class Kotlin2JvmSourceSetProcessor( kotlinAfterJavaTask?.let { it.source(kotlinSourceSet.kotlin) } configureJavaTask(kotlinTask, javaTask, logger) createSyncOutputTask(project, kotlinTask, javaTask, kotlinAfterJavaTask, sourceSetName) + configureMultiProjectIncrementalCompilation(project, kotlinTask, javaTask, kotlinAfterJavaTask, artifactDifferenceRegistry) } } } @@ -222,10 +217,11 @@ abstract class AbstractKotlinPlugin(val tasksProvider: KotlinTasksProvider, val open class KotlinPlugin( tasksProvider: KotlinTasksProvider, kotlinSourceSetProvider: KotlinSourceSetProvider, - kotlinPluginVersion: String + kotlinPluginVersion: String, + private val artifactDifferenceRegistry: ArtifactDifferenceRegistry ) : AbstractKotlinPlugin(tasksProvider, kotlinSourceSetProvider, kotlinPluginVersion) { override fun buildSourceSetProcessor(project: Project, javaBasePlugin: JavaBasePlugin, sourceSet: SourceSet, kotlinPluginVersion: String) = - Kotlin2JvmSourceSetProcessor(project, javaBasePlugin, sourceSet, tasksProvider, kotlinSourceSetProvider, kotlinPluginVersion) + Kotlin2JvmSourceSetProcessor(project, javaBasePlugin, sourceSet, tasksProvider, kotlinSourceSetProvider, kotlinPluginVersion, artifactDifferenceRegistry) override fun apply(project: Project) { project.createKaptExtension() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index ce28927b44f..7b6a22531e0 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -19,28 +19,28 @@ abstract class KotlinBasePluginWrapper(protected val fileResolver: FileResolver) override fun apply(project: Project) { // TODO: consider only set if if daemon or parallel compilation are enabled, though this way it should be safe too System.setProperty(org.jetbrains.kotlin.cli.common.KOTLIN_COMPILER_ENVIRONMENT_KEEPALIVE_PROPERTY, "true") + val kotlinGradleBuildServices = KotlinGradleBuildServices.getInstance(project.gradle) - val plugin = getPlugin() + val plugin = getPlugin(kotlinGradleBuildServices) plugin.apply(project) - - KotlinGradleBuildServices.init(project.gradle) } - protected abstract fun getPlugin(): Plugin + protected abstract fun getPlugin(kotlinGradleBuildServices: KotlinGradleBuildServices): Plugin } open class KotlinPluginWrapper @Inject constructor(fileResolver: FileResolver): KotlinBasePluginWrapper(fileResolver) { - override fun getPlugin() = - KotlinPlugin(KotlinTasksProvider(), KotlinSourceSetProviderImpl(fileResolver), kotlinPluginVersion) + override fun getPlugin(kotlinGradleBuildServices: KotlinGradleBuildServices) = + KotlinPlugin(KotlinTasksProvider(), KotlinSourceSetProviderImpl(fileResolver), kotlinPluginVersion, + kotlinGradleBuildServices.artifactDifferenceRegistry) } open class KotlinAndroidPluginWrapper @Inject constructor(fileResolver: FileResolver): KotlinBasePluginWrapper(fileResolver) { - override fun getPlugin() = + override fun getPlugin(kotlinGradleBuildServices: KotlinGradleBuildServices) = KotlinAndroidPlugin(AndroidTasksProvider(), KotlinSourceSetProviderImpl(fileResolver), kotlinPluginVersion) } open class Kotlin2JsPluginWrapper @Inject constructor(fileResolver: FileResolver): KotlinBasePluginWrapper(fileResolver) { - override fun getPlugin() = + override fun getPlugin(kotlinGradleBuildServices: KotlinGradleBuildServices) = Kotlin2JsPlugin(KotlinTasksProvider(), KotlinSourceSetProviderImpl(fileResolver), kotlinPluginVersion) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/ArtifactDifferenceRegistryImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/ArtifactDifferenceRegistryImpl.kt new file mode 100644 index 00000000000..1b1cefe76b3 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/ArtifactDifferenceRegistryImpl.kt @@ -0,0 +1,102 @@ +package org.jetbrains.kotlin.gradle.tasks.incremental + +import org.jetbrains.kotlin.com.intellij.util.io.DataExternalizer +import org.jetbrains.kotlin.gradle.tasks.ArtifactDifference +import org.jetbrains.kotlin.gradle.tasks.ArtifactDifferenceRegistry +import org.jetbrains.kotlin.incremental.DirtyData +import org.jetbrains.kotlin.incremental.LookupSymbol +import org.jetbrains.kotlin.incremental.storage.BasicStringMap +import org.jetbrains.kotlin.name.FqName +import java.io.DataInput +import java.io.DataOutput +import java.io.File +import java.util.* + +internal class ArtifactDifferenceRegistryImpl( + storageFile: File +) : ArtifactDifferenceRegistry, BasicStringMap>( + storageFile, + ArtifactDifferenceCollectionExternalizer +) { + companion object { + private val MAX_BUILDS_PER_ARTIFACT = 10 + } + + override fun get(artifact: File): Iterable? = + storage[artifact.canonicalPath] + + override fun add(artifact: File, difference: ArtifactDifference) { + val key = artifact.canonicalPath + val oldVal = storage[key] ?: emptyList() + val newVal = ArrayList(oldVal) + newVal.add(difference) + newVal.sortBy { it.buildTS } + storage[key] = newVal.takeLast(MAX_BUILDS_PER_ARTIFACT) + } + + override fun remove(artifact: File) { + storage.remove(artifact.canonicalPath) + } + + override fun dumpValue(value: Collection): String = + value.sortedBy { it.buildTS }.joinToString(separator = ",\n\t") { diff -> + "{ " + + "timestamp: ${diff.buildTS}, " + + "lookup symbols: [${diff.dirtyData.dirtyLookupSymbols.dumpLookupSymbols()}], " + + "fq names: [${diff.dirtyData.dirtyClassesFqNames.dumpFqNames()}]" + "}" + } +} + +private fun Collection.takeLast(n: Int): Collection = + drop(Math.max(size - n, 0)) + +private fun Collection.dumpLookupSymbols(): String = + map { "${it.scope}#${it.name}" }.sorted().joinToString() + +private fun Collection.dumpFqNames(): String = + map(FqName::asString).sorted().joinToString() + +private object ArtifactDifferenceCollectionExternalizer : DataExternalizer> { + override fun read(input: DataInput): Collection = + input.readCollectionTo(ArrayList()) { + val buildTS = readLong() + val dirtyLookupSymbols = readCollectionTo(HashSet()) { + val scope = readUTF() + val name = readUTF() + LookupSymbol(name, scope) + } + val dirtyFqNames = readCollectionTo(HashSet()) { + FqName(readUTF()) + } + ArtifactDifference(buildTS, DirtyData(dirtyLookupSymbols, dirtyFqNames)) + } + + override fun save(output: DataOutput, value: Collection) { + output.writeCollection(value) { diff -> + writeLong(diff.buildTS) + writeCollection(diff.dirtyData.dirtyLookupSymbols) { + writeUTF(it.scope) + writeUTF(it.name) + } + writeCollection(diff.dirtyData.dirtyClassesFqNames) { + writeUTF(it.asString()) + } + } + } +} + +private inline fun DataInput.readCollectionTo(col: MutableCollection, readT: DataInput.()->T): Collection { + val size = readInt() + + repeat(size) { + col.add(readT()) + } + + return col +} + +private inline fun DataOutput.writeCollection(col: Collection, writeT: DataOutput.(T)->Unit) { + writeInt(col.size) + col.forEach { writeT(it) } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/BuildCacheStorage.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/BuildCacheStorage.kt new file mode 100644 index 00000000000..0d3244d828d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/BuildCacheStorage.kt @@ -0,0 +1,59 @@ +package org.jetbrains.kotlin.gradle.tasks.incremental + +import org.gradle.api.logging.Logging +import org.jetbrains.kotlin.gradle.plugin.kotlinDebug +import org.jetbrains.kotlin.gradle.tasks.ArtifactDifferenceRegistry +import org.jetbrains.kotlin.incremental.CacheVersion +import org.jetbrains.kotlin.incremental.storage.BasicMapsOwner +import java.io.File + +/** + * "Global" cache holder. Should be created once per root project. + */ +internal class BuildCacheStorage(workingDir: File) : BasicMapsOwner() { + companion object { + private val OWN_VERSION = 0 + private val ARTIFACT_DIFFERENCE = "artifact-difference" + } + + private val log = Logging.getLogger(this.javaClass) + private val cachesDir: File = File(workingDir, "caches").apply { mkdirs() } + private val versionFile = File(cachesDir, "version.txt") + private val version = CacheVersion( + OWN_VERSION, + versionFile, + whenVersionChanged = CacheVersion.Action.REBUILD_ALL_KOTLIN, + whenTurnedOff = CacheVersion.Action.REBUILD_ALL_KOTLIN, + whenTurnedOn = CacheVersion.Action.REBUILD_ALL_KOTLIN, + // assume it's always enabled for simplicity (if IC is not enabled, just don't write to cache) + isEnabled = { true }) + + internal val artifactDifferenceRegistry: ArtifactDifferenceRegistry + + private val String.storageFile: File + get() = File(cachesDir, this + "." + CACHE_EXTENSION) + + init { + if (version.checkVersion() != CacheVersion.Action.DO_NOTHING) { + log.kotlinDebug { "Cache version is not up-to-date. Removing $cachesDir" } + cachesDir.deleteRecursively() + cachesDir.mkdirs() + } + + artifactDifferenceRegistry = registerMap(ArtifactDifferenceRegistryImpl(ARTIFACT_DIFFERENCE.storageFile)) + } + + override fun clean() { + super.clean() + versionFile.delete() + } + + override fun close() { + super.close() + version.saveIfNeeded() + } + + override fun flush(memoryCachesOnly: Boolean) { + super.flush(memoryCachesOnly) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/multiProjectIncrementalCompilationUtil.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/multiProjectIncrementalCompilationUtil.kt new file mode 100644 index 00000000000..40164db69eb --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/incremental/multiProjectIncrementalCompilationUtil.kt @@ -0,0 +1,51 @@ +package org.jetbrains.kotlin.gradle.tasks.incremental + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.compile.AbstractCompile +import org.gradle.api.tasks.compile.JavaCompile +import org.jetbrains.kotlin.com.intellij.openapi.util.io.FileUtil +import org.jetbrains.kotlin.gradle.plugin.kotlinDebug +import org.jetbrains.kotlin.gradle.tasks.ArtifactDifferenceRegistry +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.tryGetSingleArtifact +import java.io.File + +fun configureMultiProjectIncrementalCompilation( + project: Project, + kotlinTask: KotlinCompile, + javaTask: JavaCompile, + kotlinAfterJavaTask: KotlinCompile?, + artifactDifferenceRegistry: ArtifactDifferenceRegistry +) { + val log = kotlinTask.logger + log.kotlinDebug { "Configuring multi-project incremental compilation for project ${project.path}" } + + fun cannotPerformMultiProjectIC(artifact: File, reason: String) { + log.kotlinDebug { + "Multi-project kotlin incremental compilation cannot be performed for project ${project.path}: $reason" + } + artifactDifferenceRegistry.remove(artifact) + } + + fun isUnknownTaskOutputtingToJavaDestination(task: Task): Boolean { + return task !is JavaCompile && + task !is KotlinCompile && + task is AbstractCompile && + FileUtil.isAncestor(javaTask.destinationDir, task.destinationDir, /* strict = */ false) + } + + val artifact = project.tryGetSingleArtifact() ?: return + if (!kotlinTask.incremental) { + return cannotPerformMultiProjectIC(artifact, reason = "incremental compilation is not enabled") + } + + val illegalTask = project.tasks.find(::isUnknownTaskOutputtingToJavaDestination) + if (illegalTask != null) { + return cannotPerformMultiProjectIC(artifact, + reason = "unknown task outputs to java destination dir ${illegalTask.path} $(${illegalTask.javaClass})") + } + + val kotlinCompile = kotlinAfterJavaTask ?: kotlinTask + kotlinCompile.artifactDifferenceRegistry = artifactDifferenceRegistry +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt index b2702a3b05c..d1455ae762f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt @@ -92,7 +92,7 @@ fun getSomething() = 10 project.build("build", options = options) { assertSuccessful() - assertCompiledKotlinSources(listOf("src/main/kotlin/foo/KotlinActivity1.kt", "src/main/kotlin/foo/getSomething.kt")) + assertCompiledKotlinSources(listOf("app/src/main/kotlin/foo/KotlinActivity1.kt", "app/src/main/kotlin/foo/getSomething.kt")) assertCompiledJavaSources(listOf("app/src/main/java/foo/JavaActivity.java"), weakTesting = true) } } @@ -165,7 +165,7 @@ fun getSomething() = 10 project.build(":app:assembleDebug", options = options) { assertSuccessful() - assertCompiledKotlinSources(project.relativizeToSubproject("app", + assertCompiledKotlinSources(project.relativize( androidModuleKt, baseApplicationKt, useBuildConfigJavaKt, diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt index 6be017bd059..dcf4d6df84d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt @@ -95,11 +95,6 @@ abstract class BaseGradleIT { fun relativize(vararg files: File): List = files.map { it.relativeTo(projectDir).path } - fun relativizeToSubproject(subproject: String, vararg files: File): List { - val subprojectSir = File(projectDir, subproject) - return files.map { it.relativeTo(subprojectSir).path } - } - fun performModifications() { for (file in projectDir.walk()) { if (!file.isFile) continue diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt new file mode 100644 index 00000000000..fa89849a6a9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/IncrementalCompilationMultiProjectIT.kt @@ -0,0 +1,188 @@ +package org.jetbrains.kotlin.gradle + +import org.jetbrains.kotlin.gradle.util.allKotlinFiles +import org.jetbrains.kotlin.gradle.util.getFileByName +import org.jetbrains.kotlin.gradle.util.getFilesByNames +import org.jetbrains.kotlin.gradle.util.modify +import org.junit.Test +import java.io.File + +class IncrementalCompilationMultiProjectIT : BaseGradleIT() { + companion object { + private val GRADLE_VERSION = "2.10" + } + + override fun defaultBuildOptions(): BuildOptions = + super.defaultBuildOptions().copy(withDaemon = true, incremental = true) + + + @Test + fun testMoveFunctionFromLib() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + val barUseABKt = project.projectDir.getFileByName("barUseAB.kt") + val barInApp = File(project.projectDir, "app/src/main/java/bar").apply { mkdirs() } + barUseABKt.copyTo(File(barInApp, barUseABKt.name)) + barUseABKt.delete() + + project.build("build") { + assertSuccessful() + val affectedSources = project.projectDir.getFilesByNames("fooCallUseAB.kt", "barUseAB.kt") + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + @Test + fun testAddNewMethodToLib() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + val aKt = project.projectDir.getFileByName("A.kt") + aKt.writeText(""" +package bar + +open class A { + fun a() {} + fun newA() {} +} +""") + + project.build("build") { + assertSuccessful() + val affectedSources = project.projectDir.getFilesByNames("A.kt", "B.kt", "AA.kt", "BB.kt") + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + @Test + fun testLibClassBecameFinal() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + val bKt = project.projectDir.getFileByName("B.kt") + bKt.modify { it.replace("open class", "class") } + + project.build("build") { + assertFailed() + val affectedSources = project.projectDir.getFilesByNames( + "B.kt", "barUseAB.kt", "barUseB.kt", + "BB.kt", "fooCallUseAB.kt", "fooUseB.kt") + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + @Test + fun testModifyJavaInLib() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + val javaClassJava = project.projectDir.getFileByName("JavaClass.java") + javaClassJava.modify { it.replace("String getString", "Object getString") } + + project.build("build") { + assertSuccessful() + val affectedSources = project.projectDir.getFilesByNames("JavaClassChild.kt", "useJavaClass.kt") + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + @Test + fun testCleanBuildLib() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + project.build(":lib:clean", ":lib:build") { + assertSuccessful() + val affectedSources = File(project.projectDir, "lib").allKotlinFiles() + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + + project.build("build") { + assertSuccessful() + val affectedSources = File(project.projectDir, "app").allKotlinFiles() + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + @Test + fun testCompileErrorInLib() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.build("build") { + assertSuccessful() + } + + val bKt = project.projectDir.getFileByName("B.kt") + bKt.delete() + + project.build("build") { + assertFailed() + } + + project.projectDir.getFileByName("barUseB.kt").delete() + project.projectDir.getFileByName("barUseAB.kt").delete() + + project.build("build") { + assertFailed() + val affectedSources = project.projectDir.allKotlinFiles() + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } + + // checks that multi-project ic is disabled when there is a task that outputs to javaDestination dir + // that is not JavaCompile or KotlinCompile + @Test + fun testCompileLibWithGroovy() { + val project = Project("incrementalMultiproject", GRADLE_VERSION) + project.setupWorkingDir() + val lib = File(project.projectDir, "lib") + val libBuildGradle = File(lib, "build.gradle") + libBuildGradle.modify { + """ + apply plugin: 'groovy' + apply plugin: 'kotlin' + + dependencies { + compile 'org.codehaus.groovy:groovy-all:2.4.7' + } + """.trimIndent() + } + + val libGroovySrcBar = File(lib, "src/main/groovy/bar").apply { mkdirs() } + val groovyClass = File(libGroovySrcBar, "GroovyClass.groovy") + groovyClass.writeText(""" + package bar + + class GroovyClass {} + """) + + project.build("build") { + assertSuccessful() + } + + project.projectDir.getFileByName("barUseB.kt").delete() + project.build("build") { + assertSuccessful() + val affectedSources = File(project.projectDir, "app").allKotlinFiles() + val relativePaths = project.relativize(affectedSources) + assertCompiledKotlinSources(relativePaths, weakTesting = false) + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/util/fileUtil.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/util/fileUtil.kt index e04b9294da9..86ffe4ffc6d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/util/fileUtil.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/util/fileUtil.kt @@ -5,6 +5,9 @@ import java.io.File fun File.getFileByName(name: String): File = findFileByName(name) ?: throw AssertionError("Could not find file with name '$name' in $this") +fun File.getFilesByNames(vararg names: String): List = + names.map { getFileByName(it) } + fun File.findFileByName(name: String): File? = walk().filter { it.isFile && it.name.equals(name, ignoreCase = true) }.firstOrNull() diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/build.gradle new file mode 100644 index 00000000000..ed6cb6a85e3 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/build.gradle @@ -0,0 +1,5 @@ +apply plugin: 'kotlin' + +dependencies { + compile project(':lib') +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/AA.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/AA.kt new file mode 100644 index 00000000000..ace5a2395c2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/AA.kt @@ -0,0 +1,7 @@ +package foo + +import bar.* + +class AA : A() { + fun aa() {} +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/BB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/BB.kt new file mode 100644 index 00000000000..7e939751563 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/BB.kt @@ -0,0 +1,7 @@ +package foo + +import bar.* + +class BB : B() { + fun bb() {} +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/FooDummy.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/FooDummy.kt new file mode 100644 index 00000000000..cc6a6bd1ebe --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/FooDummy.kt @@ -0,0 +1,3 @@ +package foo + +class FooDummy \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/JavaClassChild.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/JavaClassChild.kt new file mode 100644 index 00000000000..1458b663d9d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/JavaClassChild.kt @@ -0,0 +1,5 @@ +package foo + +import bar.* + +class JavaClassChild : JavaClass() {} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooCallUseAB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooCallUseAB.kt new file mode 100644 index 00000000000..cfeb9308c0e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooCallUseAB.kt @@ -0,0 +1,8 @@ +package foo + +import bar.* + +fun callUseAB() { + val b = B() + useAB(b) +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseA.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseA.kt new file mode 100644 index 00000000000..7f83bd6a580 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseA.kt @@ -0,0 +1,7 @@ +package foo + +import bar.* + +private fun fooUseA(a: A) { + a.a() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseAA.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseAA.kt new file mode 100644 index 00000000000..4a9d80985a1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseAA.kt @@ -0,0 +1,5 @@ +package foo + +private fun fooUseAA(aa: AA) { + aa.aa() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseB.kt new file mode 100644 index 00000000000..815f3cb78a9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseB.kt @@ -0,0 +1,7 @@ +package foo + +import bar.* + +private fun fooUseB(b: B) { + b.b() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseBB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseBB.kt new file mode 100644 index 00000000000..a9a78c922f1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/fooUseBB.kt @@ -0,0 +1,5 @@ +package foo + +private fun fooUseBB(bb: BB) { + bb.bb() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/useJavaClass.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/useJavaClass.kt new file mode 100644 index 00000000000..7aefd470128 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/app/src/main/java/foo/useJavaClass.kt @@ -0,0 +1,7 @@ +package foo + +import bar.* + +fun useJavaClass(jc: JavaClass) { + jc.getString() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/build.gradle new file mode 100644 index 00000000000..cd35414a055 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/build.gradle @@ -0,0 +1,16 @@ +buildscript { + ext.kotlin_version = '0.1-SNAPSHOT' + repositories { + maven { url 'file://' + pathToKotlinPlugin } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + // for test with groovy + repositories { + mavenCentral() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/build.gradle new file mode 100644 index 00000000000..90470485bb0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/build.gradle @@ -0,0 +1 @@ +apply plugin: 'kotlin' diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/A.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/A.kt new file mode 100644 index 00000000000..fa4a783eb56 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/A.kt @@ -0,0 +1,5 @@ +package bar + +open class A { + fun a() {} +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/B.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/B.kt new file mode 100644 index 00000000000..48570ada2c7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/B.kt @@ -0,0 +1,5 @@ +package bar + +open class B : A() { + fun b() {} +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/BarDummy.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/BarDummy.kt new file mode 100644 index 00000000000..0c43afe7ba6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/BarDummy.kt @@ -0,0 +1,3 @@ +package bar + +class BarDummy \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/JavaClass.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/JavaClass.java new file mode 100644 index 00000000000..1cab11554a1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/JavaClass.java @@ -0,0 +1,5 @@ +package bar; + +public class JavaClass { + public String getString() { return "Hello, World!"; } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseA.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseA.kt new file mode 100644 index 00000000000..38632c6e331 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseA.kt @@ -0,0 +1,5 @@ +package bar + +private fun barUseA(a: A) { + a.a() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseAB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseAB.kt new file mode 100644 index 00000000000..66ef988f639 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseAB.kt @@ -0,0 +1,5 @@ +package bar + +fun useAB(b: B) { + b.b() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseB.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseB.kt new file mode 100644 index 00000000000..c9fddc5ef43 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/lib/src/main/java/bar/barUseB.kt @@ -0,0 +1,5 @@ +package bar + +private fun barUseB(b: B) { + b.b() +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/settings.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/settings.gradle new file mode 100644 index 00000000000..3cbe2493511 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/incrementalMultiproject/settings.gradle @@ -0,0 +1 @@ +include ':app', ':lib'