Skip to content

Commit

Permalink
Exporting and beyond (#24)
Browse files Browse the repository at this point in the history
Better support for importing your own code and third dependencies to your Kotlin
Better performance and Caching
A full working example for using the plugin with iOS
  • Loading branch information
frankois944 authored Jan 10, 2025
1 parent 199d1ee commit cf57c26
Show file tree
Hide file tree
Showing 38 changed files with 1,326 additions and 353 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout latest code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@v4 # v4
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4

9 changes: 7 additions & 2 deletions .github/workflows/pre-merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ jobs:
uses: actions/cache@v4
with:
path: |
example/build/spmKmpPlugin/input/scratch
example/build/spmKmpPlugin/input/Package.resolved
example/build/spmKmpPlugin/scratch
example/build/spmKmpPlugin/Package.resolved
example/iosApp/build
example/iosApp/spm
key: ${{ runner.os }}-example-scratch
- name: Cache Gradle Caches
uses: gradle/actions/setup-gradle@v4
Expand All @@ -65,6 +67,9 @@ jobs:
with:
report_paths: '**/build/test-results/**/TEST-*.xml'
include_passed: true
- name: build iosExampleApp
run: xcodebuild -project iosApp.xcodeproj -scheme iosApp -configuration Debug -destination 'generic/platform=iOS Simulator' ARCHS=arm64 -derivedDataPath "./build" -clonedSourcePackagesDirPath "./spm" build
working-directory: "example/iosApp"
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
Expand Down
10 changes: 1 addition & 9 deletions .github/workflows/publish-plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
steps:
- name: Checkout Repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@v4 # v4
- name: Cache Gradle Caches
uses: gradle/actions/setup-gradle@v4
- name: Cache Konan
Expand All @@ -32,14 +32,6 @@ jobs:
with:
path: /Users/runner/work/spm4Kmp/spm4Kmp/example/build/spmKmpPlugin/input/scratch
key: ${{ runner.os }}-example-scratch
- name: Run Gradle tasks
run: ./gradlew preMerge --continue
- name: Archive test results
uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: test-pre-merge-report
path: plugin-build/plugin/build/reports/
- name: Publish on Plugin Portal
run: ./gradlew :plugin-build:plugin:setupPluginUploadFromEnvironment :plugin-build:plugin:publishPlugins
if: success()
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@
build
plugin-build/plugin/src/functionalTest/resources/LocalDummyFramework/.build
.kotlin
example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata
example/iosApp/iosApp.xcodeproj/xcuserdata
example/exportedNativeShared
example/iosApp/spm
plugin-build/plugin/src/functionalTest/resources/LocalSourceDummyFramework/.build
154 changes: 57 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,27 @@
[![GitHub Release](https://img.shields.io/github/v/release/frankois944/spm4Kmp)](https://github.com/frankois944/spm4Kmp/releases/)
[![build&tests](https://github.com/frankois944/spm4Kmp/actions/workflows/pre-merge.yaml/badge.svg)](https://github.com/frankois944/spm4Kmp/actions/workflows/pre-merge.yaml)
[![codecov](https://codecov.io/gh/frankois944/spm4Kmp/graph/badge.svg?token=OXEHFLQG1I)](https://codecov.io/gh/frankois944/spm4Kmp)
![GitHub License](https://img.shields.io/github/license/frankois944/spm4kmp)
[![GitHub License](https://img.shields.io/github/license/frankois944/spm4kmp)](https://github.com/frankois944/spm4Kmp/blob/main/LICENSE)

The Swift Package Manager for Kotlin multiplatform Plugin aka `spmForKmp` gradle plugin is a Gradle plugin designed to simplify integrating Swift Package Manager (SPM) dependencies into Kotlin Multiplatform (KMP) projects. It allows you to effortlessly configure and use Swift packages in your Kotlin projects targeting Apple platforms, such as iOS.
The Swift Package Manager for Kotlin multiplatform Plugin aka `spmForKmp` gradle plugin is a Gradle plugin designed to simplify integrating Swift Package Manager (SPM) dependencies into Kotlin Multiplatform (KMP) projects. It allows you to (almost) effortlessly configure and use Swift packages in your Kotlin projects targeting Apple platforms, such as iOS.

---

## Feedback

This project greatly needs feedback and get information about the edge case for progressing; the discussion tab is welcomed.
This project greatly needs feedback and information about the edge case for progressing; the discussion tab is welcomed.

## Features

- **Support for SPM Dependencies**: Seamlessly add remote SPM dependencies to your KMP modules.
- **KMP Compatibility**: Configure Swift packages for iOS and other Apple targets.
- **Export Dependencies to Kotlin**: Enable specific SPM dependencies to be exposed directly in your Kotlin code (if compatible).
- **Create a bridge easily**: Import your own Swift code for functionality can't be done in Kotlin for example.
- **Import Swift compatible code to Kotlin**: Enable **SPM dependencies** and your **own Swift code** to be exposed directly in your Kotlin code (if compatible).
- **Automatic CInterop Configuration**: Simplify the process of creating native CInterop definitions for your Swift packages with dependencies.

---

## Prerequisites

Before using the `spmForKmp` plugin, ensure the following:

1. Your project is set up as a Kotlin Multiplatform (KMP) project.
2. You are using the Gradle build system.
3. You have added Apple targets (e.g., `iosArm64`, `iosSimulatorArm64`, etc.) to your KMP project.

---

## Getting Started

Follow these steps to add and configure the `spmForKmp` plugin in your project:
A fully working sample is [available](https://github.com/frankois944/spm4Kmp/tree/main/example) as a playground.

### 1. Apply the Plugin

Expand All @@ -44,7 +33,7 @@ Add the plugin to your `build.gradle.kts` or the appropriate Gradle module’s `
```kotlin
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("io.github.frankois944.spmForKmp").version("0.0.3") // Apply the spmForKmp plugin
id("io.github.frankois944.spmForKmp").version("0.0.5") // Apply the spmForKmp plugin
}
```

Expand All @@ -58,8 +47,6 @@ kotlin.mpp.enableCInteropCommonization=true

### 2. Configure Kotlin Multiplatform Targets

Make sure you define your Kotlin Multiplatform targets for Apple. Here is an example configuration:

```kotlin
kotlin {
listOf(
Expand All @@ -77,107 +64,59 @@ kotlin {

swiftPackageConfig {
create("nativeExample") { // same name as the one in `cinterops.create("...")`
}
}
```

---

### 3. Configure Swift Package Dependencies Plugin

To use Swift Package Manager (SPM) dependencies in your project, define them in your `swiftPackageConfig` block.

You have many options to configure the plugin, some examples hereafter.

### 3.1 With no external dependencies

The following example creates a `nativeExample` Kotlin module with the content of the `src/swift` folder, it's the default behavior.

The content of `src/swift` is optional and will be replaced with a dummy Swift class, so you can only declare the dependencies.

```kotlin
swiftPackageConfig {
create("nativeExample") { // same name as the one in `cinterops.create("...")`
// optional content
}
}
```

### 3.2 With external dependencies

The following example creates a `nativeExample` Kotlin module with the content of the `src/swift` folder and the declared dependencies.

- `CryptoSwift` is a Swift package that will be used in the Swift code.
- `firebase-ios-sdk` is a ObjC compatible package that can be used in the Swift code and in the Kotlin code.

The `exportToKotlin` parameter is used to export the compatible code of the package to Kotlin for use in shared Kotlin code.

By default, the package is not exported to Kotlin.

```kotlin
swiftPackageConfig {
create("nativeExample") { // same name as the one in `cinterops.create("...")`
customPackageSourcePath = "src/nativeExample" // (Optional) Custom path for your own Swift source files
// add your own swift code and/or your external dependencies, it's optional
dependency(
// available only in the Swift code
// available only in your own Swift code
SwiftDependency.Package.Remote.Version(
url = "https://github.com/krzyzanowskim/CryptoSwift.git", // Repository URL
names = listOf("CryptoSwift"), // Library names
version = "1.8.4", // Package version
url = "https://github.com/krzyzanowskim/CryptoSwift.git", // Repository URL
names = listOf("CryptoSwift"), // Library names
version = "1.8.4", // Package version
)
)
dependency(
// available in the Swift and kolin code
// available in the Swift and Kolin code
SwiftDependency.Package.Remote.Version(
url = "https://github.com/firebase/firebase-ios-sdk.git", // Repository URL
names = listOf("FirebaseAnalytics", "FirebaseCore"), // Libraries from the package
packageName = "firebase-ios-sdk", // (Optional) Package name, can be required in some cases
version = "11.6.0", // Package version
exportToKotlin = true // Export to Kotlin for use in shared Kotlin code
exportToKotlin = true // Export to Kotlin for usage in shared Kotlin code
)
)
// and more Swift packages...
}
}
```

### 3.3 Supported Swift Package Types
### 2.1. Supported Swift Package Dependency Types

The plugin supports the following configurations :
- **Remote Package**: A Swift package hosted on a remote repository using version, commit, or branch.
- **Local Package**: A Swift package located in a local directory.
- **Local Binary Package**: A xcFramework in a local directory.
- **Remote Binary Package**: A xcFramework hosted on a remote repository.
- **Local Binary xcFramework**: A xcFramework in a local directory.
- **Remote Binary xcFramework**: A xcFramework hosted on a remote repository.

For more information, refer to the [SwiftDependency](https://github.com/frankois944/spm4Kmp/blob/main/plugin-build/plugin/src/main/java/io/github/frankois944/spmForKmp/definition/SwiftDependency.kt) file.

---

### 4. Add your own Swift code
### 3. Add your own Swift code

You can now add your own Swift code in the `src/swift` folder.

```swift
import Foundation
// inside the folder src/swift
// everything will be automatically accessible from your Kotlin code
@objcMembers public class MySwiftDummyClass: NSObject {
func mySwiftDummyFunction() -> String {
return "Hello from Swift!"
}
}
```

```kotlin
package com.example
import nativeExample.MySwiftDummyClass
@kotlinx.cinterop.ExperimentalForeignApi
val dummyClass = MySwiftDummyClass()
```
> [!IMPORTANT]
> Your swift code need to be mark as [@objc/@objcMembers](https://akdebuging.com/posts/what-is-objc-and-objcmember/) and the visibility set as `public`
> or it won't be exported and available from your Kotlin code
> ```swift
> @objcMembers public class MyOwnSwiftCode: NSObject {
> public func exportedMethod() -> String {
> return "value"
> }
> }
> ```
### 5. Add your own Swift code and uses the dependencies
### 3.1. With external dependencies
You can also use the Swift packages you have added in the `swiftPackageConfig` block in your Swift code.
You can also use the dependency you have added in the `swiftPackageConfig` block in your Swift code.
For example, `CryptoSwift` is not a library that can be used directly in Kotlin code, but you can create a bridge in your Swift code.
Expand Down Expand Up @@ -207,16 +146,37 @@ import CryptoSwift
}
```

### 3.2. Export your dependency directly to your Kotlin Code

You can also use the dependency you have added in the `swiftPackageConfig` in your Kotlin and Swift applications.

> [!WARNING]
> This feature is highly experimental
```kotlin
package com.example
import dummy.MySwiftClass
swiftPackageConfig {
create("dummy") {
dependency(
SwiftDependency.Binary.Local(
path = "path/to/DummyFramework.xcframework.zip",
packageName = "DummyFramework",
exportToKotlin = true, // by default false
),
)
}
}
```

---

Please take a look at the functional tests in the [plugin-test](https://github.com/frankois944/spm4Kmp/tree/main/plugin-build/plugin/src/functionalTest/kotlin/io/github/frankois944/spmForKmp) folder for more examples.
> [!IMPORTANT]
> When exporting dependency, some configuration need to be added to your xcode project.
>
> A local swift package is being generated during the build and this message diplayed
> ```
> Spm4Kmp: A local Swift package has been generated in /path/to/the/local/package
> Please add it to your project as a local package dependency.
> ```
> Add the folder to your project as a Local package, that's all.
The `example` module of this repository is a playground where you can try to use the plugin
---
Expand Down
33 changes: 31 additions & 2 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ kotlin {
}
}

val copyTestResources =
tasks.register<Copy>("copyTestResources") {
from(
"${layout.projectDirectory.asFile.path}/../plugin-build/plugin/src/functionalTest/resources" +
"/DummyFramework.xcframework/ios-arm64_x86_64-simulator/",
) {
include("*.framework/**")
}
into("${layout.projectDirectory.asFile.path}/build/bin/iosSimulatorArm64/debugTest/Frameworks/")
}

tasks.named("iosSimulatorArm64Test") {
dependsOn(copyTestResources)
}

android {
namespace = "com.example"
compileSdk = 34
Expand All @@ -53,7 +68,7 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
}

val testRessources = "${layout.projectDirectory.asFile.path}/../plugin-build/plugin/src/functionalTest/resources"
swiftPackageConfig {
create("nativeShared") {
// optional parameters
Expand All @@ -74,14 +89,28 @@ swiftPackageConfig {
// Repository URL
url = "https://github.com/firebase/firebase-ios-sdk.git",
// Libraries from the package
names = listOf("FirebaseAnalytics", "FirebaseCore"),
names =
listOf(
"FirebaseCore",
"FirebaseAnalytics",
),
// (Optional) Package name, can be required in some cases
packageName = "firebase-ios-sdk",
// Package version
version = "11.6.0",
// Export to Kotlin for use in shared Kotlin code, false by default
exportToKotlin = true,
),
SwiftDependency.Binary.Local(
path = "$testRessources/DummyFramework.xcframework.zip",
packageName = "DummyFramework",
exportToKotlin = true,
),
SwiftDependency.Package.Local(
path = "$testRessources/LocalSourceDummyFramework",
packageName = "LocalSourceDummyFramework",
exportToKotlin = true,
),
SwiftDependency.Package.Remote.Version(
url = "https://github.com/krzyzanowskim/CryptoSwift.git",
names = listOf("CryptoSwift"),
Expand Down
30 changes: 30 additions & 0 deletions example/iosApp/GoogleService-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyCfrkiVxg1CHk8FwdMIOP8dWcWa4BMR7YQ</string>
<key>GCM_SENDER_ID</key>
<string>986130107097</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>fr.frankois944.kmp.smp4kmp.example</string>
<key>PROJECT_ID</key>
<string>fir-kmpdemo-6cde4</string>
<key>STORAGE_BUCKET</key>
<string>fir-kmpdemo-6cde4.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false/>
<key>IS_ANALYTICS_ENABLED</key>
<false/>
<key>IS_APPINVITE_ENABLED</key>
<true/>
<key>IS_GCM_ENABLED</key>
<true/>
<key>IS_SIGNIN_ENABLED</key>
<true/>
<key>GOOGLE_APP_ID</key>
<string>1:986130107097:ios:bc74692aa1cda1762fccc8</string>
</dict>
</plist>
Loading

0 comments on commit cf57c26

Please sign in to comment.