Kotlin native interop linker could not find framework - kotlin

I'm trying to use cocoapods framework in Kotlin Multiplatform project.
So I
added framework to Pods file.
ran pod install.
created .def file
added cinterop config in build.gradle
./gradlew cinteropFirebaseIos runs successfully. It generates .klib so I can see classes in kotlin code.
But when I'm trying to run iOS app build fails with message:
Showing Recent Messages
> Task :app:linkDebugFrameworkIos
ld: framework not found FirebaseDatabase
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld invocation reported errors
Here is my config in build.gradle
fromPreset(presets.iosX64, 'ios') {
compilations.main {
outputKinds('FRAMEWORK')
cinterops {
firebase {
def proj = "${System.getProperty("user.home")}/Projects/kmpp"
def pods = "${proj}/iosApp/Pods"
defFile "${proj}/app/src/iosMain/c_interop/libfirebase.def"
includeDirs "${pods}/Firebase",
"${pods}/Firebase/CoreOnly/Sources",
"${pods}/FirebaseAnalytics/Frameworks/FirebaseAnalytics.framework/Headers"
}
}
}
}
here is my .def file:
language = Objective-C
headers = /Users/oleg/Projects/klug/crckalculator/iosApp/Pods/FirebaseCore/Firebase/Core/Public/FIRApp.h /Users/oleg/Projects/klug/crckalculator/iosApp/Pods/FirebaseDatabase/Firebase/Database/Public/FIRDatabase.h /Users/oleg/Projects/klug/crckalculator/iosApp/Pods/FirebaseCore/Firebase/Core/Public/FirebaseCore.h
compilerOpts = -framework FirebaseDatabase
linkerOpts = -framework FirebaseDatabase
How can I figure out what is wrong ? Did I miss something in .def file ? In build.gradle ?

There are two problematic moments here:
full paths to C headers in .def file are usually not desirable, instead passing includeDirs to Firebase installation, like in https://github.com/JetBrains/kotlin-native/blob/c7c566ce0f12221088a8908b6dc8e116c56a931b/samples/gtk/build.gradle#L22 would be helpful
linking problem comes from the similar issue - linker just got no idea where to look for framework libraries, so passing to compilations.main.linkerOpts smth like -F /Users/oleg/Projects/klug/crckalculator/iosApp/Pods/FirebaseCore/ shall help, see for example https://github.com/JetBrains/kotlin-native/blob/c7c566ce0f12221088a8908b6dc8e116c56a931b/samples/videoplayer/build.gradle#L15

Related

Resolving third party cocoapod dependencies in Kotlin MPP

I am trying to setup a tracking library written in Kotlin Multiplatform to support all our mobile clients.
Tests for Android went well (integrating snowplow via gradle).
I also managed to integrate Snowplow via cocoapods into the MPP.
kotlin {
...
cocoapods {
...
pod("SnowplowTracker") {
version = "~> 1.3.0"
}
}
And wrote following class in the X64 sourceset:
import cocoapods.SnowplowTracker.*
import com.tracking.domain.model.*
class X64Tracker {
private val tracker: SPTracker
init {
val emitter = SPEmitter.build {
it?.setUrlEndpoint("our.staging.endpoint")
it?.setProtocol(SPProtocol.SPProtocolHttps)
}
tracker = SPTracker.build {
emitter?.let { spEmitter -> it?.setEmitter(spEmitter) }
it?.setBase64Encoded(false)
it?.setSubject(SPSubject(platformContext = true, andGeoContext = true))
it?.setAppId("MPP.test")
it?.setApplicationContext(true)
}
}
fun trackSomething() {
track(
eventData = getEventData(
name = "MPP_test_event",
appArea = EventArea.Lifecycle,
action = EventAction.Check,
objectType = EventObjectType.Device,
source = EventSource.Client,
screenName = EventScreenName.Congratulations,
), contexts = emptyList()
)
}
private fun track(eventData: SPSelfDescribingJson, contexts: List<SPSelfDescribingJson?>) {
try {
val yo = SPSelfDescribing.build {
it?.setEventData(eventData)
it?.setContexts(contexts.toMutableList())
}
tracker.track(yo)
} catch (e: IllegalStateException) {
print("snowplow was not yet initialized when the following event occurred: $eventData")
}
}
private fun getEventData(
name: String,
appArea: EventArea,
action: EventAction,
objectType: EventObjectType,
source: EventSource,
screenName: EventScreenName,
) = SPSelfDescribingJson(
SCHEMA_EVENT, mapOf(
PROPERTY_EVENT_NAME to name,
PROPERTY_APP_AREA to appArea.serializedName,
PROPERTY_ACTION to action.serializedName,
PROPERTY_OBJECT_TYPE to objectType.serializedName,
PROPERTY_SOURCE to source.serializedName,
PROPERTY_SCREEN_NAME to screenName.serializedName,
)
)
}
Which is compiling and building our .framework files fine. Here is how we do that:
tasks.register<org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask>("debugFatFramework") {
baseName = frameworkName + "_sim"
group = "Universal framework"
description = "Builds a universal (fat) debug framework"
from(iosX64.binaries.getFramework("DEBUG"))
}
tasks.register<org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask>("releaseFatFramework") {
baseName = frameworkName + "_arm"
group = "Universal framework"
description = "Builds a universal (release) debug framework"
from(iosArm64.binaries.getFramework("RELEASE"))
}
Afterwards we combine this into an .xcframework file using following command:
xcrun xcodebuild -create-xcframework \
-framework tracking_arm.framework \
-framework tracking_sim.framework \
-output tracking.xcframework
We use Carthage to integrate it into our main app, but as soon I try to build the iOS project following error pops up:
Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_SPSelfDescribing", referenced from:
objc-class-ref in tracking_sim(result.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The weird thing: No matter which version of Snowplow I define in the cocoapods block - the syntax in my class does not need to change. Even updating to Snowplow 2.x doesn't require me to get rid of the SP prefixes.
I am not sure how to continue at all and appreciate any help.
The following:
Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_SPSelfDescribing", referenced from:
objc-class-ref in kaiaTracking_sim(result.o)
ld: symbol(s) not found for architecture x86_64
That says the linker can't find SPSelfDescribing. I assume SPSelfDescribing is part of SnowplowTracker. You'll need to add SnowplowTracker to the iOS app.
If you were using Cocoapods to integrate Kotlin into your iOS project, the podspec generated would include SnowplowTracker as a dependency.
Since you are not using Cocoapods, you need to include it yourself. We recently had to figure out Carthage for a client. I would highly recommend moving to SPM or Cocoapods for a number of reasons, but that's a different discussion. To add SnowplowTracker with Carthage, add this to your Cartfile:
github "snowplow/snowplow-objc-tracker" ~> 1.3
Then when you update Carthage, add that to your iOS project. The linker will be able to find it.
To be clear, the Undefined symbols for architecture x86_64 error isn't complaining about Kotlin. It's saying it can't find SnowplowTracker, which you need to add to the iOS project.

Androidx Proto datastore gradle setup

I'm trying to get the new (alpha) android datastore using protobuf support configured in gradle using Kotlin DSL (build.gradle.kts). The first attempts are not generating any java source classes from the xxx.proto (made-up name) file that is present. The protobuf plugin is generating the correct android tasks, but running them generates nothing, so obviously the default setup is not finding the directory my initial xxx.proto file is located in. The existing doc is thin on gradle setup, especially for Kotlin Gradle DSL (most all the gradle doc from google so far is for groovy), and my initial attempts at defining the location of the xxx.proto file are not working.
Does anyone have or has anyone seen working gradle config that specifies a custom source directory for .proto file(s) using Kotlin (build.gradle.kts)?
Got it working after some experimentation and floundering, but a hack is involved. If anyone can suggest improvements, it would be appreciated. In case this is useful here are the config snippets from the working setup. Module is kotlin 1.4.21-2 multiplatform with android, ios64, and jvm targets, with more planned. It has the KMP default setup for source directories:
The .proto file is in src/androidMain/proto subdirectory.
build.gradle.kts snippets are below. All the changes are in the android block, except for the plugin of course:
plugins {
id("com.android.library")
kotlin("multiplatform")
id("kotlinx-atomicfu")
kotlin("plugin.serialization") version Versions.kotlinVersion
id("com.google.protobuf") version "0.8.14"
}
...
kotlin {
... no changes here
}
...
android {
...
sourceSets {
...
getByName("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
java.srcDirs("src/androidMain/kotlin")
assets.srcDirs(File("src/commonMain/resources"))
withGroovyBuilder {
"proto" {
"srcDir" ("src/androidMain/proto")
}
}
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.0.0-rc-2"
}
plugins {
id("javalite") { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" }
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
id("java") {
option("lite")
}
}
task.plugins{
}
}
}
}
dependencies {
api("com.google.protobuf:protobuf-javalite:4.0.0-rc-2")
implementation("androidx.datastore:datastore:1.0.0-alpha05")
...
}
}
Note the withGroovyBuilder hack in the android sourceset - the srcdir definition is required for the plugin to find the .proto file I had, but in the current version of the plugin I couldn't figure out the correct Kotlin DSL syntax. Seems like the plugin needs to define a Kotlin extension function to make this work better.
It would be really nice if instead of requiring this stuff, that the datastore stuff could use the protobuf serialization available with kotlinx.serialization, and skip this java code generation step in gradle all together. But I'm sure that's down the road...
Anyway, thanks in advance if anyone has improvements etc...

Kotlin's cinterop .def wont work with relative paths

I have a static library that needs to link to other static libraries.
When I attempt to run ./gradlew common:cinteropSomeLibIos :
I always get IllegalStateException: Could not find 'libabseil.a' binary in neither of [external/lib]
my def file:
headers = SomeHeader.h
language = Objective-C
package = com.mypackage
staticLibraries = libabseil.a libMyLib.a
libraryPaths = external/lib
linkerOpts = -lObjC -all_load
Everything works within AndroidStudio/IntelliJ but when using the command line interface or building with bazel i consistently get the above error.
I tried adding:
val main by compilations.getting {
kotlinOptions {
freeCompilerArgs = listOf(
"-include-binary", "$libLocation/libabseil.a"
)
}
}
as well as setting linkerOpts within the gradle file but that results in:
warning: -linker-option(s)/-linkerOpts option is not supported by cinterop. Please add linker options to .def file or binary compilation instead.
Is there any way to get this working or at least to call the cinterop task in a way that the relative paths in the .def file will work?

why kotlin multiplatform don't execute and export iOS framework?

I started develop kotlin multiplatform and I developed a simple lib for test. I can exported .jar file for android but I can't export .framework file for iOS.
I reviewed other project but I didn't find my issue.
my Gradle script for lib is:
apply plugin: 'kotlin-multiplatform'
kotlin {
targets {
final def iOSTarget =
System.getenv('SDK_NAME')?.startsWith("iphoneos") \
? presets.iosArm64 : presets.iosX64
fromPreset(iOSTarget, 'iOS') {
compilations.main.outputKinds('FRAMEWORK')
}
fromPreset(presets.jvm, 'android')
}
sourceSets {
core.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib-common'
}
android.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib'
}
}
Have you added the task to build the actual framework? If not, try adding this code at the end of your build.gradle file:
task packForXCode(type: Sync) {
final File frameworkDir = new File(buildDir, "xcode-frameworks")
final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
inputs.property "mode", mode
dependsOn kotlin.targets.iOS.compilations.main.linkTaskName("FRAMEWORK", mode)
from { kotlin.targets.iOS.compilations.main.getBinary("FRAMEWORK", mode).parentFile }
into frameworkDir
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$#\n"
setExecutable(true)
}
}
}
tasks.build.dependsOn packForXCode
The iOS framework will be available on the build/xcode-frameworks directory of your library.
You'll have to configure also your Xcode project to use the framework. For further details you can read Setting up Framework Dependency in Xcode.
What do you mean by "exporting a framework"? Are you going to use it from another Gradle project or from XCode or from something else?
P.S. Sorry for asking in answers: just have no enough reputation to leave a comment. So I think it would be more convenient to discuss you problem in issues at GitHub.

How to specify IntelliJ Exclude Directories using Gradle?

Using IntelliJ to open a build.gradle file, in the "Import Project from Gradle" window, the "Excluded Roots" are pre-populated with the .gradle and build directories.
How do I specify what directories should be excluded (or not excluded) in the build.gradle file?
Specifically I am using a protocol buffer plugin that places generated sources in the /build/generated-sources/ directory. If the build directory is excluded then my source class do not see the generated classes.
Details: IntelliJ 12.1.3, Gradle 1.4
As shown in the Gradle Build Language Reference, you can configure the idea.module.excludeDirs property, which is of type List<File>. Apparently IDEA doesn't support including subdirectories of excluded directories, so you'll have to exclude all siblings of build/generated-sources. For example:
idea {
module {
excludeDirs = [file(".gradle")]
["classes", "docs", "dependency-cache", "libs", "reports", "resources", "test-results", "tmp"].each {
excludeDirs << file("$buildDir/$it")
}
}
}
If supported by the Protocol Buffer plugin, it may be easier to put the generated sources into a place outside build, and make that place known to the clean task (e.g. clean.delete "generated-sources").
Another solution. Works with Idea 13.
idea.module {
excludeDirs -= file(buildDir) //1
buildDir.listFiles({d,f->f != 'generated-sources'} as FilenameFilter).each {excludeDirs += it}} //2
Remove buildDir from excludeDirs.
Exclude each buildDir child (except generating-source).
If you are using the Gradle Kotlin DSL, use the following snippet:
idea {
module {
excludeDirs.add(file("..."))
}
}
for exclude folder in module i'm use
idea {
module.apply {
val file = file("$projectDir/node_modules")
val exclude = HashSet<File>(excludeDirs)
exclude.add(file)
excludeDirs = exclude
}
}