Skip to content

Commit

Permalink
Fix dynamic framework support and fix binary compatibility of SkikoUI…
Browse files Browse the repository at this point in the history
…View

This change configures Kotlin Gradle
plugin to include a CInterop def file
(https://kotlinlang.org/docs/native-c-interop.html)
to fix linking a dynamic framework for iOS

Also helps with https://youtrack.jetbrains.com/issue/KT-60399
(at least in some cases). Internal discussion https://jetbrains.slack.com/archives/G017NLN12D8/p1689594898607469
  • Loading branch information
AlexeyTsvetkov committed Jul 17, 2023
1 parent 39fb88f commit a930272
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 17 deletions.
62 changes: 46 additions & 16 deletions skiko/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.gradle.crypto.checksum.Checksum
import org.jetbrains.compose.internal.publishing.MavenCentralProperties
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool

plugins {
Expand Down Expand Up @@ -427,6 +428,32 @@ kotlin {
}
}

fun configureCinterop(
cinteropName: String,
os: OS,
arch: Arch,
target: KotlinNativeTarget,
targetString: String,
linkerOpts: List<String>,
) {
val tasks = target.project.tasks
val taskNameSuffix = joinToTitleCamelCase(os.idWithSuffix(isIosSim = target.isIosSimArm64()), arch.id)
val writeCInteropDef = tasks.register("writeCInteropDef$taskNameSuffix", WriteCInteropDefFile::class.java) {
this.linkerOpts.set(linkerOpts)
outputFile.set(project.layout.buildDirectory.file("cinterop/$targetString/skiko.def"))
}
tasks.withType(CInteropProcess::class.java).configureEach {
if (konanTarget == target.konanTarget) {
dependsOn(writeCInteropDef)
}
}
target.compilations.getByName("main") {
cinterops.create(cinteropName).apply {
defFileProperty.set(writeCInteropDef.map { it.outputFile.get().asFile })
}
}
}

fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) {
if (!os.isCompatibleWithHost) return

Expand All @@ -444,24 +471,27 @@ fun configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) {

val skiaBinDir = "$skiaDir/out/${buildType.id}-$targetString"
val linkerFlags = when (os) {
OS.MacOS -> mutableListOf("-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreGraphics",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreServices"
OS.MacOS -> mutableListOfLinkerOptions(
listOfFrameworks("Metal", "CoreGraphics", "CoreText", "CoreServices")
)
OS.IOS -> mutableListOf("-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreGraphics",
"-linker-option", "-framework", "-linker-option", "UIKit",
"-linker-option", "-framework", "-linker-option", "CoreText")
OS.Linux -> mutableListOf(
"-linker-option", "-L/usr/lib/x86_64-linux-gnu",
"-linker-option", "-lfontconfig",
"-linker-option", "-lGL",
OS.IOS -> {
val iosFrameworks = listOfFrameworks("Metal", "CoreGraphics", "CoreText", "UIKit")
// list of linker options to be included into klib, which are needed for skiko consumers
// https://github.com/JetBrains/compose-multiplatform/issues/3178
// Important! Removing or renaming cinterop-uikit publication might cause compile error
// for projects depending on older Compose/Skiko transitively https://youtrack.jetbrains.com/issue/KT-60399
configureCinterop("uikit", os, arch, target, targetString, iosFrameworks)
mutableListOfLinkerOptions(iosFrameworks)
}
OS.Linux -> mutableListOfLinkerOptions(
"-L/usr/lib/x86_64-linux-gnu",
"-lfontconfig",
"-lGL",
// TODO: an ugly hack, Linux linker searches only unresolved symbols.
"-linker-option", "$skiaBinDir/libsksg.a",
"-linker-option", "$skiaBinDir/libskshaper.a",
"-linker-option", "$skiaBinDir/libskunicode.a",
"-linker-option", "$skiaBinDir/libskia.a"
"$skiaBinDir/libsksg.a",
"$skiaBinDir/libskshaper.a",
"$skiaBinDir/libskunicode.a",
"$skiaBinDir/libskia.a"
)
else -> mutableListOf()
}
Expand Down
27 changes: 27 additions & 0 deletions skiko/buildSrc/src/main/kotlin/WriteCInteropDefFile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class WriteCInteropDefFile : DefaultTask() {
@get:Input
abstract val linkerOpts: ListProperty<String>

@get:OutputFile
abstract val outputFile: RegularFileProperty

@TaskAction
fun run() {
val outputFile = outputFile.get().asFile
outputFile.parentFile.mkdirs()

outputFile.bufferedWriter().use { writer ->
val linkerOpts = linkerOpts.get()
if (linkerOpts.isNotEmpty()) {
writer.appendLine("linkerOpts=${linkerOpts.joinToString(" ")}")
}
}
}
}
14 changes: 14 additions & 0 deletions skiko/buildSrc/src/main/kotlin/utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ fun Task.projectDirs(vararg relativePaths: String): List<Directory> {
val projectDir = project.layout.projectDirectory
return relativePaths.map { path -> projectDir.dir(path) }
}

fun listOfFrameworks(vararg frameworks: String): List<String> =
frameworks.flatMap { listOf("-framework", it) }

fun mutableListOfLinkerOptions(options: Collection<String>): MutableList<String> =
ArrayList<String>(options.size * 2).also { result ->
options.forEach { option ->
result.add("-linker-option")
result.add(option)
}
}

fun mutableListOfLinkerOptions(vararg options: String): MutableList<String> =
mutableListOfLinkerOptions(options.toList())
15 changes: 14 additions & 1 deletion skiko/src/iosMain/kotlin/org/jetbrains/skiko/SkikoUIView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import platform.darwin.NSInteger
import kotlin.math.max
import kotlin.math.min

/*
TODO: remove org.jetbrains.skiko.objc.UIViewExtensionProtocol after Kotlin 1.8.20
https://youtrack.jetbrains.com/issue/KT-40426
*/
@Suppress("CONFLICTING_OVERLOADS")
@ExportObjCClass
class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
Expand All @@ -30,11 +34,20 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
private var _inputDelegate: UITextInputDelegateProtocol? = null
private var _currentTextMenuActions: TextActions? = null

// merging two constructors might cause a binary incompatibility, which will result in a unclear linking error,
// if a project using newer compose depends on an older compose transitively
// https://youtrack.jetbrains.com/issue/KT-60399
constructor(
skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true }
) : this(skiaLayer, frame, pointInside, skikoUITextInputTrains = object : SkikoUITextInputTraits {})

constructor(
skiaLayer: SkiaLayer,
frame: CValue<CGRect> = CGRectNull.readValue(),
pointInside: (Point, UIEvent?) -> Boolean = {_,_-> true },
skikoUITextInputTrains: SkikoUITextInputTraits = object : SkikoUITextInputTraits {},
skikoUITextInputTrains: SkikoUITextInputTraits
) : super(frame) {
this.skiaLayer = skiaLayer
_pointInside = pointInside
Expand Down

0 comments on commit a930272

Please sign in to comment.