Gradle: copy kotlin classes after java compilation

#KT-9392 fixed
    #KT-12295 fixed
    #KT-12736 fixed
This commit is contained in:
Alexey Tsvetkov
2016-06-24 18:00:14 +03:00
parent 3a59e1d222
commit 16b1616ebc
23 changed files with 418 additions and 85 deletions
@@ -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
@@ -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()
@@ -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())
}
}
@@ -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 <reified T : Any> TaskPropertyDelegate(propertyName: String) =
@@ -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<File, Long> = readTimestamps(timestampsFile)
@Suppress("unused")
@get:OutputFiles
val kotlinClassesInJavaOutputDir: Collection<File>
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<File, Long> {
val result = HashMap<File, Long>()
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<File, Long>) {
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)
}
}
}
@@ -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") {
@@ -39,6 +39,7 @@ abstract class KaptIncrementalBaseIT(val shouldUseStubs: Boolean): BaseGradleIT(
checkStubUsage()
checkGenerated(*annotatedElements)
checkNotGenerated("notAnnotatedFun")
assertContains("foo.ATest PASSED")
}
project.build("build") {
@@ -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")
}
}
}
@@ -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 {
}
@@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m
@@ -0,0 +1,13 @@
package foo;
public class Bar {
private final int x;
Bar(int x) {
this.x = x;
}
public int getX() {
return x;
}
}
@@ -0,0 +1,7 @@
package foo;
public class JavaBarUser {
public void use(Bar bar) {
System.out.println("Used from java Bar.getX() = " + bar.getX());
}
}
@@ -0,0 +1,7 @@
package foo
class KotlinBarUser {
fun use(bar: Bar) {
println("Used from kotlin Bar.getX() = ${bar.x}")
}
}
@@ -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 {
@@ -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());
}
}
@@ -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 << {
@@ -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());
}
}
@@ -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)
}
}
@@ -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 << {
@@ -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());
}
}