Support Gradle instant execution with Kotlin/JVM and Android tasks

Issue #KT-35126 Fixed
This commit is contained in:
Sergey Igushkin
2019-11-27 04:52:56 +03:00
parent d59a171b65
commit fa2ef816b1
8 changed files with 226 additions and 41 deletions
@@ -0,0 +1,122 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.util.AGPVersion
import org.jetbrains.kotlin.gradle.util.findFileByName
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Test
import java.io.File
import java.net.URI
import kotlin.test.fail
class InstantExecutionIT : BaseGradleIT() {
private val androidGradlePluginVersion: AGPVersion
get() = AGPVersion.v4_0_ALPHA_1
override fun defaultBuildOptions() =
super.defaultBuildOptions().copy(
androidHome = KotlinTestUtils.findAndroidSdk(),
androidGradlePluginVersion = androidGradlePluginVersion
)
private val minimumGradleVersion = GradleVersionRequired.AtLeast("6.1-milestone-1")
@Test
fun testSimpleKotlinJvmProject() = with(Project("kotlinProject", minimumGradleVersion)) {
testInstantExecutionOf(":compileKotlin")
}
@Test
fun testSimpleKotlinAndroidProject() = with(Project("AndroidProject", minimumGradleVersion)) {
applyAndroidAndroid40Alpha4KotlinVersionWorkaround()
testInstantExecutionOf(":Lib:compileFlavor1DebugKotlin", ":Android:compileFlavor1DebugKotlin")
}
private fun Project.testInstantExecutionOf(vararg taskNames: String) {
// First, run a build that serializes the tasks state for instant execution in further builds
instantExecutionOf(*taskNames) {
assertSuccessful()
assertTasksExecuted(*taskNames)
checkInstantExecutionSucceeded()
}
build("clean") {
assertSuccessful()
}
// Then run a build where tasks states are deserialized to check that they work correctly in this mode
instantExecutionOf(*taskNames) {
assertSuccessful()
assertTasksExecuted(*taskNames)
}
instantExecutionOf(*taskNames) {
assertSuccessful()
assertTasksUpToDate(*taskNames)
}
}
private fun Project.checkInstantExecutionSucceeded() {
instantExecutionReportFile()?.let { htmlReportFile ->
fail("Instant execution problems were found, check ${htmlReportFile.asClickableFileUrl()} for details.")
}
}
private fun Project.instantExecutionOf(vararg tasks: String, check: CompiledProject.() -> Unit) =
build("-Dorg.gradle.unsafe.instant-execution=true", *tasks, check = check)
/**
* Copies all files from the directory containing the given [htmlReportFile] to a
* fresh temp dir and returns a reference to the copied [htmlReportFile] in the new
* directory.
*/
private fun copyReportToTempDir(htmlReportFile: File): File =
createTempDir().let { tempDir ->
htmlReportFile.parentFile.copyRecursively(tempDir)
tempDir.resolve(htmlReportFile.name)
}
/**
* The instant execution report file, if exists, indicates problems were
* found while caching the task graph.
*/
private fun Project.instantExecutionReportFile() = projectDir
.resolve(".instant-execution-state")
.findFileByName("instant-execution-report.html")
?.let { copyReportToTempDir(it) }
private fun File.asClickableFileUrl(): String =
URI("file", "", toURI().path, null, null).toString()
/**
* Android Gradle plugin 4.0-alpha4 depends on the EAP versions of some o.j.k modules.
* Force the current Kotlin version, so the EAP versions are not queried from the
* test project's repositories, where there's no 'kotlin-eap' repo.
* TODO remove this workaround once an Android Gradle plugin version is used that depends on the stable Kotlin version
*/
private fun Project.applyAndroidAndroid40Alpha4KotlinVersionWorkaround() {
setupWorkingDir()
val resolutionStrategyHack = """
configurations.all {
resolutionStrategy.dependencySubstitution.all { dependency ->
def requested = dependency.requested
if (requested instanceof ModuleComponentSelector && requested.group == 'org.jetbrains.kotlin') {
dependency.useTarget requested.group + ':' + requested.module + ':' + '${defaultBuildOptions().kotlinVersion}'
}
}
}
""".trimIndent()
gradleBuildScript().appendText("\n" + """
buildscript {
$resolutionStrategyHack
}
$resolutionStrategyHack
""".trimIndent())
}
}
@@ -22,5 +22,6 @@ class AGPVersion private constructor(private val versionNumber: VersionNumber) {
val v3_1_0 = fromString("3.1.0")
val v3_2_0 = fromString("3.2.0")
val v3_3_2 = fromString("3.3.2")
val v4_0_ALPHA_1 = fromString("4.0.0-alpha01")
}
}
@@ -93,11 +93,14 @@ internal abstract class KotlinSourceSetProcessor<T : AbstractKotlinCompile<*>>(
destinationDir.set(project.provider { defaultKotlinDestinationDir })
}
return doRegisterTask(project, name) {
val result = doRegisterTask(project, name) {
it.description = taskDescription
it.mapClasspath { kotlinCompilation.compileDependencyFiles }
kotlinCompilation.output.addClassesDir { project.files(kotlinTask.get().destinationDir).builtBy(kotlinTask.get()) }
}
kotlinCompilation.output.addClassesDir { project.files(result.map { it.destinationDir }) }
return result
}
open fun run() {
@@ -439,7 +442,8 @@ internal abstract class AbstractKotlinPlugin(
val inspectTask =
registerTask(project, "inspectClassesForKotlinIC", InspectClassesForMultiModuleIC::class.java) {
it.sourceSetName = SourceSet.MAIN_SOURCE_SET_NAME
it.jarTask = jarTask
it.archivePath.set(project.provider { jarTask.archivePathCompatible.canonicalPath })
it.archiveName.set(project.provider { jarTask.archiveNameCompatible })
it.dependsOn(classesTask)
}
jarTask.dependsOn(inspectTask)
@@ -14,19 +14,24 @@ import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleJavaTargetExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.utils.archivePathCompatible
import org.jetbrains.kotlin.gradle.utils.newProperty
import java.io.File
internal open class InspectClassesForMultiModuleIC : DefaultTask() {
@get:Internal
lateinit var jarTask: Jar
@get:Input
internal val archivePath = project.newProperty<String>()
@get:Input
internal val archiveName = project.newProperty<String>()
@get:Input
lateinit var sourceSetName: String
@Suppress("MemberVisibilityCanBePrivate")
@get:OutputFile
internal val classesListFile: File
get() = (project.kotlinExtension as KotlinSingleJavaTargetExtension).target.defaultArtifactClassesListFile
internal val classesListFile: File by lazy {
(project.kotlinExtension as KotlinSingleJavaTargetExtension).target.defaultArtifactClassesListFile
}
@Suppress("MemberVisibilityCanBePrivate")
@get:InputFiles
@@ -39,10 +44,6 @@ internal open class InspectClassesForMultiModuleIC : DefaultTask() {
return project.files(fileTrees)
}
@get:Input
internal val archivePath: String
get() = jarTask.archivePathCompatible.canonicalPath
@TaskAction
fun run() {
classesListFile.parentFile.mkdirs()
@@ -36,16 +36,12 @@ import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.associateWithTransitiveClosure
import org.jetbrains.kotlin.gradle.plugin.mpp.ownModuleName
import org.jetbrains.kotlin.gradle.report.BuildReportMode
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
import org.jetbrains.kotlin.gradle.utils.isParentOf
import org.jetbrains.kotlin.gradle.utils.pathsAsStringRelativeTo
import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.classpathAsList
import org.jetbrains.kotlin.incremental.destinationAsFile
import org.jetbrains.kotlin.utils.LibraryUtils
import java.io.File
import java.util.*
import javax.inject.Inject
const val KOTLIN_BUILD_DIR_NAME = "kotlin"
@@ -93,8 +89,8 @@ abstract class AbstractKotlinCompileTool<T : CommonToolArguments>
@get:Classpath
@get:InputFiles
internal val computedCompilerClasspath: List<File>
get() = compilerClasspath?.takeIf { it.isNotEmpty() }
internal val computedCompilerClasspath: List<File> by project.provider {
compilerClasspath?.takeIf { it.isNotEmpty() }
?: compilerJarFile?.let {
// a hack to remove compiler jar from the cp, will be dropped when compilerJarFile will be removed
listOf(it) + findKotlinCompilerClasspath(project).filter { !it.name.startsWith("kotlin-compiler") }
@@ -113,6 +109,7 @@ abstract class AbstractKotlinCompileTool<T : CommonToolArguments>
findKotlinCompilerClasspath(project)
}
?: throw IllegalStateException("Could not find Kotlin Compiler classpath")
}
protected abstract fun findKotlinCompilerClasspath(project: Project): List<File>
@@ -126,8 +123,9 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
// avoid creating directory in getter: this can lead to failure in parallel build
@get:LocalState
internal val taskBuildDirectory: File
get() = File(File(project.buildDir, KOTLIN_BUILD_DIR_NAME), name)
internal val taskBuildDirectory: File by project.provider {
File(File(project.buildDir, KOTLIN_BUILD_DIR_NAME), name)
}
override fun localStateDirectories(): FileCollection = project.files(taskBuildDirectory)
@@ -145,8 +143,9 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
internal var buildReportMode: BuildReportMode? = null
@get:Internal
internal val taskData: KotlinCompileTaskData
get() = KotlinCompileTaskData.get(project, name)
internal val taskData: KotlinCompileTaskData by project.provider {
KotlinCompileTaskData.get(project, name)
}
@get:Input
internal open var useModuleDetection: Boolean
@@ -161,8 +160,9 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
@get:Classpath
@get:InputFiles
val pluginClasspath: FileCollection
get() = project.configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
val pluginClasspath: FileCollection by project.provider {
project.configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
}
@get:Internal
internal val pluginOptions = CompilerPluginOptions()
@@ -171,16 +171,23 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
@get:InputFiles
protected val additionalClasspath = arrayListOf<File>()
// Store this file collection before it is filtered by File::exists to ensure that Gradle Instant execution doesn't serialize the
// filtered files, losing those that don't exist yet and will only be created during build
private val compileClasspathImpl by project.provider {
classpath + additionalClasspath
}
@get:Internal // classpath already participates in the checks
internal val compileClasspath: Iterable<File>
get() = (classpath + additionalClasspath)
.filterTo(LinkedHashSet(), File::exists)
get() = compileClasspathImpl.filter { it.exists() }
@field:Transient
private val sourceFilesExtensionsSources: MutableList<Iterable<String>> = mutableListOf()
@get:Input
val sourceFilesExtensions: List<String>
get() = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS + sourceFilesExtensionsSources.flatten()
val sourceFilesExtensions: List<String> by project.provider {
DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS + sourceFilesExtensionsSources.flatten()
}
internal fun sourceFilesExtensions(extensions: Iterable<String>) {
sourceFilesExtensionsSources.add(extensions)
@@ -211,15 +218,18 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
internal val coroutinesStr: String
get() = coroutines.name
private val coroutines: Coroutines
get() = kotlinExt.experimental.coroutines
private val coroutines: Coroutines by project.provider {
kotlinExt.experimental.coroutines
?: coroutinesFromGradleProperties
?: Coroutines.DEFAULT
}
@get:Internal
internal var javaOutputDir: File?
get() = taskData.javaOutputDir
set(value) { taskData.javaOutputDir = value }
set(value) {
taskData.javaOutputDir = value
}
@get:Internal
internal val sourceSetName: String
@@ -230,17 +240,19 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
internal var commonSourceSet: FileCollection = project.files()
@get:Input
internal val moduleName: String
get() = taskData.compilation.moduleName
internal val moduleName: String by project.provider {
taskData.compilation.moduleName
}
@get:Internal // takes part in the compiler arguments
val friendPaths: Array<String>
get() = taskData.compilation.run {
val friendPaths: Array<String> by project.provider {
taskData.compilation.run {
associateWithTransitiveClosure
.flatMap { it.output.classesDirs }
.plus(friendArtifacts)
.map { it.canonicalPath }.toTypedArray()
}
}
private val kotlinLogger by lazy { GradleKotlinLogger(logger) }
@@ -308,6 +320,10 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
*/
internal abstract fun callCompilerAsync(args: T, sourceRoots: SourceRoots, changedFiles: ChangedFiles)
private val isMultiplatform: Boolean by project.provider {
project.plugins.any { it is KotlinPlatformPluginBase || it is KotlinMultiplatformPluginWrapper }
}
override fun setupCompilerArgs(args: T, defaultsOnly: Boolean, ignoreClasspathResolutionErrors: Boolean) {
args.coroutinesState = when (coroutines) {
Coroutines.ENABLE -> CommonCompilerArguments.ENABLE
@@ -322,7 +338,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
args.verbose = true
}
args.multiPlatform = project.plugins.any { it is KotlinPlatformPluginBase || it is KotlinMultiplatformPluginWrapper }
args.multiPlatform = isMultiplatform
setupPlugins(args)
}
@@ -380,7 +396,7 @@ open class KotlinCompile : AbstractKotlinCompile<K2JVMCompilerArguments>(), Kotl
args.apply { fillDefaultValues() }
super.setupCompilerArgs(args, defaultsOnly = defaultsOnly, ignoreClasspathResolutionErrors = ignoreClasspathResolutionErrors)
args.moduleName = taskData.compilation.moduleName
args.moduleName = moduleName
logger.kotlinDebug { "args.moduleName = ${args.moduleName}" }
args.friendPaths = friendPaths
@@ -9,19 +9,26 @@ import org.gradle.api.Project
import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinCompilation
import org.jetbrains.kotlin.gradle.utils.getValue
import java.io.File
internal open class KotlinCompileTaskData(
val taskName: String,
@field:Transient // cannot be serialized for Gradle Instant Execution, but actually is not needed when a task is deserialized
val compilation: AbstractKotlinCompilation<*>,
val destinationDir: Property<File>,
val useModuleDetection: Property<Boolean>
) {
private val taskBuildDirectory: File
get() = File(File(compilation.target.project.buildDir, KOTLIN_BUILD_DIR_NAME), taskName)
private val project: Project
get() = compilation.target.project
val buildHistoryFile: File
get() = File(taskBuildDirectory, "build-history.bin")
private val taskBuildDirectory: File by project.provider {
File(File(compilation.target.project.buildDir, KOTLIN_BUILD_DIR_NAME), taskName)
}
val buildHistoryFile: File by project.provider {
File(taskBuildDirectory, "build-history.bin")
}
var javaOutputDir: File? = null
@@ -79,6 +79,15 @@ internal val AbstractArchiveTask.archivePathCompatible: File
archivePath
}
internal val AbstractArchiveTask.archiveNameCompatible: String
get() =
if (isGradleVersionAtLeast(5, 1)) {
archiveFileName.get()
} else {
@Suppress("DEPRECATION")
archiveName
}
internal fun AbstractArchiveTask.setArchiveClassifierCompatible(classifierProvider: () -> String) {
if (isGradleVersionAtLeast(5, 2)) {
archiveClassifier.set(project.provider { classifierProvider() })
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.utils
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import kotlin.reflect.KProperty
internal operator fun <T> Provider<T>.getValue(thisRef: Any?, property: KProperty<*>) = get()
internal operator fun <T> Property<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
set(value)
}
internal fun <T : Any> Project.newProperty(initialize: (() -> T)? = null): Property<T> =
@Suppress("UNCHECKED_CAST")
// use Any and not T::class to allow using lists and maps as the property type, which is otherwise not allowed
(project.objects.property(Any::class.java) as Property<T>).apply {
if (initialize != null)
set(provider(initialize))
}