Kotlin-native: Is it possible to use a Kotlin/Native framework via CocoaPods from a remote GitHub repository

Created on 2 Jul 2019  路  13Comments  路  Source: JetBrains/kotlin-native

In my case I have an android project with a K/N submodule and for jvm it works fine (I shouldn't do anything, it just works). For iOS I want to use CocoaPods (kotlin.native.cocoapods plugin) to get the framework into my iOS project.

And it works fine too if the both projects are exists locally. I do something like that in my Podfile:

pod 'myKotlinNativeModule', :path => '/local/path/to/project/'

And it works, but its not a good idea to store the absolute path to another project in Podfile and I want to do like that:

pod 'myKotlinNativeModule', :git => '[email protected]:myRepo.git'

My .podspec file:

    spec.name                     = 'myModule'
    spec.version                  = '1.0'
    spec.homepage            = 'http://google.com'
    spec.source                   = { :git => '[email protected]:myRepo.git' }
    spec.authors                 = '...'
    spec.license                  = '...'
    spec.summary                  = '...'

    spec.static_framework         = true
    spec.vendored_frameworks      = "myModule/build/cocoapods/framework/#{spec.name}.framework"
    spec.libraries                = "c++"
    spec.module_name      = "#{spec.name}_umbrella"

    spec.pod_target_xcconfig = {
        'KOTLIN_TARGET[sdk=iphonesimulator*]' => 'ios_x64',
        'KOTLIN_TARGET[sdk=iphoneos*]' => 'ios_arm'
    }

    spec.script_phases = [
        {
            :name => 'Build KN',
            :execution_position => :before_compile,
            :shell_path => '/bin/sh',
            :script => <<-SCRIPT
                set -ev
                REPO_ROOT="$PODS_TARGET_SRCROOT"
                "$REPO_ROOT/gradlew" -p "$REPO_ROOT" :myModule:syncFramework \
                    -Pkotlin.native.cocoapods.target=$KOTLIN_TARGET \
                    -Pkotlin.native.cocoapods.configuration=DEBUG \
                    -Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \
                    -Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \
                    -Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"
            SCRIPT
        }
    ]

I believe CocoaPods have to download my android project from the GitHub repo, store it in a cache folder and when I gonna start to build my iOS project in Xcode, the framework will be compiled via script_phases. But in the real world this isn't happening, CocoaPods create project's folder in the cache and the folder is empty. What am i do wrong?

Is it currently possible? I use Kotlin 1.3.40

Previously I did the same with https://github.com/AlecStrong/kotlin-native-cocoapods but this lib doesn't support Kotlin 1.3.40 yet (and maybe wouldn't).

Thanks in advance.

Most helpful comment

Follow the guide https://github.com/JetBrains/kotlin-native/blob/master/COCOAPODS.md
When ./gradlew podspec will generate a Podspec file for you just change it from:

spec.script_phases = [
        {
            :name => 'Build KN',
            :execution_position => :before_compile,
            :shell_path => '/bin/sh',
            :script => <<-SCRIPT
                set -ev
                REPO_ROOT="$PODS_TARGET_SRCROOT"
                "$REPO_ROOT/gradlew" -p "$REPO_ROOT" :myModule:syncFramework \
                    -Pkotlin.native.cocoapods.target=$KOTLIN_TARGET \
                    -Pkotlin.native.cocoapods.configuration=DEBUG \
                    -Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \
                    -Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \
                    -Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"
            SCRIPT
        }
    ]

to:

spec.prepare_command = <<-SCRIPT
      set -ev
      ./gradlew --no-daemon -Pframework=#{spec.name}.framework linkReleaseFrameworkIos --stacktrace --info
    SCRIPT

make sure that your spec.vendored_frameworks looks to a right directory (where your framework is, after you build it).

Than just run pod install in your iOS project. CocoaPods runs prepare_command and build the framework, than the compiled framework will be copied from spec.vendored_frameworks to your iOS project's Pods folder and it will be available to import in Xcode.

If you have any questions feel free to ask :)

All 13 comments

Hello!
The difficulty, in this situation, is that CocoaPods by default deletes all downloaded files. Mention of this behavior is here. Also, it says that you can protect the project putting it to this parameter.
Here is another thing that is important for your case. This command can be used to call a Gradle generateDummyFramework task. It might be needed to make the framework visible to CocoaPods. You can also take a look at the usage here.

It works good with spec.prepare_command. Thank you so much!

My final Podspec file looks like:

Pod::Spec.new do |spec|
    spec.name                     = 'myModule'
    spec.version                  = '1.0'
    spec.homepage                 = '...'
    spec.source                   = { :git => "[email protected]" }
    spec.authors                  = '...'
    spec.license                  = '...'
    spec.summary                  = '...'

    spec.static_framework         = true
    spec.vendored_frameworks      = "path/to/framework/#{spec.name}.framework"
    spec.libraries                = "c++"
    spec.module_name              = "#{spec.name}_umbrella"

    spec.prepare_command = <<-SCRIPT
      set -ev
      ./gradlew --no-daemon -Pframework=#{spec.name}.framework linkReleaseFrameworkIos --stacktrace --info
    SCRIPT
end

@romanandreyvich Hi, How to do that? Can you describe the detail steps, I also want the same function thanks very much 馃槃

Follow the guide https://github.com/JetBrains/kotlin-native/blob/master/COCOAPODS.md
When ./gradlew podspec will generate a Podspec file for you just change it from:

spec.script_phases = [
        {
            :name => 'Build KN',
            :execution_position => :before_compile,
            :shell_path => '/bin/sh',
            :script => <<-SCRIPT
                set -ev
                REPO_ROOT="$PODS_TARGET_SRCROOT"
                "$REPO_ROOT/gradlew" -p "$REPO_ROOT" :myModule:syncFramework \
                    -Pkotlin.native.cocoapods.target=$KOTLIN_TARGET \
                    -Pkotlin.native.cocoapods.configuration=DEBUG \
                    -Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \
                    -Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \
                    -Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"
            SCRIPT
        }
    ]

to:

spec.prepare_command = <<-SCRIPT
      set -ev
      ./gradlew --no-daemon -Pframework=#{spec.name}.framework linkReleaseFrameworkIos --stacktrace --info
    SCRIPT

make sure that your spec.vendored_frameworks looks to a right directory (where your framework is, after you build it).

Than just run pod install in your iOS project. CocoaPods runs prepare_command and build the framework, than the compiled framework will be copied from spec.vendored_frameworks to your iOS project's Pods folder and it will be available to import in Xcode.

If you have any questions feel free to ask :)

@romanandreyvich Oh, I got it thanks, And what about the version(git tag), could you show me an example 馃槃 , I want to know how to control the version of kotlin_library pod. Also, do we need to switch linkReleaseFrameworkIos between linkDebugFrameworkIos according to the XCode building type?

This seems like a better solution:

spec.preserve_paths           = "**/*.*"

Hi, @romanandreyvich.
I want to install kotlin/native framework as you said,
pod 'myKotlinNativeModule', :git => '[email protected]:myRepo.git'

Can I ask you some question?

  1. Where shoud I put gradle file?
    I tried your solution with spec.prepare_command, but I had error that ./gradlew: No such file or directory.

  2. What are pushed in the remote repository?
    I thought I can install pods if I push myKotlinNativeModule .podspec and myModule/build/cocoapods/framework/myKotlinNativeModule.framework, but I can't.
    Please show me the tree of your remote repository.

Thank you in advance:)

Hi @romanandreyvich, thanks for the help. It works like a charm.

@robotan0921 for the beginning you need to replace your podspec at the root of the project and add a path to the framework that you build locally with a Gradle task. In my case:

path -> builds/fat-framework/release/name.framework
podspec file -> spec.vendored_frameworks = "path where the framework is stored".

Also, you need to remove *.framework, *.podspec, and the folder where you located your framework from your .gitignore file and push it to git repo.

Than open your podfile and add next:
pod 'your pod name', :git => "https://path to repo.git"

@romanandreyvich @VitaliiVasylyda How to make a fat library? What I did I basically I removed the post script from the podspec as I push to the build folder with the framework. But the issue is that this works fine when I build for iOS simulator. When I try to build on device then I get x64 architecture compilation errors

Btw do you have maybe an example how to make such setup that we have standalone GitHub repository with multi platform kotlin native framework with cocoapods support? The best if coworkers doesn't have to have java/gradle etc when the you'll like to use on iOS?

I agree with @nonameplum, I think this would be the last hurdle in adopting at many organizations. I'd love to build a framework for the iOS team without making it a hassle for them. Are there any samples to fully automate/support a fat framework and remote cocoapods?

I'm getting an error when pod lib lint'ing: building for watchOS Simulator, but linking in object file built for iOS, file shared/build/cocoapods/framework/arccosAbShared.framework/arccosAbShared' for architecture arm64

I'm running into the same roadblock myself. Creating KMM libraries for Android/JVM and pushing them to a dependency manager is super easy but I haven't found any good examples for pushing the library as a cocoapod to our internal Specs repo.

I have an alternative solution which works pretty well for us: we bundle all the KMM dependencies as one framework, this is done as part of the Xcode build using a run script build phase. Gradle takes care of retrieving all your dependencies as it would normally, so this bypasses cocoapods completely for your KMM dependencies.

Basically the procedure is as follows:

  • Create a sub directory for your aggregate framework which should contain a build.gradle, gradle.properties, which will bundle your KMM dependencies. This is completely independent from Xcode. Example build.gradle:
kotlin {
    sourceSets {
        iosMain {
            dependencies {
                api 'org.yourcompany:foo-model:1.0.0-SNAPSHOT'
                api 'org.yourcompany:foo-module:1.0.0-SNAPSHOT'
                api 'org.yourcompany:foo-foundation:1.0.0-SNAPSHOT'
            }
        }
    }

    def buildForDevice = System.getenv('SDK_NAME')?.startsWith("iphoneos")
    if (buildForDevice) {
        println("Building for device")
        iosArm64("ios") {
            binaries {
                framework()
            }
            compilations.all {
                kotlinOptions {
                }
            }
        }
    } else {
        println("Building for simulator")
        iosX64("ios") {
            binaries {
                framework()
            }
            compilations.all {
                kotlinOptions {
                }
            }
        }
    }

    kotlin.targets.ios.binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework) {
        export 'org.yourcompany:foo-model:1.0.0-SNAPSHOT'
        export 'org.yourcompany:foo-module:1.0.0-SNAPSHOT'
        export 'org.yourcompany:foo-foundation:1.0.0-SNAPSHOT'
        transitiveExport = true
        isStatic = false
    }
}
  • Now in the Xcode project, create an aggregate target with a build script phase which runs a script follows (assuming the name of the aggregate framework is 'MHKit'):
cd MHKit

BUILD_DIR="$BUILT_PRODUCTS_DIR/MHKit"

if [ "$CONFIGURATION" == "Debug" ]; then
  echo "Building debug framework"
  ./gradlew -Dorg.gradle.project.buildDir="$BUILD_DIR" $DAEMON --info --offline $REFRESH linkDebugFrameworkIos || exit 1
  cp -RP "$BUILD_DIR/bin/ios/debugFramework/MHKit.framework" "$CONFIGURATION_BUILD_DIR"
  cp -RP "$BUILD_DIR/bin/ios/debugFramework/MHKit.framework.dSYM" "$CONFIGURATION_BUILD_DIR"
else
  echo "Building release framework"
  ./gradlew -Dorg.gradle.project.buildDir="$BUILD_DIR" $DAEMON --info --offline $REFRESH clean linkReleaseFrameworkIos || exit 1
  cp -RP "$BUILD_DIR/bin/ios/releaseFramework/MHKit.framework" "$CONFIGURATION_BUILD_DIR"
fi
  • Link your framework (MHKit.framework) as you would normally do an external framework in Xcode, also add a dependency to the aggregate target so it will be built if needed.
  • In your code, just do 'import MHKit' and you will have access to all your KMM classes.

The approach above has the added benefit that it only builds the framework for the required configuration/architecture as part of the Xcode build. In you KMM projects you should only have to build and deploy the klib, not the debug/release architecture dependent frameworks which significantly reduces the build time (and I mean a lot!).
Gradle/Xcode will be smart enough to only rebuild the aggregate framework if anything has changed in the dependencies.

I think that would work for us. I was hoping to be able to use our KMM libraries in existing iOS apps via cocoapods, but that isn't necessarily a dealbreaker. For newer projects, we are using KMM so pulling in dependencies is handled via gradle anyway.

Was this page helpful?
0 / 5 - 0 ratings