Allow a target to have multiple publications, restructure the code

Instead of exposing a single `component` in a target, allow it to have a
set of `components`, then create a Maven publication for each of the
components.

Move `createUsageContexts` to private API since a target that has
multiple publications may need to create the usage contexts for those
publications differently.

Move some of the components technical stuff to interfaces
`DelegatedToPublication` and
`ComponentWithCoordinatesDelegatedToPublication` in order to reuse it
later for Android's components.
This commit is contained in:
Sergey Igushkin
2018-12-25 16:33:59 +03:00
parent a6b55cc5d7
commit d211bd53c9
7 changed files with 153 additions and 95 deletions
@@ -13,7 +13,6 @@ import org.gradle.api.Project
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.attributes.HasAttributes
import org.gradle.api.component.SoftwareComponent
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
@@ -21,6 +20,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
interface KotlinTargetComponent : SoftwareComponent {
val target: KotlinTarget
val publishable: Boolean
val defaultArtifactId: String
}
interface KotlinTarget : Named, HasAttributes {
@@ -41,9 +41,7 @@ interface KotlinTarget : Named, HasAttributes {
val publishable: Boolean
val component: KotlinTargetComponent
fun createUsageContexts(): Set<UsageContext>
val components: Set<KotlinTargetComponent>
fun mavenPublication(action: Closure<Unit>)
fun mavenPublication(action: Action<MavenPublication>)
@@ -1114,7 +1114,7 @@ class NewMultiplatformIT : BaseGradleIT() {
assertTasksExecuted(":publishedLibrary:cinteropStdio${host.capitalize()}")
assertTrue(output.contains("Published test"), "No test output found")
assertFileExists("publishedLibrary/build/classes/kotlin/$host/main/publishedLibrary-cinterop-stdio.klib")
assertFileExists("repo/org/example/publishedLibrary-$host/1.0/publishedLibrary-$host-1.0-cinterop-stdio.klib")
assertFileExists("repo/org/example/publishedlibrary-$host/1.0/publishedlibrary-$host-1.0-cinterop-stdio.klib")
}
build(":build") {
@@ -14,6 +14,7 @@ import org.gradle.api.internal.FeaturePreviews
import org.gradle.api.internal.file.FileResolver
import org.gradle.api.internal.plugins.DslObject
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.publish.PublicationContainer
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
@@ -155,12 +156,13 @@ class KotlinMultiplatformPlugin(
}
val targets = project.multiplatformExtension!!.targets
val kotlinSoftwareComponent = KotlinSoftwareComponent(project, "kotlin", targets)
val kotlinSoftwareComponent = KotlinSoftwareComponent("kotlin", targets)
project.extensions.configure(PublishingExtension::class.java) { publishing ->
// The root publication that references the platform specific publications as its variants:
val rootPublication = publishing.publications.create("kotlinMultiplatform", MavenPublication::class.java).apply {
artifactId = project.name.toLowerCase()
from(kotlinSoftwareComponent)
(this as MavenPublicationInternal).publishWithOriginalFileName()
}
@@ -170,36 +172,34 @@ class KotlinMultiplatformPlugin(
publishTask.onlyIf { publishTask.publication != rootPublication || project.multiplatformExtension!!.isGradleMetadataAvailable }
}
fun createTargetPublication(target: KotlinTarget) {
val variant = target.component as KotlinVariant
val name = target.name
// Enforce the order of creating the publications, since the metadata publication is used in the other publications:
(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) }
}
val variantPublication = publishing.publications.create(name, MavenPublication::class.java).apply {
project.components.add(kotlinSoftwareComponent)
}
private fun AbstractKotlinTarget.createMavenPublications(publications: PublicationContainer) {
components
.filter { it.publishable }
.forEach { variant ->
val variantPublication = publications.create(variant.name, MavenPublication::class.java).apply {
// do this in whenEvaluated since older Gradle versions seem to check the files in the variant eagerly:
project.whenEvaluated {
from(variant)
(project.tasks.findByName(target.sourcesJarTaskName) as Jar?)?.let { sourcesJar ->
(project.tasks.findByName(sourcesJarTaskName) as Jar?)?.let { sourcesJar ->
artifact(sourcesJar)
}
}
(this as MavenPublicationInternal).publishWithOriginalFileName()
artifactId = variant.target.defaultArtifactId
artifactId = variant.defaultArtifactId
}
variant.publicationDelegate = variantPublication
(target as AbstractKotlinTarget).publicationConfigureActions.all {
it.execute(variantPublication)
}
}
// Enforce the order of creating the publications, since the metadata publication is used in the other publications:
createTargetPublication(targets.getByName(METADATA_TARGET_NAME))
targets.matching { it.publishable && it.name != METADATA_TARGET_NAME }.all(::createTargetPublication)
}
project.components.add(kotlinSoftwareComponent)
targets.all {
project.components.add(it.component)
(variant as? KotlinTargetComponentWithPublication)?.publicationDelegate = variantPublication
publicationConfigureActions.all { it.execute(variantPublication) }
}
}
@@ -293,8 +293,6 @@ class KotlinMultiplatformPlugin(
}
}
internal val KotlinTarget.defaultArtifactId get() = "${project.name}-${name.toLowerCase()}"
internal fun compilationsBySourceSet(project: Project): Map<KotlinSourceSet, Set<KotlinCompilation<*>>> =
HashMap<KotlinSourceSet, MutableSet<KotlinCompilation<*>>>().also { result ->
for (target in project.multiplatformExtension!!.targets) {
@@ -17,7 +17,6 @@ import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.utils.ifEmpty
class KotlinSoftwareComponent(
private val project: Project,
private val name: String,
private val kotlinTargets: Iterable<KotlinTarget>
) : SoftwareComponentInternal, ComponentWithVariants {
@@ -25,7 +24,7 @@ class KotlinSoftwareComponent(
override fun getUsages(): Set<UsageContext> = emptySet()
override fun getVariants(): Set<KotlinTargetComponent> =
kotlinTargets.map { it.component }.toSet()
kotlinTargets.flatMap { it.components }.toSet()
override fun getName(): String = name
}
@@ -36,12 +35,20 @@ object NativeUsage {
const val KOTLIN_KLIB = "kotlin-klib"
}
internal class KotlinPlatformUsageContext(
val kotlinTarget: KotlinTarget,
interface KotlinUsageContext : UsageContext {
val compilation: KotlinCompilation<*>
val dependencyConfigurationName: String
}
class DefaultKotlinUsageContext(
override val compilation: KotlinCompilation<*>,
private val usage: Usage,
val dependencyConfigurationName: String,
private val publishWithGradleMetadata: Boolean
) : UsageContext {
override val dependencyConfigurationName: String,
private val publishWithGradleMetadata: Boolean,
private val overrideConfigurationArtifacts: Set<PublishArtifact>? = null
) : KotlinUsageContext {
val kotlinTarget: KotlinTarget get() = compilation.target
private val project: Project get() = kotlinTarget.project
override fun getUsage(): Usage = usage
@@ -49,7 +56,7 @@ internal class KotlinPlatformUsageContext(
override fun getName(): String = kotlinTarget.targetName + when (dependencyConfigurationName) {
kotlinTarget.apiElementsConfigurationName -> "-api"
kotlinTarget.runtimeElementsConfigurationName -> "-runtime"
else -> error("unexpected configuration")
else -> "-$dependencyConfigurationName" // for Android variants
}
private val configuration: Configuration
@@ -64,7 +71,8 @@ internal class KotlinPlatformUsageContext(
override fun getDependencyConstraints(): MutableSet<out DependencyConstraint> =
configuration.incoming.dependencyConstraints
override fun getArtifacts(): MutableSet<out PublishArtifact> =
override fun getArtifacts(): Set<PublishArtifact> =
overrideConfigurationArtifacts ?:
// TODO Gradle Java plugin does that in a different way; check whether we can improve this
configuration.artifacts
@@ -77,7 +85,7 @@ internal class KotlinPlatformUsageContext(
}
private fun rewriteMppDependenciesToTargetModuleDependencies(
context: KotlinPlatformUsageContext,
context: DefaultKotlinUsageContext,
configuration: Configuration
): Set<ModuleDependency> = with(context.kotlinTarget.project) {
val target = context.kotlinTarget
@@ -126,12 +134,13 @@ private fun rewriteMppDependenciesToTargetModuleDependencies(
)
} ?: return@map dependency
val publicationDelegate = (dependencyTarget.component as KotlinVariant).publicationDelegate
val dependencyTargetComponent = dependencyTarget.components.single() // FIXME handle multiple components later
val publicationDelegate = (dependencyTargetComponent as KotlinVariant).publicationDelegate
dependencies.module(
listOf(
publicationDelegate?.groupId ?: dependency.group,
publicationDelegate?.artifactId ?: dependencyTarget.defaultArtifactId,
publicationDelegate?.artifactId ?: dependencyTargetComponent.defaultArtifactId,
publicationDelegate?.version ?: dependency.version
).joinToString(":")
) as ModuleDependency
@@ -14,7 +14,6 @@ import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.attributes.Usage.JAVA_API
import org.gradle.api.attributes.Usage.JAVA_RUNTIME_JARS
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.util.ConfigureUtil
@@ -52,52 +51,67 @@ abstract class AbstractKotlinTarget(
override val publishable: Boolean
get() = true
override val component: KotlinTargetComponent by lazy {
if (isGradleVersionAtLeast(4, 7)) {
createKotlinTargetComponent()
} else {
KotlinVariant(this)
}
override val components: Set<KotlinTargetComponent> by lazy {
val mainCompilation = compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
val usageContexts = createUsageContexts(mainCompilation, targetName, targetName.toLowerCase())
setOf(
if (isGradleVersionAtLeast(4, 7)) {
createKotlinVariant(mainCompilation.target.name, mainCompilation, usageContexts)
} else {
KotlinVariant(mainCompilation, usageContexts)
}
).also { project.components.addAll(it) }
}
private fun createKotlinTargetComponent(): KotlinTargetComponent {
protected fun createKotlinVariant(
componentName: String,
compilation: KotlinCompilation<*>,
usageContexts: Set<DefaultKotlinUsageContext>
): KotlinVariant {
val kotlinExtension = project.kotlinExtension as? KotlinMultiplatformExtension
?: return KotlinVariantWithCoordinates(this)
?: return KotlinVariantWithCoordinates(compilation, usageContexts)
if (targetName == KotlinMultiplatformPlugin.METADATA_TARGET_NAME)
return KotlinVariantWithCoordinates(this)
return KotlinVariantWithCoordinates(compilation, usageContexts)
val separateMetadataTarget = kotlinExtension.targets.getByName(KotlinMultiplatformPlugin.METADATA_TARGET_NAME)
return if (kotlinExtension.isGradleMetadataAvailable) {
KotlinVariantWithMetadataVariant(this, separateMetadataTarget)
val result = if (kotlinExtension.isGradleMetadataAvailable) {
KotlinVariantWithMetadataVariant(compilation, usageContexts, separateMetadataTarget)
} else {
// we should only add the Kotlin metadata dependency if we publish no Gradle metadata related to Kotlin MPP;
// with metadata, such a dependency would get invalid, since a platform module should only depend on modules for that
// same platform, not Kotlin metadata modules
KotlinVariantWithMetadataDependency(this, separateMetadataTarget)
KotlinVariantWithMetadataDependency(compilation, usageContexts, separateMetadataTarget)
}
result.componentName = componentName
return result
}
override fun createUsageContexts(): Set<UsageContext> {
private fun createUsageContexts(
producingCompilation: KotlinCompilation<*>,
componentName: String,
artifactNameAppendix: String
): Set<DefaultKotlinUsageContext> {
val publishWithKotlinMetadata = (project.kotlinExtension as? KotlinMultiplatformExtension)?.isGradleMetadataAvailable
?: true // In non-MPP project, these usage contexts do not get published anyway
// Here, `JAVA_API` and `JAVA_RUNTIME_JARS` are used intentionally as Gradle needs this for
// ordering of the usage contexts (prioritizing the dependencies);
// 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 listOfNotNull(
KotlinPlatformUsageContext(
this, project.usageByName(JAVA_API),
apiElementsConfigurationName, publishWithKotlinMetadata
),
if (compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) is KotlinCompilationToRunnableFiles) {
KotlinPlatformUsageContext(
this, project.usageByName(JAVA_RUNTIME_JARS),
runtimeElementsConfigurationName, publishWithKotlinMetadata
JAVA_API to apiElementsConfigurationName,
(JAVA_RUNTIME_JARS to runtimeElementsConfigurationName).takeIf { producingCompilation is KotlinCompilationToRunnableFiles }
).mapTo(mutableSetOf()) { (usageName, dependenciesConfigurationName) ->
DefaultKotlinUsageContext(
producingCompilation,
project.usageByName(usageName),
dependenciesConfigurationName,
publishWithKotlinMetadata
)
} else null
).toSet()
}
}
@Suppress("UNCHECKED_CAST")
@@ -134,10 +148,8 @@ open class KotlinAndroidTarget(
override val compilations: NamedDomainObjectContainer<out KotlinJvmAndroidCompilation> =
project.container(compilationFactory.itemClass, compilationFactory)
override fun createUsageContexts(): Set<UsageContext> {
//TODO setup Android libraries publishing. This will likely require new API in the Android Gradle plugin
return emptySet()
}
override val components: Set<KotlinTargetComponent>
get() = emptySet()
}
open class KotlinWithJavaTarget<KotlinOptionsType : KotlinCommonOptions>(
@@ -14,27 +14,22 @@ import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.publish.maven.MavenPublication
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetComponent
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.lowerSpinalCaseName
open class KotlinVariant(
override val target: KotlinTarget
) : SoftwareComponentInternal, KotlinTargetComponent {
override fun getUsages(): Set<UsageContext> = target.createUsageContexts()
override fun getName(): String = target.name
// This property is declared in the parent class to allow usages to reference it without forcing the subclass to load,
internal interface KotlinTargetComponentWithPublication : KotlinTargetComponent {
// This property is declared in the separate parent type to allow the usages to reference it without forcing the subtypes to load,
// which is needed for compatibility with older Gradle versions
internal var publicationDelegate: MavenPublication? = null
override val publishable: Boolean
get() = target.publishable
var publicationDelegate: MavenPublication?
}
open class KotlinVariantWithCoordinates(
target: KotlinTarget
) : KotlinVariant(target),
ComponentWithCoordinates /* Gradle 4.7+ API, don't use with older versions */ {
private interface KotlinTargetComponentWithCoordinatesAndPublication :
KotlinTargetComponentWithPublication,
ComponentWithCoordinates /* Gradle 4.7+ API, don't use with older versions */
{
override fun getCoordinates() = object : ModuleVersionIdentifier {
private val project get() = target.project
@@ -57,29 +52,65 @@ open class KotlinVariantWithCoordinates(
}
}
class KotlinVariantWithMetadataVariant(target: KotlinTarget, private val metadataTarget: KotlinTarget) :
KotlinVariantWithCoordinates(target), ComponentWithVariants {
override fun getVariants() = setOf(metadataTarget.component)
open class KotlinVariant(
val producingCompilation: KotlinCompilation<*>,
private val usages: Set<DefaultKotlinUsageContext>
) : KotlinTargetComponentWithPublication, SoftwareComponentInternal {
var componentName: String? = null
final override val target: KotlinTarget
get() = producingCompilation.target
override fun getUsages(): Set<UsageContext> = usages
override fun getName(): String = componentName ?: producingCompilation.target.targetName
override val publishable: Boolean
get() = target.publishable
internal var defaultArtifactIdSuffix: String? = null
override val defaultArtifactId: String
get() = lowerSpinalCaseName(target.project.name, target.targetName, defaultArtifactIdSuffix)
override var publicationDelegate: MavenPublication? = null
}
class KotlinVariantWithMetadataDependency(target: KotlinTarget, private val metadataTarget: KotlinTarget) :
KotlinVariantWithCoordinates(target) {
override fun getUsages(): Set<UsageContext> = target.createUsageContexts().mapTo(mutableSetOf()) { usageContext ->
UsageContextWithAdditionalDependencies(usageContext, setOf(metadataDependency()))
open class KotlinVariantWithCoordinates(
producingCompilation: KotlinCompilation<*>,
usages: Set<DefaultKotlinUsageContext>
) : KotlinVariant(producingCompilation, usages),
KotlinTargetComponentWithCoordinatesAndPublication /* Gradle 4.7+ API, don't use with older versions */
class KotlinVariantWithMetadataVariant(
producingCompilation: KotlinCompilation<*>,
usages: Set<DefaultKotlinUsageContext>,
private val metadataTarget: KotlinTarget
) : KotlinVariantWithCoordinates(producingCompilation, usages), ComponentWithVariants {
override fun getVariants() = metadataTarget.components
}
class KotlinVariantWithMetadataDependency(
producingCompilation: KotlinCompilation<*>,
val originalUsages: Set<DefaultKotlinUsageContext>,
private val metadataTarget: KotlinTarget
) : KotlinVariantWithCoordinates(producingCompilation, originalUsages) {
override fun getUsages(): Set<UsageContext> = originalUsages.mapTo(mutableSetOf()) { usageContext ->
KotlinUsageContextWithAdditionalDependencies(usageContext, setOf(metadataDependency()))
}
private fun metadataDependency(): ModuleDependency {
val metadataPublication = (metadataTarget.component as KotlinVariant).publicationDelegate!!
val metadataPublication = (metadataTarget.components.single() as KotlinTargetComponentWithPublication).publicationDelegate!!
val metadataGroupId = metadataPublication.groupId
val metadataArtifactId = metadataPublication.artifactId
val metadataVersion = metadataPublication.version
return target.project.dependencies.module("$metadataGroupId:$metadataArtifactId:$metadataVersion") as ModuleDependency
}
private class UsageContextWithAdditionalDependencies(
val parentUsageContext: UsageContext,
class KotlinUsageContextWithAdditionalDependencies(
val parentUsageContext: DefaultKotlinUsageContext,
val additionalDependencies: Set<ModuleDependency>
) : UsageContext by parentUsageContext {
) : KotlinUsageContext by parentUsageContext {
override fun getDependencies() = parentUsageContext.dependencies + additionalDependencies
override fun getGlobalExcludes(): Set<ExcludeRule> = emptySet()
@@ -13,3 +13,13 @@ internal fun lowerCamelCaseName(vararg nameParts: String?): String {
transform = String::capitalize
)
}
internal fun lowerSpinalCaseName(nameParts: Iterable<String?>) = lowerSpinalCaseName(*nameParts.toList().toTypedArray())
internal fun lowerSpinalCaseName(vararg nameParts: String?): String {
val nonEmptyParts = nameParts.mapNotNull { it?.takeIf(String::isNotEmpty) }
return nonEmptyParts.joinToString(
separator = "-",
transform = String::toLowerCase
)
}