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 c7a7d0763f4..094b6aa7057 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 @@ -23,7 +23,7 @@ class KotlinAndroid32GradleIT : KotlinAndroid3GradleIT(androidGradlePluginVersio get() = GradleVersionRequired.AtLeast("4.6") @Test - fun testAndroidWithNewMppApp() = with(Project("new-mpp-android")) { + fun testAndroidWithNewMppApp() = with(Project("new-mpp-android", GradleVersionRequired.AtLeast("4.7"))) { build("assemble", "compileDebugUnitTestJavaWithJavac", "printCompilerPluginOptions") { assertSuccessful() @@ -70,6 +70,107 @@ class KotlinAndroid32GradleIT : KotlinAndroid3GradleIT(androidGradlePluginVersio ) } } + + // By default, no Android variant should be published in 1.3.20: + val groupDir = "lib/build/repo/com/example/" + build("publish") { + assertSuccessful() + assertFileExists(groupDir + "lib-jvmlib") + assertFileExists(groupDir + "lib-jslib") + assertNoSuchFile(groupDir + "lib-androidlib") + assertNoSuchFile(groupDir + "lib-androidlib-debug") + projectDir.resolve(groupDir).deleteRecursively() + } + + // Choose a single variant to publish, check that it's there: + gradleBuildScript("lib").appendText("\nkotlin.android('androidLib').publishLibraryVariants = ['release']") + build("publish") { + assertSuccessful() + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0.aar") + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0-sources.jar") + assertNoSuchFile(groupDir + "lib-androidlib-debug") + projectDir.resolve(groupDir).deleteRecursively() + } + + // Enable publishing for all Android variants: + gradleBuildScript("lib").appendText("\nkotlin.android('androidLib') { publishAllLibraryVariants() }") + build("publish") { + assertSuccessful() + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0.aar") + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0-sources.jar") + assertFileExists(groupDir + "lib-androidlib-debug/1.0/lib-androidlib-debug-1.0.aar") + assertFileExists(groupDir + "lib-androidlib-debug/1.0/lib-androidlib-debug-1.0-sources.jar") + projectDir.resolve(groupDir).deleteRecursively() + } + + // Then group the variants by flavor and check that only one publication is created: + gradleBuildScript("lib").appendText("\nkotlin.android('androidLib').publishLibraryVariantsGroupedByFlavor = true") + build("publish") { + assertSuccessful() + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0.aar") + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0-sources.jar") + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0-debug.aar") + assertFileExists(groupDir + "lib-androidlib/1.0/lib-androidlib-1.0-debug-sources.jar") + projectDir.resolve(groupDir).deleteRecursively() + } + + // Add one flavor dimension with two flavors, check that the flavors produce grouped publications: + gradleBuildScript("lib").appendText( + "\nandroid { flavorDimensions('foo'); productFlavors { foo1 { dimension 'foo' }; foo2 { dimension 'foo' } } }" + ) + build("publish") { + assertSuccessful() + listOf("foo1", "foo2").forEach { flavor -> + assertFileExists(groupDir + "lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0.aar") + assertFileExists(groupDir + "lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-sources.jar") + assertFileExists(groupDir + "lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug.aar") + assertFileExists(groupDir + "lib-androidlib-$flavor/1.0/lib-androidlib-$flavor-1.0-debug-sources.jar") + } + projectDir.resolve(groupDir).deleteRecursively() + } + + // Disable the grouping and check that all the variants are published under separate artifactIds: + gradleBuildScript("lib").appendText( + "\nkotlin.android('androidLib') { publishLibraryVariantsGroupedByFlavor = false }" + ) + build("publish") { + assertSuccessful() + listOf("foo1", "foo2").forEach { flavor -> + listOf("-debug", "").forEach { buildType -> + assertFileExists(groupDir + "lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0.aar") + assertFileExists(groupDir + "lib-androidlib-$flavor$buildType/1.0/lib-androidlib-$flavor$buildType-1.0-sources.jar") + } + } + projectDir.resolve(groupDir).deleteRecursively() + } + + // Convert the 'app' project to a library, publish the two without metadata, + // check that the dependencies in the POMs are correctly rewritten: + gradleSettingsScript().modify { it.replace("enableFeaturePreview", "//") } + gradleBuildScript("app").modify { + it.replace("com.android.application", "com.android.library") + .replace("applicationId", "//") + "\n" + """ + apply plugin: 'maven-publish' + publishing { repositories { maven { url = uri("${'$'}buildDir/repo") } } } + kotlin.android('androidApp') { publishAllLibraryVariants() } + android { flavorDimensions('foo'); productFlavors { foo1 { dimension 'foo' }; foo2 { dimension 'foo' } } } + """.trimIndent() + } + build("publish") { + assertSuccessful() + val appGroupDir = "app/build/repo/com/example/" + listOf("foo1", "foo2").forEach { flavor -> + listOf("-debug", "").forEach { buildType -> + assertFileExists(appGroupDir + "app-androidapp-$flavor$buildType/1.0/app-androidapp-$flavor$buildType-1.0.aar") + assertFileExists(appGroupDir + "app-androidapp-$flavor$buildType/1.0/app-androidapp-$flavor$buildType-1.0-sources.jar") + val pomText = projectDir.resolve( + appGroupDir + "app-androidapp-$flavor$buildType/1.0/app-androidapp-$flavor$buildType-1.0.pom" + ).readText() + assertTrue { "lib-androidlib-$flavor$buildType" in pomText } + } + } + projectDir.resolve(groupDir).deleteRecursively() + } } @Test 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 3f93e8853c0..a53f35cae83 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 @@ -2,6 +2,9 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-multiplatform' apply plugin: 'kotlin-android-extensions' +group 'com.example' +version '1.0' + android { compileSdkVersion 27 defaultConfig { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/lib/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/lib/build.gradle index 8ee1a222fcb..70703d9f6f8 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/lib/build.gradle +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-android/lib/build.gradle @@ -1,5 +1,9 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-multiplatform' +apply plugin: 'maven-publish' + +group 'com.example' +version '1.0' android { compileSdkVersion 27 @@ -51,4 +55,12 @@ kotlin { fromPreset(presets.jvm, 'jvmLib') fromPreset(presets.js, 'jsLib') } +} + +publishing { + repositories { + maven { + url = uri("$buildDir/repo") + } + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/agp25/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt b/libraries/tools/kotlin-gradle-plugin/src/agp25/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt index 5ac1bfe660a..ac330fde923 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/agp25/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/agp25/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt @@ -6,6 +6,7 @@ import com.android.build.gradle.tasks.MergeResources import com.android.builder.model.SourceProvider import org.gradle.api.Project import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.compile.AbstractCompile import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin import org.jetbrains.kotlin.gradle.internal.KaptTask @@ -83,6 +84,10 @@ class Android25ProjectHandler(kotlinConfigurationTools: KotlinConfigurationTools override fun getVariantName(variant: BaseVariant): String = variant.name + override fun getFlavorNames(variant: BaseVariant): List = variant.productFlavors.map { it.name } + + override fun getBuildTypeName(variant: BaseVariant): String = variant.buildType.name + override fun getTestedVariantData(variantData: BaseVariant): BaseVariant? = when (variantData) { is TestVariant -> variantData.testedVariant is UnitTestVariant -> variantData.testedVariant as? BaseVariant @@ -114,6 +119,9 @@ class Android25ProjectHandler(kotlinConfigurationTools: KotlinConfigurationTools return project.files(Callable { variantData.mergeResources?.computeResourceSetList0() ?: emptyList() }) } + override fun getLibraryOutputTask(variant: BaseVariant): AbstractArchiveTask? = + (variant as? LibraryVariant)?.packageLibrary + override fun setUpDependencyResolution(variant: BaseVariant, compilation: KotlinJvmAndroidCompilation) { val project = compilation.target.project 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 a0a9814bfb2..cabe474ffb2 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 @@ -17,6 +17,7 @@ import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.CompileClasspathNormalizer import org.gradle.api.tasks.Delete import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.tasks.Jar @@ -658,10 +659,13 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationT abstract fun forEachVariant(project: Project, action: (V) -> Unit): Unit abstract fun getTestedVariantData(variantData: V): V? abstract fun getResDirectories(variantData: V): FileCollection + abstract fun getVariantName(variant: V): String + abstract fun getFlavorNames(variant: V): List + abstract fun getBuildTypeName(variant: V): String + abstract fun getLibraryOutputTask(variant: V): AbstractArchiveTask? protected abstract fun getSourceProviders(variantData: V): Iterable protected abstract fun getAllJavaSources(variantData: V): Iterable - protected abstract fun getVariantName(variant: V): String protected abstract fun getJavaTask(variantData: V): AbstractCompile? protected abstract fun addJavaSourceDirectoryToVariantModel(variantData: V, javaSourceDirectory: File): Unit @@ -705,11 +709,11 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationT ext.addExtension(KOTLIN_OPTIONS_DSL_NAME, kotlinOptions) project.afterEvaluate { project -> - forEachVariant(project) { variant -> - val variantName = getVariantName(variant) - val compilation = kotlinAndroidTarget.compilations.create(variantName) - setUpDependencyResolution(variant, compilation) - } + forEachVariant(project) { variant -> + val variantName = getVariantName(variant) + val compilation = kotlinAndroidTarget.compilations.create(variantName) + setUpDependencyResolution(variant, compilation) + } val androidPluginIds = listOf( "android", "com.android.application", "android-library", "com.android.library", diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/LegacyAndroidAndroidProjectHandler.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/LegacyAndroidAndroidProjectHandler.kt index 09337b34324..fa776ae5d8a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/LegacyAndroidAndroidProjectHandler.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/LegacyAndroidAndroidProjectHandler.kt @@ -15,6 +15,7 @@ import com.android.builder.model.SourceProvider import org.gradle.api.Project import org.gradle.api.ProjectConfigurationException import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.compile.AbstractCompile import org.jetbrains.kotlin.gradle.internal.KaptTask import org.jetbrains.kotlin.gradle.internal.KaptVariantData @@ -44,6 +45,9 @@ internal class LegacyAndroidAndroidProjectHandler(kotlinConfigurationTools: Kotl variantManager.variantDataList.forEach(action) } + override fun getLibraryOutputTask(variant: BaseVariantData): AbstractArchiveTask? = + null // We don't support publishing AARs of old Android in MPP + override fun wireKotlinTasks( project: Project, compilation: KotlinJvmAndroidCompilation, @@ -92,7 +96,16 @@ internal class LegacyAndroidAndroidProjectHandler(kotlinConfigurationTools: Kotl override fun getVariantName(variant: BaseVariantData): String = variant.name - override fun checkVariantIsValid(variant: BaseVariantData): Unit { + private fun operationNotSupportedInOldAndroidPlugin(): Nothing = + throw UnsupportedOperationException("Publishing is not supported for Android Gradle plugin 2.3.x and below. Please upgrade to 3.0+") + + override fun getFlavorNames(variant: BaseVariantData): List = + operationNotSupportedInOldAndroidPlugin() + + override fun getBuildTypeName(variant: BaseVariantData): String = + operationNotSupportedInOldAndroidPlugin() + + override fun checkVariantIsValid(variant: BaseVariantData) { if (AndroidGradleWrapper.isJackEnabled(variant)) { throw ProjectConfigurationException( "Kotlin Gradle plugin does not support the deprecated Jack toolchain.\n" + 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 4847a09f501..32092b35be8 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 @@ -176,7 +176,13 @@ class KotlinMultiplatformPlugin( (targets.getByName(METADATA_TARGET_NAME) as AbstractKotlinTarget).createMavenPublications(publishing.publications) targets .withType(AbstractKotlinTarget::class.java).matching { it.publishable && it.name != METADATA_TARGET_NAME } - .all { it.createMavenPublications(publishing.publications) } + .all { + if (it is KotlinAndroidTarget) + // Android targets have their variants created in afterEvaluate; TODO handle this better? + project.whenEvaluated { it.createMavenPublications(publishing.publications) } + else + it.createMavenPublications(publishing.publications) + } } project.components.add(kotlinSoftwareComponent) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt index 3f031ab27b7..95df74a4048 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt @@ -50,7 +50,7 @@ class DefaultKotlinUsageContext( private val overrideConfigurationArtifacts: Set? = null ) : KotlinUsageContext { - val kotlinTarget: KotlinTarget get() = compilation.target + private val kotlinTarget: KotlinTarget get() = compilation.target private val project: Project get() = kotlinTarget.project override fun getUsage(): Usage = usage @@ -87,21 +87,28 @@ class DefaultKotlinUsageContext( } private fun rewriteMppDependenciesToTargetModuleDependencies( - context: DefaultKotlinUsageContext, - configuration: Configuration -): Set = with(context.kotlinTarget.project) { - val target = context.kotlinTarget - val moduleDependencies = configuration.incoming.dependencies.withType(ModuleDependency::class.java).ifEmpty { return emptySet() } - - val targetMainCompilation = target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) - ?: return moduleDependencies // Android is not yet supported + usageContext: KotlinUsageContext, + fromConfiguration: Configuration +): Set = with(usageContext.compilation.target.project) { + val compilation = usageContext.compilation + val moduleDependencies = fromConfiguration.incoming.dependencies.withType(ModuleDependency::class.java).ifEmpty { return emptySet() } val targetCompileDependenciesConfiguration = project.configurations.getByName( - when (context.dependencyConfigurationName) { - target.apiElementsConfigurationName -> targetMainCompilation.compileDependencyConfigurationName - target.runtimeElementsConfigurationName -> - (targetMainCompilation as KotlinCompilationToRunnableFiles).runtimeDependencyConfigurationName - else -> error("unexpected configuration") + when (compilation) { + is KotlinJvmAndroidCompilation -> { + // TODO handle Android configuration names in a general way once we drop AGP < 3.0.0 + val variantName = compilation.name + when (usageContext.usage.name) { + Usage.JAVA_API -> variantName + "CompileClasspath" + Usage.JAVA_RUNTIME_JARS -> variantName + "RuntimeClasspath" + else -> error("Unexpected Usage for usage context: ${usageContext.usage}") + } + } + else -> when (usageContext.usage.name) { + Usage.JAVA_API -> compilation.compileDependencyConfigurationName + Usage.JAVA_RUNTIME_JARS -> (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyConfigurationName + else -> error("Unexpected Usage for usage context: ${usageContext.usage}") + } } ) @@ -128,16 +135,18 @@ private fun rewriteMppDependenciesToTargetModuleDependencies( val resolvedToConfiguration = resolved.configuration - val dependencyTarget = dependencyProjectKotlinExtension.targets.singleOrNull { - resolvedToConfiguration in setOf( - it.apiElementsConfigurationName, - it.runtimeElementsConfigurationName, - it.defaultConfigurationName - ) - } ?: return@map dependency + val dependencyTargetComponent: KotlinTargetComponent = run { + dependencyProjectKotlinExtension.targets.forEach { target -> + target.components.forEach { component -> + if (component.findUsageContext(resolvedToConfiguration) != null) + return@run component + } + } + // Failed to find a matching component: + return@map dependency + } - val dependencyTargetComponent = dependencyTarget.components.single() // FIXME handle multiple components later - val publicationDelegate = (dependencyTargetComponent as KotlinVariant).publicationDelegate + val publicationDelegate = (dependencyTargetComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate dependencies.module( listOf( @@ -150,3 +159,19 @@ private fun rewriteMppDependenciesToTargetModuleDependencies( } }.toSet() } + +internal fun KotlinTargetComponent.findUsageContext(configurationName: String): UsageContext? { + val usageContexts = when (this) { + is KotlinVariantWithMetadataDependency -> originalUsages + is SoftwareComponentInternal -> usages + else -> emptySet() + } + return usageContexts.find { usageContext -> + if (usageContext !is KotlinUsageContext) return@find false + val compilation = usageContext.compilation + configurationName in compilation.relatedConfigurationNames || + configurationName == compilation.target.apiElementsConfigurationName || + configurationName == compilation.target.runtimeElementsConfigurationName || + configurationName == compilation.target.defaultConfigurationName + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt index 5701624c9bb..8e5dafdcc44 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt @@ -264,6 +264,9 @@ class KotlinJvmAndroidCompilation( ) : AbstractKotlinCompilationToRunnableFiles(target, name) { override val compileKotlinTask: org.jetbrains.kotlin.gradle.tasks.KotlinCompile get() = super.compileKotlinTask as org.jetbrains.kotlin.gradle.tasks.KotlinCompile + + override val relatedConfigurationNames: List + get() = super.relatedConfigurationNames + listOf("${name}ApiElements", "${name}RuntimeElements") } class KotlinJsCompilation( diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt index 1581b1a0a1a..a22d179c3a9 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt @@ -5,10 +5,7 @@ package org.jetbrains.kotlin.gradle.plugin.mpp import groovy.lang.Closure -import org.gradle.api.Action -import org.gradle.api.DomainObjectSet -import org.gradle.api.NamedDomainObjectContainer -import org.gradle.api.Project +import org.gradle.api.* import org.gradle.api.artifacts.ConfigurablePublishArtifact import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.PublishArtifact @@ -178,8 +175,162 @@ open class KotlinAndroidTarget( override val compilations: NamedDomainObjectContainer = project.container(compilationFactory.itemClass, compilationFactory) - override val components: Set - get() = emptySet() + /** Names of the Android library variants that should be published from the target's project within the default publications which are + * set up if the `maven-publish` Gradle plugin is applied. + * + * Item examples: + * * 'release' (in case no product flavors were defined) + * * 'fooRelease' (for the release build type of a flavor 'foo') + * * 'fooBarRelease' (for the release build type multi-dimensional flavors 'foo' and 'bar'). + * + * If set to null, which can also be done with [publishAllLibraryVariants], + * all library variants will be published, but not test or application variants. */ + var publishLibraryVariants: List? = listOf() + + /** Add Android library variant names to [publishLibraryVariants]. */ + fun publishLibraryVariants(vararg names: String) { + publishLibraryVariants = publishLibraryVariants.orEmpty() + names + } + + /** Set up all of the Android library variants to be published from this target's project within the default publications, which are + * set up if the `maven-publish` Gradle plugin is applied. This overrides the variants chosen with [publishLibraryVariants] */ + fun publishAllLibraryVariants() { + publishLibraryVariants = null + } + + /** If true, a publication will be created per merged product flavor, with the build types used as classifiers for the artifacts + * published within each publication. If set to false, each Android variant will have a separate publication. */ + var publishLibraryVariantsGroupedByFlavor = false + + private fun checkPublishLibraryVariantsExist() { + // Capture type parameter T + fun AbstractAndroidProjectHandler.getVariantNames() = + mutableSetOf().apply { forEachVariant(project) { add(getVariantName(it)) } } + + val variantNames = + KotlinAndroidPlugin.androidTargetHandler(project.getKotlinPluginVersion()!!, this) + .getVariantNames() + + val missingVariants = + publishLibraryVariants?.minus(variantNames).orEmpty() + + if (missingVariants.isNotEmpty()) + throw InvalidUserDataException( + "Kotlin target '$targetName' tried to set up publishing for Android library variants that are not present " + + "in the project:\n" + missingVariants.joinToString("\n") { "* $it" } + + "\nCheck the 'publishLibraryVariants' property, it should point to existing Android build variants." + ) + } + + override val components by lazy { + checkPublishLibraryVariantsExist() + + KotlinAndroidPlugin.androidTargetHandler(project.getKotlinPluginVersion()!!, this).doCreateComponents() + .also { project.components.addAll(it) } + } + + // Capture the type parameter T for `AbstractAndroidProjectHandler` + private fun AbstractAndroidProjectHandler.doCreateComponents(): Set { + if (!isGradleVersionAtLeast(4, 7)) + return emptySet() + + val publishableVariants = mutableListOf() + .apply { forEachVariant(project) { add(it) } } + .toList() // Defensive copy against unlikely modification by the lambda that captures the list above in forEachVariant { } + .filter { getLibraryOutputTask(it) != null && publishLibraryVariants?.contains(getVariantName(it)) ?: true } + + val publishableVariantGroups = publishableVariants.groupBy { variant -> + val flavorNames = getFlavorNames(variant) + if (publishLibraryVariantsGroupedByFlavor) { + // For each flavor, we group its variants (which differ only in the build type) in a single component in order to publish + // all of the build types of the flavor as a single module with the build type as the classifier of the artifacts + flavorNames + } else { + flavorNames + getBuildTypeName(variant) + } + } + + return publishableVariantGroups.map { (flavorGroupNameParts, androidVariants) -> + val nestedVariants = androidVariants.mapTo(mutableSetOf()) { androidVariant -> + val androidVariantName = getVariantName(androidVariant) + val compilation = compilations.getByName(androidVariantName) + val buildTypeName = getBuildTypeName(androidVariant) + val usageContexts = createAndroidUsageContexts(androidVariant, compilation, getFlavorNames(androidVariant), buildTypeName) + createKotlinVariant( + lowerCamelCaseName(compilation.target.name, *flavorGroupNameParts.toTypedArray()), + compilation, + usageContexts + ).apply { + if (!publishLibraryVariantsGroupedByFlavor) { + defaultArtifactIdSuffix = + lowerSpinalCaseName(getFlavorNames(androidVariant) + getBuildTypeName(androidVariant).takeIf { it != "release" }) + .takeIf { it.isNotEmpty() } + } + } + } + + if (publishLibraryVariantsGroupedByFlavor) { + JointAndroidKotlinTargetComponent( + this@KotlinAndroidTarget, + nestedVariants, + flavorGroupNameParts + ) + } else { + nestedVariants.single() + } as KotlinTargetComponent + }.toSet() + } + + private fun AbstractAndroidProjectHandler.createAndroidUsageContexts( + variant: T, + compilation: KotlinCompilation<*>, + flavorNames: List, + buildTypeName: String + ): Set { + val artifactClassifier = buildTypeName.takeIf { it != "release" && publishLibraryVariantsGroupedByFlavor } + + val sourcesJarArtifact = sourcesJarArtifact( + compilation, compilation.disambiguateName(""), + lowerSpinalCaseName(compilation.target.name, *flavorNames.toTypedArray(), buildTypeName.takeIf { it != "release" }), + classifierPrefix = artifactClassifier + ) + + val publishWithKotlinMetadata = (project.kotlinExtension as? KotlinMultiplatformExtension)?.isGradleMetadataAvailable + ?: true // In non-MPP project, these usage contexts do not get published anyway + + val variantName = getVariantName(variant) + val outputTask = getLibraryOutputTask(variant) ?: return emptySet() + val artifact = run { + val archivesConfigurationName = lowerCamelCaseName(targetName, variantName, "archives") + project.configurations.maybeCreate(archivesConfigurationName).apply { + isCanBeConsumed = false + isCanBeResolved = false + } + project.artifacts.add(archivesConfigurationName, outputTask) { artifact -> + artifact.classifier = artifactClassifier + } + } + + val apiElementsConfigurationName = lowerCamelCaseName(variantName, "apiElements") + val runtimeElementsConfigurationName = lowerCamelCaseName(variantName, "runtimeElements") + + // Here, `JAVA_API` and `JAVA_RUNTIME_JARS` are used intentionally as Gradle needs this for + // ordering of the usage contexts (prioritizing the dependencies) when merging them into the POM; + // These Java usages should not be replaced with the custom Kotlin usages. + return listOf( + apiElementsConfigurationName to JAVA_API, + runtimeElementsConfigurationName to JAVA_RUNTIME_JARS + ).mapTo(mutableSetOf()) { (dependencyConfigurationName, usageName) -> + DefaultKotlinUsageContext( + compilation, + project.usageByName(usageName), + dependencyConfigurationName, + publishWithKotlinMetadata, + sourcesJarArtifact, + overrideConfigurationArtifacts = setOf(artifact) + ) + } + } } open class KotlinWithJavaTarget( diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinVariants.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinVariants.kt index bd3b4814097..8cd91f9e830 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinVariants.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinVariants.kt @@ -116,3 +116,22 @@ class KotlinVariantWithMetadataDependency( override fun getGlobalExcludes(): Set = emptySet() } } + +class JointAndroidKotlinTargetComponent( + override val target: KotlinAndroidTarget, + private val nestedVariants: Set, + val flavorNames: List +) : KotlinTargetComponentWithCoordinatesAndPublication, SoftwareComponentInternal { + + override fun getUsages(): Set = nestedVariants.flatMap { it.usages }.toSet() + + override fun getName(): String = lowerCamelCaseName(target.targetName, *flavorNames.toTypedArray()) + + override val publishable: Boolean + get() = target.publishable + + override val defaultArtifactId: String = + lowerSpinalCaseName(target.project.name, target.targetName, *flavorNames.toTypedArray()) + + override var publicationDelegate: MavenPublication? = null +}