JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.02k stars 566 forks source link

Task :BcLib:linkBC_libDebugFrameworkIos ld: file not found: UIKit #3827

Closed ArthurBrum closed 4 years ago

ArthurBrum commented 4 years ago

Please someone help me. I cant find good documentation for what I'm trying to do, and I really have to do this at my job. Summing up the problem: I'm trying to build a multiplatform library to be used for android and iOS applications. But the hard part is that I have to consume another library. On the ios side I'm trying to import the framework using cinterop, but its proving to be a very difficult task.

Let me show the current status of things:

Main project build.gradle

buildscript {
    ext.kotlin_version = '1.3.61'
    ext.ktor_version = '1.1.2'
    ext.ktor_json_version = '1.0.1'
    ext.kotlinx_coroutines_version = '1.1.1'
    ext.serialization_version = '0.10.0'

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            // --- repository for android lib, no problem here
        }
    }
}

task clean(type: Delete) {
    setDelete rootProject.buildDir
}

KMP project build.gradle

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.kotlin.multiplatform'

group = 'com.fidential.project'
version = '0.0.1'

android {
    compileSdkVersion 27
    defaultConfig {
        minSdkVersion 19
    }
    buildTypes {
        release {
            minifyEnabled false
        }
    }
}

dependencies {
    // Specify Kotlin/JVM stdlib dependency.
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7'

    testImplementation 'junit:junit:4.12'
    testImplementation 'org.jetbrains.kotlin:kotlin-test'
    testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'

    androidTestImplementation 'junit:junit:4.12'
    androidTestImplementation 'org.jetbrains.kotlin:kotlin-test'
    androidTestImplementation 'org.jetbrains.kotlin:kotlin-test-junit'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

kotlin {
    targets {
        fromPreset(presets.android, 'android')

        def buildForDevice = project.findProperty("device")?.toBoolean() ?: false
        def iosPreset = (buildForDevice) ? presets.iosArm64 : presets.iosX64
        def iosTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') \
                            ? presets.iosArm64 : presets.iosX64

        fromPreset(iosPreset, 'ios') {
            binaries {
                framework {

                    linkerOpts "-Fsrc/nativeInterop/frameworks"

                    // Disable bitcode embedding for the simulator build.
                    if (!buildForDevice) {
                        embedBitcode("disable")
                    }
                }
            }
            compilations.main {
                cinterops {
                    External_lib {
                        // Package to place the Kotlin API generated
                        packageName 'com.fidential.project''

                        // Options to be passed to compiler by cinterop tool
                        compilerOpts '-Isrc/nativeInterop/frameworks/External_lib.framework/Headers'

                    }
                }
            }
        }

       // Duplicated because I dont really know the difference, but wouldn't work without below code
        fromPreset(iosTarget, 'ios') {
            binaries {
                framework('BC_lib'){
                    linkerOpts "-Fsrc/nativeInterop/frameworks"
                }
            }
            compilations.main {
                cinterops {
                    External_lib {
                        // Package to place the Kotlin API generated
                        packageName 'com.fidential.project'

                        // Options to be passed to compiler by cinterop tool
                        compilerOpts '-Isrc/nativeInterop/frameworks/External_lib.framework/Headers'

                    }
                }
            }
        }
    }

    sourceSets {
        commonMain {
            dependencies {
            }
        }

        androidMain {
            dependencies {
                // External_lib for android
                api 'com.fidential:androidApi:2.0.2.3'
            }
        }

        iosMain {
            dependencies {
            }
        }
    }
}

configurations {
    compileClasspath
}

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
    final def framework = kotlin.targets.ios.binaries.getFramework("BC_lib", mode)

    inputs.property "mode", mode
    dependsOn framework.linkTask

    from { framework.outputFile.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

external_lib.def

language = Objective-C
package = com.fidential.project
headers = External_lib.h
libraryPaths = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks
linkerOpts = -framework External_lib UIKit

// Without the UIKit, I get       "unresolved symbols" for bunch of things like $_UIDevice, etc..
// And with it, it just says       "ld: file not found: UIKit"

As it should be clear by now, I'm no good at using gradle, so I'm desperately asking for help. I have tried several solutions from other issues of this github, but none solved or made sense in my case, since I'm a bit loss with my build.gradle file.

I tried this: https://github.com/JetBrains/kotlin-native/issues/3299#issuecomment-527024708 But got: Key testDebugExecutable is missing in the map.

Following this: https://medium.com/androidiots/the-magic-of-kotlin-native-part-2-49097c2dea1a I got to add the "libraryPaths" part of the .def file, but now sure if it was supposed to be there or in the gradle file.. or even if it should be in the -F parameter used in the gradle.build file...

Also, I'll have to use several frameworks from Apple, not just UIKit one, so if someone could put me in the right direction to not only link them, but also export them in my generated framework, I would be really grateful.

ilmat192 commented 4 years ago

Hello!

But the hard part is that I have to consume another library.

Just for more context: is it an ObjC or a Kotlin/Native library?

As for the issue itself, please add -framework before UIKit in your def-file:

linkerOpts = -framework External_lib -framework UIKit
ilmat192 commented 4 years ago

I tried this:

3299 (comment)

But got: Key testDebugExecutable is missing in the map.

That's because you tried to change settings of a test executable, not a framework. To change settings of a framework, you need to write something like this:

binaries.getByName("debugFramework").linkerOpts("-framework UIKit")
binaries.getByName("releaseFramework").linkerOpts("-framework UIKit")

But you don't have to do this if you add linker opts to your def-file. See more about Kotlin/Native binaries in this doc.

ArthurBrum commented 4 years ago

Yeess!! it worked! Thanks a lot @ilmat192 ! One last thing: I'm trying to remove the duplicated code by doing:

kotlin {
    targets {
        ...
        fromPreset(presets.android, 'android')

        def buildForDevice = project.findProperty("device")?.toBoolean() ?: false
        def iosPreset = (buildForDevice) ? presets.iosArm64 : presets.iosX64
        def iosTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') \
                            ? presets.iosArm64 : presets.iosX64
        fromPreset(iosPreset, 'ios')
        fromPreset(iosTarget, 'ios')

        ios {
            binaries {
                framework('BC_lib'){
                    linkerOpts "-Fsrc/nativeInterop/frameworks"
                }
            }
            compilations.main {
                cinterops {
                ...
                }
            }
            ...
      }
}

It built successfully, but now I'm not sure if I'm overwriting the two targets... I understand one of them is for the iOS device and the other one is for the simulator, but what happens when iosPreset and iosTarget gets the same name 'ios' ? Is there a better way of targeting both arm64 and iosX64 and applying these same compilations to both of them? Im not quite sure of these formPreset calls would do in different situations =/

ilmat192 commented 4 years ago

@ArthurBrum Well, the plugin allows you to run fromPreset several time with the same arguments. It does so because the fromPreset method has a full version allowing one to configure a target. E.g.:

fromPreset(presets.iosX64, "ios") {
    // Configuration goes here.
}

So the plugin allows configuring the same target several times.

But if you call fromPreset twice with different presets and the same name, you'll get an error because a target name must be unique. Since your build runs successfully, most probably in your case iosPreset just equals to iosTarget.

You do can create both device and simulator targets but it may be inconvenient due to IDE limitations. You said that you write a Kotlin library. Do you plan to publish this library to Maven and then depend on it in another Kotlin/Native project? Or just build a framework from this library and include it into an iOS app?

ArthurBrum commented 4 years ago

Second option, just build a framework to be included into iOS apps. But I just notice I get architecture errors when trying to execute on real device (running on simulator goes ok)

ilmat192 commented 4 years ago

Knowing your use case is important because currently IDE support has an important limitation: navigation/completion for iOS SDKs works only for leaf source sets (see this section to learn more about project structure). In other words, if you have both device and simulator targets in you project and share some code between them using a shared source set, you'll get no IDE support for iOS SDKs in this shared code.

There are some workarounds for this issue. In your case you don't need to always create both device and simulator targets. Since Gradle build is often started from Xcode, you can infer if this build is for a device or a simulator and create an appropriate target. This part of your build script does exactly this thing:

def iosTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') ? presets.iosArm64 : presets.iosX64
fromPreset(iosPreset, 'ios') { ... }

It uses the environment variable SDK_NAME set by Xcode to infer a platform we need to build for and create a target for this platform. If this environment variable isn't set (e.g. if we run Gradle from terminal), it creates a target for a simulator. Then you put your iOS code into leaf source sets (iosMain and iosTest), so you get the IDE support for it.

The downside of such an approach is that Gradle will recompile the framework every time you switch from device to simulator and vice versa.

ArthurBrum commented 4 years ago

Hey @ilmat192! Sorry to bother, but could you take a look at: https://github.com/JetBrains/kotlin-native/issues/3851#issuecomment-586638966 I thinks it is related, and since you know better about cinterop... I thought about asking you, just in case

artdfel commented 4 years ago

I'm closing this one as solved. Feel free to reopen if something left here.