Support Gradle instant execution with Kotlin/JVM and Android tasks
Issue #KT-35126 Fixed
This commit is contained in:
+122
@@ -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())
|
||||
}
|
||||
}
|
||||
+1
@@ -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")
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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)
|
||||
|
||||
+9
-8
@@ -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()
|
||||
|
||||
+42
-26
@@ -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
|
||||
|
||||
+11
-4
@@ -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
|
||||
|
||||
|
||||
+9
@@ -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() })
|
||||
|
||||
+25
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user