From 9f2e5cdc4dfa21ea66603ca36699ffa9bef830ae Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Mon, 15 Oct 2018 21:47:18 +0300 Subject: [PATCH] Propagate the subplugin options from the tasks to the source sets If a source set is used in only one compilation, take the options from its compile task. If a source set is used by multiple compilations of a single target, either choose the 'main' compilation or choose any (this will happen for Android, and it looks OK for the first time). If there are multiple compilations of different targets, use the metadata compilation. Issue #KT-27499 In Progress --- .../AbstractKotlinAndroidGradleTests.kt | 23 ++++- .../kotlin/gradle/NewMultiplatformIT.kt | 88 +++++++++++++++++++ .../new-mpp-android/app/build.gradle | 13 +++ .../plugin/mpp/KotlinMultiplatformPlugin.kt | 60 +++++++++++-- .../sources/DefaultLanguageSettingsBuilder.kt | 28 +++++- 5 files changed, 204 insertions(+), 8 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt index 6758e816484..64ff6c0cd23 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/AbstractKotlinAndroidGradleTests.kt @@ -8,6 +8,8 @@ import org.jetbrains.kotlin.gradle.util.modify import org.jetbrains.kotlin.test.KotlinTestUtils import org.junit.Test import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue class KotlinAndroidGradleIT : AbstractKotlinAndroidGradleTests(androidGradlePluginVersion = "2.3.0") { @@ -22,7 +24,7 @@ class KotlinAndroid32GradleIT : KotlinAndroid3GradleIT(androidGradlePluginVersio @Test fun testAndroidWithNewMppApp() = with(Project("new-mpp-android")) { - build("assemble", "compileDebugUnitTestJavaWithJavac") { + build("assemble", "compileDebugUnitTestJavaWithJavac", "printCompilerPluginOptions") { assertSuccessful() assertTasksExecuted( @@ -48,6 +50,25 @@ class KotlinAndroid32GradleIT : KotlinAndroid3GradleIT(androidGradlePluginVersio assertFileExists("app/build/tmp/kotlin-classes/$variant/com/example/app/AKt.class") assertFileExists("app/build/tmp/kotlin-classes/$variant/com/example/app/KtUsageKt.class") } + + // Check that Android extensions arguments are available only in the Android source sets: + val compilerPluginArgsRegex = "(\\w+)${Regex.escape("=args=>")}(.*)".toRegex() + val compilerPluginOptionsBySourceSet = + compilerPluginArgsRegex.findAll(output).associate { it.groupValues[1] to it.groupValues[2] } + + compilerPluginOptionsBySourceSet.entries.forEach { (sourceSetName, argsString) -> + val shouldHaveAndroidExtensionArgs = sourceSetName.startsWith("androidApp") + if (shouldHaveAndroidExtensionArgs) + assertTrue("$sourceSetName is an Android source set and should have Android Extensions in the args") { + "plugin:org.jetbrains.kotlin.android" in argsString + } + else + assertEquals( + "[]", + argsString, + "$sourceSetName is not an Android source set and should not have Android Extensions in the args" + ) + } } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt index aab5390a896..6ec900f4f01 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt @@ -928,6 +928,94 @@ class NewMultiplatformIT : BaseGradleIT() { } } + @Test + fun testMppBuildWithCompilerPlugins() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) { + setupWorkingDir() + + val printOptionsTaskName = "printCompilerPluginOptions" + val argsMarker = "=args=>" + val classpathMarker = "=cp=>" + val compilerPluginArgsRegex = "(\\w+)${Regex.escape(argsMarker)}(.*)".toRegex() + val compilerPluginClasspathRegex = "(\\w+)${Regex.escape(classpathMarker)}(.*)".toRegex() + + gradleBuildScript().appendText( + "\n" + """ + buildscript { + dependencies { + classpath "org.jetbrains.kotlin:kotlin-allopen:${'$'}kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-noarg:${'$'}kotlin_version" + } + } + apply plugin: 'kotlin-allopen' + apply plugin: 'kotlin-noarg' + + allOpen { annotation 'com.example.Annotation' } + noArg { annotation 'com.example.Annotation' } + + task $printOptionsTaskName { + doFirst { + kotlin.sourceSets.each { sourceSet -> + def args = sourceSet.languageSettings.compilerPluginArguments + def cp = sourceSet.languageSettings.compilerPluginClasspath.files + println sourceSet.name + '$argsMarker' + args + println sourceSet.name + '$classpathMarker' + cp + } + } + } + """.trimIndent() + ) + + projectDir.resolve("src/commonMain/kotlin/Annotation.kt").writeText( + """ + package com.example + annotation class Annotation + """.trimIndent() + ) + projectDir.resolve("src/commonMain/kotlin/Annotated.kt").writeText( + """ + package com.example + @Annotation + open class Annotated(var y: Int) { var x = 2 } + """.trimIndent() + ) + // TODO once Kotlin/Native properly supports compiler plugins, move this class to the common sources + listOf("jvm6", "nodeJs").forEach { + projectDir.resolve("src/${it}Main/kotlin/Override.kt").writeText( + """ + package com.example + @Annotation + class Override : Annotated(0) { + override var x = 3 + } + """.trimIndent() + ) + } + + build("assemble", printOptionsTaskName) { + assertSuccessful() + assertTasksExecuted(*listOf("Jvm6", "NodeJs", nativeHostTargetName.capitalize()).map { ":compileKotlin$it" }.toTypedArray()) + assertFileExists("build/classes/kotlin/jvm6/main/com/example/Annotated.class") + assertFileExists("build/classes/kotlin/jvm6/main/com/example/Override.class") + assertFileContains("build/classes/kotlin/nodeJs/main/sample-lib.js", "Override") + + val (compilerPluginArgsBySourceSet, compilerPluginClasspathBySourceSet) = + listOf(compilerPluginArgsRegex, compilerPluginClasspathRegex) + .map { marker -> + marker.findAll(output).associate { it.groupValues[1] to it.groupValues[2] } + } + + // TODO once Kotlin/Native properly supports compiler plugins, expand this to all source sets: + listOf("commonMain", "commonTest", "jvm6Main", "jvm6Test", "nodeJsMain", "nodeJsTest").forEach { + val expectedArgs = "[plugin:org.jetbrains.kotlin.allopen:annotation=com.example.Annotation, " + + "plugin:org.jetbrains.kotlin.noarg:annotation=com.example.Annotation]" + + assertEquals(expectedArgs, compilerPluginArgsBySourceSet[it], "Expected $expectedArgs as plugin args for $it") + assertTrue { compilerPluginClasspathBySourceSet[it]!!.contains("kotlin-allopen") } + assertTrue { compilerPluginClasspathBySourceSet[it]!!.contains("kotlin-noarg") } + } + } + } + @Test fun testJsDceInMpp() = with(Project("new-mpp-js-dce", gradleVersion)) { build("runRhino") { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/app/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/app/build.gradle index db2f059f755..3f93e8853c0 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/app/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-multiplatform' +apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 27 @@ -55,4 +56,16 @@ kotlin { fromPreset(presets.jvm, 'jvmApp') fromPreset(presets.js, 'jsApp') } +} + +// test diagnostic task, not needed by the build +task printCompilerPluginOptions { + doFirst { + kotlin.sourceSets.each { sourceSet -> + def args = sourceSet.languageSettings.compilerPluginArguments + def cp = sourceSet.languageSettings.compilerPluginClasspath.files + println sourceSet.name + '=args=>' + args + println sourceSet.name + '=cp=>' + cp + } + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt index 7f7c7c251f7..074bbaeecd5 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt @@ -19,18 +19,21 @@ import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven +import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.internal.reflect.Instantiator import org.gradle.jvm.tasks.Jar import org.gradle.util.ConfigureUtil import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.sources.DefaultLanguageSettingsBuilder import org.jetbrains.kotlin.gradle.utils.SingleWarningPerBuild import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.presetName -internal val Project.multiplatformExtension get(): KotlinMultiplatformExtension? = - project.extensions.findByName("kotlin") as? KotlinMultiplatformExtension +internal val Project.multiplatformExtension + get(): KotlinMultiplatformExtension? = + project.extensions.findByName("kotlin") as? KotlinMultiplatformExtension class KotlinMultiplatformPlugin( private val fileResolver: FileResolver, @@ -88,6 +91,53 @@ class KotlinMultiplatformPlugin( KotlinMetadataTargetPreset(project, instantiator, fileResolver, kotlinPluginVersion), METADATA_TARGET_NAME ) + + // propagate compiler plugin options to the source set language settings + setupCompilerPluginOptions(project) + } + + private fun setupCompilerPluginOptions(project: Project) { + // common source sets use the compiler options from the metadata compilation: + val metadataCompilation = + project.multiplatformExtension!!.targets + .getByName(METADATA_TARGET_NAME) + .compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + + val primaryCompilationsBySourceSet by lazy { // don't evaluate eagerly: Android targets are not created at this point + val allCompilationsForSourceSets = + project.multiplatformExtension!!.targets + .flatMap { target -> + target.compilations.flatMap { compilation -> + compilation.allKotlinSourceSets.map { it to compilation } + } + } + .groupBy(keySelector = { (sourceSet, _) -> sourceSet }, valueTransform = { (_, compilation) -> compilation }) + + allCompilationsForSourceSets.mapValues { (_, compilations) -> // choose one primary compilation + when (compilations.size) { + 0 -> metadataCompilation + 1 -> compilations.single() + else -> { + val sourceSetTargets = compilations.map { it.target }.distinct() + when (sourceSetTargets.size) { + 1 -> sourceSetTargets.single().compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) + ?: // use any of the compilations for now, looks OK for Android TODO maybe reconsider + compilations.first() + else -> metadataCompilation + } + } + } + } + } + + project.kotlinExtension.sourceSets.all { sourceSet -> + (sourceSet.languageSettings as? DefaultLanguageSettingsBuilder)?.run { + compilerPluginOptionsTask = lazy { + val associatedCompilation = primaryCompilationsBySourceSet[sourceSet] ?: metadataCompilation + project.tasks.getByName(associatedCompilation.compileKotlinTaskName) as AbstractCompile + } + } + } } fun setupDefaultPresets(project: Project) { @@ -170,7 +220,7 @@ class KotlinMultiplatformPlugin( private fun configureSourceJars(project: Project) = with(project.kotlinExtension as KotlinMultiplatformExtension) { targets.all { target -> val mainCompilation = target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) - // If a target has no `main` compilation (e.g. Android), don't create the source JAR + // If a target has no `main` compilation (e.g. Android), don't create the source JAR ?: return@all val sourcesJar = project.tasks.create(target.sourcesJarTaskName, Jar::class.java) { sourcesJar -> @@ -189,7 +239,7 @@ class KotlinMultiplatformPlugin( } } - private fun configureSourceSets(project: Project) = with (project.kotlinExtension as KotlinMultiplatformExtension) { + private fun configureSourceSets(project: Project) = with(project.kotlinExtension as KotlinMultiplatformExtension) { val production = sourceSets.create(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) val test = sourceSets.create(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) @@ -247,7 +297,7 @@ class KotlinMultiplatformPlugin( const val METADATA_TARGET_NAME = "metadata" const val GRADLE_METADATA_WARNING = - // TODO point the user to some MPP docs explaining this in more detail + // TODO point the user to some MPP docs explaining this in more detail "This build is set up to publish Kotlin multiplatform libraries with experimental Gradle metadata. " + "Future Gradle versions may fail to resolve dependencies on these publications. " + "You can disable Gradle metadata usage during publishing and dependencies resolution by removing " + diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultLanguageSettingsBuilder.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultLanguageSettingsBuilder.kt index 203b2b9278f..794821382ce 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultLanguageSettingsBuilder.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultLanguageSettingsBuilder.kt @@ -6,10 +6,14 @@ package org.jetbrains.kotlin.gradle.plugin.sources import org.gradle.api.InvalidUserDataException +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.compile.AbstractCompile import org.jetbrains.kotlin.config.ApiVersion import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.config.LanguageVersion import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile internal class DefaultLanguageSettingsBuilder : LanguageSettingsBuilder { private var languageVersionImpl: LanguageVersion? = null @@ -58,8 +62,28 @@ internal class DefaultLanguageSettingsBuilder : LanguageSettingsBuilder { experimentalAnnotationsInUseImpl += name } - companion object { - } + /* A Kotlin task that is responsible for code analysis of the owner of this language settings builder. */ + lateinit var compilerPluginOptionsTask: Lazy + + val compilerPluginArguments: List + get() { + val pluginOptionsTask = compilerPluginOptionsTask.value + return when (pluginOptionsTask) { + is AbstractKotlinCompile<*> -> pluginOptionsTask.pluginOptions + is KotlinNativeCompile -> pluginOptionsTask.compilerPluginOptions + else -> error("Unexpected task: $compilerPluginOptionsTask") + }.arguments + } + + val compilerPluginClasspath: FileCollection + get() { + val pluginClasspathTask = compilerPluginOptionsTask.value + return when (pluginClasspathTask) { + is AbstractKotlinCompile<*> -> pluginClasspathTask.pluginClasspath + is KotlinNativeCompile -> pluginClasspathTask.compilerPluginClasspath ?: pluginClasspathTask.project.files() + else -> error("Unexpected task: $compilerPluginOptionsTask") + } + } } internal fun applyLanguageSettingsToKotlinTask(