mcpField
K
kotlin-tooling-cocoapods-spm-migration

kotlin-tooling-cocoapods-spm-migration

github.com/kotlin开发1.1k1.9k 次安装7 周前更新
代码仓库

关于 kotlin-tooling-cocoapods-spm-migration

帮助 Kotlin Multiplatform 项目从 CocoaPods Gradle 插件迁移到 `swiftPMDependencies {}` DSL。流程先并行配置 SwiftPM 与 CocoaPods,再指导项目重配、清理和验证。

主要功能

  1. 01分析 CocoaPods 与项目配置
  2. 02在保留 CocoaPods 时添加 SwiftPM
  3. 03转换 Kotlin 导入语句
  4. 04重新配置 iOS 项目
  5. 05移除 CocoaPods 并验证构建

适用场景

  1. 01替换 KMP 项目中的 CocoaPods 依赖
  2. 02将 iOS 框架依赖迁移到 SwiftPM
  3. 03核对导入与 Podfile 映射
  4. 04迁移后验证 Gradle 和 Xcode 构建

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-cocoapods-spm-migration

安装后在 Claude Code 中输入/kotlin-tooling-cocoapods-spm-migration确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
cocoapods-extras-patterns.md
common-pods-mapping.md
dsl-reference.md
migration-report-template.md
troubleshooting.md

详细介绍

面向 KMP 的 CocoaPods 到 SwiftPM 迁移

将 Kotlin Multiplatform 项目从 kotlin("native.cocoapods") 迁移到 swiftPMDependencies {} DSL。

要求

  • Kotlin:2.4.0-Beta2 或更高版本(首个公开支持 swiftPMDependencies 的版本,可在 Maven Central 获取)
  • Xcode:16.4 或 26.0+
  • iOS 部署目标:建议 16.0+

迁移概述

重要:在第 6 阶段之前,保留 cocoapods {} 块和插件处于启用状态。迁移时,先在现有 CocoaPods 配置旁添加 swiftPMDependencies {},重新配置 Xcode,然后再移除 CocoaPods。

阶段操作
1分析现有 CocoaPods 配置
2更新 Gradle 配置(仓库、Kotlin 版本)
3在现有 cocoapods {} 旁添加 swiftPMDependencies {}
4转换 Kotlin 导入语句
5重新配置 iOS 项目并解除 CocoaPods 集成
6从 Gradle 中移除 CocoaPods 插件
7验证 Gradle 构建和 Xcode 项目构建
8编写 MIGRATION_REPORT.md

阶段 1:迁移前分析

1.0 验证项目能否构建

开始迁移之前,找出要迁移的模块,并确认它能够成功编译。

  1. 找到使用 CocoaPods 的模块——查找包含 cocoapods 的 build.gradle.kts 文件:

    grep -rl "cocoapods" --include="build.gradle.kts" .
    

    从路径中提取模块名称(例如,./shared/build.gradle.kts → 模块名称为 shared)。注意:多个模块可能使用 CocoaPods,请记录所有模块。通常,只有生成链接到 iOS 应用的框架的模块需要配置 swiftPMDependencies;其他模块只需移除 CocoaPods(第 6 阶段)。

  2. 编译 Kotlin 代码——运行该模块的 Kotlin 编译任务,以验证 Kotlin 源代码能够编译:

    ./gradlew :moduleName:compileKotlinIosSimulatorArm64
    

    将 moduleName 替换为模块目录名称(例如,:shared:compileKotlinIosSimulatorArm64)。这比完整的 build 更快(完整构建还会运行 release 链接),并且足以验证 Kotlin 代码是否正确。

  3. 构建 iOS 应用(可选)——尝试找到 Xcode 项目并进行构建,以确认整个应用能够编译:

    # 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
    

    如果用户想跳过 Xcode 构建,或找不到 Xcode 项目,则无需构建,直接继续即可——第 2 步的 Kotlin 编译足以继续迁移。

  4. 如果 Kotlin 编译失败,请让用户选择:

    • 提供正确的 Gradle 命令,以验证该模块能否构建;或
    • 确认该模块当前处于可正常工作的状态,可以安全地继续

    如果用户确认但未提供构建命令,请记录迁移前的构建无法验证,并在迁移结束时(第 7 阶段)提醒用户。

1.0a 确认支持 Swift 导入的 Kotlin 版本

从 gradle/libs.versions.toml(或 build.gradle.kts)中读取当前 Kotlin 版本。

如果项目已使用 Kotlin 2.4.0-Beta2 或更高版本 → 记录该版本并跳过第 2.1 阶段(无需更改版本)。

如果项目使用较早的 Kotlin 版本 → 第 2.1 阶段会将其升级到 2.4.0-Beta2(首个公开支持 swiftPMDependencies 的版本,可在 Maven Central 获取——无需自定义仓库)。提醒用户:“⚠️ Kotlin 版本跨度较大——跨次要版本升级可能会引入与此次迁移无关的破坏性变更。建议先更新版本并确认构建成功,然后重新运行此次迁移。”用户确认后即可继续。

1.1 检查已弃用的 CocoaPods 变通属性

在 gradle.properties 中搜索已弃用的属性:

kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true

此属性是一个变通方案(参见 KT-64096),用于同时使用 embedAndSign 和 CocoaPods 依赖项的项目。它会抑制有关不受支持配置的错误;此类配置可能导致运行时崩溃或符号重复。迁移到 SwiftPM 导入后,不再需要此属性,且必须在第 6 阶段将其移除。如果发现该属性,请记录下来。

1.2 检查是否有禁用 EmbedAndSign 的代码

在所有 build.gradle.kts 文件中搜索禁用 EmbedAndSign 任务的代码(例如,TaskGraph.whenReady 过滤器、tasks.matching 代码块)。这是 CocoaPods 时代的变通方案,会破坏迁移,因为它也会禁用 integrateEmbedAndSign(Phase 5 需要此任务)。记录所有此类代码——必须在 Phase 6 移除,也可能需要提前移除。有关具体模式,请参阅 troubleshooting.md § "integrateEmbedAndSign Skipped"。

1.3 检查是否有捆绑 cinterop klib 的第三方 KMP 库

一些 KMP 库会附带预构建的 cinterop klib,其包命名空间为 cocoapods.*。迁移后,swiftPMDependencies cinterop 生成器会检测到这些现有绑定,并跳过为相应 Clang 模块生成新绑定,以避免重复。这意味着这些模块的 cocoapods.* 导入必须保持原样——它们解析到的是第三方库捆绑的 klib,而非实际的 CocoaPods。

已知捆绑 cocoapods.* klib 的库:

库Maven 构件捆绑的 klib 命名空间提供的类
KMPNotifierio.github.mirzemehdi:kmpnotifiercocoapods.FirebaseMessagingFIRMessaging、FIRMessagingAPNSTokenType 等

**检测方法:**搜索 Gradle 依赖声明,查找已知库,然后将其捆绑的命名空间与步骤 4 中找到的 import cocoapods.* 语句进行交叉比对。标记所有匹配项——这些导入不会在 Phase 4 转换。

如果不确定第三方 KMP 库是否捆绑了 cinterop klib,请检查项目中是否有该库的 linkOnly = true pod 依赖——这是该库为这些类提供自身 klib 的有力迹象。

有关如何检查 klib 内容并验证捆绑的绑定,请参阅 troubleshooting.md § "Third-Party KMP Libraries with Bundled Klibs"。

查找并记录:

  1. CocoaPods 配置 - 在 build.gradle.kts 文件中搜索 cocoapods
  2. Pod 依赖项 - 从 cocoapods {} 块中提取 Pod 名称和版本
  3. 框架配置 - 记录 cocoapods.framework {} 中的 baseName、isStatic 和部署目标
  4. linkOnly Pod - 记录声明了 linkOnly = true 的 Pod。常见的模式有两种:
    • KMP 包装库(例如 dev.gitlive:firebase-*):包装库提供 Kotlin API,而 Pod 仅用于链接。详情及相关影响请参阅 common-pods-mapping.md。
    • 多模块项目:由于子模块已为该 Pod 提供 cinterop 绑定,使用该子模块的模块会声明 linkOnly = true。在 SwiftPM 中,swiftPackage() 声明应仅放在直接使用该 Pod 的子模块中。使用该子模块的模块不得重新声明相同的包——只需一个不包含这些包的 swiftPMDependencies {} 块(如果所有 Pod 都是 linkOnly,也可以是空块)。导入命名空间的影响:使用该子模块的模块导入来自子模块 swiftPMDependencies 的 SPM 类时,导入路径会使用子模块的组名和模块名作为命名空间(请参阅阶段 4 的导入命名空间公式)。
  5. Kotlin 导入 - 查找所有 import cocoapods.* 语句。与步骤 1.3 交叉核对,确定哪些导入来自捆绑的 klib(必须保留),哪些来自直接 Pod cinterop(必须转换)。
  6. 将 Pod 映射到 SPM - 请参阅 common-pods-mapping.md
  7. 定位 iOS 项目目录 - 找到包含 Podfile 和 .xcworkspace 的目录:
    find . -name "Podfile" -type f
    
    记录此路径(例如 iosApp/、ios/ 或项目根目录)——阶段 5 需要用到
  8. 检查是否存在非 KMP 的 CocoaPods 依赖 - 确定项目是否使用 CocoaPods 管理 KMP 以外的依赖。这会影响阶段 5 的清理策略。
  9. 将 Podfile 与 cocoapods {} 块交叉核对 - 解析 Podfile,并将其中的 Pod 条目与 Gradle cocoapods {} 块中声明的 Pod 进行比较。记录所有存在于 Podfile 中、但未列在 cocoapods {} 中的依赖。这些仅在 Podfile 中声明的依赖仍会通过 CocoaPods 链接到应用,必须迁移到 swiftPMDependencies——遗漏它们会导致运行时出现难以排查的链接错误。
  10. 检查 Xcode 构建阶段 - 打开 .xcodeproj 中的 project.pbxproj,搜索 Gradle 构建阶段脚本。检查其中是否存在但被注释掉(前缀为 #)的 embedAndSignAppleFrameworkForXcode。如果已注释,必须在阶段 5 取消注释——integrateEmbedAndSign 任务可能会自动处理,也可能不会。
  11. 检查是否已有 Crashlytics dSYM 上传脚本 - 如果使用 FirebaseCrashlytics,请在 project.pbxproj 中搜索 dSYM 上传 Shell 脚本阶段。记录其当前路径(CocoaPods 时代的脚本会引用 ${PODS_ROOT}/FirebaseCrashlytics/upload-symbols)。阶段 5 必须将其更新为 SPM 路径。
  12. 识别构建脚本中与 CocoaPods 相关的额外配置 - 在所有 build.gradle.kts 文件中搜索标准 cocoapods {} 块以外的 CocoaPods 变通方案(例如挂接到 podInstall 的自定义任务、对 Pods.xcodeproj 的修补、podspec 元数据、extraSpecAttributes、noPodspec() 等)。完整模式列表请参阅 cocoapods-extras-patterns.md。记录所有发现——这些内容将在阶段 6 处理。

阶段 2:Gradle 配置

**重要范围说明:**本次迁移期间请勿升级 Gradle wrapper 版本、更新 KSP 或更新任何其他依赖项。这些属于单独事项,不在本次范围内。只更改以下列出的内容。

2.1 更新 Kotlin 版本

如果项目已使用 Kotlin 2.4.0-Beta2 或更高版本(已在 Phase 1.0a 中记录),请跳过此步骤。

在 gradle/libs.versions.toml 中更新为 2.4.0-Beta2(或支持 Swift Import 的最新可用版本):

[versions]
kotlin = "2.4.0-Beta2"

2.4.0-Beta2 已在 Maven Central 上提供,无需自定义仓库。


Phase 3:添加 swiftPMDependencies(保留 CocoaPods)

暂时不要移除 cocoapods {} 代码块或 kotlin("native.cocoapods") 插件。 在现有 CocoaPods 配置旁添加 swiftPMDependencies {}。

3.1 添加 group 属性
group = "org.example.myproject"  // Required for import namespace

Compose Resources 警告: 如果项目使用 Compose Multiplatform 资源(org.jetbrains.compose 插件或 compose.resources),group 属性也会用作生成的资源访问器(例如 Res.string.*、Res.drawable.*)的命名空间。如果 build.gradle.kts 中已存在 group,请勿更改。如果这是首次添加 group,请提醒用户,项目中现有 Compose 资源访问器调用点的命名空间将发生变化,可能需要更新。

3.2 在 cocoapods 旁添加 swiftPMDependencies 块

对于每个 Pod 依赖,添加对应的 SwiftPM 包声明。使用 common-pods-mapping.md 将每个 Pod 映射到其 SPM 包 URL、产品名称和 importedClangModules。

**保留版本:**迁移期间切勿升级依赖版本。使用 cocoapods {} 块中指定的完全相同版本。更改版本可能会解析到不同的库构建版本,导致 cinterop API 出现问题(符号被移除、签名发生变化),并引入与迁移本身无关的问题。

CocoaPods 版本规范SPM 对应写法示例
version = "1.2.3"(精确)version = "1.2.3"(简单)或 exact("1.2.3")(类型化)pod("GoogleMaps") { version = "10.3.0" } → version = "10.3.0"
version = "~> 1.2"(乐观)version = "1.2.0"(简单)或 from("1.2.0")(类型化)pod("FirebaseAuth") { version = "~> 12.5" } → version = "12.5.0"
未指定版本询问用户要锁定哪个版本询问用户要使用哪个版本

**两种 API 形式:**该 DSL 提供简单字符串 API 和类型化 API。大多数包请使用简单字符串 API:

swiftPackage(url = "https://github.com/owner/repo.git", version = "1.0.0", products = listOf("ProductName"))

简单 API 会自动将 importedClangModules 默认为 products 列表。仅当需要精确锁定版本、平台约束或显式控制 Clang 模块时,才使用类型化 API(带有 url()、exact()、product() 包装器)。类型化 API 详见 dsl-reference.md。

关键概念:products = SPM 产品名称(控制链接)。importedClangModules = 用于 cinterop 绑定的 Clang 模块名称(仅当 discoverClangModulesImplicitly = false 时使用)。discoverClangModulesImplicitly 默认为 true(为所有 Clang 模块生成绑定);当传递依赖中的 C/C++ 模块导致 cinterop 失败时(Firebase、gRPC),将其设为 false,然后显式列出所需模块。

**重要:**SPM 产品名称和 Clang 模块名称并不总是相同。请务必查阅 common-pods-mapping.md 获取正确值。

**仅存在于 Podfile 中的依赖:**如果 Phase 1 的步骤 9 发现某些依赖存在于 Podfile 中,但不在 Gradle 的 cocoapods {} 块中,也必须将它们作为 products 条目添加到 swiftPMDependencies。尽管 KMP 模块未声明这些依赖,CocoaPods 仍将它们链接到应用中,因此应用构建时可能需要它们。查找每个仅存在于 Podfile 中的 Pod 对应的 SPM 包 URL,并将其作为 swiftPackage() 添加,至少包含其 products。如果通过 cinterop 使用了其中任何 Pod(检查是否存在引用它们的 import cocoapods.* 语句),还要添加 importedClangModules。

不要让同一库套件同时使用 CocoaPods 和 SPM。共享仓库的库(例如所有 Firebase 产品)会共享传递依赖。部分产品通过 CocoaPods 链接、其余产品通过 SPM 链接,会导致符号重复或冲突,并在运行时引发 dyld 崩溃。迁移此类套件时,请一次性将该套件中的所有 Pod 转移到 SPM,包括 Kotlin 不会直接使用的纯 Swift Pod。纯 Swift Pod 作为 products 条目添加(无需 importedClangModules)。添加新产品后,重新运行 integrateLinkagePackage 以重新生成链接关系 Swift 包。

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 将框架配置移出 cocoapods 块

如果 cocoapods 块包含 framework {} 配置,请将其移至每个目标上的 binaries API。建议设置 isStatic = true——动态框架在通过 SwiftPM 导入时存在已知边缘情况,可能导致链接器错误、dyld 崩溃或重复类警告:

listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { iosTarget ->
    iosTarget.binaries.framework { baseName = "Shared"; isStatic = true }
}

如果 cocoapods.framework {} 块包含 export(project(...)) 或 transitiveExport = true,请在新的 binaries.framework {} 块中保留这些配置——对于导出子模块的多模块项目,它们至关重要。

3.4 处理 dev.gitlive/firebase-kotlin-sdk 及类似的 CocoaPods 时代 KMP 封装库

如果项目使用 dev.gitlive:firebase-* 或类似的 KMP 封装库,还需要执行以下两个步骤:

A. 切换到 isStatic = true — 动态框架 + Firebase SPM 会导致运行时 dyld 崩溃。切换后:重新运行 integrateLinkagePackage,移除任何「Embed Frameworks」复制阶段,并将链接器标志移至 OTHER_LDFLAGS。

B. 添加框架搜索路径 — 在 build.gradle.kts 中添加条件式 -F linkerOpts,并在 Xcode 项目中添加对应的 FRAMEWORK_SEARCH_PATHS。

代码片段和完整产品列表请参阅 common-pods-mapping.md § dev.gitlive 和 troubleshooting.md。

3.5 添加选择启用注解

swiftPackage() 和 localSwiftPackage() DSL 函数带有 @ExperimentalKotlinGradlePluginApi 注解。添加此选择启用,以抑制编译器警告:

@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)

将此内容放在每个调用 swiftPackage() 或 localSwiftPackage() 的 build.gradle.kts 文件顶部。

还要为 Kotlin 源文件添加 cinterop 选择启用:

kotlin.compilerOptions {
    optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
}

完整的 DSL 参考请参阅 dsl-reference.md。


阶段 4:更新 Kotlin 源代码

导入命名空间公式
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)

命名空间使用声明模块的 group+name,而不是导入模块的。 这是 Agent 最常犯的错误。当模块 A 依赖模块 B,且模块 B 声明了 swiftPMDependencies 时,模块 A 使用模块 B 的 group 和模块名导入 SPM 类,而不是模块 A 的。

转换示例 — 单模块
// 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
转换示例 — 多模块(linkOnly pod)

当使用方模块通过 pod("GoogleMaps") { linkOnly = true } 声明依赖,而子模块提供 cinterop 绑定时:

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

导入路径不会使用 composeApp 的 group(org.jetbrains.kotlin.compose.sample)。它使用声明模块的标识,因为 cinterop 绑定是在该模块中生成的。

导入扁平化: Clang 模块名(例如 FirebaseFirestoreInternal、FirebaseAuth)会从导入路径中消失——无论类来自哪个库,都会扁平化到相同的 swiftPMImport.<group>.<module> 前缀下。例如,cocoapods.FirebaseAuth.FIRAuth 和 cocoapods.FirebaseFirestoreInternal.FIRFirestore 都会变为 swiftPMImport.<group>.<module>.FIRAuth 和 swiftPMImport.<group>.<module>.FIRFirestore。

保留捆绑的 Klib 导入

重要: 不要替换解析到第三方 KMP 库捆绑的 cinterop klib 的 cocoapods.* 导入(在阶段 1 步骤 1.3 中识别)。这些导入必须保持原样——cocoapods 前缀是已发布库 klib 中的包命名空间,并非实际的 CocoaPods 依赖。swiftPMDependencies cinterop 生成器会跳过依赖 klib 已提供的模块,因此这些类对应的 swiftPMImport.* 会导致「Unresolved reference」错误。

示例(项目使用 KMPNotifier):

// KEEP — resolves to kmpnotifier's bundled cinterop klib
import cocoapods.FirebaseMessaging.FIRMessaging
批量替换

使用正则表达式在所有 Kotlin 源文件中查找并替换,但排除阶段 1 步骤 1.3 中识别的导入。在多模块项目中,针对每个模块分别使用该模块的 group 和名称运行替换:

Find:    cocoapods\.\w+\.
Replace: swiftPMImport.<declaring.module.group>.<declaring.module.name>.

批量替换后,手动还原所有应保留的 cocoapods.* 导入(来自捆绑的 klib)。

查找正确的导入路径: 运行 ./gradlew :moduleName:compileKotlinIosSimulatorArm64——错误信息会显示可用的类。


阶段 5:重新配置 iOS 项目

5.1 获取迁移命令

构建 CocoaPods 工作区以获取迁移命令:

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'

构建输出中会包含类似这样的命令:

XCODEPROJ_PATH='/path/to/project/iosApp.xcodeproj' GRADLE_PROJECT_PATH=':shared' '/path/to/project/gradlew' -p '/path/to/project' ':shared:integrateEmbedAndSign' ':shared:integrateLinkagePackage'

运行此命令。它会修改 .xcodeproj,以便在构建期间触发 embedAndSignAppleFrameworkForXcode。integrateLinkagePackage 是一次性设置,无需将其添加为构建阶段。如果跳过了 integrateEmbedAndSign,请检查是否存在 EmbedAndSign 禁用项(第 1 阶段第 1.2 步)——先将其移除,然后重新运行。

**确认 embedAndSignAppleFrameworkForXcode 已启用:**运行集成后,检查 project.pbxproj 中的构建阶段脚本。如果 embedAndSignAppleFrameworkForXcode 被注释掉(前缀为 #),请取消注释。

integrateLinkagePackage 任务会在 <iosDir>/ 下生成 KotlinMultiplatformLinkedPackage/,这是一个本地 Swift 包,它会镜像你的 products 列表,并确保 SPM 库链接到最终二进制文件中。

运行集成任务后,请在 .xcodeproj 中禁用 User Script Sandboxing(ENABLE_USER_SCRIPT_SANDBOXING = NO)。Xcode 16+ 默认启用此功能,会阻止 Gradle 构建阶段写入项目目录:

sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' "$XCODEPROJ_PATH/project.pbxproj"

如果缺少此设置(Xcode 默认值为 YES),请在应用目标的 buildSettings 部分添加 ENABLE_USER_SCRIPT_SANDBOXING = NO;。然后重启 Gradle 守护进程:./gradlew --stop

**替代方案(如果 xcodebuild 方法失败):**请参阅 troubleshooting.md 中的 §“手动查找集成命令”,了解用于查找路径并直接运行集成任务的备用脚本。

5.2 更新 Crashlytics dSYM 上传脚本(如适用)

如果项目使用 FirebaseCrashlytics,并且包含 dSYM 上传运行脚本阶段(在第 1 阶段第 11 步中确认),请将脚本路径从 ${PODS_ROOT}/FirebaseCrashlytics/upload-symbols 更新为 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"。完整脚本和输入文件列表请参阅 troubleshooting.md 中的 §“Firebase Crashlytics:dSYM 上传脚本”以及 common-pods-mapping.md。

5.3 解除 CocoaPods 集成

选项 A:完全解除集成(如果 CocoaPods 仅用于 KMP 依赖项):

删除文件之前,运行 git status --short 并确认路径。如果不确定,请先将文件移至备份位置,而不是立即删除。

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

此清理代码段独立完整,不会假定你当前的 shell 中仍有早前一次性迁移命令使用的 XCODEPROJ_PATH 或 GRADLE_PROJECT_PATH。

如果无法使用 pod deintegrate,请参阅 troubleshooting.md 中的 §“手动从 pbxproj 解除 CocoaPods 集成”,了解需要移除的完整引用列表。还要从 .gitignore 中移除 Pods/,并删除 .xcworkspace 目录。

选项 B:部分移除(如果仍有其他非 KMP 的 CocoaPods 依赖项):

仅从 Podfile 中移除 KMP pod 行,然后重新运行 pod install:

target 'iosApp' do
  # Remove this line:
  pod 'shared', :path => '../shared'
  # Keep other non-KMP pods
end
cd /path/to/iosApp && pod install

**提示:**也可以考虑将剩余的 pods 迁移到 SPM——大多数常用 iOS 库都原生支持 SPM。在 Xcode 中通过“File → Add Package Dependencies”添加它们,然后在所有 pods 都替换后彻底解除 CocoaPods 集成。

5.4 手动集成(自动集成失败时)

请参阅 troubleshooting.md 中的 §“手动 Xcode 集成步骤”,了解包含 5 个步骤的手动设置(构建阶段、沙盒、链接包)。


第 6 阶段:从 Gradle 中移除 CocoaPods

现在 iOS 项目已重新配置,请从所有使用了 CocoaPods 的模块中移除 CocoaPods 插件和代码块(而不仅仅是主要模块):

6.1 移除 CocoaPods 插件

从每个使用了 CocoaPods 的模块的 plugins {} 中移除 kotlin("native.cocoapods") 或 alias(libs.plugins.kotlinCocoapods):

plugins {
    // REMOVE: kotlin("native.cocoapods")  or  alias(libs.plugins.kotlinCocoapods)
    alias(libs.plugins.kotlinMultiplatform)  // Keep
}

如果所有模块都已迁移,还要从 gradle/libs.versions.toml 中移除 kotlinCocoapods 插件条目:

[plugins]
# REMOVE: kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
6.2 移除 cocoapods 块

从 build.gradle.kts 中删除整个 cocoapods { ... } 块。Phase 3 中添加的 swiftPMDependencies {} 块和 binaries.framework {} 配置会取代它。还要从模块目录中删除所有生成的 .podspec 文件(例如 shared/shared.podspec)——这些文件由 CocoaPods 插件生成,不再需要。

6.3 移除已弃用的 gradle.properties 条目

如果在 Phase 1.1 中发现了以下条目,请从 gradle.properties 中移除:

# REMOVE — no longer needed after migrating away from CocoaPods (KT-64096)
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true
6.4 清理 CocoaPods 相关的其他内容

检查 Phase 1 步骤 12 中发现的其他内容。Podspec 元数据、noPodspec()、CocoaPods 任务钩子和 Pods.xcodeproj 补丁代码都可以安全移除,无需咨询用户。非标准 pod 配置(extraOpts、moduleName)、自定义 cinterop defFile 设置以及 CocoaPods 专用编译器/链接器标志需要分析——如果不确定 SPM 是否会自动处理,请咨询用户。

完整的分类列表和示例请参见 cocoapods-extras-patterns.md。


Phase 7:验证

在应用成功构建之前,绝不要停止。 此阶段是迭代式的——如果任何步骤失败,请诊断错误并修复(参见 troubleshooting.md,并重新检查 Phase 2–6),然后重新运行失败的步骤。重复此过程,直到构建成功,或确认问题明显超出迁移范围(例如,迁移前就存在的 bug 或无关的工具问题)。在构建成功之前,绝不要编写迁移报告(Phase 8)。

7.1 编译 Kotlin 代码

编译已迁移的模块,以验证 Kotlin 源代码正确无误:

./gradlew :moduleName:compileKotlinIosSimulatorArm64

如果编译因未解析的引用而失败,请检查导入转换(Phase 4)和 SwiftPM 依赖声明(Phase 3.2)。常见原因包括:缺少 importedClangModules、Clang 模块名称错误、保留了本应转换的捆绑 klib 导入(或反之)。

7.2 链接框架
./gradlew :moduleName:linkDebugFrameworkIosSimulatorArm64

如果链接失败,请检查是否声明了所有必需的 SPM 产品,以及版本约束是否能正确解析。缺少符号的链接错误通常表明某个产品未添加到 swiftPMDependencies,或版本不匹配导致 API 被移除。

7.3 构建 iOS/macOS Xcode 项目

Gradle 步骤成功后,构建 Xcode 项目以验证整个应用能够编译。如果已移除所有 CocoaPods(选项 A),请使用 -project *.xcodeproj;如果仍有非 KMP CocoaPods(选项 B),请使用 -workspace *.xcworkspace:

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

如果 checkSandboxAndWriteProtection 失败——说明未在 Phase 5.1 中禁用沙盒。返回并应用 Phase 5.1 中的沙盒修复,然后重试。

如果迁移前的构建未经验证(使用了 Phase 1.0 的备用方案),请提醒用户:

注意:无法完整验证迁移前的构建。如果现在出现构建错误,其中一些可能是迁移之前就存在、与迁移无关的问题。请将这些错误与迁移前的构建输出进行比较,以区分迁移问题和已有问题。

如果构建失败

不要回退迁移。 阅读错误日志,重新检查 Phase 2–6,并参阅 troubleshooting.md。如果不确定,请向用户提供选项——不要悄悄撤销迁移工作。修复问题后,重新运行失败的验证步骤。持续迭代,直到构建成功。


阶段 8:迁移报告

构建成功后,在项目根目录中编写一份完整的 MIGRATION_REPORT.md。使用 migration-report-template.md 中的模板。

报告必须包含:

  1. 迁移前状态——CocoaPods 依赖项(名称、版本、linkOnly)、框架配置、cocoapods.* 导入、非 KMP Pods、非典型配置
  2. 迁移步骤——每个阶段的具体变更;对于非简单变更,提供变更前后的代码片段
  3. 导入转换——列出每项导入变更的表格,明确标记保留的 cocoapods.* 导入及其由哪个捆绑的 klib 提供
  4. 遇到的错误——按结构列出 Error #N 条目:阶段、具体症状、根本原因、修复方法、是否可推广
  5. 非简单决策——isStatic 变更、保留的导入、框架搜索路径、权衡取舍
  6. 变更文件——按类型分组列出完整清单(Gradle、Kotlin、Xcode、新建、删除)

其他资源

评论

支持 Markdown