JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.03k stars 568 forks source link

Is it possible to use a Kotlin/Native framework via CocoaPods from a remote GitHub repository #3140

Closed romanandreyvich closed 5 years ago

romanandreyvich commented 5 years ago

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 => 'git@github.com:myRepo.git'

My .podspec file:

    spec.name                     = 'myModule'
    spec.version                  = '1.0'
    spec.homepage            = 'http://google.com'
    spec.source                   = { :git => 'git@github.com: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.

artdfel commented 5 years ago

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.

romanandreyvich commented 5 years ago

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 => "git@myModuleRepo.git" }
    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
wuseal commented 5 years ago

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

romanandreyvich commented 5 years ago

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 :)

wuseal commented 5 years ago

@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?

werner77 commented 4 years ago

This seems like a better solution:

spec.preserve_paths           = "**/*.*"
robotan0921 commented 4 years ago

Hi, @romanandreyvich. I want to install kotlin/native framework as you said, pod 'myKotlinNativeModule', :git => 'git@github.com: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:)

VitaliiVasylyda commented 3 years ago

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"

nonameplum commented 3 years ago

@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?

kevinskrei commented 3 years ago

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

etwilliams commented 3 years ago

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.

werner77 commented 3 years ago

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:

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
    }
}
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

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.

etwilliams commented 3 years ago

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.