From 16b1616ebce920c0022ff2b8b19f10453f12925f Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Fri, 24 Jun 2016 18:00:14 +0300 Subject: [PATCH] Gradle: copy kotlin classes after java compilation #KT-9392 fixed #KT-12295 fixed #KT-12736 fixed --- .../jetbrains/kotlin/gradle/tasks/Tasks.kt | 101 +++------- .../internal/AnnotationProcessingManager.kt | 1 - .../kotlin/gradle/plugin/KotlinPlugin.kt | 48 ++++- .../gradle/plugin/KotlinPluginWrapper.kt | 6 + .../kotlin/gradle/plugin/taskUtils.kt | 2 +- .../kotlin/gradle/tasks/SyncOutputTask.kt | 181 ++++++++++++++++++ .../org/jetbrains/kotlin/gradle/KaptIT.kt | 3 + .../kotlin/gradle/KaptIncrementalIT.kt | 1 + .../kotlin/gradle/KotlinGradlePluginIT.kt | 47 +++++ .../convertBetweenJavaAndKotlin/build.gradle | 27 +++ .../gradle.properties | 1 + .../src/main/java/foo/Bar.java | 13 ++ .../src/main/java/foo/Bar.kt | 3 + .../src/main/java/foo/Dummy.java | 4 + .../src/main/java/foo/JavaBarUser.java | 7 + .../src/main/java/foo/KotlinBarUser.kt | 7 + .../build.gradle | 1 + .../src/test/java/foo/ATest.java | 12 ++ .../testProject/kaptSimple/build.gradle | 1 + .../src/test/java/example/JavaTest.java | 12 ++ .../src/test/java/example/KotlinTest.kt | 12 ++ .../testProject/kaptStubs/build.gradle | 1 + .../src/test/java/example/JavaTest.java | 12 ++ 23 files changed, 418 insertions(+), 85 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/SyncOutputTask.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/build.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/gradle.properties create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Dummy.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/JavaBarUser.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/KotlinBarUser.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/src/test/java/foo/ATest.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/JavaTest.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/KotlinTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/src/test/java/example/JavaTest.java 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 bed3d46ed8b..9ede901c73f 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 @@ -1,14 +1,5 @@ package org.jetbrains.kotlin.gradle.tasks -import org.jetbrains.kotlin.com.intellij.ide.highlighter.JavaFileType -import org.jetbrains.kotlin.com.intellij.lang.Language -import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer -import org.jetbrains.kotlin.com.intellij.openapi.util.io.FileUtil -import org.jetbrains.kotlin.com.intellij.psi.PsiClass -import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory -import org.jetbrains.kotlin.com.intellij.psi.PsiJavaFile -import org.jetbrains.kotlin.com.intellij.psi.impl.PsiFileFactoryImpl -import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils import org.apache.commons.lang.StringUtils import org.codehaus.groovy.runtime.MethodClosure @@ -36,6 +27,14 @@ import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.com.intellij.ide.highlighter.JavaFileType +import org.jetbrains.kotlin.com.intellij.lang.Language +import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer +import org.jetbrains.kotlin.com.intellij.openapi.util.io.FileUtil +import org.jetbrains.kotlin.com.intellij.psi.PsiClass +import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory +import org.jetbrains.kotlin.com.intellij.psi.PsiJavaFile +import org.jetbrains.kotlin.com.intellij.psi.impl.PsiFileFactoryImpl import org.jetbrains.kotlin.compilerRunner.ArgumentUtils import org.jetbrains.kotlin.compilerRunner.OutputItemsCollector import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl @@ -69,11 +68,11 @@ abstract class AbstractKotlinCompile() : AbstractCo // it's not possible when IncrementalTaskInputs#isIncremental returns false (i.e first build) var incremental: Boolean = false var kotlinOptions: T = createBlankArgs() - var kotlinDestinationDir: File? = destinationDir var compilerCalled: Boolean = false // TODO: consider more reliable approach (see usage) var anyClassesCompiled: Boolean = false var friendTaskName: String? = null + var javaOutputDir: File? = null private val loggerInstance = Logging.getLogger(this.javaClass) override fun getLogger() = loggerInstance @@ -104,14 +103,7 @@ abstract class AbstractKotlinCompile() : AbstractCo populateCommonArgs(args) populateTargetSpecificArgs(args) compilerCalled = true - beforeCompileHook(args) - - try { - callCompiler(args, sources, inputs.isIncremental, modified, removed) - } - finally { - afterCompileHook(args) - } + callCompiler(args, sources, inputs.isIncremental, modified, removed) } private fun getKotlinSources(): List = (getSource() as Iterable).filter { it.isKotlinFile() } @@ -148,14 +140,10 @@ abstract class AbstractKotlinCompile() : AbstractCo open class KotlinCompile() : AbstractKotlinCompile() { override val compiler = K2JVMCompiler() override fun createBlankArgs(): K2JVMCompilerArguments = K2JVMCompilerArguments() - private val kotlinClassFiles = HashSet() // Should be SourceDirectorySet or File val srcDirsSources = HashSet() - // TODO: find out whether we really need to be able to override destination dir here, and how it should work with destinationDir property - private val compilerDestinationDir: String get() = if (StringUtils.isEmpty(kotlinOptions.destination)) { kotlinDestinationDir?.path.orEmpty() } else { kotlinOptions.destination } - // lazy because name is probably not available when constructor is called 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) } @@ -180,7 +168,12 @@ open class KotlinCompile() : AbstractKotlinCompile() { logger.kotlinDebug("args.classpath = ${args.classpath}") } - logger.kotlinDebug("destinationDir = $compilerDestinationDir") + if (args.destination?.isNotBlank() ?: false) { + // TODO: fix if needed + logger.warn("Setting kotlinOptions.destination is not supported in gradle") + } + + logger.kotlinDebug("destinationDir = $destinationDir") val extraProperties = extensions.extraProperties args.pluginClasspaths = extraProperties.getOrNull>("compilerPluginClasspaths") ?: arrayOf() @@ -204,18 +197,11 @@ open class KotlinCompile() : AbstractKotlinCompile() { args.languageVersion = kotlinOptions.languageVersion args.jvmTarget = kotlinOptions.jvmTarget - fun addFriendPathForTestTask(taskName: String) { - logger.kotlinDebug("try to determine the output directory of corresponding $taskName task") - val tasks = project.getTasksByName("$taskName", false) - logger.kotlinDebug("tasks for $taskName: ${tasks}") - if (tasks.size == 1) { - val task = tasks.firstOrNull() as? KotlinCompile - if (task != null) { - logger.kotlinDebug("destination directory for production = ${task.destinationDir}") - args.friendPaths = arrayOf(task.destinationDir.absolutePath) - args.moduleName = task.kotlinOptions.moduleName ?: task.extensions.extraProperties.getOrNull("defaultModuleName") - } - } + fun addFriendPathForTestTask(friendKotlinTaskName: String) { + val friendTask = project.getTasksByName(friendKotlinTaskName, /* recursive = */false).firstOrNull() as? KotlinCompile ?: return + args.friendPaths = arrayOf(friendTask.javaOutputDir!!.absolutePath) + args.moduleName = friendTask.kotlinOptions.moduleName ?: friendTask.extensions.extraProperties.getOrNull("defaultModuleName") + logger.kotlinDebug("java destination directory for production = ${friendTask.javaOutputDir}") } logger.kotlinDebug { "friendTaskName = $friendTaskName" } @@ -237,7 +223,7 @@ open class KotlinCompile() : AbstractKotlinCompile() { val targetType = "java-production" val moduleName = args.moduleName val targets = listOf(TargetId(moduleName, targetType)) - val outputDir = File(compilerDestinationDir) + val outputDir = destinationDir val caches = hashMapOf() val lookupStorage = LookupStorage(File(cacheDirectory, "lookups")) val lookupTracker = LookupTrackerImpl(LookupTracker.DO_NOTHING) @@ -331,14 +317,9 @@ open class KotlinCompile() : AbstractKotlinCompile() { } fun cleanupOnError() { - val outputDirFile = File(compilerDestinationDir) - - assert(outputDirFile.exists()) - val generatedRelPaths = allGeneratedFiles.map { it.outputFile.toRelativeString(outputDirFile) } - logger.kotlinInfo("deleting output on error: ${generatedRelPaths.joinToString()}") - - allGeneratedFiles.forEach { it.outputFile.delete() } - generatedRelPaths.forEach { File(destinationDir, it).delete() } + val filesToDelete = allGeneratedFiles.map { it.outputFile } + logger.kotlinInfo("deleting output on error: ${filesToDelete.joinToString()}") + filesToDelete.forEach { it.delete() } } fun processCompilerExitCode(exitCode: ExitCode) { @@ -527,38 +508,6 @@ open class KotlinCompile() : AbstractKotlinCompile() { private fun File.isJavaFile() = extension.equals(JavaFileType.INSTANCE.defaultExtension, ignoreCase = true) - override fun beforeCompileHook(args: K2JVMCompilerArguments) { - kotlinClassFiles.addAll(listClassFiles(compilerDestinationDir)) - } - - override fun afterCompileHook(args: K2JVMCompilerArguments) { - kaptAnnotationsFileUpdater?.dispose() - - if (kaptStubGeneratingMode) return - - logger.debug("Copying resulting files to classes") - - // Copy kotlin classes to all classes directory - val outputDirFile = File(compilerDestinationDir) - if (outputDirFile.exists()) { - FileUtils.copyDirectory(outputDirFile, destinationDir) - } - - kotlinClassFiles.removeAll(listClassFiles(compilerDestinationDir)) - if (kotlinClassFiles.isNotEmpty()) { - // some classes were removed during compilation - val filesToRemove = kotlinClassFiles.map { - val relativePath = it.relativeTo(outputDirFile).path - File(destinationDir, relativePath) - } - - val notRemoved = filesToRemove.filter { !it.delete() } - if (notRemoved.isNotEmpty()) { - logger.kotlinDebug("Could not delete classfiles: $notRemoved") - } - } - } - // override setSource to track source directory sets and files (for generated android folders) override fun setSource(source: Any?) { srcDirsSources.clear() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/AnnotationProcessingManager.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/AnnotationProcessingManager.kt index 28a4ca63003..ca5502bcb39 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/AnnotationProcessingManager.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/AnnotationProcessingManager.kt @@ -106,7 +106,6 @@ private fun Project.createKotlinAfterJavaTask( taskFactory: (suffix: String) -> AbstractCompile ): AbstractCompile { val kotlinAfterJavaTask = with (taskFactory(KOTLIN_AFTER_JAVA_TASK_SUFFIX)) { - kotlinDestinationDir = kotlinTask.kotlinDestinationDir destinationDir = kotlinTask.destinationDir classpath = kotlinTask.classpath - project.files(javaTask.destinationDir) this 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 247f37f738d..73cad19c81e 100644 --- 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 @@ -33,6 +33,7 @@ import org.jetbrains.kotlin.gradle.internal.KotlinSourceSetImpl import org.jetbrains.kotlin.gradle.internal.initKapt import org.jetbrains.kotlin.gradle.plugin.android.AndroidGradleWrapper import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider +import org.jetbrains.kotlin.gradle.tasks.SyncOutputTask import java.io.File import java.net.URL import java.util.* @@ -145,7 +146,7 @@ class Kotlin2JvmSourceSetProcessor( project.afterEvaluate { project -> if (project != null) { - kotlinTask.kotlinDestinationDir = File(project.buildDir, "kotlin-classes/$sourceSetName") + kotlinTask.destinationDir = File(project.buildDir, "kotlin-classes/$sourceSetName") for (dir in sourceSet.getJava().srcDirs) { kotlinDirSet?.srcDir(dir) @@ -180,6 +181,7 @@ class Kotlin2JvmSourceSetProcessor( } configureJavaTask(kotlinTask, javaTask, kotlinAfterJavaTask, logger) + createSyncOutputTask(project, kotlinTask, javaTask, kotlinAfterJavaTask, sourceSetName) } } } @@ -203,7 +205,7 @@ class Kotlin2JsSourceSetProcessor( val build = project.tasks.findByName("build") val defaultKotlinDestinationDir = File(project.buildDir, "kotlin2js/${sourceSetName}") - private fun kotlinTaskDestinationDir(): File? = kotlinTask.kotlinDestinationDir + private fun kotlinTaskDestinationDir(): File? = kotlinTask.destinationDir private fun kotlinJsDestinationDir(): File? = (kotlinTask.property("outputFile") as String).let { File(it) }.let { if (it.isDirectory) it else it.parentFile } private fun kotlinSourcePathsForSourceMap() = sourceSet.getAllSource() @@ -217,7 +219,7 @@ class Kotlin2JsSourceSetProcessor( tasksProvider.createKotlinJSTask(project, taskName) override fun doTargetSpecificProcessing() { - kotlinTask.kotlinDestinationDir = defaultKotlinDestinationDir + kotlinTask.destinationDir = defaultKotlinDestinationDir build?.dependsOn(kotlinTaskName) clean?.dependsOn("clean" + kotlinTaskName.capitalize()) @@ -373,8 +375,7 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand } // store kotlin classes in separate directory. They will serve as class-path to java compiler - kotlinTask.kotlinDestinationDir = File(project.buildDir, "tmp/kotlin-classes/$variantDataName") - kotlinTask.destinationDir = javaTask.destinationDir + kotlinTask.destinationDir = File(project.buildDir, "tmp/kotlin-classes/$variantDataName") kotlinTask.description = "Compiles the ${variantDataName} kotlin." kotlinTask.setDependsOn(javaTask.dependsOn) @@ -420,7 +421,7 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand // java classpath during project evaluation (see prepareComAndroidSupportSupportV42311Library) val fullClasspath = lazy { val androidRT = project.files(AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt)) - (javaTask.classpath + androidRT) - project.files(kotlinTask.kotlinDestinationDir) + (javaTask.classpath + androidRT) - project.files(kotlinTask.destinationDir) } kotlinTask.classpath = javaTask.classpath kotlinTask.updateClasspathBeforeTask { fullClasspath.value } @@ -446,6 +447,7 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand } configureJavaTask(kotlinTask, javaTask, kotlinAfterJavaTask, logger) + createSyncOutputTask(project, kotlinTask, javaTask, kotlinAfterJavaTask, variantDataName) } } @@ -487,10 +489,42 @@ private fun configureJavaTask(kotlinTask: AbstractCompile, javaTask: AbstractCom * so it's only safe to modify javaTask.classpath right before its usage */ if (kotlinAfterJavaTask == null) { - javaTask.appendClasspathDynamically(kotlinTask.kotlinDestinationDir!!) + javaTask.appendClasspathDynamically(kotlinTask.destinationDir!!) } } +private fun createSyncOutputTask( + project: Project, + kotlinTask: AbstractCompile, + javaTask: AbstractCompile, + kotlinAfterJavaTask: AbstractCompile?, + variantName: String +) { + // if kotlinAfterJavaTask is not null then kotlinTask compiles stubs, so don't sync them + val kotlinDir = (kotlinAfterJavaTask ?: kotlinTask).destinationDir + val javaDir = javaTask.destinationDir + val taskName = "copy${variantName.capitalize()}KotlinClasses" + + val syncTask = project.tasks.create(taskName, SyncOutputTask::class.java) + syncTask.kotlinOutputDir = kotlinDir + syncTask.javaOutputDir = javaDir + kotlinTask.javaOutputDir = javaDir + kotlinAfterJavaTask?.javaOutputDir = javaDir + + // copying should be executed after a latter task + if (kotlinAfterJavaTask != null) { + // finalizer tasks get executed even if finalizing task has failed; + // we want to avoid copying classes if kotlin compilation has failed + syncTask.onlyIf { kotlinAfterJavaTask.state.failure == null } + kotlinAfterJavaTask.finalizedBy(syncTask) + } + else { + javaTask.finalizedBy(syncTask) + } + + project.logger.kotlinDebug { "Created task ${syncTask.path} to copy kotlin classes from $kotlinDir to $javaDir" } +} + private fun loadSubplugins(project: Project): SubpluginEnvironment { try { val subplugins = ServiceLoader.load(KotlinGradleSubplugin::class.java, project.buildscript.classLoader).toList() 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 7bbd85cf4c6..61317a64649 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 @@ -70,3 +70,9 @@ open class Kotlin2JsPluginWrapper : KotlinBasePluginWrapper() { fun Logger.kotlinDebug(message: String) { this.debug("[KOTLIN] $message") } + +inline fun Logger.kotlinDebug(message: () -> String) { + if (isDebugEnabled) { + kotlinDebug(message()) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/taskUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/taskUtils.kt index 75369313a48..9f1e4e6f83b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/taskUtils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/taskUtils.kt @@ -52,8 +52,8 @@ internal fun AbstractCompile.appendClasspathDynamically(file: File) { } internal var AbstractTask.anyClassesCompiled: Boolean? by TaskPropertyDelegate("anyClassesCompiled") -internal var AbstractTask.kotlinDestinationDir: File? by TaskPropertyDelegate("kotlinDestinationDir") internal var AbstractTask.friendTaskName: String? by TaskPropertyDelegate("friendTaskName") +internal var AbstractTask.javaOutputDir: File? by TaskPropertyDelegate("javaOutputDir") inline internal fun TaskPropertyDelegate(propertyName: String) = diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/SyncOutputTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/SyncOutputTask.kt new file mode 100644 index 00000000000..3398599de25 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/SyncOutputTask.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2010-2016 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.gradle.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.incremental.IncrementalTaskInputs +import org.gradle.api.tasks.incremental.InputFileDetails +import org.jetbrains.kotlin.gradle.plugin.kotlinDebug +import java.io.File +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.util.* +import kotlin.properties.Delegates + +/** + * Copies kotlin classes to java output directory. + * No other way to get our classes into apk for android until API is provided https://code.google.com/p/android/issues/detail?id=200714 + * + * Utilises gradle for tracking input/output changes. + * 0. Gradle takes snapshots of annotated properties after compilation. + * 1. kotlinOutputDir as input files. These files will be copied. + * Copying will be done incrementally unless output classes are wiped out + * (by IncrementalJavaCompilationSafeguard task from android gradle plugin for example). + * 2. kotlinClassesInJavaOutputDir as output files -- copied classes in java output dir. + * When they are changed or removed gradle tells us that incremental build is not possible (inputs.isIncremental is false). + * 3. We do non-incremental sync in the following way: + * a. previously copied files are deleted (to do so we save the list of copied files after copying); + * b. all kotlin files are copied to java output dir. + * + * It is possible that some file was converted from kotlin to java. + * In this case gradle will tell us to make non-incremental sync. + * To workaround this scenario we also save timestamp of copied class. + * On step (a) of sync process we do not delete the class in java output dir + * if it's timestamp now is newer than when we copied it + * (assuming it was modified by javac when class was converted to java). + */ +open class SyncOutputTask : DefaultTask() { + @get:InputFiles + var kotlinOutputDir: File by Delegates.notNull() + var javaOutputDir: File by Delegates.notNull() + + // OutputDirectory needed for task to be incremental + @get:OutputDirectory + val workingDir = File(project.buildDir, name).apply { mkdirs() } + private val timestampsFile = File(workingDir, TIMESTAMP_FILE_NAME) + private val timestamps: MutableMap = readTimestamps(timestampsFile) + + @Suppress("unused") + @get:OutputFiles + val kotlinClassesInJavaOutputDir: Collection + get() = timestamps.keys + + @Suppress("unused") + @TaskAction + fun execute(inputs: IncrementalTaskInputs): Unit { + if (inputs.isIncremental) { + logger.kotlinDebug { "Incremental copying files from $kotlinOutputDir to $javaOutputDir" } + inputs.outOfDate { processIncrementally(it) } + inputs.removed { processIncrementally(it) } + } + else { + logger.kotlinDebug { "Non-incremental copying files from $kotlinOutputDir to $javaOutputDir" } + processNonIncrementally() + } + + saveTimestamps(timestampsFile, timestamps) + } + + private fun processNonIncrementally() { + for ((file, timestamp) in timestamps) { + // wipe all files written by us + // do not remove files converted to java (newer timestamp) + if (file.lastModified() == timestamp) { + file.delete() + } + } + + timestampsFile.delete() + timestamps.clear() + + kotlinOutputDir.walkTopDown().forEach { + copy(it, it.siblingInJavaDir) + } + } + + private fun processIncrementally(input: InputFileDetails) { + val fileInKotlinDir = input.file + val fileInJavaDir = fileInKotlinDir.siblingInJavaDir + + if (input.isRemoved) { + // file was removed in kotlin dir, remove from java as well + remove(fileInJavaDir) + } + else { + // copy modified or added file from kotlin to java + copy(fileInKotlinDir, fileInJavaDir) + } + } + + private fun remove(fileInJavaDir: File) { + if (!fileInJavaDir.isFile) return + + fileInJavaDir.delete() + timestamps.remove(fileInJavaDir) + + logger.kotlinDebug { + "Removed kotlin class ${fileInJavaDir.relativeTo(javaOutputDir).path} from $javaOutputDir" + } + } + + private fun copy(fileInKotlinDir: File, fileInJavaDir: File) { + if (!fileInKotlinDir.isFile) return + + fileInJavaDir.parentFile.mkdirs() + fileInKotlinDir.copyTo(fileInJavaDir, overwrite = true) + timestamps[fileInJavaDir] = fileInJavaDir.lastModified() + + logger.kotlinDebug { + "Copied kotlin class ${fileInKotlinDir.relativeTo(kotlinOutputDir).path} from $kotlinOutputDir to $javaOutputDir" + } + } + + private val File.siblingInJavaDir: File + get() = File(javaOutputDir, this.relativeTo(kotlinOutputDir).path) +} + +private val TIMESTAMP_FILE_NAME = "kotlin-files-in-java-timestamps.bin" + +private fun readTimestamps(file: File): MutableMap { + val result = HashMap() + if (!file.isFile) return result + + ObjectInputStream(file.inputStream()).use { input -> + val size = input.readInt() + + repeat(size) { + val path = input.readUTF() + val timestamp = input.readLong() + result[File(path)] = timestamp + } + } + + return result +} + +private fun saveTimestamps(snapshotFile: File, timestamps: Map) { + if (!snapshotFile.exists()) { + snapshotFile.parentFile.mkdirs() + snapshotFile.createNewFile() + } + else { + snapshotFile.delete() + } + + ObjectOutputStream(snapshotFile.outputStream()).use { out -> + out.writeInt(timestamps.size) + + for ((file, timestamp) in timestamps) { + out.writeUTF(file.canonicalPath) + out.writeLong(timestamp) + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIT.kt index b4e259a8043..6e43774e2c4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIT.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIT.kt @@ -29,6 +29,8 @@ class KaptIT: BaseGradleIT() { assertNoSuchFile("build/classes/main/example/SourceAnnotatedTestClassGenerated.class") assertFileExists("build/classes/main/example/BinaryAnnotatedTestClassGenerated.class") assertFileExists("build/classes/main/example/RuntimeAnnotatedTestClassGenerated.class") + assertContains("example.JavaTest PASSED") + assertContains("example.KotlinTest PASSED") } project.build("build") { @@ -69,6 +71,7 @@ class KaptIT: BaseGradleIT() { assertFileExists("build/classes/main/example/BinaryAnnotatedTestClassGenerated.class") assertFileExists("build/classes/main/example/RuntimeAnnotatedTestClassGenerated.class") assertNotContains("w: Classpath entry points to a non-existent location") + assertContains("example.JavaTest PASSED") } project.build("build") { diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt index 04abd4e2e9c..559c5f1898d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt @@ -39,6 +39,7 @@ abstract class KaptIncrementalBaseIT(val shouldUseStubs: Boolean): BaseGradleIT( checkStubUsage() checkGenerated(*annotatedElements) checkNotGenerated("notAnnotatedFun") + assertContains("foo.ATest PASSED") } project.build("build") { diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt index 691500c9bba..5a8cdc046a0 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinGradlePluginIT.kt @@ -3,8 +3,10 @@ package org.jetbrains.kotlin.gradle import org.gradle.api.logging.LogLevel import org.jetbrains.kotlin.gradle.plugin.CleanUpBuildListener import org.jetbrains.kotlin.gradle.tasks.USING_EXPERIMENTAL_INCREMENTAL_MESSAGE +import org.jetbrains.kotlin.gradle.util.getFileByName import org.junit.Test import java.io.File +import kotlin.test.assertNotEquals import kotlin.test.assertTrue class KotlinGradleIT: BaseGradleIT() { @@ -199,4 +201,49 @@ class KotlinGradleIT: BaseGradleIT() { assertContains(USING_EXPERIMENTAL_INCREMENTAL_MESSAGE) } } + + @Test + fun testConvertJavaToKotlin() { + val project = Project("convertBetweenJavaAndKotlin", GRADLE_VERSION) + project.setupWorkingDir() + + val barKt = project.projectDir.getFileByName("Bar.kt") + val barKtContent = barKt.readText() + barKt.delete() + + project.build("build") { + assertSuccessful() + } + + val barClass = project.projectDir.getFileByName("Bar.class") + val barClassTimestamp = barClass.lastModified() + + val barJava = project.projectDir.getFileByName("Bar.java") + barJava.delete() + barKt.writeText(barKtContent) + + project.build("build") { + assertSuccessful() + assertNotContains(":compileKotlin UP-TO-DATE", ":compileJava UP-TO-DATE") + assertNotEquals(barClassTimestamp, barClass.lastModified(), "Bar.class timestamp hasn't been updated") + } + } + + @Test + fun testWipeClassesDirectoryBetweenBuilds() { + val project = Project("kotlinJavaProject", GRADLE_VERSION) + + project.build("build") { + assertSuccessful() + } + + val javaOutputDir = File(project.projectDir, "build/classes") + assert(javaOutputDir.isDirectory) { "Classes directory does not exist $javaOutputDir" } + javaOutputDir.deleteRecursively() + + project.build("build") { + assertSuccessful() + assertContains(":compileKotlin UP-TO-DATE") + } + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/build.gradle new file mode 100644 index 00000000000..741e9027d14 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/build.gradle @@ -0,0 +1,27 @@ +buildscript { + repositories { + maven { url 'file://' + pathToKotlinPlugin } + } + dependencies { + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1-SNAPSHOT' + } +} + +apply plugin: "java" +apply plugin: "kotlin" + +repositories { + maven { url 'file://' + pathToKotlinPlugin } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT' +} + +compileJava { + options.incremental = true +} + +compileJava.doLast { + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/gradle.properties b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/gradle.properties new file mode 100644 index 00000000000..93ce6a94e63 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.java new file mode 100644 index 00000000000..302417f3a9c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.java @@ -0,0 +1,13 @@ +package foo; + +public class Bar { + private final int x; + + Bar(int x) { + this.x = x; + } + + public int getX() { + return x; + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.kt new file mode 100644 index 00000000000..efb94aa4bf1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Bar.kt @@ -0,0 +1,3 @@ +package foo + +class Bar(val x: Int) diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Dummy.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Dummy.java new file mode 100644 index 00000000000..25cb141a5ae --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/Dummy.java @@ -0,0 +1,4 @@ +package foo; + +public class Dummy { +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/JavaBarUser.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/JavaBarUser.java new file mode 100644 index 00000000000..5c98a9065ab --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/JavaBarUser.java @@ -0,0 +1,7 @@ +package foo; + +public class JavaBarUser { + public void use(Bar bar) { + System.out.println("Used from java Bar.getX() = " + bar.getX()); + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/KotlinBarUser.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/KotlinBarUser.kt new file mode 100644 index 00000000000..f79da39e1b6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/convertBetweenJavaAndKotlin/src/main/java/foo/KotlinBarUser.kt @@ -0,0 +1,7 @@ +package foo + +class KotlinBarUser { + fun use(bar: Bar) { + println("Used from kotlin Bar.getX() = ${bar.x}") + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/build.gradle index ecdf8773160..c28df75858e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/build.gradle @@ -24,6 +24,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT" compile "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" kapt "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" + testCompile 'junit:junit:4.12' } kapt { diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/src/test/java/foo/ATest.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/src/test/java/foo/ATest.java new file mode 100644 index 00000000000..989d218a539 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptIncrementalCompilationProject/src/test/java/foo/ATest.java @@ -0,0 +1,12 @@ +package foo; + +import org.junit.Test; +import org.junit.Assert; + +public class ATest { + @Test + public void testValA() { + A a = new A(); + Assert.assertEquals("text", a.getValA()); + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/build.gradle index ed54853398a..8dfa59408c6 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/build.gradle @@ -24,6 +24,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT" compile "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" kapt "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" + testCompile 'junit:junit:4.12' } task show << { diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/JavaTest.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/JavaTest.java new file mode 100644 index 00000000000..02f59df5730 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/JavaTest.java @@ -0,0 +1,12 @@ +package example; + +import org.junit.Assert; +import org.junit.Test; + +public class JavaTest { + @Test + public void test() { + TestClass testClass = new TestClass(); + Assert.assertEquals("text", testClass.getTestVal()); + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/KotlinTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/KotlinTest.kt new file mode 100644 index 00000000000..57b1c96690d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptSimple/src/test/java/example/KotlinTest.kt @@ -0,0 +1,12 @@ +package example + +import org.junit.Assert +import org.junit.Test + +class KotlinTest { + @Test + fun test() { + val testClass = TestClass() + Assert.assertEquals("text", testClass.testVal) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/build.gradle b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/build.gradle index daff350b26a..c12251e38c3 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/build.gradle @@ -24,6 +24,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT" compile "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" kapt "org.jetbrains.kotlin:annotation-processor-example:1.1-SNAPSHOT" + testCompile 'junit:junit:4.12' } task show << { diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/src/test/java/example/JavaTest.java b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/src/test/java/example/JavaTest.java new file mode 100644 index 00000000000..02f59df5730 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/resources/testProject/kaptStubs/src/test/java/example/JavaTest.java @@ -0,0 +1,12 @@ +package example; + +import org.junit.Assert; +import org.junit.Test; + +public class JavaTest { + @Test + public void test() { + TestClass testClass = new TestClass(); + Assert.assertEquals("text", testClass.getTestVal()); + } +} \ No newline at end of file