[Gradle] Introduce defaultFragmentName for K2 MPP Structure

It is possible to add source files to task directly. Especially this is
a very popular case for Android integration. Android variants can
contain any number of extra-source files that should be compiled
together with other android-specific sources.
Android specific sources that come from Android source sets already
compiled together as part of "main" fragment (KT-62508).
With this change this logic is extended to any extra sources
that are added directly to the compile task.

^KT-62508 Verification Pending
This commit is contained in:
Anton Lakotka
2024-02-15 07:33:49 +01:00
committed by Space Team
parent 020bb72d2b
commit 7a150cce47
8 changed files with 117 additions and 12 deletions
@@ -27,11 +27,13 @@ import org.gradle.work.Incremental
import org.gradle.work.NormalizeLineEndings
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptionsDefault
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptionsHelper
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerArgumentsProducer.CreateCompilerArgumentsContext
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerArgumentsProducer.CreateCompilerArgumentsContext.Companion.create
import org.jetbrains.kotlin.gradle.report.BuildReportMode
import org.jetbrains.kotlin.gradle.tasks.K2MultiplatformStructure
import org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.toSingleCompilerPluginOptions
@@ -59,6 +61,13 @@ abstract class KaptGenerateStubsTask @Inject constructor(
@get:Internal
abstract override val libraries: ConfigurableFileCollection
/**
* [K2MultiplatformStructure] is not required for Kapt stubs
*/
@InternalKotlinGradlePluginApi
@get:Internal
override val multiplatformStructure: K2MultiplatformStructure get() = super.multiplatformStructure
/* Used as input as empty kapt classpath should not trigger stub generation, but a non-empty one should. */
@Input
fun getIfKaptClasspathIsPresent() = !kaptClasspath.isEmpty
@@ -51,6 +51,8 @@ internal object KotlinCompilationK2MultiplatformConfigurator : KotlinCompilation
K2MultiplatformStructure.Fragment(fragmentName, sourceFiles.reduce { acc, fileTree -> acc + fileTree })
}
})
compileTask.multiplatformStructure.defaultFragmentName.set(compilation.defaultSourceSet.fragmentName())
}
}
}
@@ -14,6 +14,7 @@ import org.gradle.api.tasks.*
import org.gradle.work.NormalizeLineEndings
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompilerOptionsDefault
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerArgumentsProducer.ContributeCompilerArgumentsContext
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
@@ -21,6 +22,7 @@ import org.jetbrains.kotlin.gradle.plugin.statistics.CompileKotlinJsIrLinkMetric
import org.jetbrains.kotlin.gradle.plugin.statistics.UsesBuildFusService
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode.DEVELOPMENT
import org.jetbrains.kotlin.gradle.tasks.K2MultiplatformStructure
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import org.jetbrains.kotlin.gradle.utils.configureExperimentalTryNext
import javax.inject.Inject
@@ -44,6 +46,13 @@ abstract class KotlinJsIrLink @Inject constructor(
@get:Internal
override val sources: FileCollection = super.sources
/**
* [K2MultiplatformStructure] is not required for JS IR link
*/
@InternalKotlinGradlePluginApi
@get:Internal
override val multiplatformStructure: K2MultiplatformStructure get() = super.multiplatformStructure
override fun skipCondition(): Boolean {
return !entryModule.get().asFile.exists()
}
@@ -500,7 +500,7 @@ internal constructor(
sources { args ->
/* Shared native compilations in K2 still use -Xcommon-sources and klib dependencies */
if (compilerOptions.usesK2.get() && sharedCompilationData == null) {
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sourceFileFilter)
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sources.files, sourceFileFilter)
} else {
args.commonSources = commonSourcesTree.files.takeIf { it.isNotEmpty() }?.toPathsArray()
}
@@ -7,12 +7,14 @@ package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.util.PatternFilterable
import org.gradle.work.Incremental
import org.gradle.work.NormalizeLineEndings
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import java.io.File
@InternalKotlinGradlePluginApi
abstract class K2MultiplatformStructure {
@@ -43,17 +45,48 @@ abstract class K2MultiplatformStructure {
@get:Nested
abstract val fragments: ListProperty<Fragment>
/**
* If new sources were added to the Compile Task,
* and they weren't mapped to any of the fragments then [defaultFragmentName] will be used
*
* It is marked with @Optional as an extra protection measure for cases when some task extends
* a compile task but doesn't need K2 Structure for example [KotlinJsIrLink]
*
* @see KotlinCompileTool.source
*/
@get:Input
@get:Optional
abstract val defaultFragmentName: Property<String>
}
internal val K2MultiplatformStructure.fragmentsCompilerArgs: Array<String>
get() = fragments.get().map { it.fragmentName }.toSet().toTypedArray()
internal fun K2MultiplatformStructure.fragmentSourcesCompilerArgs(sourceFileFilter: PatternFilterable? = null): Array<String> =
fragments.get().flatMap { sourceSet ->
private fun fragmentSourceCompilerArg(sourceFile: File, fragmentName: String) = "$fragmentName:${sourceFile.absolutePath}"
internal fun K2MultiplatformStructure.fragmentSourcesCompilerArgs(
allSources: Collection<File>,
sourceFileFilter: PatternFilterable? = null
): Array<String> {
val sourcesWithKnownFragment = mutableSetOf<File>()
val fragmentSourcesCompilerArgs = fragments.get().flatMap { sourceSet ->
sourceSet.sources
.run { if (sourceFileFilter != null) asFileTree.matching(sourceFileFilter) else this }
.files.map { sourceFile -> "${sourceSet.fragmentName}:${sourceFile.absolutePath}" }
}.toTypedArray()
.files.map { sourceFile ->
sourcesWithKnownFragment.add(sourceFile)
fragmentSourceCompilerArg(sourceFile, sourceSet.fragmentName)
}
}.toMutableList()
val sourcesWithUnknownFragment = allSources - sourcesWithKnownFragment
val defaultFragmentName = defaultFragmentName.orNull
if (defaultFragmentName != null) {
sourcesWithUnknownFragment.mapTo(fragmentSourcesCompilerArgs) { fragmentSourceCompilerArg(it, defaultFragmentName) }
}
return fragmentSourcesCompilerArgs.toTypedArray()
}
internal val K2MultiplatformStructure.fragmentRefinesCompilerArgs: Array<String>
get() = refinesEdges.get().map { edge ->
@@ -178,7 +178,7 @@ abstract class Kotlin2JsCompile @Inject constructor(
if (multiPlatformEnabled.get()) {
if (compilerOptions.usesK2.get()) {
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sourceFileFilter)
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sources.files, sourceFileFilter)
} else {
args.commonSources = commonSourceSet.asFileTree.toPathsArray()
}
@@ -263,18 +263,18 @@ abstract class KotlinCompile @Inject constructor(
}
sources { args ->
val sourcesFiles = sources.asFileTree.files.toList()
val javaSourcesFiles = javaSources.files.toList()
val scriptSourcesFiles = scriptSources.asFileTree.files.toList()
if (multiPlatformEnabled.get()) {
if (compilerOptions.usesK2.get()) {
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sourceFileFilter)
args.fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(sourcesFiles, sourceFileFilter)
} else {
args.commonSources = commonSourceSet.asFileTree.toPathsArray()
}
}
val sourcesFiles = sources.asFileTree.files.toList()
val javaSourcesFiles = javaSources.files.toList()
val scriptSourcesFiles = scriptSources.asFileTree.files.toList()
if (logger.isInfoEnabled) {
logger.info("Kotlin source files: ${sourcesFiles.joinToString()}")
logger.info("Java source files: ${javaSourcesFiles.joinToString()}")
@@ -7,21 +7,25 @@
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.newInstance
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
import org.jetbrains.kotlin.compilerRunner.ArgumentUtils
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.internal.CompilerArgumentAware
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerArgumentsProducer.CreateCompilerArgumentsContext.Companion.lenient
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.IR
import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.tasks.K2MultiplatformStructure.Fragment
import org.jetbrains.kotlin.gradle.tasks.K2MultiplatformStructure.RefinesEdge
import org.jetbrains.kotlin.gradle.util.*
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@@ -46,6 +50,7 @@ class K2MultiplatformStructureTest {
Fragment("c", project.files())
)
)
structure.defaultFragmentName.set("a")
val sourceArguments = K2JVMCompilerArguments()
sourceArguments.configureK2Multiplatform(structure)
@@ -98,6 +103,53 @@ class K2MultiplatformStructureTest {
}
}
@Test
fun `test - extra sources that were added to compile task directly is included into default source set fragment`() {
val targets = listOf(kotlin.jvm(), kotlin.js(), kotlin.linuxX64())
kotlin.writeSource("commonMain")
kotlin.writeSource("jvmMain")
kotlin.writeSource("linuxX64Main")
kotlin.writeSource("jsMain")
// Add extra source file
targets.forEach { target ->
val compileTask = target.compilations.get("main").compileTaskProvider.get() as KotlinCompileTool
val extraSourceFile = project.file("${target.disambiguateName("generated")}.kt").also {
it.parentFile.mkdirs()
it.writeText("fun generated() {}")
}
compileTask.source(extraSourceFile)
}
project.evaluate()
val actualFragmentSources = targets.associate { target ->
val compileTask = target.compilations.get("main").compileTaskProvider.get() as K2MultiplatformCompilationTask
val args = compileTask.buildCompilerArguments()
target.name to args.fragmentSources?.toList().orEmpty().map {
val fragment = it.substringBefore(":")
val filePath = it.substringAfter(":")
"$fragment:${File(filePath).name}"
}
}
assertEquals(
mapOf(
"jvm" to listOf("jvmMain:jvmMain.kt", "commonMain:commonMain.kt", "jvmMain:jvmGenerated.kt"),
"js" to listOf("jsMain:jsMain.kt", "commonMain:commonMain.kt", "jsMain:jsGenerated.kt"),
"linuxX64" to listOf("linuxX64Main:linuxX64Main.kt", "commonMain:commonMain.kt", "linuxX64Main:linuxX64Generated.kt"),
),
actualFragmentSources
)
}
private fun KotlinMultiplatformExtension.writeSource(sourceSet: String, fileName: String = "${sourceSet}.kt") {
val srcDir = sourceSets.maybeCreate(sourceSet).kotlin.srcDirs.first()
srcDir.mkdirs()
srcDir.resolve(fileName).writeText("""fun $sourceSet() {}""")
}
private fun `test compilations multiplatformStructure configuration`(compilation: KotlinCompilation<*>) {
val defaultSourceSet = compilation.defaultSourceSet
/* Create an additional intermediate source set for testing */
@@ -172,6 +224,6 @@ private fun K2MultiplatformCompilationTask.buildCompilerArguments(): CommonCompi
internal fun CommonCompilerArguments.configureK2Multiplatform(multiplatformStructure: K2MultiplatformStructure) {
fragments = multiplatformStructure.fragmentsCompilerArgs
fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs()
fragmentSources = multiplatformStructure.fragmentSourcesCompilerArgs(emptyList())
fragmentRefines = multiplatformStructure.fragmentRefinesCompilerArgs
}