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(