[Gradle] ExternalTargetApi: Implement ExternalKotlinTargetSourcesJarUtils

This utils are intended to be used external target authors exclusively
and shall help to create sources.jar files using regular
Kotlin Multiplatform conventions.

^KT-58109 Verification Pending
This commit is contained in:
Sebastian Sellmair
2023-06-29 14:35:40 +02:00
committed by Space Team
parent 3f1faa26ac
commit 24debfd9b7
8 changed files with 326 additions and 86 deletions
@@ -831,6 +831,13 @@ public final class org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotli
public static final fun ExternalKotlinTargetDescriptor (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetDescriptor;
}
public final class org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetSourcesJarUtilsKt {
public static final fun includeSources (Lorg/gradle/jvm/tasks/Jar;Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinCompilation;)V
public static final fun publishSources (Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinTarget;Lorg/gradle/api/tasks/TaskProvider;)V
public static final fun publishSources (Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinTarget;Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinCompilation;)V
public static final fun sourcesJarTask (Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinTarget;Lorg/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedExternalKotlinCompilation;)Lorg/gradle/api/tasks/TaskProvider;
}
public abstract class org/jetbrains/kotlin/gradle/plugin/mpp/targetHierarchy/SourceSetTreeClassifier {
}
@@ -8,7 +8,9 @@ package org.jetbrains.kotlin.gradle.plugin.mpp
import org.gradle.api.file.FileCollection
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.await
import org.jetbrains.kotlin.gradle.plugin.mpp.compilationImpl.KotlinCompilationConfigurationsContainer
import org.jetbrains.kotlin.gradle.utils.ObservableSet
import org.jetbrains.kotlin.tooling.core.HasMutableExtras
@@ -25,4 +27,9 @@ internal interface InternalKotlinCompilation<out T : KotlinCommonOptions> : Kotl
internal val <T : KotlinCommonOptions> KotlinCompilation<T>.internal: InternalKotlinCompilation<T>
get() = (this as? InternalKotlinCompilation<T>) ?: throw IllegalArgumentException(
"KotlinCompilation($name) ${this::class} does not implement ${InternalKotlinCompilation::class}"
)
)
internal suspend fun InternalKotlinCompilation<*>.awaitAllKotlinSourceSets(): Set<KotlinSourceSet> {
KotlinPluginLifecycle.Stage.AfterFinaliseCompilations.await()
return allKotlinSourceSets
}
@@ -7,14 +7,11 @@ package org.jetbrains.kotlin.gradle.plugin.mpp
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.explicitApiModeAsCompilerArg
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.internal.customizeKotlinDependencies
import org.jetbrains.kotlin.gradle.plugin.*
@@ -37,14 +34,12 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinWasmTargetPreset
import org.jetbrains.kotlin.gradle.targets.native.createFatFrameworks
import org.jetbrains.kotlin.gradle.targets.native.tasks.artifact.registerKotlinArtifactsExtension
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool
import org.jetbrains.kotlin.gradle.tasks.locateTask
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.checkGradleCompatibility
import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheck
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget.*
import org.jetbrains.kotlin.konan.target.presetName
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
import java.io.File
class KotlinMultiplatformPlugin : Plugin<Project> {
@@ -259,62 +254,7 @@ internal fun applyUserDefinedAttributes(target: AbstractKotlinTarget) {
}
}
internal fun sourcesJarTask(compilation: KotlinCompilation<*>, componentName: String, artifactNameAppendix: String): TaskProvider<Jar> =
sourcesJarTask(
compilation.target.project,
compilation.target.project.future {
KotlinPluginLifecycle.Stage.AfterFinaliseCompilations.await()
compilation.allKotlinSourceSets.associate { it.name to it.kotlin }
},
componentName,
artifactNameAppendix
)
private fun sourcesJarTask(
project: Project,
sourceSets: Future<Map<String, Iterable<File>>>,
taskNamePrefix: String,
artifactNameAppendix: String,
): TaskProvider<Jar> =
sourcesJarTaskNamed(lowerCamelCaseName(taskNamePrefix, "sourcesJar"), taskNamePrefix, project, sourceSets, artifactNameAppendix)
internal fun sourcesJarTaskNamed(
taskName: String,
componentName: String,
project: Project,
sourceSets: Future<Map<String, Iterable<File>>>,
artifactNameAppendix: String,
componentTypeName: String = "target",
): TaskProvider<Jar> {
project.locateTask<Jar>(taskName)?.let {
return it
}
val result = project.registerTask<Jar>(taskName) { sourcesJar ->
sourcesJar.archiveAppendix.set(artifactNameAppendix)
sourcesJar.archiveClassifier.set("sources")
sourcesJar.isPreserveFileTimestamps = false
sourcesJar.isReproducibleFileOrder = true
sourcesJar.group = BasePlugin.BUILD_GROUP
sourcesJar.description = "Assembles a jar archive containing the sources of $componentTypeName '$componentName'."
}
result.configure {
project.launch {
sourceSets.await().forEach { (sourceSetName, sourceSetFiles) ->
it.from(sourceSetFiles) { copySpec ->
copySpec.into(sourceSetName)
// Duplicates are coming from `SourceSets` that `sourceSet` depends on.
// Such dependency was added by Kotlin compilation.
// TODO: rethink approach for adding dependent `SourceSets` to Kotlin compilation `SourceSet`
copySpec.duplicatesStrategy = DuplicatesStrategy.WARN
}
}
}
}
return result
}
internal fun Project.setupGeneralKotlinExtensionParameters() {
project.launch {
@@ -0,0 +1,78 @@
/*
* Copyright 2010-2023 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.plugin.mpp.external
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.ExternalKotlinTargetApi
import org.jetbrains.kotlin.gradle.plugin.mpp.includeSources
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.isMain
import org.jetbrains.kotlin.gradle.plugin.mpp.sourcesJarTask
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
/**
*
* Will add all sources (including transitive dependsOn edges) from the compilation into this jar task given
* the multiplatform convention.
*
* e.g.
* ```
* src/commonMain/kotlin/CommonMain.kt
* src/jvmMain/kotlin/JvmMain.kt
* ```
*
* will be packaged like
* ```
* /commonMain/CommonMain.kt
* /jvmMain/JvmMain.kt
* ```
*
* @since 1.9.20
*/
@ExternalKotlinTargetApi
fun Jar.includeSources(compilation: DecoratedExternalKotlinCompilation) {
includeSources(compilation.internal)
}
/**
* Registers, or returns if already existing, a sources jar task that contains
* all sources of the given compilation (see [includeSources])
*
* @since 1.9.20
*/
@ExternalKotlinTargetApi
fun DecoratedExternalKotlinTarget.sourcesJarTask(compilation: DecoratedExternalKotlinCompilation): TaskProvider<Jar> {
return sourcesJarTask(
compilation,
componentName = lowerCamelCaseName(targetName, compilation.name.takeUnless { compilation.isMain() }),
artifactNameAppendix = targetName.toLowerCaseAsciiOnly()
)
}
/**
* Publishes the sources packaged by the given [jarTask] in this targets' publication.
*
* @since 1.9.20
*/
@ExternalKotlinTargetApi
fun DecoratedExternalKotlinTarget.publishSources(jarTask: TaskProvider<Jar>) {
delegate.sourcesElementsPublishedConfiguration.outgoing.artifact(jarTask) { artifact ->
artifact.classifier = "sources"
}
}
/**
* Publishes the sources associated with the given [compilation] in this targets' publication.
* Will register the corresponding [sourcesJarTask] if necessary.
*
* @since 1.9.20
*/
@ExternalKotlinTargetApi
fun DecoratedExternalKotlinTarget.publishSources(compilation: DecoratedExternalKotlinCompilation) {
publishSources(sourcesJarTask(compilation))
}
@@ -0,0 +1,92 @@
/*
* Copyright 2010-2023 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.plugin.mpp
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.launch
import org.jetbrains.kotlin.gradle.tasks.locateTask
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.Future
import org.jetbrains.kotlin.gradle.utils.future
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import java.io.File
internal fun sourcesJarTask(compilation: KotlinCompilation<*>, componentName: String, artifactNameAppendix: String): TaskProvider<Jar> =
sourcesJarTask(
project = compilation.target.project,
sourceSets = compilation.target.project.future {
compilation.internal.awaitAllKotlinSourceSets().associate { it.name to it.kotlin }
},
componentName = componentName,
artifactNameAppendix = artifactNameAppendix
)
private fun sourcesJarTask(
project: Project,
sourceSets: Future<Map<String, Iterable<File>>>,
componentName: String,
artifactNameAppendix: String,
): TaskProvider<Jar> =
sourcesJarTaskNamed(
taskName = lowerCamelCaseName(componentName, "sourcesJar"),
componentName = componentName,
project = project,
sourceSets = sourceSets,
artifactNameAppendix = artifactNameAppendix
)
internal fun sourcesJarTaskNamed(
taskName: String,
componentName: String,
project: Project,
sourceSets: Future<Map<String, Iterable<File>>>,
artifactNameAppendix: String,
componentTypeName: String = "target",
): TaskProvider<Jar> {
project.locateTask<Jar>(taskName)?.let {
return it
}
val result = project.registerTask<Jar>(taskName) { sourcesJar ->
sourcesJar.archiveAppendix.set(artifactNameAppendix)
sourcesJar.archiveClassifier.set("sources")
sourcesJar.isPreserveFileTimestamps = false
sourcesJar.isReproducibleFileOrder = true
sourcesJar.group = BasePlugin.BUILD_GROUP
sourcesJar.description = "Assembles a jar archive containing the sources of $componentTypeName '$componentName'."
project.launch {
sourcesJar.includeSources(sourceSets.await())
}
}
return result
}
internal fun Jar.includeSources(compilation: KotlinCompilation<*>) {
compilation.internal.allKotlinSourceSets.forAll { sourceSet ->
includeSources(sourceSet.name, sourceSet.kotlin)
}
}
internal fun Jar.includeSources(sourceSets: Map<String, Iterable<File>>) {
sourceSets.forEach { (name, sources) -> includeSources(name, sources) }
}
internal fun Jar.includeSources(name: String, sources: Iterable<File>) {
from(sources) { spec ->
spec.into(name)
// Duplicates are coming from `SourceSets` that `sourceSet` depends on.
// Such dependency was added by Kotlin compilation.
// TODO: rethink approach for adding dependent `SourceSets` to Kotlin compilation `SourceSet`
spec.duplicatesStrategy = DuplicatesStrategy.WARN
}
}
@@ -17,39 +17,24 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
import org.jetbrains.kotlin.gradle.plugin.hierarchy.KotlinSourceSetTreeClassifier
import org.jetbrains.kotlin.gradle.plugin.hierarchy.orNull
import org.jetbrains.kotlin.gradle.plugin.launchInStage
import org.jetbrains.kotlin.gradle.plugin.mpp.external.*
import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinCompilationDescriptor.CompilationFactory
import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinTargetDescriptor.TargetFactory
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.util.runLifecycleAwareTest
import org.jetbrains.kotlin.gradle.plugin.mpp.external.createCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.external.createExternalKotlinTarget
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.utils.property
import org.jetbrains.kotlin.gradle.utils.toMap
import kotlin.test.*
class ExternalKotlinTargetApiTests {
class FakeTarget(delegate: Delegate) : DecoratedExternalKotlinTarget(delegate)
class FakeCompilation(delegate: Delegate) : DecoratedExternalKotlinCompilation(delegate)
val project = buildProjectWithMPP()
val kotlin = project.multiplatformExtension
private fun ExternalKotlinTargetDescriptorBuilder<FakeTarget>.defaults() {
targetName = "fake"
platformType = KotlinPlatformType.jvm
targetFactory = TargetFactory(::FakeTarget)
}
private fun ExternalKotlinCompilationDescriptorBuilder<FakeCompilation>.defaults() {
compilationName = "fake"
compilationFactory = CompilationFactory(::FakeCompilation)
defaultSourceSet = kotlin.sourceSets.maybeCreate("fake")
}
@Test
fun `test - sourceSetClassifier - default`() = buildProjectWithMPP().runLifecycleAwareTest {
val target = kotlin.createExternalKotlinTarget<FakeTarget> { defaults() }
val compilation = target.createCompilation<FakeCompilation> { defaults() }
val compilation = target.createCompilation<FakeCompilation> { defaults(kotlin) }
assertEquals(KotlinSourceSetTree("fake"), KotlinSourceSetTree.orNull(compilation))
}
@@ -58,7 +43,7 @@ class ExternalKotlinTargetApiTests {
fun `test - sourceSetClassifier - custom name`() = buildProjectWithMPP().runLifecycleAwareTest {
val target = kotlin.createExternalKotlinTarget<FakeTarget> { defaults() }
val compilation = target.createCompilation<FakeCompilation> {
defaults()
defaults(kotlin)
sourceSetTreeClassifierV2 = KotlinSourceSetTreeClassifier.Name("mySourceSetTree")
}
@@ -73,7 +58,7 @@ class ExternalKotlinTargetApiTests {
val target = kotlin.createExternalKotlinTarget<FakeTarget> { defaults() }
val mainCompilation = target.createCompilation<FakeCompilation> {
defaults()
defaults(kotlin)
sourceSetTreeClassifierV2 = KotlinSourceSetTreeClassifier.Property(myProperty)
}
@@ -0,0 +1,104 @@
/*
* Copyright 2010-2023 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.
*/
@file:Suppress("FunctionName")
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.internal.file.copy.SingleParentCopySpec
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.create
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
import org.jetbrains.kotlin.gradle.plugin.configurationResult
import org.jetbrains.kotlin.gradle.plugin.hierarchy.KotlinSourceSetTreeClassifier
import org.jetbrains.kotlin.gradle.plugin.mpp.external.*
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.tooling.core.withClosure
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.fail
class ExternalKotlinTargetSourcesJarUtilsTest {
@Test
fun `test - includeSources`() = buildProjectWithMPP().runLifecycleAwareTest {
val target = multiplatformExtension.createExternalKotlinTarget<FakeTarget> { defaults() }
val compilation = target.createCompilation<FakeCompilation> {
defaults(multiplatformExtension)
defaultSourceSet = multiplatformExtension.sourceSets.maybeCreate("testSourceSet")
sourceSetTreeClassifierV2 = KotlinSourceSetTreeClassifier.Value(KotlinSourceSetTree.main)
}
val jar = tasks.create<Jar>("forTest")
jar.includeSources(compilation)
configurationResult.await()
val allSpecs = jar.rootSpec.withClosure { it.children }
val testSourceSetSpec = allSpecs.filterIsInstance<SingleParentCopySpec>().find { it.destPath == "testSourceSet" }
?: fail("Missing 'testSourceSet' in jar")
assertEquals(
setOf(file("src/testSourceSet/kotlin")),
(testSourceSetSpec.sourcePaths.single() as SourceDirectorySet).srcDirs
)
val commonMainSpec = allSpecs.filterIsInstance<SingleParentCopySpec>().find { it.destPath == "commonMain" }
?: fail("Missing 'commonMain' in jar")
assertEquals(
setOf(file("src/commonMain/kotlin")),
(commonMainSpec.sourcePaths.single() as SourceDirectorySet).srcDirs
)
}
@Test
fun `test - sourcesJarTask`() = buildProjectWithMPP().runLifecycleAwareTest {
val target = multiplatformExtension.createExternalKotlinTarget<FakeTarget> { defaults() }
val mainCompilation = target.createCompilation<FakeCompilation> {
defaults(multiplatformExtension)
compilationName = "main"
}
val auxCompilation = target.createCompilation<FakeCompilation> {
defaults(multiplatformExtension)
compilationName = "aux"
}
val mainJar = target.sourcesJarTask(mainCompilation)
assertEquals(mainJar, target.sourcesJarTask(mainCompilation))
assertEquals("fakeSourcesJar", mainJar.name)
val auxJar = target.sourcesJarTask(auxCompilation)
assertNotEquals(mainJar, auxJar)
assertEquals(auxJar, target.sourcesJarTask(auxCompilation))
assertEquals("fakeAuxSourcesJar", auxJar.name)
}
@Test
fun `test - publishSources`() = buildProjectWithMPP().runLifecycleAwareTest {
val target = multiplatformExtension.createExternalKotlinTarget<FakeTarget> { defaults() }
val mainCompilation = target.createCompilation<FakeCompilation> {
defaults(multiplatformExtension)
compilationName = "main"
}
target.publishSources(mainCompilation)
configurationResult.await()
val sourcesArtifacts = target.sourcesElementsPublishedConfiguration.outgoing.artifacts.filter { artifact ->
artifact.classifier == "sources"
}
if (sourcesArtifacts.isEmpty()) fail("Missing 'sources' artifacts")
if (sourcesArtifacts.size > 1) fail("Expected single 'sources' artifact. Found $sourcesArtifacts")
val sourcesArtifact = sourcesArtifacts.single()
assertEquals(target.sourcesJarTask(mainCompilation).get().archiveFile.get().asFile, sourcesArtifact.file)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2023 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.util
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.external.*
import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinCompilationDescriptor.CompilationFactory
import org.jetbrains.kotlin.gradle.plugin.mpp.external.ExternalKotlinTargetDescriptor.TargetFactory
class FakeCompilation(delegate: Delegate) : DecoratedExternalKotlinCompilation(delegate)
class FakeTarget(delegate: Delegate) : DecoratedExternalKotlinTarget(delegate)
fun ExternalKotlinTargetDescriptorBuilder<FakeTarget>.defaults() {
targetName = "fake"
platformType = KotlinPlatformType.jvm
targetFactory = TargetFactory(::FakeTarget)
}
fun ExternalKotlinCompilationDescriptorBuilder<FakeCompilation>.defaults(kotlin: KotlinMultiplatformExtension) {
compilationName = "fake"
compilationFactory = CompilationFactory(::FakeCompilation)
defaultSourceSet = kotlin.sourceSets.maybeCreate("fake")
}