diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetConfigurator.kt index 2ea5c552ba7..7cff38c33ed 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetConfigurator.kt @@ -10,7 +10,6 @@ import org.gradle.api.DefaultTask import org.gradle.api.NamedDomainObjectCollection import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency import org.gradle.api.attributes.Attribute import org.gradle.api.attributes.AttributeContainer @@ -24,6 +23,7 @@ import org.gradle.api.tasks.Exec import org.gradle.api.tasks.TaskProvider import org.gradle.language.base.plugins.LifecycleBasePlugin import org.jetbrains.kotlin.gradle.dsl.KotlinNativeCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationInfo.KPM import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.KOTLIN_NATIVE_IGNORE_INCORRECT_DEPENDENCIES @@ -47,12 +47,9 @@ import org.jetbrains.kotlin.gradle.testing.internal.kotlinTestRegistry import org.jetbrains.kotlin.gradle.testing.testTaskName import org.jetbrains.kotlin.gradle.utils.Xcode import org.jetbrains.kotlin.gradle.utils.klibModuleName -import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.newInstance import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget -import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly -import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly import java.io.File open class KotlinNativeTargetConfigurator : AbstractKotlinTargetConfigurator( @@ -66,7 +63,7 @@ open class KotlinNativeTargetConfigurator : AbstractKotl // this afterEvaluate comes from NativeCompilerOptions val compilationCompilerOptions = binary.compilation.compilerOptions val konanPropertiesBuildService = KonanPropertiesBuildService.registerIfAbsent(project) - val result = registerTask( + val linkTask = registerTask( binary.linkTaskName, listOf(binary) ) { val target = binary.target @@ -81,12 +78,12 @@ open class KotlinNativeTargetConfigurator : AbstractKotl if (binary !is TestExecutable) { - tasks.named(binary.compilation.target.artifactsTaskName).dependsOn(result) - locateOrRegisterTask(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(result) + tasks.named(binary.compilation.target.artifactsTaskName).dependsOn(linkTask) + locateOrRegisterTask(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(linkTask) } if (binary is Framework) { - createFrameworkArtifact(binary, result) + createFrameworkArtifact(binary, linkTask) } } @@ -102,102 +99,6 @@ open class KotlinNativeTargetConfigurator : AbstractKotl } } - private fun Project.createFrameworkArtifact( - binary: Framework, - linkTask: TaskProvider - ) { - fun Configuration.configureConfiguration(taskProvider: TaskProvider) { - project.afterEvaluate { - val task = taskProvider.get() - val artifactFile = when (task) { - is FatFrameworkTask -> task.fatFramework - else -> binary.outputFile - } - val linkArtifact = project.artifacts.add(name, artifactFile) { artifact -> - artifact.name = name - artifact.extension = "framework" - artifact.type = "binary" - artifact.classifier = "framework" - artifact.builtBy(task) - } - project.extensions.getByType(org.gradle.api.internal.plugins.DefaultArtifactPublicationSet::class.java) - .addCandidate(linkArtifact) - artifacts.add(linkArtifact) - attributes.attribute(KotlinPlatformType.attribute, binary.target.platformType) - attributes.attribute( - project.artifactTypeAttribute, - NativeArtifactFormat.FRAMEWORK - ) - attributes.attribute( - KotlinNativeTarget.kotlinNativeBuildTypeAttribute, - binary.buildType.name - ) - if (attributes.getAttribute(Framework.frameworkTargets) == null) { - attributes.attribute( - Framework.frameworkTargets, - setOf(binary.target.konanTarget.name) - ) - } - // capture type parameter T - fun copyAttribute(key: Attribute, from: AttributeContainer, to: AttributeContainer) { - to.attribute(key, from.getAttribute(key)!!) - } - binary.attributes.keySet().filter { it != KotlinNativeTarget.konanTargetAttribute }.forEach { - copyAttribute(it, binary.attributes, this.attributes) - } - } - } - - fun configureFatFramework() { - val fatFrameworkConfigurationName = lowerCamelCaseName( - binary.name, - binary.target.konanTarget.family.name.toLowerCaseAsciiOnly(), - "fat" - ) - val fatFrameworkTaskName = "link${fatFrameworkConfigurationName.capitalizeAsciiOnly()}" - - val fatFrameworkTask = if (fatFrameworkTaskName in tasks.names) { - tasks.named(fatFrameworkTaskName, FatFrameworkTask::class.java) - } else { - tasks.register(fatFrameworkTaskName, FatFrameworkTask::class.java) { - it.baseName = binary.baseName - it.destinationDir = it.destinationDir.resolve(binary.buildType.name.toLowerCaseAsciiOnly()) - } - } - - fatFrameworkTask.configure { - try { - it.from(binary) - } catch (e: Exception) { - logger.warn("Cannot add binary ${binary.name} dependency to default fat framework", e) - } - } - - // maybeCreate is not used as it does not provide way to configure once - val fatConfiguration = - configurations.findByName(fatFrameworkConfigurationName) ?: configurations.create(fatFrameworkConfigurationName) { - it.isCanBeConsumed = true - it.isCanBeResolved = false - it.configureConfiguration(fatFrameworkTask) - } - - fatConfiguration.attributes.attribute( - Framework.frameworkTargets, - (fatConfiguration.attributes.getAttribute(Framework.frameworkTargets) ?: setOf()) + binary.target.konanTarget.name - ) - } - - configurations.create(lowerCamelCaseName(binary.name, binary.target.name)) { - it.isCanBeConsumed = true - it.isCanBeResolved = false - it.configureConfiguration(linkTask) - } - - if (FatFrameworkTask.isSupportedTarget(binary.target)) { - configureFatFramework() - } - } - private fun Project.createRunTask(binary: Executable) { val taskName = binary.runTaskName ?: return registerTask(taskName) { exec -> @@ -337,6 +238,9 @@ open class KotlinNativeTargetConfigurator : AbstractKotl target.binaries.all { project.createLinkTask(it) } + project.runOnceAfterEvaluated("Create fat frameworks") { + project.multiplatformExtensionOrNull?.createFatFrameworks() + } project.runOnceAfterEvaluated("Sync language settings for NativeLinkTask") { target.binaries.all { binary -> project.syncLanguageSettingsToLinkTask(binary) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/configureBinaryFrameworks.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/configureBinaryFrameworks.kt new file mode 100644 index 00000000000..e67f8596a3d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/configureBinaryFrameworks.kt @@ -0,0 +1,121 @@ +/* + * 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.targets.native + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinNativeTargetConfigurator +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.internal.artifactTypeAttribute +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink +import org.jetbrains.kotlin.gradle.utils.getOrCreate +import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName +import org.jetbrains.kotlin.gradle.utils.markConsumable +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly +import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly +import java.io.File + +/** + * Contains common data between frameworks that can be bundled to a fat framework. + */ +private data class FrameworkGroupDescription( + val frameworkName: String, + val targetFamilyName: String, + val baseName: String, + val buildType: NativeBuildType +) + +private val Framework.frameworkGroupDescription + get() = FrameworkGroupDescription( + frameworkName = name, + targetFamilyName = target.konanTarget.family.name.toLowerCaseAsciiOnly(), + baseName = baseName, + buildType = buildType + ) + +internal fun Project.createFrameworkArtifact(binaryFramework: Framework, linkTask: TaskProvider) { + val frameworkConfiguration = configurations.getOrCreate(binaryFramework.binaryFrameworkConfigurationName, invokeWhenCreated = { + it.markConsumable() + it.applyBinaryFrameworkAttributes(project, binaryFramework.frameworkGroupDescription, listOf(binaryFramework.target)) + }) + + addFrameworkArtifact(frameworkConfiguration, linkTask.flatMap { it.outputFile }) +} + +internal fun KotlinMultiplatformExtension.createFatFrameworks() { + targets + .filterIsInstance() + .filter { FatFrameworkTask.isSupportedTarget(it) } + .flatMap { it.binaries } + .filterIsInstance() + .groupBy { it.frameworkGroupDescription } + .filter { (_, frameworks) -> frameworks.size > 1 } + .forEach { (groupDescription, frameworks) -> project.createFatFramework(groupDescription, frameworks) } +} + +private val Framework.binaryFrameworkConfigurationName get() = lowerCamelCaseName(name, target.name) +private val FrameworkGroupDescription.fatFrameworkConfigurationName get() = lowerCamelCaseName(frameworkName, targetFamilyName, "fat") + +private fun Configuration.applyBinaryFrameworkAttributes( + project: Project, + frameworkDescription: FrameworkGroupDescription, + targets: List +) { + with(attributes) { + attribute(KotlinPlatformType.attribute, KotlinPlatformType.native) + attribute(project.artifactTypeAttribute, KotlinNativeTargetConfigurator.NativeArtifactFormat.FRAMEWORK) + attribute(KotlinNativeTarget.kotlinNativeBuildTypeAttribute, frameworkDescription.buildType.name) + attribute(Framework.frameworkTargets, targets.map { it.konanTarget.name }.toSet()) + } +} + +private fun Project.addFrameworkArtifact(configuration: Configuration, artifactFile: Provider) { + val frameworkArtifact = artifacts.add(configuration.name, artifactFile) { artifact -> + artifact.name = name + artifact.extension = "framework" + artifact.type = "binary" + artifact.classifier = "framework" + } + project.extensions.getByType(org.gradle.api.internal.plugins.DefaultArtifactPublicationSet::class.java) + .addCandidate(frameworkArtifact) +} + +private fun Project.createFatFramework(groupDescription: FrameworkGroupDescription, frameworks: List) { + require(frameworks.size > 1) { "Can't create binary fat framework from a single framework" } + val fatFrameworkConfigurationName = groupDescription.fatFrameworkConfigurationName + val fatFrameworkTaskName = "link${fatFrameworkConfigurationName.capitalizeAsciiOnly()}" + + val fatFrameworkTask = if (fatFrameworkTaskName in tasks.names) { + tasks.named(fatFrameworkTaskName, FatFrameworkTask::class.java) + } else { + tasks.register(fatFrameworkTaskName, FatFrameworkTask::class.java) { + it.baseName = groupDescription.baseName + it.destinationDir = it.destinationDir.resolve(groupDescription.buildType.name.toLowerCaseAsciiOnly()) + } + } + + fatFrameworkTask.configure { + try { + it.from(frameworks) + } catch (e: Exception) { + logger.warn("Cannot make fat framework from frameworks: ${frameworks.map { it.name }}", e) + } + } + + val fatFrameworkConfiguration = project.configurations.getOrCreate(fatFrameworkConfigurationName, invokeWhenCreated = { + it.markConsumable() + it.applyBinaryFrameworkAttributes(project, groupDescription, targets = frameworks.map(Framework::target)) + }) + + addFrameworkArtifact(fatFrameworkConfiguration, fatFrameworkTask.map { it.fatFramework }) +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FatFrameworksTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FatFrameworksTest.kt new file mode 100644 index 00000000000..f9c360b234c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/FatFrameworksTest.kt @@ -0,0 +1,93 @@ +/* + * 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.unitTests + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP +import org.jetbrains.kotlin.gradle.util.kotlin +import kotlin.test.* + +class FatFrameworksTest { + @Test + fun `two apple frameworks get bundled to a fat framework`() { + val project = buildProjectWithMPP { + kotlin { + iosX64 { binaries.framework("foo", listOf(DEBUG)) } + iosArm64 { binaries.framework("foo", listOf(DEBUG)) } + } + } + project.evaluate() + project.configurations.names.also(::println) + project.assertConfigurationExists("fooDebugFrameworkIosX64") + project.assertConfigurationExists("fooDebugFrameworkIosArm64") + project.assertConfigurationExists("fooDebugFrameworkIosFat") + } + + @Test + fun `single binary framework doesn't produce a fat framework`() { + val project = buildProjectWithMPP { + kotlin { + iosX64 { binaries.framework("foo", listOf(DEBUG)) } + } + } + project.evaluate() + project.assertConfigurationExists("fooDebugFrameworkIosX64") + project.assertConfigurationDoesntExist("fooDebugFrameworkIosFat") + } + + @Test + fun `fat framework grouping -- different families`() = testFatFrameworkGrouping( + "fooDebugFrameworkIosFat", + "fooDebugFrameworkOsxFat", + ) { + iosX64 { binaries.framework("foo", listOf(DEBUG)) } + iosArm64 { binaries.framework("foo", listOf(DEBUG)) } + macosX64 { binaries.framework("foo", listOf(DEBUG)) } + macosArm64 { binaries.framework("foo", listOf(DEBUG)) } + } + + @Test + fun `fat framework grouping -- different families and different names within one family`() = testFatFrameworkGrouping( + "fooDebugFrameworkOsxFat", + ) { + iosX64 { binaries.framework("foo", listOf(DEBUG)) } + iosArm64 { binaries.framework("bar", listOf(DEBUG)) } + macosX64 { binaries.framework("foo", listOf(DEBUG)) } + macosArm64 { binaries.framework("foo", listOf(DEBUG)) } + } + + @Test + fun `fat framework grouping -- build types intersection`() = testFatFrameworkGrouping( + "fooReleaseFrameworkIosFat", + ) { + iosX64 { binaries.framework("foo", listOf(RELEASE)) } + iosArm64 { binaries.framework("foo", listOf(DEBUG, RELEASE)) } + } + + private fun testFatFrameworkGrouping( + vararg allExpectedFatFrameworks: String, + configureTargets: KotlinMultiplatformExtension.() -> Unit, + ) { + val project = buildProjectWithMPP { + kotlin { + configureTargets() + } + } + project.evaluate() + val allFatFrameworks = project.configurations.names.filter { it.endsWith("Fat") }.toSet() + assertEquals(allExpectedFatFrameworks.toSet(), allFatFrameworks) + } + + private fun Project.assertConfigurationDoesntExist(name: String) { + val configuration = project.configurations.findByName(name) + if (configuration != null) fail("'$name' configuration was not expected") + } + + private fun Project.assertConfigurationExists(name: String) { + project.configurations.findByName(name) ?: fail("'$name' configuration was expected to be created") + } +} \ No newline at end of file