Search code examples
androidioskotlingradlekotlin-multiplatform

How to add two or more kotlin native modules on an iOS project


TL;DR;

How to add two or more kotlin native modules on an iOS project without getting duplicate symbols error?

The detailed question

Let's assume a multi-module KMP project as a follow where there exists a native app for Android and a native app for iOS and two common modules to hold shared kotlin code.

.
├── android
│   └── app
├── common
│   ├── moduleA
│   └── moduleB
├── ios
│   └── app

The module A contains a data class HelloWorld and has no module dependencies:

package hello.world.modulea

data class HelloWorld(
    val message: String
)

Module B contains an extension function for HelloWorld class so it depends on module A:

package hello.world.moduleb

import hello.world.modulea.HelloWorld

fun HelloWorld.egassem() = message.reversed()

The build.gradle configuration of the modules are:

  • Module A
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"

…

kotlin {
    targets {
        jvm("android")

        def iosClosure = {
            binaries {
                framework("moduleA")
            }
        }
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
    }

    cocoapods {…}

    sourceSets {
        commonMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
        }
        androidMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
        }
        iosMain.dependencies {
        }
    }
}
  • Module B
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…

kotlin {
    targets {
        jvm("android")

        def iosClosure = {
            binaries {
                framework("moduleB")
            }
        }
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
    }

    cocoapods {…}

    sourceSets {
        commonMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
            implementation project(":common:moduleA")
        }
        androidMain.dependencies {
            implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
        }
        iosMain.dependencies {
        }
    }
}

It looks pretty straightforward and it even works on android if I configure the android build gradle dependencies as a following:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72"
    implementation project(":common:moduleA")
    implementation project(":common:moduleB")
}

However, this does not seem to be the correct way to organize multi modules on iOS, because running the ./gradlew podspec I get a BUILD SUCCESSFUL as expected with the following pods:

pod 'moduleA', :path => '…/HelloWorld/common/moduleA'
pod 'moduleB', :path => '…/HelloWorld/common/moduleB'

Even running a pod install I get a success output Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed. whats looks correctly once the Xcode shows the module A and module B on the Pods section.

However, if I try to build the iOS project I get the following error:

Ld …/Hello_World-…/Build/Products/Debug-iphonesimulator/Hello\ World.app/Hello\ World normal x86_64 (in target 'Hello World' from project 'Hello World')
    cd …/HelloWorld/ios/app
…
duplicate symbol '_ktypew:kotlin.Any' in:
    …/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
    …/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
… a lot of duplicate symbol more …
duplicate symbol '_kfun:kotlin.throwOnFailure$stdlib@kotlin.Result<#STAR>.()' in:
    …/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
    …/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
ld: 9928 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

My knowledge in iOS is not that much, so to my untrained eyes, it looks like each module is adding its own version of the things instead of using some resolutions strategy to share it.

If I use only the module A the code works and run as expected, so I know the code itself is correct, the problem is how to manage more than 1 module, so that the question, how to add both (module A and module B) on iOS and make things works?

P.S

I did reduce the code as much as I could, trying to keep only the parts that I guess is the source of the problem, however, the complete code is available here if you want to check anything missing in the snippets, or if you want to run and try to solve the problem…


Solution

  • Multiple Kotlin frameworks can be tricky, but should be working as of 1.3.70 which I see you have.

    The issue seems to be that both frameworks are static, which is currently an issue in 1.3.70 so it isn't working. (This should be updated by 1.40). It looks like by default the cocoapods plugin sets the frameworks to be static which won't work. I'm unaware of a way to change cocoapods to set it as dynamic but I've tested building without cocoapods and using the isStatic variable in a gradle task, and have gotten an iOS project to compile. Something like:

    binaries {
        framework("moduleA"){
            isStatic = false
        }
    }
    

    For now you can work around the issue using this method by using the code above and creating a task to build the frameworks(here's an example)

    Another thing worth noting is that on the iOS side, the HelloWorld classes will appear as two separate classes despite both coming from moduleA. It's another strange situation with multiple Kotlin frameworks, but I think the extension will still work in this case since you're returning a string.

    I actually just wrote up a blog post about multiple Kotlin frameworks that may help with some other questions if you'd like to take a look. https://touchlab.co/multiple-kotlin-frameworks-in-application/

    EDIT: Looks like cocoapodsext also has an isStatic variable, so set it to isStatic = false

    tl:dr You currently can't have more than one static Kotlin frameworks in the same iOS project. Set them to not be static using isStatic = false.