https://kotlinlang.org logo
#multiplatform
Title
# multiplatform
j

Jeff Lockhart

02/05/2021, 10:12 PM
Is anyone using cocoapods dependencies in tests? I've asked about a linker error with this, but haven't received any answers. Is anyone doing this successfully?
j

Jeff Lockhart

02/06/2021, 7:48 AM
Thanks for the link. So it seems I need to provide the path to the cocoapods dependency framework, where it’s located in the iOS app Pods path I’m assuming. I still can’t get it to find it using the example linkerOpts code, getting the same linker error:
Copy code
> Task :shared:linkDebugTestIos
e: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld invocation reported errors
The /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld command returned non-zero exit code: 1.
output:
Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$_CBLMutableDocument", referenced from:
      objc-class-ref in result.o
  "_OBJC_CLASS_$_CBLMutableDictionary", referenced from:
      objc-class-ref in result.o
  "_OBJC_CLASS_$_CBLMutableArray", referenced from:
      objc-class-ref in result.o
  "_OBJC_CLASS_$_CBLBlob", referenced from:
      objc-class-ref in result.o
ld: symbol(s) not found for architecture x86_64
> Task :shared:linkDebugTestIos FAILED
The frameworksPath I’m providing is relative to the KMM shared module’s build folder to the iOS app’s Pods folder, where the CouchbaseLite.framework folder is:
../../ios/app/Pods/CouchbaseLite-Enterprise/iOS
The other code in the example gives me the error:
Copy code
Could not create task ':shared:iosTest'.
> Replacing an existing task with an incompatible type is not supported.  Use a different name for this task ('iosTest') or use a compatible type (org.gradle.api.DefaultTask)
Not sure what’s causing that, but seems I need to get past the linker error first anyway. @alex009 if you wouldn’t mind explaining some more about what the process would be to utilize cocoapods dependencies in Kotlin tests, and the steps to get it working, I’d be grateful to you.
t

Tijl

02/06/2021, 10:35 AM
as a sort of “sanity” check you can manually copy all the frameworks you use into a directory called
Frameworks
next to
test.kexe
ah but that is post linking
you can also manually add the
binary.linkerOpts("-F$frameworksPath")
for every framework
you might also need to throw in
linkerOpts("-framework", "$frameworkname}")
Copy code
val copyFrameworksForX64Test by tasks.registering(Copy::class) {
            val tree = fileTree("$buildDir/cocoapods/synthetic/iosX64/${project.name.replace('-','_')}/build/Release-iphonesimulator")

            from(tree) {
                include("**/*.framework/**")
                include("**/*.framework.DSYM/**")
                include("**/*.bundle")
            }
            eachFile {
                path = path.substring(1).substringAfter("/", path)
            }
            into ("$buildDir/bin/iosX64/debugTest/Frameworks")
        }

        val linkDebugTestIosX64 by tasks.getting {
            dependsOn(copyFrameworksForX64Test)
        }
copies this from an old gradle task I made to do the copying
Copy code
iosX64  {
        binaries {
            getTest("DEBUG").apply {
                linkerOpts("-ObjC")
                linkerOpts("-framework", "someFramework")
                linkerOpts("-F$buildDir/bin/iosX64/debugTest/Frameworks")
and from an old build file
k

Kris Wong

02/08/2021, 2:05 PM
Copy code
iosX64("ios") {
        compilations {
            "test" {
                cinterops.create("OHHTTPStubs") {
                    includeDirs("c_interop/OHHTTPStubs/Headers")
                }
            }
        }
        binaries.getTest(DEBUG).linkerOpts("-Lc_interop/OHHTTPStubs")
    }
and my .def file contains
linkerOpts = -lOHHTTPStubs -ObjC
j

Jeff Lockhart

02/09/2021, 10:15 PM
Thanks for these tips! So adding
linkerOpts("-framework", "CouchbaseLite")
got the linking working. Then
iosTest
is failing because the library isn’t loaded.
Copy code
> Task :shared:linkDebugTestIos
> Task :shared:iosTest FAILED
dyld: Library not loaded: @rpath/CouchbaseLite.framework/CouchbaseLite
  Referenced from: /.../shared/build/bin/ios/debugTest/test.kexe
  Reason: image not found
Child process terminated with signal 6: Abort trap
Do you have any tips for loading the library at test runtime?
I figured this out! The missing key was setting the
-rpath
to the frameworks path, which is used to load the library at runtime. Combining it all:
Copy code
iosX64("ios") {
    binaries {
        getTest("DEBUG").apply {
            val frameworksPath = "${buildDir.absolutePath}/cocoapods/synthetic/IOS/shared/Pods/CouchbaseLite-Enterprise/iOS"
            linkerOpts("-F$frameworksPath")
            linkerOpts("-rpath", frameworksPath)
            linkerOpts("-framework", "CouchbaseLite")
        }
    }
}
I switched the path to the
cocoapods/synthetic/…
path, since that’s contained within the shared module build path and doesn’t depend on my iOS app at all. If I was using more than one framework, it’d be a bit more complicated, since the frameworks would need to be copied into the same directory. But this could be done in combination with setting the
-rpath
to that directory as part of the build process. Thanks again for your help!
t

Tijl

02/10/2021, 8:15 AM
yeah that might have been fixed by copying the frameworks (as per the task I shared), but
-rpath
is a way better method! reading the docs (of
ld
) it seems multiple
-rpath
arguments should work too.
k

Kris Wong

02/10/2021, 1:52 PM
i've never had to set the rpath or manually copy any framework. strange
t

Tijl

02/10/2021, 1:59 PM
I only have this with cocoapods dependencies, never with e.g carthage. I’m very puzzled as to why.
Anyone know if there is a youtrack for this yet btw?
k

Kris Wong

02/10/2021, 2:02 PM
ok, I am not using cocoapods dependencies in my KMM project
but I am using cinterop
j

Jeff Lockhart

02/10/2021, 10:46 PM
That might be the difference @Kris Wong. Looks like your cinterop is compiled as a library, linked with
-l
, which is slightly different than the framework format from cocoapods, linked with
-framework
. The default
rpath
expects the frameworks to be in a folder called
Frameworks
in the same directory as the
test.kexe
binary
build/bin/ios/debugTest
. Now I see that’s where your code is copying the frameworks. So yeah, setting the
-rpath
option allows you to reference the frameworks from the original location without needing to copy. Since
-rpath
can be provided multiple times, then seems the cocoapods plugin should be able to append these combination of
-F
,
-rpath
, and
-framework
linker opts to the test binary for each of the cocoapods dependencies added. I haven’t found a YouTrack issue for this. So I created one.
k

Kris Wong

02/11/2021, 1:56 PM
yeah I've got libOHTTPStubs.a within the framework, so it just made sense to link it that way. but that also explains why I didn't have your issue - static linking. so that could be another option for you.
i honestly don't even remember where I got this framework originally 😛
😄 1
b

Ben Deming

02/12/2021, 12:14 AM
Thanks for the great workaround for Cocoapods users! In case anyone is using pods w/vendored frameworks in addition to pods compiled from source, or pods whose name is different from their module name, here's a slight extension to the workaround described above:
Copy code
binaries {
            getTest("DEBUG").apply {
                // Map of pod names to their `module_name`s
                val regularPodToModuleNameMap = mapOf(
                    "CocoaLumberjack" to null,
                    "Split" to null,
                    "DatadogSDKObjc" to "DatadogObjc",
                    "DatadogSDK" to "Datadog"
                )

                // Map of vendored pod names to their `module_name`s (maybe redundant, they might need to be the same?)
                val vendoredPodToModuleNameMap = mapOf("TwilioChatClient" to null)

                // common is the name of our project, substitute accordingly
                val commonSyntheticPath = "${buildDir.absolutePath}/cocoapods/synthetic/IOS/common"
                val regularFrameworksPath = "$commonSyntheticPath/build/Release-iphonesimulator"
                val vendoredFrameworkPath = "$commonSyntheticPath/Pods"

                regularPodToModuleNameMap.forEach { entry ->
                    val podDirectoryName = entry.key
                    val moduleName = entry.value ?: entry.key

                    val frameworkPath = "$regularFrameworksPath/$podDirectoryName"
                    linkerOpts("-framework", moduleName)
                    linkerOpts("-F$frameworkPath")
                    linkerOpts("-rpath", frameworkPath)
                }

                vendoredPodToModuleNameMap.forEach { entry ->
                    val podDirectoryName = entry.key
                    val moduleName = entry.value ?: entry.key

                    val frameworkPath = "$vendoredFrameworkPath/$podDirectoryName"
                    linkerOpts("-framework", moduleName)
                    linkerOpts("-F$frameworkPath")
                    linkerOpts("-rpath", frameworkPath)
                }

                // Don't forget framework flags for system frameworks any pods depend on
                linkerOpts("-framework", "UIKit")
            }
        }
Could use some DRYing up but may be helpful nevertheless.
👍 1
t

Tijl

02/12/2021, 3:04 PM
🙌 1
👍 1
20 Views