[Gradle] Don't create fat frameworks from single binary framework

KT-55751 Verification Pending
This commit is contained in:
Anton Lakotka
2023-03-15 17:15:43 +07:00
committed by Space Team
parent 371e1205c0
commit 427ef8fc47
3 changed files with 222 additions and 104 deletions
@@ -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<T : KotlinNativeTarget> : AbstractKotlinTargetConfigurator<T>(
@@ -66,7 +63,7 @@ open class KotlinNativeTargetConfigurator<T : KotlinNativeTarget> : AbstractKotl
// this afterEvaluate comes from NativeCompilerOptions
val compilationCompilerOptions = binary.compilation.compilerOptions
val konanPropertiesBuildService = KonanPropertiesBuildService.registerIfAbsent(project)
val result = registerTask<KotlinNativeLink>(
val linkTask = registerTask<KotlinNativeLink>(
binary.linkTaskName, listOf(binary)
) {
val target = binary.target
@@ -81,12 +78,12 @@ open class KotlinNativeTargetConfigurator<T : KotlinNativeTarget> : AbstractKotl
if (binary !is TestExecutable) {
tasks.named(binary.compilation.target.artifactsTaskName).dependsOn(result)
locateOrRegisterTask<Task>(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(result)
tasks.named(binary.compilation.target.artifactsTaskName).dependsOn(linkTask)
locateOrRegisterTask<Task>(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(linkTask)
}
if (binary is Framework) {
createFrameworkArtifact(binary, result)
createFrameworkArtifact(binary, linkTask)
}
}
@@ -102,102 +99,6 @@ open class KotlinNativeTargetConfigurator<T : KotlinNativeTarget> : AbstractKotl
}
}
private fun Project.createFrameworkArtifact(
binary: Framework,
linkTask: TaskProvider<KotlinNativeLink>
) {
fun <T : Task> Configuration.configureConfiguration(taskProvider: TaskProvider<T>) {
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 <T> copyAttribute(key: Attribute<T>, 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<String>()) + 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<Exec>(taskName) { exec ->
@@ -337,6 +238,9 @@ open class KotlinNativeTargetConfigurator<T : KotlinNativeTarget> : 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)
@@ -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<KotlinNativeLink>) {
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<KotlinNativeTarget>()
.filter { FatFrameworkTask.isSupportedTarget(it) }
.flatMap { it.binaries }
.filterIsInstance<Framework>()
.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<KotlinNativeTarget>
) {
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<File>) {
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<Framework>) {
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 })
}
@@ -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")
}
}