refactor(page): 删除旧版页面文件
This commit is contained in:
523
.agents/skills/kotlin-tooling-cocoapods-spm-migration/SKILL.md
Normal file
523
.agents/skills/kotlin-tooling-cocoapods-spm-migration/SKILL.md
Normal file
@@ -0,0 +1,523 @@
|
||||
---
|
||||
name: kotlin-tooling-cocoapods-spm-migration
|
||||
description: Migrate KMP projects from CocoaPods (kotlin("native.cocoapods")) to Swift Package Manager (swiftPMDependencies DSL) — replaces pod() with swiftPackage(), transforms cocoapods.* imports to swiftPMImport.*, and reconfigures the Xcode project.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: JetBrains
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# CocoaPods to SwiftPM Migration for KMP
|
||||
|
||||
Migrate Kotlin Multiplatform projects from `kotlin("native.cocoapods")` to `swiftPMDependencies {}` DSL.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Kotlin**: 2.4.0-Beta2 or later (first public release with `swiftPMDependencies` support, available on Maven Central)
|
||||
- **Xcode**: 16.4 or 26.0+
|
||||
- **iOS Deployment Target**: 16.0+ recommended
|
||||
|
||||
## Migration Overview
|
||||
|
||||
**IMPORTANT**: Keep the `cocoapods {}` block and plugin active until Phase 6. The migration adds `swiftPMDependencies {}` alongside the existing CocoaPods setup first, reconfigures Xcode, and only then removes CocoaPods.
|
||||
|
||||
| Phase | Action |
|
||||
|-------|--------|
|
||||
| 1 | Analyze existing CocoaPods configuration |
|
||||
| 2 | Update Gradle configuration (repos, Kotlin version) |
|
||||
| 3 | Add `swiftPMDependencies {}` alongside existing `cocoapods {}` |
|
||||
| 4 | Transform Kotlin imports |
|
||||
| 5 | Reconfigure iOS project and deintegrate CocoaPods |
|
||||
| 6 | Remove CocoaPods plugin from Gradle |
|
||||
| 7 | Verify Gradle build and Xcode project build |
|
||||
| 8 | Write MIGRATION_REPORT.md |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Pre-Migration Analysis
|
||||
|
||||
### 1.0 Verify the project builds
|
||||
|
||||
Before starting migration, identify the module to migrate and confirm it compiles successfully.
|
||||
|
||||
1. **Find the module that uses CocoaPods** — look for `build.gradle.kts` files containing `cocoapods`:
|
||||
```bash
|
||||
grep -rl "cocoapods" --include="build.gradle.kts" .
|
||||
```
|
||||
Extract the module name from the path (e.g., `./shared/build.gradle.kts` → module name is `shared`). Note: multiple modules may use CocoaPods — record all of them. Typically only the module that produces the framework linked into the iOS app needs `swiftPMDependencies`; the others only need CocoaPods removed (Phase 6).
|
||||
|
||||
2. **Compile Kotlin code** — run the Kotlin compilation task for that module to verify the Kotlin source compiles:
|
||||
```bash
|
||||
./gradlew :moduleName:compileKotlinIosSimulatorArm64
|
||||
```
|
||||
Replace `moduleName` with the directory name of the module (e.g., `:shared:compileKotlinIosSimulatorArm64`). This is faster than a full `build` (which also runs release linkage) and sufficient to verify Kotlin code correctness.
|
||||
|
||||
3. **Build the iOS app (optional)** — try to locate the Xcode project and build it to confirm the full app compiles:
|
||||
```bash
|
||||
# Find the Xcode project
|
||||
find . -name "*.xcworkspace" -not -path "*/Pods/*" -maxdepth 2
|
||||
# Build (replace scheme name with the actual app scheme)
|
||||
cd /path/to/iosApp
|
||||
xcodebuild -workspace *.xcworkspace -scheme "<AppScheme>" -destination 'generic/platform=iOS Simulator' ARCHS=arm64
|
||||
```
|
||||
If the user wants to skip the Xcode build or no Xcode project is found, proceed without it — the Kotlin compilation from step 2 is sufficient to continue.
|
||||
|
||||
4. **If the Kotlin compilation fails**, ask the user to either:
|
||||
- Provide the correct Gradle command to verify the module builds, or
|
||||
- Confirm the module is in a working state and it's safe to proceed
|
||||
|
||||
If the user confirms without providing a build command, **record that the pre-migration build could not be verified** and warn about this at the end of migration (Phase 7).
|
||||
|
||||
### 1.0a Confirm Kotlin version with Swift Import support
|
||||
|
||||
Read the current Kotlin version from `gradle/libs.versions.toml` (or `build.gradle.kts`).
|
||||
|
||||
**If the project already uses Kotlin 2.4.0-Beta2 or later** → record the version and skip Phase 2.1 (no version change needed).
|
||||
|
||||
**If the project uses an older Kotlin version** → Phase 2.1 will upgrade it to `2.4.0-Beta2` (the first public release with `swiftPMDependencies` support, available on Maven Central — no custom repository needed). Warn the user: "⚠️ Kotlin version jump — upgrading across minor versions can introduce breaking changes unrelated to this migration. Recommended: update first, verify it builds, then re-run this migration." If the user confirms, proceed.
|
||||
|
||||
### 1.1 Check for deprecated CocoaPods workaround property
|
||||
|
||||
Search `gradle.properties` for the deprecated property:
|
||||
|
||||
```properties
|
||||
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true
|
||||
```
|
||||
|
||||
This property was a workaround (see [KT-64096](https://youtrack.jetbrains.com/issue/KT-64096)) for projects using `embedAndSign` alongside CocoaPods dependencies. It suppresses an error about unsupported configurations that can cause runtime crashes or symbol duplication. After migrating to SwiftPM import, this property is no longer needed and **must be removed** in Phase 6. Record its presence if found.
|
||||
|
||||
### 1.2 Check for EmbedAndSign disablers
|
||||
|
||||
Search all `build.gradle.kts` files for code that disables `EmbedAndSign` tasks (e.g., `TaskGraph.whenReady` filters, `tasks.matching` blocks). This is a CocoaPods-era workaround that **breaks the migration** because `integrateEmbedAndSign` (needed in Phase 5) gets disabled too. Record any such code — it **must be removed** in Phase 6, and may need to be removed earlier. See [troubleshooting.md](references/troubleshooting.md) § "`integrateEmbedAndSign` Skipped" for patterns.
|
||||
|
||||
### 1.3 Check for third-party KMP libraries with bundled cinterop klibs
|
||||
|
||||
Some KMP libraries ship pre-built cinterop klibs with `cocoapods.*` package namespaces. After migration, the swiftPMDependencies cinterop generator detects these existing bindings and **skips generating new bindings** for those Clang modules to avoid duplicates. This means `cocoapods.*` imports for those modules must be **kept as-is** — they resolve to the third-party library's bundled klib, not to actual CocoaPods.
|
||||
|
||||
**Known libraries with bundled `cocoapods.*` klibs:**
|
||||
|
||||
| Library | Maven artifact | Bundled klib namespace | Classes provided |
|
||||
|---------|---------------|----------------------|-----------------|
|
||||
| [KMPNotifier](https://github.com/mirzemehdi/KMPNotifier) | `io.github.mirzemehdi:kmpnotifier` | `cocoapods.FirebaseMessaging` | `FIRMessaging`, `FIRMessagingAPNSTokenType`, etc. |
|
||||
|
||||
**How to detect:** Search Gradle dependency declarations for known libraries, then cross-reference their bundled namespaces against the `import cocoapods.*` statements found in step 4. Mark any matches — these imports will NOT be transformed in Phase 4.
|
||||
|
||||
If unsure whether a third-party KMP library bundles cinterop klibs, check if it has a `linkOnly = true` pod dependency in the project — this is a strong indicator that the library provides its own klib for those classes.
|
||||
|
||||
To inspect klib contents and verify bundled bindings, see [troubleshooting.md](references/troubleshooting.md) § "Third-Party KMP Libraries with Bundled Klibs".
|
||||
|
||||
**Find and record:**
|
||||
|
||||
1. **CocoaPods configuration** - Search for `cocoapods` in `build.gradle.kts` files
|
||||
2. **Pod dependencies** - Extract pod names, versions from `cocoapods {}` blocks
|
||||
3. **Framework configuration** - Record `baseName`, `isStatic`, deployment target from `cocoapods.framework {}`
|
||||
4. **linkOnly pods** - Record pods declared with `linkOnly = true`. These have two common patterns:
|
||||
- **KMP wrapper libraries** (e.g., `dev.gitlive:firebase-*`): the wrapper provides Kotlin APIs, and the pod is only linked. See [common-pods-mapping.md](references/common-pods-mapping.md) for implications.
|
||||
- **Multi-module projects**: the consuming module declares `linkOnly = true` because a child module already provides cinterop bindings for that pod. In SwiftPM, the `swiftPackage()` declaration should go **only** in the child module that uses the pod directly. The consuming module must NOT redeclare the same packages — it only needs a `swiftPMDependencies {}` block without those packages (or an empty one if all pods were `linkOnly`). **Import namespace implication:** when the consuming module imports SPM classes that come from a child module's `swiftPMDependencies`, the import path uses the **child module's** group and name as the namespace (see Phase 4 Import Namespace Formula).
|
||||
5. **Kotlin imports** - Find all `import cocoapods.*` statements. Cross-reference with step 1.3 to identify which imports come from bundled klibs (and must be preserved) vs. which come from direct pod cinterop (and must be transformed).
|
||||
6. **Map pods to SPM** - See [common-pods-mapping.md](references/common-pods-mapping.md)
|
||||
7. **Locate iOS project directory** - Find the directory containing `Podfile` and `.xcworkspace`:
|
||||
```bash
|
||||
find . -name "Podfile" -type f
|
||||
```
|
||||
Record this path (e.g., `iosApp/`, `ios/`, or project root) - needed for Phase 5
|
||||
8. **Check for non-KMP CocoaPods** - Determine if the project uses CocoaPods for dependencies other than KMP. This affects cleanup strategy in Phase 5.
|
||||
9. **Cross-reference Podfile against `cocoapods {}` block** - Parse the `Podfile` and compare its pod entries with the pods declared in the Gradle `cocoapods {}` block. Record any dependencies that exist in the `Podfile` but are **not** listed in `cocoapods {}`. These Podfile-only dependencies still linked into the app via CocoaPods and must be migrated to `swiftPMDependencies` — dropping them silently causes obscure linkage errors at runtime.
|
||||
10. **Check Xcode build phases** - Open the `.xcodeproj`'s `project.pbxproj` and search for the Gradle build phase script. Check if `embedAndSignAppleFrameworkForXcode` is present but **commented out** (prefixed with `#`). If commented out, it must be uncommented during Phase 5 — the `integrateEmbedAndSign` task may or may not handle this automatically.
|
||||
11. **Check for existing Crashlytics dSYM upload script** - If using FirebaseCrashlytics, search `project.pbxproj` for a dSYM upload shell script phase. Record its current path (CocoaPods-era scripts reference `${PODS_ROOT}/FirebaseCrashlytics/upload-symbols`). This must be updated to the SPM path in Phase 5.
|
||||
12. **Identify CocoaPods-related extras in build scripts** - Search all `build.gradle.kts` files for CocoaPods workarounds beyond the standard `cocoapods {}` block (custom tasks hooking into `podInstall`, `Pods.xcodeproj` patching, podspec metadata, `extraSpecAttributes`, `noPodspec()`, etc.). See [cocoapods-extras-patterns.md](references/cocoapods-extras-patterns.md) for the full pattern list. Record all findings — these will be handled in Phase 6.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Gradle Configuration
|
||||
|
||||
**Important scope note:** Do NOT upgrade the Gradle wrapper version, update KSP, or update any other dependencies during this migration. Those are separate concerns and out of scope. Only change what is listed below.
|
||||
|
||||
### 2.1 Update Kotlin version
|
||||
|
||||
**Skip this step** if the project already uses Kotlin 2.4.0-Beta2 or later (recorded in Phase 1.0a).
|
||||
|
||||
Update to `2.4.0-Beta2` (or the latest available release with Swift Import support) in `gradle/libs.versions.toml`:
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
kotlin = "2.4.0-Beta2"
|
||||
```
|
||||
|
||||
`2.4.0-Beta2` is available on Maven Central — no custom repository is needed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Add swiftPMDependencies (Keep CocoaPods)
|
||||
|
||||
**Do NOT remove the `cocoapods {}` block or `kotlin("native.cocoapods")` plugin yet.** Add `swiftPMDependencies {}` alongside the existing CocoaPods configuration.
|
||||
|
||||
### 3.1 Add group property
|
||||
|
||||
```kotlin
|
||||
group = "org.example.myproject" // Required for import namespace
|
||||
```
|
||||
|
||||
**Compose Resources warning:** If the project uses Compose Multiplatform resources (`org.jetbrains.compose` plugin or `compose.resources`), the `group` property is also used as the namespace for generated resource accessors (e.g., `Res.string.*`, `Res.drawable.*`). If `group` already exists in `build.gradle.kts`, do **not** change it. If you are adding `group` for the first time, warn the user that existing Compose resource accessor call sites throughout the project will change namespace and may need updating.
|
||||
|
||||
### 3.2 Add swiftPMDependencies block alongside cocoapods
|
||||
|
||||
For each pod dependency, add the equivalent SwiftPM package declaration. Use [common-pods-mapping.md](references/common-pods-mapping.md) to map each pod to its SPM package URL, product name, and `importedClangModules`.
|
||||
|
||||
**Version preservation:** Do NOT bump dependency versions during migration. Use the exact same version that was specified in the `cocoapods {}` block. Changing versions can resolve to different library builds that break cinterop APIs (removed symbols, changed signatures) and introduce issues unrelated to the migration itself.
|
||||
|
||||
| CocoaPods version spec | SPM equivalent | Example |
|
||||
|------------------------|---------------|---------|
|
||||
| `version = "1.2.3"` (exact) | `version = "1.2.3"` (simple) or `exact("1.2.3")` (typed) | `pod("GoogleMaps") { version = "10.3.0" }` → `version = "10.3.0"` |
|
||||
| `version = "~> 1.2"` (optimistic) | `version = "1.2.0"` (simple) or `from("1.2.0")` (typed) | `pod("FirebaseAuth") { version = "~> 12.5" }` → `version = "12.5.0"` |
|
||||
| No version specified | Ask user which version to pin | Ask the user which version to use |
|
||||
|
||||
**Two API forms:** The DSL has a simple string API and a typed API. **Use the simple string API** for most packages:
|
||||
```kotlin
|
||||
swiftPackage(url = "https://github.com/owner/repo.git", version = "1.0.0", products = listOf("ProductName"))
|
||||
```
|
||||
The simple API auto-defaults `importedClangModules` to the `products` list. Use the typed API (with `url()`, `exact()`, `product()` wrappers) only when you need exact version pinning, platform constraints, or explicit Clang module control. See [dsl-reference.md](references/dsl-reference.md) for the typed API.
|
||||
|
||||
**Key concepts:** `products` = SPM product names (controls linking). `importedClangModules` = Clang module names for cinterop bindings (only when `discoverClangModulesImplicitly = false`). `discoverClangModulesImplicitly` defaults to `true` (bindings for all Clang modules); set `false` when transitive C/C++ modules fail cinterop (Firebase, gRPC), then list needed modules explicitly.
|
||||
|
||||
**Important:** SPM product names and Clang module names don't always match. Always consult [common-pods-mapping.md](references/common-pods-mapping.md) for correct values.
|
||||
|
||||
**Podfile-only dependencies:** If Phase 1 step 9 identified dependencies that exist in the `Podfile` but not in the Gradle `cocoapods {}` block, these must also be added to `swiftPMDependencies` as `products` entries. Even though the KMP module didn't declare them, they were linked into the app by CocoaPods and may be required for the app to build. Look up each Podfile-only pod's SPM package URL and add it as a `swiftPackage()` with at least its `products`. If any of these pods were used via cinterop (check for `import cocoapods.*` statements referencing them), also add `importedClangModules`.
|
||||
|
||||
**Do not mix the same library suite across CocoaPods and SPM.** Libraries that share a common repository (e.g., all Firebase products) share transitive dependencies. Having some products linked via CocoaPods and others via SPM causes duplicate/conflicting symbols and dyld crashes at runtime. When migrating such a suite, move **all** pods from that suite to SPM at once — including Swift-only pods that Kotlin doesn't use directly. Add Swift-only pods as `products` entries (no `importedClangModules` needed). After adding new products, re-run `integrateLinkagePackage` to regenerate the linkage Swift package.
|
||||
|
||||
```kotlin
|
||||
kotlin {
|
||||
// Keep existing targets
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
iosX64()
|
||||
|
||||
swiftPMDependencies {
|
||||
iosMinimumDeploymentTarget = "16.0"
|
||||
|
||||
swiftPackage(
|
||||
url = "https://github.com/owner/repo.git",
|
||||
version = "1.0.0",
|
||||
products = listOf("ProductName"),
|
||||
)
|
||||
}
|
||||
|
||||
cocoapods {
|
||||
// ... keep existing cocoapods block for now
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Move framework configuration out of cocoapods block
|
||||
|
||||
If the `cocoapods` block contains a `framework {}` configuration, move it to the `binaries` API on each target. **`isStatic = true` is recommended** — dynamic frameworks have known edge cases with SwiftPM import that can cause linker errors, dyld crashes, or duplicate class warnings:
|
||||
|
||||
```kotlin
|
||||
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework { baseName = "Shared"; isStatic = true }
|
||||
}
|
||||
```
|
||||
|
||||
If the `cocoapods.framework {}` block contained `export(project(...))` or `transitiveExport = true`, preserve these in the new `binaries.framework {}` block — they are essential for multi-module projects where the framework exports child modules.
|
||||
|
||||
### 3.4 Handle dev.gitlive/firebase-kotlin-sdk and similar CocoaPods-era KMP wrappers
|
||||
|
||||
If the project uses `dev.gitlive:firebase-*` or similar KMP wrapper libraries, two additional steps are required:
|
||||
|
||||
**A. Switch to `isStatic = true`** — dynamic frameworks + Firebase SPM = runtime `dyld` crash. After switching: re-run `integrateLinkagePackage`, remove any "Embed Frameworks" copy phase, move linker flags to `OTHER_LDFLAGS`.
|
||||
|
||||
**B. Add framework search paths** — add conditional `-F` linkerOpts in `build.gradle.kts` and matching `FRAMEWORK_SEARCH_PATHS` in the Xcode project.
|
||||
|
||||
See [common-pods-mapping.md](references/common-pods-mapping.md) § dev.gitlive and [troubleshooting.md](references/troubleshooting.md) for code snippets and the full product list.
|
||||
|
||||
### 3.5 Add opt-in annotations
|
||||
|
||||
The `swiftPackage()` and `localSwiftPackage()` DSL functions are annotated with `@ExperimentalKotlinGradlePluginApi`. Add this opt-in to suppress the compiler warning:
|
||||
|
||||
```kotlin
|
||||
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
|
||||
```
|
||||
|
||||
Place this at the top of each `build.gradle.kts` file that calls `swiftPackage()` or `localSwiftPackage()`.
|
||||
|
||||
Also add the cinterop opt-in for Kotlin source files:
|
||||
|
||||
```kotlin
|
||||
kotlin.compilerOptions {
|
||||
optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
|
||||
}
|
||||
```
|
||||
|
||||
For full DSL reference, see [dsl-reference.md](references/dsl-reference.md).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Kotlin Source Updates
|
||||
|
||||
### Import Namespace Formula
|
||||
|
||||
```
|
||||
swiftPMImport.<group>.<module>.<ClassName>
|
||||
|
||||
Where:
|
||||
- group: build.gradle.kts `group` property of the MODULE THAT DECLARES the swiftPMDependencies, dashes (-) → dots (.)
|
||||
- module: Gradle module name of the MODULE THAT DECLARES the swiftPMDependencies, dashes (-) → dots (.), underscores (_) preserved as-is
|
||||
- ClassName: Objective-C class name (FIR* for Firebase, GMS* for Google Maps)
|
||||
```
|
||||
|
||||
**The namespace uses the declaring module's group+name, not the importing module's.** This is the most common mistake agents make. When module A depends on module B, and module B declares `swiftPMDependencies`, module A imports SPM classes using module B's group and module name — NOT module A's.
|
||||
|
||||
### Example Transformation — Single Module
|
||||
|
||||
```kotlin
|
||||
// group = "org.jetbrains.kotlin.firebase.sample", module = "kotlin-library"
|
||||
|
||||
// BEFORE:
|
||||
import cocoapods.FirebaseAnalytics.FIRAnalytics
|
||||
|
||||
// AFTER:
|
||||
import swiftPMImport.org.jetbrains.kotlin.firebase.sample.kotlin.library.FIRAnalytics
|
||||
```
|
||||
|
||||
### Example Transformation — Multi-Module (linkOnly pods)
|
||||
|
||||
When a consuming module had `pod("GoogleMaps") { linkOnly = true }` because a child module provides the cinterop bindings:
|
||||
|
||||
```kotlin
|
||||
// composeApp/App.kt — composeApp depends on :google-maps Gradle module
|
||||
// google-maps has group = "org.jetbrains.kotlin.google-maps", module name = "google-maps"
|
||||
// google-maps declares swiftPMDependencies with GoogleMaps
|
||||
|
||||
// BEFORE (in composeApp):
|
||||
import cocoapods.GoogleMaps.GMSServices
|
||||
|
||||
// AFTER — uses google-maps module's namespace, NOT composeApp's namespace:
|
||||
import swiftPMImport.org.jetbrains.kotlin.google.maps.google.maps.GMSServices
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^
|
||||
// google-maps's group (dashes→dots) google-maps's module name (dashes→dots)
|
||||
```
|
||||
|
||||
The import path does NOT use composeApp's group (`org.jetbrains.kotlin.compose.sample`). It uses the declaring module's identity because that's where the cinterop bindings are generated.
|
||||
|
||||
**Import flattening:** The Clang module name (e.g., `FirebaseFirestoreInternal`, `FirebaseAuth`) disappears from the import path — all classes are flattened under the same `swiftPMImport.<group>.<module>` prefix regardless of which library they come from. For example, both `cocoapods.FirebaseAuth.FIRAuth` and `cocoapods.FirebaseFirestoreInternal.FIRFirestore` become `swiftPMImport.<group>.<module>.FIRAuth` and `swiftPMImport.<group>.<module>.FIRFirestore`.
|
||||
|
||||
### Preserving Bundled Klib Imports
|
||||
|
||||
> **CRITICAL:** Do NOT replace `cocoapods.*` imports that resolve to third-party KMP libraries' bundled cinterop klibs (identified in Phase 1 step 1.3). These imports must remain as-is — the `cocoapods` prefix is the package namespace in the library's published klib, not an actual CocoaPods dependency. The swiftPMDependencies cinterop generator skips modules already provided by a dependency's klib, so `swiftPMImport.*` for those classes will fail with "Unresolved reference".
|
||||
|
||||
**Example** (project using [KMPNotifier](https://github.com/mirzemehdi/KMPNotifier)):
|
||||
```kotlin
|
||||
// KEEP — resolves to kmpnotifier's bundled cinterop klib
|
||||
import cocoapods.FirebaseMessaging.FIRMessaging
|
||||
```
|
||||
|
||||
### Bulk Replacement
|
||||
|
||||
Use a regex find-and-replace across all Kotlin source files, **excluding imports identified in Phase 1 step 1.3**. In multi-module projects, run the replacement separately for each module using that module's group and name:
|
||||
|
||||
```
|
||||
Find: cocoapods\.\w+\.
|
||||
Replace: swiftPMImport.<declaring.module.group>.<declaring.module.name>.
|
||||
```
|
||||
|
||||
After bulk replacement, **manually restore** any `cocoapods.*` imports that should be preserved (from bundled klibs).
|
||||
|
||||
**Finding correct import path:** Run `./gradlew :moduleName:compileKotlinIosSimulatorArm64` - errors show available classes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: iOS Project Reconfiguration
|
||||
|
||||
### 5.1 Get migration command
|
||||
|
||||
Build the CocoaPods workspace to obtain the migration command:
|
||||
|
||||
```bash
|
||||
cd /path/to/iosApp
|
||||
|
||||
xcodebuild -scheme "$(echo -n *.xcworkspace | python3 -c 'import sys, json; from subprocess import check_output; print(list(set(json.loads(check_output(["xcodebuild", "-workspace", sys.stdin.readline(), "-list", "-json"]))["workspace"]["schemes"]) - set(json.loads(check_output(["xcodebuild", "-project", "Pods/Pods.xcodeproj", "-list", "-json"]))["project"]["schemes"]))[0])')" -workspace *.xcworkspace -destination 'generic/platform=iOS Simulator' ARCHS=arm64 | grep -A5 'What went wrong'
|
||||
```
|
||||
|
||||
The build output will contain a command like:
|
||||
```bash
|
||||
XCODEPROJ_PATH='/path/to/project/iosApp.xcodeproj' GRADLE_PROJECT_PATH=':shared' '/path/to/project/gradlew' -p '/path/to/project' ':shared:integrateEmbedAndSign' ':shared:integrateLinkagePackage'
|
||||
```
|
||||
|
||||
Run this command. It modifies the `.xcodeproj` to trigger `embedAndSignAppleFrameworkForXcode` during the build. `integrateLinkagePackage` is a one-time setup — it does not need to be added as a build phase. If `integrateEmbedAndSign` is skipped, check for EmbedAndSign disablers (Phase 1 step 1.2) — remove them first, then re-run.
|
||||
|
||||
**Verify `embedAndSignAppleFrameworkForXcode` is active:** After running integration, check the build phase script in `project.pbxproj`. If `embedAndSignAppleFrameworkForXcode` is commented out (prefixed with `#`), uncomment it.
|
||||
|
||||
The `integrateLinkagePackage` task generates `KotlinMultiplatformLinkedPackage/` at `<iosDir>/` — a local Swift package that mirrors your `products` list and ensures SPM libraries are linked into the final binary.
|
||||
|
||||
After running the integration tasks, **disable User Script Sandboxing** (`ENABLE_USER_SCRIPT_SANDBOXING = NO`) in the `.xcodeproj`. Xcode 16+ enables it by default, which prevents the Gradle build phase from writing to the project directory:
|
||||
|
||||
```bash
|
||||
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' "$XCODEPROJ_PATH/project.pbxproj"
|
||||
```
|
||||
|
||||
If the setting is absent (Xcode defaults to YES), add `ENABLE_USER_SCRIPT_SANDBOXING = NO;` to the app target's `buildSettings` sections. Then restart the Gradle daemon: `./gradlew --stop`
|
||||
|
||||
**Alternative (if xcodebuild approach fails):** See [troubleshooting.md](references/troubleshooting.md) § "Manual Integration Command Discovery" for a fallback script to discover paths and run integration tasks directly.
|
||||
|
||||
### 5.2 Update Crashlytics dSYM upload script (if applicable)
|
||||
|
||||
If the project uses FirebaseCrashlytics and has a dSYM upload run script phase (identified in Phase 1 step 11), update the script path from `${PODS_ROOT}/FirebaseCrashlytics/upload-symbols` to `"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"`. See [troubleshooting.md](references/troubleshooting.md) § "Firebase Crashlytics: dSYM Upload Script" and [common-pods-mapping.md](references/common-pods-mapping.md) for the full script and input files list.
|
||||
|
||||
### 5.3 Deintegrate CocoaPods
|
||||
|
||||
**Option A: Full deintegration** (if CocoaPods was used ONLY for KMP dependencies):
|
||||
|
||||
Before deleting files, run `git status --short` and verify the paths. If unsure, move files to a backup location instead of deleting immediately.
|
||||
|
||||
```bash
|
||||
cd /path/to/iosApp
|
||||
pod deintegrate
|
||||
rm -rf Podfile Podfile.lock Pods/
|
||||
# Remove the workspace that matches your app xcodeproj name
|
||||
XCODEPROJ_NAME=$(basename "$(find . -maxdepth 1 -name "*.xcodeproj" -type d | grep -v Pods | head -1)" .xcodeproj)
|
||||
rm -rf "${XCODEPROJ_NAME}.xcworkspace"
|
||||
# Return to project root
|
||||
cd ..
|
||||
# Remove the migrated module podspec only (for example, shared.podspec)
|
||||
# If unknown, list candidates and remove the matching one explicitly:
|
||||
ls -1 *.podspec
|
||||
# rm -f shared.podspec
|
||||
```
|
||||
|
||||
This cleanup snippet is self-contained and does not assume `XCODEPROJ_PATH` or `GRADLE_PROJECT_PATH` from the earlier one-off migration command are still available in your shell.
|
||||
|
||||
If `pod deintegrate` is not available, see [troubleshooting.md](references/troubleshooting.md) § "Manual CocoaPods Deintegration from pbxproj" for the full list of references to remove. Also remove `Pods/` from `.gitignore` and delete the `.xcworkspace` directory.
|
||||
|
||||
**Option B: Partial removal** (if other non-KMP CocoaPods dependencies remain):
|
||||
|
||||
Remove only the KMP pod line from the `Podfile` and re-run pod install:
|
||||
|
||||
```ruby
|
||||
target 'iosApp' do
|
||||
# Remove this line:
|
||||
pod 'shared', :path => '../shared'
|
||||
# Keep other non-KMP pods
|
||||
end
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /path/to/iosApp && pod install
|
||||
```
|
||||
|
||||
> **Tip:** Consider migrating remaining pods to SPM too — most popular iOS libraries support it natively. Add them in Xcode via File → Add Package Dependencies, then fully deintegrate CocoaPods once all pods are replaced.
|
||||
|
||||
### 5.4 Manual integration (if automatic fails)
|
||||
|
||||
See [troubleshooting.md](references/troubleshooting.md) § "Manual Xcode Integration Steps" for the 5-step manual setup (build phase, sandboxing, linkage package).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Remove CocoaPods from Gradle
|
||||
|
||||
Now that the iOS project is reconfigured, remove the CocoaPods plugin and block from **all** modules that used it (not just the primary one):
|
||||
|
||||
### 6.1 Remove CocoaPods plugin
|
||||
|
||||
Remove `kotlin("native.cocoapods")` or `alias(libs.plugins.kotlinCocoapods)` from `plugins {}` in every module that used it:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// REMOVE: kotlin("native.cocoapods") or alias(libs.plugins.kotlinCocoapods)
|
||||
alias(libs.plugins.kotlinMultiplatform) // Keep
|
||||
}
|
||||
```
|
||||
|
||||
If all modules have been migrated, also remove the `kotlinCocoapods` plugin entry from `gradle/libs.versions.toml`:
|
||||
|
||||
```toml
|
||||
[plugins]
|
||||
# REMOVE: kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
|
||||
```
|
||||
|
||||
### 6.2 Remove cocoapods block
|
||||
|
||||
Delete the entire `cocoapods { ... }` block from `build.gradle.kts`. The `swiftPMDependencies {}` block and `binaries.framework {}` configuration added in Phase 3 replace it. Also delete any generated `.podspec` files from the module directory (e.g., `shared/shared.podspec`) — these were generated by the CocoaPods plugin and are no longer needed.
|
||||
|
||||
### 6.3 Remove deprecated gradle.properties entries
|
||||
|
||||
If found in Phase 1.1, remove from `gradle.properties`:
|
||||
|
||||
```properties
|
||||
# REMOVE — no longer needed after migrating away from CocoaPods (KT-64096)
|
||||
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true
|
||||
```
|
||||
|
||||
### 6.4 Clean up CocoaPods-related extras
|
||||
|
||||
Review the extras identified in Phase 1 step 12. Podspec metadata, `noPodspec()`, CocoaPods task hooks, and `Pods.xcodeproj` patching code are **safe to remove** without user consultation. Non-standard pod configurations (`extraOpts`, `moduleName`), custom cinterop `defFile` setups, and CocoaPods-specific compiler/linker flags **require analysis** — consult the user if unsure whether SPM handles them automatically.
|
||||
|
||||
See [cocoapods-extras-patterns.md](references/cocoapods-extras-patterns.md) for the full categorized list with examples.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Verification
|
||||
|
||||
**Do NOT stop until the application builds successfully.** This phase is iterative — if any step fails, diagnose the error, fix it (consulting [troubleshooting.md](references/troubleshooting.md) and re-checking Phases 2–6), and re-run the failing step. Repeat until the build succeeds or the issue is clearly outside the migration scope (pre-existing bug, unrelated tooling problem). Do NOT write the migration report (Phase 8) until the build succeeds.
|
||||
|
||||
### 7.1 Compile Kotlin code
|
||||
|
||||
Compile the migrated module to verify Kotlin sources are correct:
|
||||
|
||||
```bash
|
||||
./gradlew :moduleName:compileKotlinIosSimulatorArm64
|
||||
```
|
||||
|
||||
If compilation fails with unresolved references, check import transformations (Phase 4) and SwiftPM dependency declarations (Phase 3.2). Common causes: missing `importedClangModules`, wrong Clang module names, preserved bundled klib imports that should have been transformed (or vice versa).
|
||||
|
||||
### 7.2 Link framework
|
||||
|
||||
```bash
|
||||
./gradlew :moduleName:linkDebugFrameworkIosSimulatorArm64
|
||||
```
|
||||
|
||||
If linking fails, check that all required SPM products are declared and that version constraints resolve correctly. Linking errors about missing symbols often indicate a product was omitted from `swiftPMDependencies` or a version mismatch caused API removal.
|
||||
|
||||
### 7.3 Build iOS/macOS Xcode project
|
||||
|
||||
After the Gradle steps succeed, build the Xcode project to verify the full application compiles. Use `-project *.xcodeproj` if all CocoaPods were removed (Option A), or `-workspace *.xcworkspace` if non-KMP CocoaPods remain (Option B):
|
||||
|
||||
```bash
|
||||
cd /path/to/iosApp
|
||||
# Discover schemes and build (replace -project/-workspace as needed; for macOS use -destination 'platform=macOS'):
|
||||
xcodebuild -project *.xcodeproj -list -json 2>/dev/null | python3 -c "import sys,json; schemes=json.load(sys.stdin)['project']['schemes']; [print(s) for s in schemes]"
|
||||
xcodebuild -project *.xcodeproj -scheme "<AppScheme>" -destination 'generic/platform=iOS Simulator' ARCHS=arm64 build
|
||||
```
|
||||
|
||||
**If `checkSandboxAndWriteProtection` fails** — sandboxing was not disabled in Phase 5.1. Go back and apply the sandboxing fix from Phase 5.1, then retry.
|
||||
|
||||
**If the pre-migration build was not verified** (Phase 1.0 fallback was used), warn the user:
|
||||
> Note: The pre-migration build could not be fully verified. If build errors appear now, some may be pre-existing issues unrelated to the migration. Compare errors against the pre-migration build output to distinguish migration issues from prior problems.
|
||||
|
||||
### If the build fails
|
||||
|
||||
**Do NOT revert the migration.** Read the error log, re-check Phases 2-6, and consult [troubleshooting.md](references/troubleshooting.md). If unsure, present options to the user — do not silently undo migration work. Fix the issue and re-run the failing verification step. Keep iterating until the build succeeds.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Migration Report
|
||||
|
||||
After the build succeeds, write a comprehensive `MIGRATION_REPORT.md` in the project root. Use the template in [migration-report-template.md](references/migration-report-template.md).
|
||||
|
||||
The report must include:
|
||||
1. **Pre-Migration State** — CocoaPods dependencies (name, version, `linkOnly`), framework config, `cocoapods.*` imports, non-KMP pods, atypical configuration
|
||||
2. **Migration Steps** — exact changes per phase with before/after snippets for non-trivial changes
|
||||
3. **Import Transformations** — table of every import change, clearly marking preserved `cocoapods.*` imports and which bundled klib provides them
|
||||
4. **Errors Encountered** — structured `Error #N` entries: phase, exact symptom, root cause, fix, generalizable flag
|
||||
5. **Non-Trivial Decisions** — `isStatic` changes, preserved imports, framework search paths, trade-offs
|
||||
6. **Files Changed** — complete list grouped by type (Gradle, Kotlin, Xcode, created, deleted)
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [DSL Reference](references/dsl-reference.md) - Full swiftPMDependencies syntax
|
||||
- [Common Pods Mapping](references/common-pods-mapping.md) - Pod to SPM mapping table
|
||||
- [CocoaPods Extras Patterns](references/cocoapods-extras-patterns.md) - Detection and cleanup patterns for CocoaPods workarounds
|
||||
- [Troubleshooting](references/troubleshooting.md) - Issues, solutions, rollback
|
||||
- [Migration Report Template](references/migration-report-template.md) - Post-migration report template
|
||||
@@ -0,0 +1,47 @@
|
||||
# CocoaPods Extras Patterns
|
||||
|
||||
Patterns to look for in `build.gradle.kts` files beyond the standard `cocoapods {}` block. These are workarounds, hacks, and glue code that projects accumulate over time to work around CocoaPods limitations.
|
||||
|
||||
## Detection Patterns (Phase 1 step 11)
|
||||
|
||||
- **Custom tasks that hook into CocoaPods tasks** — e.g., tasks registered with `tasks.named("podInstall") { finalizedBy(...) }` or `tasks.register("fixXcodeProject")` that patch `Pods.xcodeproj/project.pbxproj` to fix paths, tweak build settings, or work around CocoaPods quirks. These are pure CocoaPods workarounds and become dead code after migration.
|
||||
- **Pods.xcodeproj patching** — any code that reads/writes `Pods.xcodeproj` files (e.g., replacing Gradle invocation paths, fixing scheme settings). The `Pods.xcodeproj` will no longer exist after migration.
|
||||
- `cocoapods.summary`, `cocoapods.homepage`, `cocoapods.version`, `cocoapods.name` — podspec metadata (safe to remove)
|
||||
- `cocoapods.podfile` — explicit Podfile path reference
|
||||
- `cocoapods.extraSpecAttributes` — custom podspec attributes
|
||||
- `pod("...", extraOpts = ...)` or `pod("...", moduleName = ...)` — non-standard pod configurations
|
||||
- `noPodspec()` — disables podspec generation
|
||||
- **Any code referencing `Pods/` directory, `.xcworkspace`, or `podspec` files** — build logic, path constants, or task inputs/outputs tied to CocoaPods artifacts
|
||||
- Compiler flags or linker settings added specifically for CocoaPods interop (e.g., `-framework`, cinterop `defFile` for pod headers)
|
||||
|
||||
---
|
||||
|
||||
## Phase 6.4 Cleanup Categories
|
||||
|
||||
### Safe to remove (no user consultation needed)
|
||||
|
||||
- `cocoapods.summary`, `cocoapods.homepage`, `cocoapods.version`, `cocoapods.name` — podspec metadata, not used by SPM
|
||||
- `cocoapods.podfile = project.file(...)` — Podfile path reference, not used by SPM
|
||||
- `cocoapods.extraSpecAttributes` — podspec attributes, not used by SPM
|
||||
- `noPodspec()` — podspec generation flag, not used by SPM
|
||||
- Custom Gradle tasks that hook into CocoaPods tasks (`podInstall`, `podSetup`, `generatePodspec`) — these tasks no longer exist without the plugin. This includes any tasks registered via `tasks.named("podInstall") { finalizedBy(...) }` or similar wiring. Example of dead code to remove entirely:
|
||||
```kotlin
|
||||
// REMOVE — CocoaPods workaround, no longer needed
|
||||
tasks.register("fixXcodeProject") {
|
||||
doLast {
|
||||
val xcodeProjectFile = project.file("../iosApp/Pods/Pods.xcodeproj/project.pbxproj")
|
||||
// ... patching Pods.xcodeproj paths ...
|
||||
}
|
||||
}
|
||||
tasks.named("podInstall") { finalizedBy("fixXcodeProject") }
|
||||
```
|
||||
- Any code that reads/writes `Pods.xcodeproj` files — the `Pods/` directory will no longer exist
|
||||
- References to `Pods/` directory paths, `.xcworkspace` files, or `podspec` files in build configurations
|
||||
|
||||
### Requires analysis — consult the user if unsure
|
||||
|
||||
- `pod("...", extraOpts = ...)` — extra options may indicate special compilation flags needed. Check if the underlying library needs equivalent flags in `swiftPMDependencies` (e.g., `importedClangModules`, platform constraints)
|
||||
- `pod("...", moduleName = ...)` — custom module name may indicate the Clang module name differs from the pod name. This likely maps to an `importedClangModules` entry in the SPM package declaration
|
||||
- Custom cinterop `defFile` configurations for pod headers — these may need to be adapted or may no longer be needed if the SwiftPM import handles the headers automatically. Present findings to the user before removing
|
||||
- Compiler or linker flags added specifically for CocoaPods interop (e.g., `-framework Pod`, custom `cinterops {}` blocks) — analyze whether the SPM integration handles this automatically. If unclear, present the flags to the user and ask whether they are still needed
|
||||
- Any custom task wiring or build logic that references CocoaPods outputs — explain what the task does and ask the user whether equivalent functionality is needed
|
||||
@@ -0,0 +1,474 @@
|
||||
# Common Pods to SwiftPM Mapping
|
||||
|
||||
Reference for migrating popular CocoaPods dependencies to SwiftPM.
|
||||
|
||||
## Firebase Suite
|
||||
|
||||
All Firebase products come from a single repository: `https://github.com/firebase/firebase-ios-sdk.git`
|
||||
|
||||
**Key facts:**
|
||||
- SPM product names match CocoaPods pod names (e.g., pod `FirebaseAuth` → product `FirebaseAuth`)
|
||||
- Exception: Beta products have a `-Beta` suffix in SPM (e.g., `FirebaseAppDistribution-Beta`)
|
||||
- The CocoaPods umbrella pod `Firebase` does not exist in SPM — import specific products
|
||||
- **Platform requirements**: iOS 15+, macOS 10.15+, tvOS 15+, watchOS 7+
|
||||
- **Xcode**: 16.2+
|
||||
|
||||
> **WARNING: Do not mix Firebase across CocoaPods and SPM.** All Firebase products share a single repository and common transitive dependencies (gRPC, abseil, leveldb, BoringSSL, nanopb, etc.). If some Firebase pods remain in CocoaPods while others are added via SPM, the shared transitive dependencies get linked twice with conflicting symbols, causing **dyld crashes at runtime** (e.g., `Symbol not found: _OBJC_CLASS_$_FIRFirestore`). When migrating Firebase, move **all** Firebase pods to SPM at once — including Swift-only pods (FirebaseAI, FirebaseFunctions, FirebaseMLModelDownloader) that Kotlin cannot use directly. Add Swift-only pods as `products` entries without `importedClangModules`. After adding new products, re-run `integrateLinkagePackage` to regenerate the linkage Swift package.
|
||||
|
||||
### Firebase SPM Products Reference
|
||||
|
||||
| CocoaPods Pod | SPM Product | Platform | KMP Notes |
|
||||
|---------------|-------------|----------|-----------|
|
||||
| FirebaseAnalytics | FirebaseAnalytics | All | ObjC classes: `FIRAnalytics`, `FIRApp` |
|
||||
| FirebaseAuth | FirebaseAuth | All (partial on macOS/tvOS/watchOS) | ObjC classes: `FIRAuth`, `FIRUser` |
|
||||
| FirebaseCore | FirebaseCore | All | ObjC class: `FIRApp` |
|
||||
| FirebaseCrashlytics | FirebaseCrashlytics | All | ObjC class: `FIRCrashlytics` |
|
||||
| FirebaseDatabase | FirebaseDatabase | All | **importedClangModules: `FirebaseDatabaseInternal`** — ObjC classes: `FIRDatabase`, `FIRDatabaseReference` |
|
||||
| FirebaseFirestore | FirebaseFirestore | All | **Special case** — see below |
|
||||
| FirebaseFunctions | FirebaseFunctions | All | Swift-only — no `importedClangModules` entry needed |
|
||||
| FirebaseMessaging | FirebaseMessaging | All | ObjC classes: `FIRMessaging` |
|
||||
| FirebaseRemoteConfig | FirebaseRemoteConfig | All | **importedClangModules: `FirebaseRemoteConfigInternal`** — ObjC class: `FIRRemoteConfig` |
|
||||
| FirebaseStorage | FirebaseStorage | All | ObjC class: `FIRStorage` |
|
||||
| FirebaseAppCheck | FirebaseAppCheck | All (watchOS 9+) | ObjC class: `FIRAppCheck` |
|
||||
| FirebasePerformance | FirebasePerformance | iOS/tvOS only | ObjC class: `FIRPerformance` |
|
||||
| FirebaseInAppMessaging | FirebaseInAppMessaging-Beta | iOS/tvOS only | `-Beta` suffix in SPM, **importedClangModules: `FirebaseInAppMessagingInternal`** |
|
||||
| FirebaseAppDistribution | FirebaseAppDistribution-Beta | iOS only | Note `-Beta` suffix in SPM |
|
||||
| FirebaseInstallations | FirebaseInstallations | All | ObjC class: `FIRInstallations` |
|
||||
| FirebaseABTesting | *(no SPM product)* | All | **Module-only**: pulled transitively by RemoteConfig. List in `importedClangModules` only |
|
||||
| FirebaseAILogic | FirebaseAI | All | **Renamed in SPM**. Swift-only — no `importedClangModules` entry needed |
|
||||
| FirebaseMLModelDownloader | FirebaseMLModelDownloader | All | Swift-only — no `importedClangModules` entry needed |
|
||||
|
||||
### FirebaseAnalytics
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("FirebaseAnalytics") { version = "12.5.0" }
|
||||
|
||||
// SwiftPM — use same version as pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
version = "12.5.0",
|
||||
products = listOf("FirebaseAnalytics"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.FIRAnalytics
|
||||
import swiftPMImport.<group>.<module>.FIRApp
|
||||
```
|
||||
|
||||
### FirebaseAuth
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("FirebaseAuth") { version = "12.5.0" }
|
||||
|
||||
// SwiftPM — use same version as pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
version = "12.5.0",
|
||||
products = listOf("FirebaseAuth"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.FIRAuth
|
||||
import swiftPMImport.<group>.<module>.FIRUser
|
||||
```
|
||||
|
||||
### FirebaseDatabase
|
||||
|
||||
Database's Clang module name differs from its SPM product name. You **must** specify `importedClangModules` (requires typed API):
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("FirebaseDatabase") { version = "12.5.0" }
|
||||
|
||||
// SwiftPM - Note the importedClangModules parameter (typed API required)
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.5.0"),
|
||||
products = listOf(product("FirebaseDatabase")),
|
||||
importedClangModules = listOf("FirebaseDatabaseInternal"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.FIRDatabase
|
||||
import swiftPMImport.<group>.<module>.FIRDatabaseReference
|
||||
```
|
||||
|
||||
### FirebaseFirestore (Special Case)
|
||||
|
||||
Firestore's Clang module name differs from its SPM product name. You **must** specify `importedClangModules` (requires typed API):
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("FirebaseFirestore") { version = "12.5.0" }
|
||||
|
||||
// SwiftPM - Note the importedClangModules parameter (typed API required)
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.5.0"),
|
||||
products = listOf(product("FirebaseFirestore")),
|
||||
importedClangModules = listOf("FirebaseFirestoreInternal"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.FIRFirestore
|
||||
import swiftPMImport.<group>.<module>.FIRDocumentReference
|
||||
```
|
||||
|
||||
**Why is this needed?** Firestore distributes as a binary xcframework. The internal Clang module exposed to Objective-C is named `FirebaseFirestoreInternal`, not `FirebaseFirestore`. Without `importedClangModules`, the KMP compiler cannot discover the Objective-C headers.
|
||||
|
||||
### FirebaseCrashlytics
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("FirebaseCrashlytics") { version = "12.5.0" }
|
||||
|
||||
// SwiftPM — use same version as pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
version = "12.5.0",
|
||||
products = listOf("FirebaseCrashlytics"),
|
||||
)
|
||||
```
|
||||
|
||||
**iOS project requirement:** Crashlytics needs a dSYM upload run script in the Xcode build phases. After migration, add a "Run Script" phase at the END of build phases:
|
||||
|
||||
```bash
|
||||
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"
|
||||
```
|
||||
|
||||
With input files:
|
||||
```
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
|
||||
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
|
||||
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
|
||||
```
|
||||
|
||||
Also set **Debug Information Format** to `DWARF with dSYM File` for all build configurations.
|
||||
|
||||
### Combined Firebase Example
|
||||
|
||||
When using multiple Firebase products, declare them in a single package. **Set `discoverClangModulesImplicitly = false`** — Firebase's transitive C++ dependencies (gRPC, abseil, leveldb, BoringSSL) contain Clang modules that fail cinterop. Explicitly list only the modules you need.
|
||||
|
||||
```kotlin
|
||||
swiftPMDependencies {
|
||||
discoverClangModulesImplicitly = false
|
||||
|
||||
// Combined Firebase requires typed API for importedClangModules control
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.5.0"),
|
||||
products = listOf(
|
||||
product("FirebaseAnalytics"),
|
||||
product("FirebaseAuth"),
|
||||
product("FirebaseDatabase"),
|
||||
product("FirebaseFirestore"),
|
||||
product("FirebaseCrashlytics"),
|
||||
product("FirebaseMessaging"),
|
||||
product("FirebaseRemoteConfig"),
|
||||
// Swift-only pods (products only, no importedClangModules):
|
||||
product("FirebaseAI"),
|
||||
product("FirebaseFunctions"),
|
||||
),
|
||||
importedClangModules = listOf(
|
||||
"FirebaseAnalytics",
|
||||
"FirebaseAuth",
|
||||
"FirebaseCore",
|
||||
"FirebaseCrashlytics",
|
||||
"FirebaseDatabaseInternal", // Not "FirebaseDatabase"
|
||||
"FirebaseFirestoreInternal", // Not "FirebaseFirestore"
|
||||
"FirebaseMessaging",
|
||||
"FirebaseRemoteConfigInternal", // Not "FirebaseRemoteConfig"
|
||||
"FirebaseABTesting", // Module-only, no product
|
||||
),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Firebase importedClangModules Reference
|
||||
|
||||
Several Firebase products expose ObjC headers through Clang modules whose names differ from the SPM product name:
|
||||
|
||||
| SPM Product | Clang Module (importedClangModules) | Notes |
|
||||
|---|---|---|
|
||||
| FirebaseAnalytics | FirebaseAnalytics | Same name |
|
||||
| FirebaseAuth | FirebaseAuth | Same name |
|
||||
| FirebaseCore | FirebaseCore | Same name |
|
||||
| FirebaseCrashlytics | FirebaseCrashlytics | Same name |
|
||||
| FirebaseDatabase | **FirebaseDatabaseInternal** | Different |
|
||||
| FirebaseFirestore | **FirebaseFirestoreInternal** | Different |
|
||||
| FirebaseInAppMessaging-Beta | **FirebaseInAppMessagingInternal** | Different |
|
||||
| FirebaseRemoteConfig | **FirebaseRemoteConfigInternal** | Different |
|
||||
| FirebaseInstallations | FirebaseInstallations | Same name |
|
||||
| FirebaseMessaging | FirebaseMessaging | Same name |
|
||||
| FirebasePerformance | FirebasePerformance | Same name |
|
||||
| FirebaseStorage | FirebaseStorage | Same name |
|
||||
| FirebaseAppCheck | FirebaseAppCheck | Same name |
|
||||
| FirebaseAppDistribution-Beta | FirebaseAppDistribution | Same name (no `-Beta`) |
|
||||
| *(transitive)* | **FirebaseABTesting** | Module-only, no product |
|
||||
| FirebaseAI | *(none)* | Swift-only, no cinterop |
|
||||
| FirebaseFunctions | *(none)* | Swift-only, no cinterop |
|
||||
| FirebaseMLModelDownloader | *(none)* | Swift-only, no cinterop |
|
||||
|
||||
**Note:** When `discoverClangModulesImplicitly = false` (recommended for Firebase), you must list every Clang module you import in `importedClangModules`. When `true` (default), `importedClangModules` is ignored — but this will fail for Firebase due to C++ transitive dependencies.
|
||||
|
||||
### Firebase Initialization
|
||||
|
||||
Ensure `GoogleService-Info.plist` is included in the iOS app target. In the app's entry point:
|
||||
|
||||
```swift
|
||||
import Firebase
|
||||
FirebaseApp.configure() // Must be called before using any Firebase service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Google Maps
|
||||
|
||||
Repository: `https://github.com/googlemaps/ios-maps-sdk.git`
|
||||
|
||||
**Key facts:**
|
||||
- **iOS 16+ only** — no macOS, tvOS, or watchOS support
|
||||
- **Xcode 16.0+** required
|
||||
- Must use `exact()` version — `from()` will fail to resolve
|
||||
- Single SPM product: `GoogleMaps` (wraps a binary xcframework via `GoogleMapsTarget`)
|
||||
- CocoaPods subspec `GoogleMaps/Maps` maps to the single `GoogleMaps` SPM product
|
||||
- Requires a Google Maps Platform API key configured in the iOS app
|
||||
- Check [releases](https://github.com/googlemaps/ios-maps-sdk/releases) for available SPM versions
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("GoogleMaps") { version = "10.10.0" }
|
||||
|
||||
// SwiftPM — use the exact same version as the pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/googlemaps/ios-maps-sdk.git",
|
||||
version = "10.10.0",
|
||||
products = listOf("GoogleMaps"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.GMSMapView
|
||||
import swiftPMImport.<group>.<module>.GMSCameraPosition
|
||||
import swiftPMImport.<group>.<module>.GMSMarker
|
||||
import swiftPMImport.<group>.<module>.GMSServices
|
||||
```
|
||||
|
||||
**iOS project requirement:** The API key must be set in the app delegate or SwiftUI app entry point:
|
||||
|
||||
```swift
|
||||
import GoogleMaps
|
||||
GMSServices.provideAPIKey("YOUR_API_KEY")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Google Sign-In
|
||||
|
||||
Repository: `https://github.com/google/GoogleSignIn-iOS.git`
|
||||
|
||||
**Key facts:**
|
||||
- **iOS 12+, macOS 10.15+** — broad platform support
|
||||
- Two SPM products: `GoogleSignIn` (core) and `GoogleSignInSwift` (SwiftUI support)
|
||||
- CocoaPods pods: `GoogleSignIn` and `GoogleSignInSwiftSupport`
|
||||
- Uses `from()` versioning (latest: 9.1.0)
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("GoogleSignIn") { version = "8.0.0" }
|
||||
|
||||
// SwiftPM — use same version as pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/google/GoogleSignIn-iOS.git",
|
||||
version = "8.0.0",
|
||||
products = listOf("GoogleSignIn"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.GIDSignIn
|
||||
import swiftPMImport.<group>.<module>.GIDSignInButton
|
||||
```
|
||||
|
||||
**iOS project requirement:** Add `GIDClientID` to `Info.plist` and configure the URL scheme for OAuth redirect. See [Google Sign-In iOS docs](https://developers.google.com/identity/sign-in/ios/start-integrating).
|
||||
|
||||
---
|
||||
|
||||
## LoremIpsum
|
||||
|
||||
Simple text generation library with direct mapping.
|
||||
|
||||
```kotlin
|
||||
// CocoaPods
|
||||
pod("LoremIpsum") { version = "2.0.1" }
|
||||
|
||||
// SwiftPM — use same version as pod
|
||||
swiftPackage(
|
||||
url = "https://github.com/lukaskubanek/LoremIpsum.git",
|
||||
version = "2.0.1",
|
||||
products = listOf("LoremIpsum"),
|
||||
)
|
||||
```
|
||||
|
||||
**Kotlin import:**
|
||||
```kotlin
|
||||
import swiftPMImport.<group>.<module>.LoremIpsum
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Pod Name | SPM Product | SPM Repository | Version Type | Platform | Notes |
|
||||
|----------|-------------|----------------|--------------|----------|-------|
|
||||
| FirebaseAnalytics | FirebaseAnalytics | firebase/firebase-ios-sdk.git | from() | All | |
|
||||
| FirebaseAuth | FirebaseAuth | firebase/firebase-ios-sdk.git | from() | All | |
|
||||
| FirebaseCore | FirebaseCore | firebase/firebase-ios-sdk.git | from() | All | |
|
||||
| FirebaseCrashlytics | FirebaseCrashlytics | firebase/firebase-ios-sdk.git | from() | All | Needs dSYM upload script |
|
||||
| FirebaseDatabase | FirebaseDatabase | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseDatabaseInternal |
|
||||
| FirebaseFirestore | FirebaseFirestore | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseFirestoreInternal |
|
||||
| FirebaseFunctions | FirebaseFunctions | firebase/firebase-ios-sdk.git | from() | All | Swift-only, no cinterop |
|
||||
| FirebaseMessaging | FirebaseMessaging | firebase/firebase-ios-sdk.git | from() | All | |
|
||||
| FirebaseRemoteConfig | FirebaseRemoteConfig | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseRemoteConfigInternal |
|
||||
| FirebaseStorage | FirebaseStorage | firebase/firebase-ios-sdk.git | from() | All | |
|
||||
| FirebasePerformance | FirebasePerformance | firebase/firebase-ios-sdk.git | from() | iOS/tvOS | |
|
||||
| FirebaseInAppMessaging | FirebaseInAppMessaging-Beta | firebase/firebase-ios-sdk.git | from() | iOS/tvOS | `-Beta` suffix, importedClangModules: FirebaseInAppMessagingInternal |
|
||||
| FirebaseAppDistribution | FirebaseAppDistribution-Beta | firebase/firebase-ios-sdk.git | from() | iOS only | `-Beta` suffix |
|
||||
| FirebaseABTesting | *(none)* | firebase/firebase-ios-sdk.git | — | All | Module-only, importedClangModules only |
|
||||
| FirebaseAILogic | FirebaseAI | firebase/firebase-ios-sdk.git | from() | All | Renamed, Swift-only |
|
||||
| GoogleMaps | GoogleMaps | googlemaps/ios-maps-sdk.git | exact() | iOS 16+ only | |
|
||||
| GoogleSignIn | GoogleSignIn | google/GoogleSignIn-iOS.git | from() | iOS 12+, macOS 10.15+ | |
|
||||
| GoogleSignInSwiftSupport | GoogleSignInSwift | google/GoogleSignIn-iOS.git | from() | iOS 12+, macOS 10.15+ | SwiftUI support |
|
||||
| LoremIpsum | LoremIpsum | lukaskubanek/LoremIpsum.git | from() | All | |
|
||||
|
||||
---
|
||||
|
||||
## KMP Wrapper Libraries with Bundled Cinterop Klibs
|
||||
|
||||
Some KMP libraries that wrap iOS SDKs ship pre-built cinterop klibs using the `cocoapods.*` package namespace. After migrating to SwiftPM, these `cocoapods.*` imports **must be preserved** — they resolve to the library's bundled klib, not to actual CocoaPods infrastructure.
|
||||
|
||||
### KMPNotifier
|
||||
|
||||
Repository: [https://github.com/mirzemehdi/KMPNotifier](https://github.com/mirzemehdi/KMPNotifier)
|
||||
Maven: `io.github.mirzemehdi:kmpnotifier`
|
||||
|
||||
**What it provides:** A KMP push notification library that wraps Firebase Cloud Messaging on iOS. The library bundles its own cinterop klib with namespace `cocoapods.FirebaseMessaging`, providing Kotlin bindings for `FIRMessaging`, `FIRMessagingAPNSTokenType`, and related classes.
|
||||
|
||||
**Impact on migration:**
|
||||
- When `swiftPMDependencies` generates cinterop bindings, it detects that `FirebaseMessaging` bindings already exist in KMPNotifier's klib and **skips generating new bindings** for that Clang module
|
||||
- `import cocoapods.FirebaseMessaging.FIRMessaging` must remain unchanged — do NOT replace with `swiftPMImport.*`
|
||||
- `FirebaseMessaging` should still be listed in `products` and `importedClangModules` for SPM linking, even though cinterop bindings won't be generated for it
|
||||
|
||||
**Verifying bundled klib contents:** Use `klib dump-metadata-signatures` to inspect what a library's klib provides ([docs](https://kotlinlang.org/docs/native-libraries.html#using-kotlin-native-compiler)):
|
||||
|
||||
```bash
|
||||
find ~/.gradle/caches -name "*.klib" -path "*kmpnotifier*" | head -1
|
||||
klib dump-metadata-signatures /path/to/cinterop.klib | grep "FIRMessaging"
|
||||
# Shows: cocoapods.FirebaseMessaging/FIRMessaging → confirms bundled klib
|
||||
```
|
||||
|
||||
**Example — project using both KMPNotifier and GoogleSignIn:**
|
||||
```kotlin
|
||||
// IOSDelegate.kt — after migration
|
||||
import cocoapods.FirebaseMessaging.FIRMessaging // KEEP — from kmpnotifier klib
|
||||
import cocoapods.FirebaseMessaging.FIRMessagingAPNSTokenType // KEEP — from kmpnotifier klib
|
||||
import swiftPMImport.com.example.app.GIDSignIn // REPLACE — direct cinterop
|
||||
```
|
||||
|
||||
### dev.gitlive/firebase-kotlin-sdk
|
||||
|
||||
Repository: [https://github.com/GitLiveApp/firebase-kotlin-sdk](https://github.com/GitLiveApp/firebase-kotlin-sdk)
|
||||
Maven: `dev.gitlive:firebase-auth`, `dev.gitlive:firebase-firestore`, `dev.gitlive:firebase-storage`, etc.
|
||||
|
||||
**What it provides:** Kotlin-first Firebase APIs for KMP. Unlike KMPNotifier, dev.gitlive libraries provide **high-level Kotlin APIs** — you typically don't use `cocoapods.*` imports directly. Instead, the Firebase pods were declared with `linkOnly = true` in CocoaPods to provide native linking only.
|
||||
|
||||
**Impact on migration:**
|
||||
|
||||
1. **Linker flags baked into published klibs.** The dev.gitlive klibs contain `-framework FirebaseCore`, `-framework FirebaseAuth`, etc. from the CocoaPods era. These persist when the consuming project switches to SPM. With SPM, Firebase frameworks land in per-product subdirectories (`$BUILT_PRODUCTS_DIR/FirebaseCore/FirebaseCore.framework`) that the K/N linker doesn't search automatically.
|
||||
|
||||
**Fix:** Add per-product `-F` linkerOpts to `build.gradle.kts`:
|
||||
```kotlin
|
||||
val builtProductsDir = System.getenv("BUILT_PRODUCTS_DIR")
|
||||
if (builtProductsDir != null) {
|
||||
listOf("FirebaseCore", "FirebaseAuth", "FirebaseCoreExtension",
|
||||
"FirebaseCoreInternal", "FirebaseCrashlytics", "FirebaseFirestore",
|
||||
"FirebaseFirestoreInternal", "FirebaseInstallations", "FirebaseMessaging",
|
||||
"FirebaseStorage", "GoogleDataTransport", "GoogleUtilities",
|
||||
"GTMSessionFetcher", "AppCheckCore", /* ... */).forEach { product ->
|
||||
linkerOpts("-F", "$builtProductsDir/$product")
|
||||
}
|
||||
}
|
||||
```
|
||||
Also add matching `FRAMEWORK_SEARCH_PATHS` in the Xcode project for both Debug and Release.
|
||||
|
||||
2. **Must use `isStatic = true`.** With a dynamic framework, the K/N linker creates `@rpath/FirebaseCore.framework/FirebaseCore` load instructions. Firebase SPM products are static libraries — their `.framework` bundles are not embedded in the app bundle. At runtime, `dyld` crashes with `Library not loaded`. Switching to `isStatic = true` embeds all symbols and defers unresolved framework flags to the final Xcode link.
|
||||
|
||||
3. **iOS test tasks may fail.** The K/N test runner cannot find Firebase frameworks outside of Xcode context. You may need to disable iOS test tasks:
|
||||
```kotlin
|
||||
tasks.matching {
|
||||
(it.name.contains("Ios") || it.name.contains("ios")) &&
|
||||
(it.name.contains("Test") || it.name.contains("test"))
|
||||
}.configureEach { enabled = false }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Researching Other Pods
|
||||
|
||||
For pods not listed here:
|
||||
|
||||
1. **Check GitHub repository** - Look for a `Package.swift` file in the repo
|
||||
2. **Check CocoaPods spec** - The `source` field often points to the Git URL
|
||||
3. **Search Swift Package Index** - https://swiftpackageindex.com/
|
||||
4. **Check library documentation** - Many libraries document SPM installation
|
||||
|
||||
### Finding the Clang Module Name
|
||||
|
||||
If you're unsure of the correct Clang module name:
|
||||
|
||||
1. Keep `discoverClangModulesImplicitly = true` (default)
|
||||
2. Run `./gradlew build`
|
||||
3. Check build errors for available class names
|
||||
4. Or check the library's `module.modulemap` file in its source
|
||||
|
||||
### Identifying Bundled Cinterop Klibs in Unknown Libraries
|
||||
|
||||
If you suspect a KMP library bundles its own cinterop klibs (common for libraries wrapping iOS SDKs), use the `klib` tool to inspect them ([docs](https://kotlinlang.org/docs/native-libraries.html#using-kotlin-native-compiler)):
|
||||
|
||||
```bash
|
||||
# Find klibs from a specific library in Gradle caches
|
||||
find ~/.gradle/caches -name "*.klib" -path "*libraryName*"
|
||||
|
||||
# Dump API signatures to see what namespaces and classes are provided
|
||||
klib dump-metadata-signatures /path/to/library.klib | grep "cocoapods\."
|
||||
|
||||
# If output shows cocoapods.* entries, the library bundles cinterop klibs
|
||||
# Those cocoapods.* imports must be preserved after migration
|
||||
```
|
||||
|
||||
Indicators that a library may bundle cinterop klibs:
|
||||
- The project has `linkOnly = true` pod declarations for the same native SDK
|
||||
- The library's documentation mentions CocoaPods integration or cinterop
|
||||
- The library provides Kotlin APIs for an iOS SDK (Firebase, Maps, etc.)
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
Do NOT bump dependency versions during migration — use the exact same version from the `cocoapods {}` block. Always:
|
||||
1. **Use the same version.** If the pod was `version = "1.4.1"`, the SPM package must be `exact("1.4.1")`. Bumping versions can break cinterop APIs and introduce issues unrelated to the migration.
|
||||
2. CocoaPods `version = "X.Y.Z"` (without `~>`) is an exact pin — use `exact("X.Y.Z")` in SPM, not `from()`. Only use `from()` when the CocoaPods spec used optimistic versioning (`~>`).
|
||||
3. Check the GitHub releases page to confirm the exact version is available as an SPM release
|
||||
4. Test thoroughly after migration
|
||||
@@ -0,0 +1,293 @@
|
||||
# SwiftPM Import DSL Reference
|
||||
|
||||
Complete reference for the `swiftPMDependencies {}` DSL in Kotlin Multiplatform.
|
||||
|
||||
## Basic Structure
|
||||
|
||||
`swiftPackage()` and `localSwiftPackage()` are annotated with `@ExperimentalKotlinGradlePluginApi` (warning level). Add the opt-in at the top of `build.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
kotlin {
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
swiftPMDependencies {
|
||||
// Deployment versions
|
||||
iosMinimumDeploymentTarget = "16.0"
|
||||
macosMinimumDeploymentTarget = "13.0"
|
||||
tvosMinimumDeploymentTarget = "16.0"
|
||||
watchosMinimumDeploymentTarget = "9.0"
|
||||
|
||||
// Module discovery (default: true)
|
||||
discoverClangModulesImplicitly = true
|
||||
|
||||
// Package declarations
|
||||
swiftPackage(...)
|
||||
localSwiftPackage(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Package Declaration
|
||||
|
||||
The DSL has two API forms. **Use the simple string API** for most packages. Use the typed API only when you need `exact()`, `branch()`, `revision()`, or platform constraints.
|
||||
|
||||
### Simple API (Preferred)
|
||||
|
||||
Plain strings for URL, version, and products. The `version` parameter maps to a **minimum version** (`from()`) internally. The `importedClangModules` defaults to the `products` list automatically.
|
||||
|
||||
```kotlin
|
||||
swiftPackage(
|
||||
url = "https://github.com/owner/repo.git",
|
||||
version = "1.0.0", // Equivalent to from("1.0.0") — minimum version
|
||||
products = listOf("ProductName", "AnotherProduct"),
|
||||
)
|
||||
```
|
||||
|
||||
### Typed API (Advanced)
|
||||
|
||||
Use when you need exact version pinning, branch tracking, platform constraints, or explicit Clang module control:
|
||||
|
||||
```kotlin
|
||||
swiftPackage(
|
||||
url = url("https://github.com/owner/repo.git"),
|
||||
version = exact("1.0.0"),
|
||||
products = listOf(
|
||||
product("ProductName"),
|
||||
product("PlatformSpecific", platforms = setOf(iOS()))
|
||||
),
|
||||
importedClangModules = listOf("CustomClangModuleName"),
|
||||
)
|
||||
```
|
||||
|
||||
### Remote Package (Swift Package Registry)
|
||||
|
||||
```kotlin
|
||||
swiftPackage(
|
||||
repository = id("scope.package-name"),
|
||||
version = from("1.0.0"),
|
||||
products = listOf(product("ProductName")),
|
||||
packageName = "package-name",
|
||||
)
|
||||
```
|
||||
|
||||
### Local Package
|
||||
|
||||
```kotlin
|
||||
localSwiftPackage(
|
||||
directory = layout.projectDirectory.dir("../LocalPackage"),
|
||||
products = listOf("LocalPackage"),
|
||||
)
|
||||
```
|
||||
|
||||
To create a new local package (e.g., a Swift/ObjC wrapper around a Swift-only library):
|
||||
|
||||
```shell
|
||||
cd /path/to/shared
|
||||
mkdir LocalPackage && cd LocalPackage
|
||||
swift package init --type library --name LocalPackage
|
||||
```
|
||||
|
||||
Then use it in Kotlin:
|
||||
```kotlin
|
||||
// src/appleMain/kotlin/useLocalPackage.kt
|
||||
import swiftPMImport.<group>.<module>.HelloFromLocalPackage
|
||||
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
fun useLocalPackage() {
|
||||
HelloFromLocalPackage().hello()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version Specification
|
||||
|
||||
| Syntax | Description | Use Case |
|
||||
|--------|-------------|----------|
|
||||
| `version = "1.0.0"` (simple API) | Minimum version — equivalent to `from("1.0.0")` | Most packages |
|
||||
| `version = exact("1.0")` | Exact version pin | Strict dependencies, migration |
|
||||
| `version = from("1.0")` | Minimum version (explicit) | Same as simple string |
|
||||
| `version = branch("name")` | Git branch | Development, testing |
|
||||
| `version = revision("hash")` | Git commit hash | Pinning specific commits |
|
||||
| `version = range("1.0", "2.0")` | Version range | Constraining upper bound |
|
||||
|
||||
**Important for migration:** The simple string `version = "X.Y.Z"` resolves to a minimum version (`from()`), which may pull a newer version than what was in CocoaPods. For exact version preservation during migration, use the typed API: `version = exact("X.Y.Z")`.
|
||||
|
||||
---
|
||||
|
||||
## Product Configuration
|
||||
|
||||
### Simple API
|
||||
|
||||
```kotlin
|
||||
products = listOf("FirebaseAnalytics", "FirebaseAuth")
|
||||
```
|
||||
|
||||
With the simple API, `importedClangModules` defaults to the same list as `products`. This works when product names match Clang module names.
|
||||
|
||||
### Typed API — Platform Constraints
|
||||
|
||||
For packages that only support certain platforms, use the typed `product()` function:
|
||||
|
||||
```kotlin
|
||||
products = listOf(
|
||||
product("GoogleMaps", platforms = setOf(iOS())) // iOS only
|
||||
)
|
||||
```
|
||||
|
||||
Available platforms: `iOS()`, `macOS()`, `tvOS()`, `watchOS()`
|
||||
|
||||
### Typed API — Per-Product Clang Module Override
|
||||
|
||||
```kotlin
|
||||
products = listOf(
|
||||
product("FirebaseDatabase", importedClangModules = setOf("FirebaseDatabaseInternal"))
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Import Configuration
|
||||
|
||||
### Automatic Discovery (Default)
|
||||
|
||||
By default, `discoverClangModulesImplicitly = true`. SwiftPM import automatically discovers and imports all accessible Clang modules.
|
||||
|
||||
**IMPORTANT:** When `discoverClangModulesImplicitly = true`, the `importedClangModules` parameter is ignored. Only set `importedClangModules` when `discoverClangModulesImplicitly = false`.
|
||||
|
||||
**IMPORTANT for Firebase:** Set `discoverClangModulesImplicitly = false` when using Firebase. Firebase's transitive C++ dependencies (gRPC, abseil, leveldb, BoringSSL) contain Clang modules that fail cinterop generation. Disable implicit discovery and explicitly list only the Firebase modules you need in `importedClangModules`.
|
||||
|
||||
### Explicit Module Import
|
||||
|
||||
When automatic discovery is disabled and the Clang module name differs from the product name, use the typed API:
|
||||
|
||||
```kotlin
|
||||
swiftPMDependencies {
|
||||
discoverClangModulesImplicitly = false // Disable auto-discovery
|
||||
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.6.0"),
|
||||
products = listOf(
|
||||
product("FirebaseAnalytics"),
|
||||
product("FirebaseFirestore")
|
||||
),
|
||||
importedClangModules = listOf(
|
||||
"FirebaseAnalytics",
|
||||
"FirebaseCore",
|
||||
"FirebaseFirestoreInternal" // Note: different from product name
|
||||
),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use importedClangModules
|
||||
|
||||
| Scenario | Use importedClangModules? |
|
||||
|----------|---------------------|
|
||||
| Simple API, product name = Clang module name | No (auto-defaulted from products) |
|
||||
| Product name != Clang module name | Yes (typed API) |
|
||||
| Multiple modules per product | Yes (typed API) |
|
||||
| Using discoverClangModulesImplicitly = false | Yes (typed API) |
|
||||
|
||||
---
|
||||
|
||||
## Deployment Versions
|
||||
|
||||
Set minimum deployment targets for each platform:
|
||||
|
||||
```kotlin
|
||||
swiftPMDependencies {
|
||||
iosMinimumDeploymentTarget = "16.0"
|
||||
macosMinimumDeploymentTarget = "13.0"
|
||||
tvosMinimumDeploymentTarget = "16.0"
|
||||
watchosMinimumDeploymentTarget = "9.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
}
|
||||
|
||||
group = "org.example.myproject"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
kotlin {
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
// Framework configuration (moved from cocoapods block)
|
||||
listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "SharedModule"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
swiftPMDependencies {
|
||||
iosMinimumDeploymentTarget = "16.0"
|
||||
|
||||
// Simple API — most packages
|
||||
swiftPackage(
|
||||
url = "https://github.com/lukaskubanek/LoremIpsum.git",
|
||||
version = "2.0.1",
|
||||
products = listOf("LoremIpsum"),
|
||||
)
|
||||
|
||||
// Simple API — Google Maps
|
||||
swiftPackage(
|
||||
url = "https://github.com/googlemaps/ios-maps-sdk.git",
|
||||
version = "10.3.0",
|
||||
products = listOf("GoogleMaps"),
|
||||
)
|
||||
|
||||
// Local package
|
||||
localSwiftPackage(
|
||||
directory = layout.projectDirectory.dir("LocalWrapper"),
|
||||
products = listOf("LocalWrapper"),
|
||||
)
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transitive Dependencies
|
||||
|
||||
SwiftPM dependencies are handled automatically. When you run Kotlin/Native tests or link a framework, the Kotlin Gradle Plugin will provision necessary machine code from transitive SwiftPM dependencies. This behavior is automatic.
|
||||
|
||||
You can optionally declare transitive dependencies explicitly to pin specific versions:
|
||||
|
||||
```kotlin
|
||||
swiftPMDependencies {
|
||||
// Main dependency
|
||||
swiftPackage(
|
||||
url = "https://github.com/firebase/firebase-ios-sdk.git",
|
||||
version = "12.5.0",
|
||||
products = listOf("FirebaseAnalytics"),
|
||||
)
|
||||
|
||||
// Transitive dependency with explicit version pin
|
||||
swiftPackage(
|
||||
url = url("https://github.com/apple/swift-protobuf.git"),
|
||||
version = exact("1.32.0"),
|
||||
products = listOf(product("SwiftProtobuf")),
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,145 @@
|
||||
# Migration Report Template
|
||||
|
||||
After migration (whether successful or not), write a comprehensive `MIGRATION_REPORT.md` in the project root. This document serves both as human-readable documentation and as structured input for AI agents analyzing the migration.
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
# Migration Report: CocoaPods to SwiftPM Import
|
||||
|
||||
**Project:** <project name>
|
||||
**Module migrated:** <module name>
|
||||
**Date:** <YYYY-MM-DD>
|
||||
**Kotlin version:** <old version> → <new version>
|
||||
**Status:** <Completed successfully | Completed with workarounds | Failed — see Errors>
|
||||
|
||||
---
|
||||
|
||||
## Pre-Migration State
|
||||
|
||||
### CocoaPods Dependencies
|
||||
|
||||
| Pod | Version | Mode | Notes |
|
||||
|-----|---------|------|-------|
|
||||
| <PodName> | <version> | Regular / linkOnly | <e.g., cinterop used in Kotlin code> |
|
||||
|
||||
### Framework Configuration
|
||||
|
||||
- **baseName:** <name>
|
||||
- **isStatic:** <true/false> → <true/false after migration>
|
||||
- **Deployment target:** <version>
|
||||
|
||||
### Kotlin Files Using `cocoapods.*` Imports
|
||||
|
||||
| File | Imports |
|
||||
|------|---------|
|
||||
| <path> | `cocoapods.<Module>.<Class>`, ... |
|
||||
|
||||
### Non-KMP CocoaPods
|
||||
|
||||
<List any pods in Podfile not managed by KMP, or "None">
|
||||
|
||||
### Atypical Project Configuration
|
||||
|
||||
<Document anything unusual found in Phase 1 that required special handling:
|
||||
EmbedAndSign disablers, commented-out build phases, custom Gradle tasks
|
||||
hooking into CocoaPods, non-standard framework configs, missing `group`
|
||||
property, etc. If nothing unusual, write "Standard configuration.">
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 2: Gradle Configuration
|
||||
|
||||
<List exact changes made to settings.gradle.kts, libs.versions.toml,
|
||||
root build.gradle.kts, gradle.properties. Include before/after snippets
|
||||
for non-trivial changes.>
|
||||
|
||||
### Phase 3: swiftPMDependencies
|
||||
|
||||
<Show the complete `swiftPMDependencies {}` block added.
|
||||
Document decisions: why `discoverClangModulesImplicitly = false`,
|
||||
which `importedClangModules` were chosen and why, framework search
|
||||
paths added, static/dynamic choice, etc.>
|
||||
|
||||
### Phase 4: Import Transformations
|
||||
|
||||
<Table of import changes. Clearly mark any preserved `cocoapods.*` imports.>
|
||||
|
||||
| File | Before | After | Source |
|
||||
|------|--------|-------|--------|
|
||||
| <path> | `cocoapods.<Mod>.<Cls>` | `swiftPMImport.<grp>.<mod>.<Cls>` | swiftPMImport cinterop |
|
||||
| <path> | `cocoapods.<Mod>.<Cls>` | `cocoapods.<Mod>.<Cls>` (unchanged) | <library> bundled klib |
|
||||
|
||||
### Phase 5: iOS Project Reconfiguration
|
||||
|
||||
<Document: Option A or B, integration commands run, sandboxing fix,
|
||||
Crashlytics dSYM script update, any manual pbxproj edits.>
|
||||
|
||||
### Phase 6: CocoaPods Removal
|
||||
|
||||
<List everything removed: plugin, cocoapods block, gradle.properties
|
||||
entries, custom tasks, podspec files, Podfile changes.>
|
||||
|
||||
### Phase 7: Verification
|
||||
|
||||
<Build commands run and their outcomes. Include the final successful
|
||||
build command or note that verification was deferred to the user.>
|
||||
|
||||
---
|
||||
|
||||
## Errors Encountered
|
||||
|
||||
<For each error, use this structure:>
|
||||
|
||||
### Error #N: <Short title>
|
||||
|
||||
**Phase:** <which phase>
|
||||
**Symptom:** <exact error message or behavior>
|
||||
**Root cause:** <why it happened>
|
||||
**Fix:** <what was done to resolve it>
|
||||
**Generalizable:** <Yes/No — is this likely to affect other projects?>
|
||||
|
||||
---
|
||||
|
||||
## Non-Trivial Decisions
|
||||
|
||||
<Document decisions that required judgment, not just following the guide:
|
||||
- Why a specific `importedClangModules` list was chosen
|
||||
- Why `isStatic` was changed (or kept)
|
||||
- Why certain `cocoapods.*` imports were preserved
|
||||
- Framework search paths added and how the product list was determined
|
||||
- Any trade-offs made (e.g., disabling iOS tests)>
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
<Complete list of files modified, created, or deleted during migration.
|
||||
Group by type: Gradle files, Kotlin sources, Xcode project files, other.>
|
||||
|
||||
### Gradle Files
|
||||
- <path> — <brief description of change>
|
||||
|
||||
### Kotlin Sources
|
||||
- <path> — <brief description of change>
|
||||
|
||||
### Xcode Project Files
|
||||
- <path> — <brief description of change>
|
||||
|
||||
### Created
|
||||
- <path> — <what it is>
|
||||
|
||||
### Deleted
|
||||
- <path> — <what it was>
|
||||
```
|
||||
|
||||
## Writing Guidelines
|
||||
|
||||
- **Be specific.** Include actual file paths, class names, error messages. Avoid vague statements like "updated the config."
|
||||
- **Show before/after.** For non-trivial changes, include code snippets of what was changed and why.
|
||||
- **Explain the "why."** Every error and non-trivial decision should include root cause analysis, not just the fix.
|
||||
- **Mark preserved `cocoapods.*` imports clearly.** These are the most confusing aspect of the migration for future readers — explain exactly why each one was kept and which library provides the bundled klib.
|
||||
- **Flag generalizable issues.** Mark errors that are likely to affect other projects so this report can improve the migration tooling.
|
||||
- **Keep it machine-parseable.** Use consistent markdown headings, tables, and the `Error #N` format so AI agents can extract structured data.
|
||||
@@ -0,0 +1,606 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
Common issues and solutions when migrating from CocoaPods to SwiftPM.
|
||||
|
||||
## Gradle Issues
|
||||
|
||||
### Import Not Found After Migration
|
||||
|
||||
**Symptom:** `Unresolved reference` errors for classes that worked with CocoaPods
|
||||
|
||||
**Solution:** The import namespace follows a specific pattern:
|
||||
|
||||
```
|
||||
swiftPMImport.<group>.<module>.<ClassName>
|
||||
```
|
||||
|
||||
**Steps to fix:**
|
||||
1. Check `group` property in build.gradle.kts
|
||||
2. Replace `-` with `.` in both group and module names
|
||||
3. Run `./gradlew build` to see available classes in error messages
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
// If group = "org.jetbrains.kotlin.firebase-sample" and module = "kotlin-library"
|
||||
// Import becomes:
|
||||
import swiftPMImport.org.jetbrains.kotlin.firebase.sample.kotlin.library.FIRAnalytics
|
||||
// ^ ^ ^
|
||||
// dashes become dots --------+------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Gradle Sync Fails
|
||||
|
||||
**Symptom:** IDE fails to sync project after adding swiftPMDependencies
|
||||
|
||||
**Solution:**
|
||||
1. Invalidate caches: File > Invalidate Caches > Invalidate and Restart
|
||||
2. Run `./gradlew --refresh-dependencies`
|
||||
3. Check all repository declarations include JetBrains Maven
|
||||
|
||||
---
|
||||
|
||||
## Linker Issues
|
||||
|
||||
### Missing Symbols / Linker Errors
|
||||
|
||||
**Symptom:** `Undefined symbols for architecture` errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Run the linkage integration task** (one-time, not a build phase):
|
||||
```bash
|
||||
./gradlew :moduleName:integrateLinkagePackage
|
||||
```
|
||||
|
||||
2. **Verify SPM package is linked in Xcode:**
|
||||
- Open project in Xcode
|
||||
- Check Package Dependencies section
|
||||
- Ensure `KotlinMultiplatformLinkedPackage` is present
|
||||
|
||||
3. **Check framework configuration** — `isStatic = true` is recommended. While `isStatic = false` can work, dynamic frameworks have known edge cases with SwiftPM import (linker errors, dyld crashes, duplicate class warnings). It is required with `dev.gitlive:firebase-*` — see below.
|
||||
|
||||
---
|
||||
|
||||
### "No such module" in Xcode
|
||||
|
||||
**Symptom:** Xcode can't find the Kotlin module
|
||||
|
||||
**Solution:**
|
||||
1. Clean Xcode build folder: Shift+Cmd+K
|
||||
2. Re-run integration:
|
||||
```bash
|
||||
./gradlew :moduleName:integrateLinkagePackage
|
||||
```
|
||||
3. Restart Xcode completely
|
||||
4. Re-open the correct Xcode project file (`.xcodeproj` if all CocoaPods were removed, `.xcworkspace` if non-KMP CocoaPods remain)
|
||||
|
||||
---
|
||||
|
||||
## Build Phase Issues
|
||||
|
||||
### Build Phase Order Problems
|
||||
|
||||
**Symptom:** Swift compilation fails because Kotlin framework isn't ready
|
||||
|
||||
**Solution:** Ensure "Compile Kotlin" runs BEFORE "Compile Sources":
|
||||
|
||||
1. Open Xcode project
|
||||
2. Select app target > Build Phases
|
||||
3. Drag "Compile Kotlin" phase above "Compile Sources"
|
||||
|
||||
---
|
||||
|
||||
### Script Sandboxing Errors
|
||||
|
||||
**Symptom:** Gradle task `checkSandboxAndWriteProtection` fails during Xcode build:
|
||||
|
||||
```
|
||||
Execution failed for task ':moduleName:checkSandboxAndWriteProtection'.
|
||||
> User Script Sandboxing Enabled in Xcode Project
|
||||
```
|
||||
|
||||
Or build scripts can't access files or run Gradle.
|
||||
|
||||
**Cause:** Xcode 16+ enables User Script Sandboxing by default. The Gradle build phase needs to write to the project directory, which sandboxing prevents.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Disable via command line:
|
||||
```bash
|
||||
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' /path/to/iosApp/*.xcodeproj/project.pbxproj
|
||||
```
|
||||
If the setting is not present in the `.pbxproj` (Xcode defaults to YES without an explicit entry), open the project in Xcode instead.
|
||||
|
||||
2. Or disable in Xcode: select app target → Build Settings → Build Options → set "User Script Sandboxing" to NO
|
||||
|
||||
3. **Important:** After changing the setting, stop the Gradle daemon:
|
||||
```bash
|
||||
./gradlew --stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Task Issues
|
||||
|
||||
### `integrateEmbedAndSign` Skipped or Does Nothing
|
||||
|
||||
**Symptom:** Running `integrateEmbedAndSign` completes without errors but the Xcode project is not modified. The `embedAndSignAppleFrameworkForXcode` build phase is not added or remains commented out.
|
||||
|
||||
**Cause:** The project has code that disables `EmbedAndSign` tasks. Common patterns:
|
||||
|
||||
```kotlin
|
||||
// In root or module build.gradle.kts
|
||||
project.gradle.taskGraph.whenReady {
|
||||
allTasks.filter { it::class.simpleName?.contains("EmbedAndSign") == true }.forEach {
|
||||
it.enabled = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This was a CocoaPods-era workaround that inadvertently disables `integrateEmbedAndSign`.
|
||||
|
||||
**Solution:** Remove the disabler code from `build.gradle.kts`, then re-run the integration command.
|
||||
|
||||
---
|
||||
|
||||
### `embedAndSignAppleFrameworkForXcode` Commented Out in Build Phase
|
||||
|
||||
**Symptom:** Xcode build succeeds but produces no Kotlin framework. The app crashes at runtime with missing module errors.
|
||||
|
||||
**Cause:** The Gradle invocation in the Xcode build phase script was commented out (prefixed with `#`) — possibly a pre-existing state from before migration.
|
||||
|
||||
**Solution:** Open `project.pbxproj` and uncomment the Gradle invocation:
|
||||
|
||||
```diff
|
||||
-#./gradlew :moduleName:embedAndSignAppleFrameworkForXcode
|
||||
+./gradlew :moduleName:embedAndSignAppleFrameworkForXcode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Third-Party KMP Libraries with Bundled Klibs
|
||||
|
||||
### `cocoapods.*` Class Not Found After Converting to `swiftPMImport.*`
|
||||
|
||||
**Symptom:** After replacing `import cocoapods.FirebaseMessaging.FIRMessaging` with `import swiftPMImport.<group>.<module>.FIRMessaging`, the build fails with `Unresolved reference 'FIRMessaging'`. Other swiftPMImport classes (e.g., `GIDSignIn`) resolve fine.
|
||||
|
||||
**Cause:** A third-party KMP library (e.g., [KMPNotifier](https://github.com/mirzemehdi/KMPNotifier) — `io.github.mirzemehdi:kmpnotifier`) bundles its own pre-built cinterop klib with the `cocoapods.FirebaseMessaging` namespace. The swiftPMDependencies cinterop generator detects these existing bindings and **deliberately skips** generating new bindings for that Clang module to avoid duplicate symbols. The `swiftPMImport.*` bindings for that module simply don't exist.
|
||||
|
||||
**Solution:** Revert the affected imports back to `cocoapods.*`:
|
||||
|
||||
```kotlin
|
||||
// These resolve to the third-party library's bundled klib, NOT actual CocoaPods
|
||||
import cocoapods.FirebaseMessaging.FIRMessaging
|
||||
import cocoapods.FirebaseMessaging.FIRMessagingAPNSTokenType
|
||||
```
|
||||
|
||||
The `cocoapods` prefix here is just a package namespace embedded in the library's published artifact — no CocoaPods infrastructure is needed at runtime.
|
||||
|
||||
**How to identify bundled klibs in advance:** Check if the project depends on KMP libraries that wrap iOS SDKs. Known libraries: [KMPNotifier](https://github.com/mirzemehdi/KMPNotifier) (bundles `cocoapods.FirebaseMessaging`). Also check for `linkOnly = true` pod declarations — this indicates the pod was only needed for linking while a KMP library provided the actual bindings.
|
||||
|
||||
**Inspecting klib contents:** Use `klib dump-metadata-signatures` to verify which classes a klib provides ([docs](https://kotlinlang.org/docs/native-libraries.html#using-kotlin-native-compiler)):
|
||||
|
||||
```bash
|
||||
# Find the klib
|
||||
find ~/.gradle/caches -name "*.klib" -path "*kmpnotifier*" | head -1
|
||||
|
||||
# Dump and search for the class in question
|
||||
klib dump-metadata-signatures /path/to/cinterop.klib | grep "FIRMessaging"
|
||||
# Output shows: cocoapods.FirebaseMessaging.FIRMessaging → confirms bundled klib
|
||||
```
|
||||
|
||||
You can also compare before/after migration by dumping the swiftPMImport klib:
|
||||
```bash
|
||||
# After build, find the swiftPMImport klib
|
||||
find . -name "*.klib" -path "*swiftPMImport*" | head -1
|
||||
|
||||
# Verify which classes are available
|
||||
klib dump-metadata-signatures /path/to/swiftPMImport.klib | grep "FIRMessaging"
|
||||
# Empty output = class NOT in swiftPMImport (must use cocoapods.* import)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## dev.gitlive/firebase-kotlin-sdk Issues
|
||||
|
||||
### `framework 'FirebaseCore' not found` (K/N Linker)
|
||||
|
||||
**Symptom:** Kotlin/Native linker fails with:
|
||||
```
|
||||
ld: framework 'FirebaseCore' not found
|
||||
```
|
||||
or similar errors for `FirebaseAuth`, `FirebaseFirestore`, etc. The Gradle compilation succeeds but the link step fails.
|
||||
|
||||
**Cause:** [firebase-kotlin-sdk](https://github.com/GitLiveApp/firebase-kotlin-sdk) (`dev.gitlive:firebase-*`) was published with CocoaPods-era cinterop klibs. These klibs have `-framework FirebaseCore`, `-framework FirebaseAuth`, etc. baked into their linker metadata. With CocoaPods, those frameworks were in `Pods/` on the search path. With SPM, they land in per-product subdirectories (`$BUILT_PRODUCTS_DIR/FirebaseCore/FirebaseCore.framework`) that the K/N linker doesn't search.
|
||||
|
||||
**Solution (two-part):**
|
||||
|
||||
**Part A — Gradle linkerOpts:**
|
||||
```kotlin
|
||||
iosTarget.binaries.framework {
|
||||
val builtProductsDir = System.getenv("BUILT_PRODUCTS_DIR")
|
||||
if (builtProductsDir != null) {
|
||||
listOf(
|
||||
"FirebaseCore", "FirebaseAuth", "FirebaseCoreExtension",
|
||||
"FirebaseCoreInternal", "FirebaseCrashlytics", "FirebaseFirestore",
|
||||
"FirebaseFirestoreInternal", "FirebaseInstallations", "FirebaseMessaging",
|
||||
"FirebaseStorage", "GoogleDataTransport", "GoogleUtilities",
|
||||
"GTMSessionFetcher", "AppCheckCore", "AppAuth", "GTMAppAuth",
|
||||
).forEach { product ->
|
||||
linkerOpts("-F", "$builtProductsDir/$product")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `if (builtProductsDir != null)` guard ensures `./gradlew :moduleName:compileKotlinIosSimulatorArm64` works without Xcode (compilation doesn't link).
|
||||
|
||||
**Part B — Xcode FRAMEWORK_SEARCH_PATHS:**
|
||||
|
||||
Add matching entries in `project.pbxproj` for both Debug and Release `buildSettings`:
|
||||
```
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(BUILT_PRODUCTS_DIR)/FirebaseCore",
|
||||
"$(BUILT_PRODUCTS_DIR)/FirebaseAuth",
|
||||
"$(BUILT_PRODUCTS_DIR)/FirebaseCoreExtension",
|
||||
// ... same list as Part A ...
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `dyld: Library not loaded: @rpath/FirebaseCore.framework/FirebaseCore` (Runtime Crash)
|
||||
|
||||
**Symptom:** The Gradle build and Xcode compilation both succeed, but the app crashes at launch with:
|
||||
```
|
||||
dyld: Library not loaded: @rpath/FirebaseCore.framework/FirebaseCore
|
||||
Referenced from: .../ComposeApp.framework/ComposeApp
|
||||
```
|
||||
|
||||
**Cause:** The KMP framework is **dynamic** (`isStatic = false` or default). The K/N linker creates `LC_LOAD_DYLIB` entries (`@rpath/FirebaseCore.framework/FirebaseCore`). Firebase SPM products are **static** libraries — their `.framework` bundles exist in `$BUILT_PRODUCTS_DIR` during build but are NOT embedded in the app bundle. At runtime, `dyld` searches `@rpath` and finds nothing.
|
||||
|
||||
**Solution:** Switch to a static framework:
|
||||
|
||||
```kotlin
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "Shared"
|
||||
isStatic = true // Required when using dev.gitlive:firebase-* with SPM
|
||||
}
|
||||
```
|
||||
|
||||
With a static framework, all symbols are embedded in the `.a` archive. No `LC_LOAD_DYLIB` entries are created. Unresolved `-framework` flags from dev.gitlive klibs are deferred to the final Xcode app link, where `KotlinMultiplatformLinkedPackage` provides them.
|
||||
|
||||
**After switching to static, also:**
|
||||
1. Re-run `integrateLinkagePackage` — regenerates `Package.swift` with `type: .none` (static)
|
||||
2. Remove any "Embed Frameworks" copy phase for the KMP framework — static frameworks must NOT be embedded
|
||||
3. Add linker flags previously resolved by the K/N linker (e.g., `-framework Accelerate`, `-weak_framework CoreML`) to `OTHER_LDFLAGS` in the Xcode project
|
||||
|
||||
---
|
||||
|
||||
## Firebase-Specific Issues
|
||||
|
||||
### cinterop Failures on C++ Modules (gRPC, abseil, leveldb, BoringSSL)
|
||||
|
||||
**Symptom:** Build fails with cinterop errors on modules like `grpc`, `absl`, `leveldb`, `openssl_grpc`, or other C++ transitive dependencies of Firebase.
|
||||
|
||||
**Cause:** `discoverClangModulesImplicitly = true` (the default) makes Kotlin attempt cinterop on every Clang module in the dependency graph, including C++ modules that are not compatible.
|
||||
|
||||
**Solution:** Set `discoverClangModulesImplicitly = false` and explicitly list only the Firebase Clang modules you need:
|
||||
|
||||
```kotlin
|
||||
swiftPMDependencies {
|
||||
discoverClangModulesImplicitly = false
|
||||
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.6.0"),
|
||||
products = listOf(product("FirebaseAnalytics"), /* ... */),
|
||||
importedClangModules = listOf("FirebaseAnalytics", "FirebaseCore", /* ... */),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
See [common-pods-mapping.md](common-pods-mapping.md) for the full importedClangModules reference.
|
||||
|
||||
---
|
||||
|
||||
### Firebase Classes Not Found (Wrong Clang Module Name)
|
||||
|
||||
**Symptom:** `Unresolved reference` for Firebase classes like `FIRDatabase`, `FIRRemoteConfig`, `FIRFirestore`, `FIRInAppMessaging` even though the product is listed.
|
||||
|
||||
**Cause:** Several Firebase products expose ObjC headers through Clang modules whose names differ from the SPM product name. Using the product name in `importedClangModules` won't find the headers.
|
||||
|
||||
**Solution:** Use the correct internal Clang module names:
|
||||
|
||||
| SPM Product | Correct importedClangModules entry |
|
||||
|---|---|
|
||||
| FirebaseDatabase | `FirebaseDatabaseInternal` |
|
||||
| FirebaseFirestore | `FirebaseFirestoreInternal` |
|
||||
| FirebaseInAppMessaging-Beta | `FirebaseInAppMessagingInternal` |
|
||||
| FirebaseRemoteConfig | `FirebaseRemoteConfigInternal` |
|
||||
|
||||
---
|
||||
|
||||
### FirebaseFirestore Import Errors
|
||||
|
||||
**Symptom:** Can't import FIRFirestore classes
|
||||
|
||||
**Cause:** Firestore's Clang module name differs from product name. The internal Clang module exposed to Objective-C is `FirebaseFirestoreInternal`, not `FirebaseFirestore`.
|
||||
|
||||
**Solution:** Add explicit importedClangModules:
|
||||
|
||||
```kotlin
|
||||
swiftPackage(
|
||||
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
|
||||
version = from("12.6.0"),
|
||||
products = listOf(product("FirebaseFirestore")),
|
||||
importedClangModules = listOf("FirebaseFirestoreInternal"), // Required
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Firebase Crashlytics: dSYM Upload Script Broken After Migration
|
||||
|
||||
**Symptom:** Crash reports don't appear in Firebase Console after migrating to SPM. Or the build phase fails with "No such file" errors referencing `${PODS_ROOT}/FirebaseCrashlytics/upload-symbols`.
|
||||
|
||||
**Cause:** The CocoaPods-era dSYM upload script references `${PODS_ROOT}` which no longer exists. The SPM equivalent is at a different path.
|
||||
|
||||
**Solution:** Update the existing "Run Script" build phase (or add one at the END if none exists):
|
||||
|
||||
```bash
|
||||
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"
|
||||
```
|
||||
|
||||
With input files:
|
||||
```
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
|
||||
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
|
||||
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
|
||||
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
|
||||
```
|
||||
|
||||
Also set **Debug Information Format** to `DWARF with dSYM File` for all build configurations in Build Settings.
|
||||
|
||||
---
|
||||
|
||||
### Firebase Beta Products: SPM Name Differs
|
||||
|
||||
**Symptom:** `FirebaseInAppMessaging` or `FirebaseAppDistribution` not found as SPM product
|
||||
|
||||
**Cause:** Beta products have a `-Beta` suffix in SPM.
|
||||
|
||||
**Solution:** Use the correct SPM product name:
|
||||
- `FirebaseInAppMessaging` → `FirebaseInAppMessaging-Beta`
|
||||
- `FirebaseAppDistribution` → `FirebaseAppDistribution-Beta`
|
||||
|
||||
---
|
||||
|
||||
### dyld Crash When Mixing Firebase Across CocoaPods and SPM
|
||||
|
||||
**Symptom:** App crashes at launch with a dyld error like:
|
||||
```
|
||||
Symbol not found: _OBJC_CLASS_$_FIRFirestore
|
||||
```
|
||||
or similar `_OBJC_CLASS_$_FIR*` symbol-not-found errors. The Gradle build and Xcode compilation both succeed, but the app crashes at runtime.
|
||||
|
||||
**Cause:** Some Firebase pods were migrated to SPM while others remained in CocoaPods. All Firebase products share transitive dependencies (gRPC, abseil, leveldb, BoringSSL, nanopb). Having both package managers link these transitive dependencies causes duplicate/conflicting symbols that the dynamic linker cannot resolve.
|
||||
|
||||
**Solution:** Migrate **all** Firebase pods to SPM at once. This includes Swift-only pods (FirebaseAI, FirebaseFunctions, FirebaseMLModelDownloader) that Kotlin cannot use directly — add them as `products` entries without `importedClangModules`:
|
||||
|
||||
```kotlin
|
||||
products = listOf(
|
||||
// ObjC pods used by Kotlin:
|
||||
product("FirebaseAnalytics"),
|
||||
product("FirebaseAuth"),
|
||||
// ...
|
||||
// Swift-only pods (no importedClangModules needed):
|
||||
product("FirebaseAI"),
|
||||
product("FirebaseFunctions"),
|
||||
),
|
||||
```
|
||||
|
||||
After adding new products, re-run `integrateLinkagePackage` to regenerate the linkage Swift package.
|
||||
|
||||
---
|
||||
|
||||
### Firebase Initialization Fails at Runtime
|
||||
|
||||
**Symptom:** App crashes on Firebase initialization
|
||||
|
||||
**Solution:**
|
||||
1. Ensure `GoogleService-Info.plist` is in iOS app target
|
||||
2. Call `FIRApp.configure()` before using any Firebase service
|
||||
3. Check Firebase console for configuration issues
|
||||
|
||||
---
|
||||
|
||||
## Google Maps Issues
|
||||
|
||||
### GoogleMaps Version Not Found
|
||||
|
||||
**Symptom:** SPM can't resolve GoogleMaps package
|
||||
|
||||
**Solution:** GoogleMaps requires exact version matching:
|
||||
|
||||
```kotlin
|
||||
swiftPackage(
|
||||
url = url("https://github.com/googlemaps/ios-maps-sdk.git"),
|
||||
version = exact("10.6.0"), // Must use exact(), not from()
|
||||
products = listOf(
|
||||
product("GoogleMaps", platforms = setOf(iOS()))
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
Check [releases page](https://github.com/googlemaps/ios-maps-sdk/releases) for valid versions.
|
||||
|
||||
---
|
||||
|
||||
## KSP (Kotlin Symbol Processing) Compatibility
|
||||
|
||||
### KSP after updating Kotlin version
|
||||
|
||||
KSP should generally work with the target Kotlin version without any changes. Do NOT update KSP as part of the migration — it is out of scope.
|
||||
|
||||
If KSP does fail (unlikely), the issue is unrelated to the CocoaPods-to-SwiftPM migration itself. Present the error to the user and let them decide how to handle it separately.
|
||||
|
||||
---
|
||||
|
||||
## Manual Integration Command Discovery
|
||||
|
||||
If the xcodebuild approach in Phase 5.1 fails, discover paths manually and run integration tasks directly:
|
||||
|
||||
```bash
|
||||
# Find iOS project directory (contains Podfile)
|
||||
IOS_DIR=$(dirname "$(find . -name "Podfile" -type f | head -1)")
|
||||
|
||||
# Find .xcodeproj (exclude Pods.xcodeproj) - use realpath for absolute path
|
||||
XCODEPROJ=$(realpath "$(find "$IOS_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | grep -v Pods | head -1)")
|
||||
|
||||
# Find KMP module with swiftPMDependencies (module directory name)
|
||||
KMP_MODULE=$(grep -rl "swiftPMDependencies" --include="build.gradle.kts" . | head -1 | xargs dirname | xargs basename)
|
||||
|
||||
XCODEPROJ_PATH="$XCODEPROJ" \
|
||||
GRADLE_PROJECT_PATH=":$KMP_MODULE" \
|
||||
./gradlew ":$KMP_MODULE:integrateEmbedAndSign" ":$KMP_MODULE:integrateLinkagePackage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual CocoaPods Deintegration from pbxproj
|
||||
|
||||
If `pod deintegrate` is not available, manually remove these CocoaPods references from `project.pbxproj`:
|
||||
|
||||
- `Pods_<target>.framework` build file and file reference
|
||||
- `Pods-<target>.debug.xcconfig` / `Pods-<target>.release.xcconfig` file references
|
||||
- `Pods` group and `Frameworks` group (if it only contained the Pods framework)
|
||||
- `[CP] Check Pods Manifest.lock` shell script build phase
|
||||
- `[CP] Embed Pods Frameworks` shell script build phase
|
||||
- `baseConfigurationReference` lines pointing to Pods xcconfig files
|
||||
|
||||
---
|
||||
|
||||
## Manual Xcode Integration Steps
|
||||
|
||||
If the automatic `integrateEmbedAndSign` / `integrateLinkagePackage` tasks fail, set up the Xcode project manually:
|
||||
|
||||
1. Open `.xcodeproj` (or `.xcworkspace` if non-KMP CocoaPods remain)
|
||||
2. Add "Compile Kotlin" run script phase BEFORE "Compile Sources":
|
||||
```bash
|
||||
cd "$SRCROOT/.."
|
||||
./gradlew :moduleName:embedAndSignAppleFrameworkForXcode
|
||||
```
|
||||
3. Set `ENABLE_USER_SCRIPT_SANDBOXING = NO` (Build Settings → Build Options → User Script Sandboxing)
|
||||
4. Run `./gradlew --stop` to restart the Gradle daemon after changing sandboxing
|
||||
5. Add local package: `../moduleName/KotlinMultiplatformLinkedPackage`
|
||||
|
||||
---
|
||||
|
||||
## When Build Fails After Migration
|
||||
|
||||
**Do NOT revert the migration as a first response.** Instead:
|
||||
|
||||
1. **Read the full error log** — identify the actual failure type (Gradle resolution, import not found, linker error, Xcode build phase)
|
||||
2. **Re-check each migration phase** — walk through Phases 2-6 and verify each step was applied. Common mistakes:
|
||||
- Missing JetBrains Maven repo in `settings.gradle.kts`
|
||||
- Wrong `group` or module name in import namespace (dashes not converted to dots)
|
||||
- `cocoapods {}` block or plugin not fully removed (Phase 6)
|
||||
- Wrong Xcode project file opened (`.xcodeproj` when non-KMP CocoaPods remain and `.xcworkspace` is needed, or vice versa)
|
||||
- `isStatic = true` missing from framework config (required with dev.gitlive or similar CocoaPods-era wrapper klibs)
|
||||
- `integrateLinkagePackage` not run
|
||||
- EmbedAndSign disabler code not removed (prevents `integrateEmbedAndSign`)
|
||||
- `embedAndSignAppleFrameworkForXcode` commented out in Xcode build phase
|
||||
- `cocoapods.*` imports replaced that should have been preserved (bundled klib from third-party library)
|
||||
3. **Consult the sections above** for specific error patterns
|
||||
4. **If unsure, present options to the user** — describe what the logs show, list possible causes, and let the user decide
|
||||
|
||||
---
|
||||
|
||||
## Rollback Instructions (Last Resort)
|
||||
|
||||
Only revert if analysis above does not resolve the issue:
|
||||
|
||||
### Step 1: Restore Git Files
|
||||
|
||||
```bash
|
||||
# Restore CocoaPods files (adjust path if iOS project is not in iosApp/)
|
||||
git checkout -- "**/Podfile" "**/Podfile.lock"
|
||||
git checkout -- *.podspec
|
||||
git checkout -- **/build.gradle.kts
|
||||
git checkout -- **/src/**/*.kt
|
||||
```
|
||||
|
||||
### Step 2: Restore CocoaPods in build.gradle.kts
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
kotlin("native.cocoapods") // Re-add
|
||||
}
|
||||
|
||||
kotlin {
|
||||
cocoapods {
|
||||
// Restore original configuration
|
||||
}
|
||||
// Remove swiftPMDependencies block
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Restore Kotlin Imports
|
||||
|
||||
Change all imports back:
|
||||
```kotlin
|
||||
// FROM:
|
||||
import swiftPMImport.group.module.ClassName
|
||||
|
||||
// TO:
|
||||
import cocoapods.PodName.ClassName
|
||||
```
|
||||
|
||||
### Step 4: Reinstall CocoaPods
|
||||
|
||||
```bash
|
||||
# Navigate to directory containing Podfile (adjust path as needed)
|
||||
cd <ios-project-directory> # e.g., iosApp/, ios/, or project root
|
||||
pod install
|
||||
```
|
||||
|
||||
### Step 5: Open Workspace
|
||||
|
||||
Open `*.xcworkspace` (not .xcodeproj) from the iOS project directory in Xcode.
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
If issues persist:
|
||||
|
||||
1. **Check sample projects:**
|
||||
- [kmp-with-cocoapods-compose-sample (spm_import branch)](https://github.com/Kotlin/kmp-with-cocoapods-compose-sample/tree/spm_import)
|
||||
- [kmp-with-cocoapods-firebase-sample (spm_import branch)](https://github.com/Kotlin/kmp-with-cocoapods-firebase-sample/tree/spm_import)
|
||||
|
||||
2. **Run verbose build:**
|
||||
```bash
|
||||
./gradlew build --info
|
||||
```
|
||||
|
||||
3. **Check generated files:**
|
||||
- Look in `moduleName/KotlinMultiplatformLinkedPackage/` for Package.swift
|
||||
|
||||
4. **Inspect klib contents** using the `klib` tool ([docs](https://kotlinlang.org/docs/native-libraries.html#using-kotlin-native-compiler)):
|
||||
```bash
|
||||
# Dump all API signatures from a klib
|
||||
klib dump-metadata-signatures /path/to/library.klib
|
||||
|
||||
# Search for specific classes
|
||||
klib dump-metadata-signatures /path/to/library.klib | grep "ClassName"
|
||||
|
||||
# Compare before/after — find klibs in build output
|
||||
find . -name "*.klib" -path "*swiftPMImport*" # new swiftPMImport klibs
|
||||
find ~/.gradle/caches -name "*.klib" -path "*libraryName*" # third-party klibs
|
||||
```
|
||||
This is particularly useful for verifying which classes are available in the swiftPMImport klib vs. bundled in third-party dependency klibs.
|
||||
Reference in New Issue
Block a user