diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/InstantExecutionIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/InstantExecutionIT.kt new file mode 100644 index 00000000000..45f5ed97917 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/InstantExecutionIT.kt @@ -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()) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/AGPVersion.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/AGPVersion.kt index 50f64333e2f..8d0a8bc5b16 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/AGPVersion.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/AGPVersion.kt @@ -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") } } \ No newline at end of file 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 5cd79174db7..155e2a615a4 100755 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt @@ -93,11 +93,14 @@ internal abstract class KotlinSourceSetProcessor>( 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) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/InspectClassesForMultiModuleIC.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/InspectClassesForMultiModuleIC.kt index 063f91e01db..2abcce81281 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/InspectClassesForMultiModuleIC.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/InspectClassesForMultiModuleIC.kt @@ -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() + + @get:Input + internal val archiveName = project.newProperty() @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() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt index 86f734721bb..ac609dc8ea9 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt @@ -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 @get:Classpath @get:InputFiles - internal val computedCompilerClasspath: List - get() = compilerClasspath?.takeIf { it.isNotEmpty() } + internal val computedCompilerClasspath: List 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 findKotlinCompilerClasspath(project) } ?: throw IllegalStateException("Could not find Kotlin Compiler classpath") + } protected abstract fun findKotlinCompilerClasspath(project: Project): List @@ -126,8 +123,9 @@ abstract class AbstractKotlinCompile() : 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() : 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() : 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() : AbstractKo @get:InputFiles protected val additionalClasspath = arrayListOf() + // 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 - get() = (classpath + additionalClasspath) - .filterTo(LinkedHashSet(), File::exists) + get() = compileClasspathImpl.filter { it.exists() } + @field:Transient private val sourceFilesExtensionsSources: MutableList> = mutableListOf() @get:Input - val sourceFilesExtensions: List - get() = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS + sourceFilesExtensionsSources.flatten() + val sourceFilesExtensions: List by project.provider { + DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS + sourceFilesExtensionsSources.flatten() + } internal fun sourceFilesExtensions(extensions: Iterable) { sourceFilesExtensionsSources.add(extensions) @@ -211,15 +218,18 @@ abstract class AbstractKotlinCompile() : 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() : 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 - get() = taskData.compilation.run { + val friendPaths: Array 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() : 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() : 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(), 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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/kotlinCompileTaskData.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/kotlinCompileTaskData.kt index e79478ed4d2..7773f03115d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/kotlinCompileTaskData.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/kotlinCompileTaskData.kt @@ -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, val useModuleDetection: Property ) { - 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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/compatibiltiy.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/compatibiltiy.kt index 2ecd0063e20..069c090b22d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/compatibiltiy.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/compatibiltiy.kt @@ -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() }) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/providerApiUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/providerApiUtils.kt new file mode 100644 index 00000000000..6d3467302cf --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/providerApiUtils.kt @@ -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 Provider.getValue(thisRef: Any?, property: KProperty<*>) = get() + +internal operator fun Property.setValue(thisRef: Any?, property: KProperty<*>, value: T) { + set(value) +} + +internal fun Project.newProperty(initialize: (() -> T)? = null): Property = + @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).apply { + if (initialize != null) + set(provider(initialize)) + } \ No newline at end of file