Kotlin Multiplatform 儲存庫能在本機順利編譯,不代表移至雲端 Mac 後自然會得到相同結果。最常見的差異不在業務程式碼,而是 Java、Gradle Wrapper、Kotlin/Native、CocoaPods 與 Xcode 的版本組合,以及實機與模擬器產物遭到誤用。可靠的做法是先固定工具鏈,再分別驗收共用模組、Framework 與最終的 Xcode 工作區。
先固定建構輸入
首次連線至節點後,不要立刻執行完整流水線。應先記錄建構實際使用的工具,而不是只查看團隊文件中的建議版本。
sw_vers
uname -m
xcodebuild -version
xcode-select -p
java -version
./gradlew --version
bundle exec pod --version
儲存庫應提交 gradlew、gradle/wrapper/gradle-wrapper.properties、相依性鎖定檔、Podfile.lock 與 Ruby 相依性鎖定檔。CI 中應使用 ./gradlew,不要呼叫機器上碰巧存在的全域 Gradle。CocoaPods 同樣應透過 bundle exec pod 執行,避免不同任務載入不同的 gem 版本。
若需要切換 Xcode,應在任務開始時明確設定並立即驗證:
sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version
工具「已經安裝」並不代表建構可重現。可重現的條件是版本、路徑、鎖定檔與呼叫入口都能從日誌中確認。
分別定義實機與模擬器目標
Apple Silicon Mac 上的 iOS 模擬器通常也是 arm64,但它和 iPhone 實機並不是同一個目標平台。若只檢查 CPU 架構,連結階段可能會出現難以理解的錯誤。
共用模組可保留兩個明確的目標:
kotlin {
iosArm64()
iosSimulatorArm64()
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
.configureEach {
binaries.framework {
baseName = "SharedKit"
isStatic = true
}
}
}
先單獨建構共用模組,以縮短故障排查路徑:
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 \
--no-daemon --stacktrace
./gradlew :shared:linkReleaseFrameworkIosArm64 \
--no-daemon --stacktrace
這兩項任務都成功後,再進入 CocoaPods 與 Xcode 層。若任務名稱不存在,應檢查模組路徑、目標宣告與 Framework 名稱,不要透過複製現有產物來繞過設定問題。
讓 CocoaPods 與 Xcode 只使用鎖定結果
CI 不應在每次建構時主動更新 Pod。先產生或檢查 podspec,再嚴格依照 Podfile.lock 安裝:
./gradlew :shared:podspec
bundle check
bundle exec pod install --project-directory=iosApp
接著必須建構 workspace,而不是專案檔:
xcodebuild \
-workspace iosApp/App.xcworkspace \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$PWD/.build/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
build
CODE_SIGNING_ALLOWED=NO 適合用於驗收模擬器編譯,但不能取代實機封存所需的簽署設定。若專案使用自訂設定,也要確認對應的 .xcconfig 沒有將 Framework 搜尋路徑寫成本機絕對路徑。
透過分層檢查縮小問題範圍
| 檢查層 | 建議命令 | 失敗時優先檢查 |
|---|---|---|
| Kotlin 原始碼 | ./gradlew :shared:compileKotlinIosSimulatorArm64 |
expect/actual、相依性可見性 |
| Framework 連結 | linkDebugFrameworkIosSimulatorArm64 |
目標平台、原生相依性 |
| Pods 整合 | bundle exec pod install |
鎖定檔、Ruby 環境、podspec |
| Xcode 建構 | xcodebuild -workspace |
Scheme、搜尋路徑、建構設定 |
快取下載內容,不共用可寫入的工作區
~/.gradle/caches 與 ~/.konan 通常值得快取,因為重新下載的成本較高;處理 Pods 和 DerivedData 時則必須更加謹慎。若並行任務共用可寫入的 DerivedData,其中一項任務可能讀取另一個分支的中間檔案。最穩妥的方式是為每項任務建立獨立目錄,並在結束後刪除。
快取鍵至少應包含:
gradle-wrapper.properties與相依性鎖定檔摘要libs.versions.toml、Podfile.lock與 Ruby 相依性鎖定檔摘要xcodebuild -version輸出- Java 主要版本與 Kotlin 外掛程式版本
- 目標設定是 Debug 還是 Release
不要將整個使用者目錄封裝成一個長期快取。這不僅難以正確失效,還會混入憑證、日誌與無關狀態。還原快取後,應執行一次輕量編譯;若執行失敗,先清除目前任務的快取,而不是直接刪除節點上的所有開發目錄。
依錯誤型態定位問題並保留證據
出現 building for iOS Simulator, but linking in object file built for iOS 時,應優先檢查 Framework 來自哪個目標,而不是重新安裝 CocoaPods。出現 framework not found 時,應檢查 Build Settings 中展開後的搜尋路徑、Framework 的實際位置,以及指令碼階段是否在編譯前執行。
可將關鍵診斷輸出儲存為任務附件:
xcodebuild -showBuildSettings \
-workspace iosApp/App.xcworkspace \
-scheme App > .build/build-settings.txt
find shared -type d -name '*.framework' -print \
> .build/framework-inventory.txt
若本機成功而遠端失敗,應進一步比較 xcodebuild -version、java -version、鎖定檔摘要、環境變數與工作區產生時間。不要一開始就清除所有快取;這會抹除問題現場,也可能讓問題暫時消失,卻無法解釋真正原因。
合併前的最低檢查項目
合併請求至少應驗證共用模組的模擬器建構、Xcode workspace 建構,以及 Release 實機 Framework 連結。並行任務應使用獨立的 DerivedData,日誌應保留工具版本與完整的失敗命令,建構產物則應依平台命名。完成這些措施後,Kotlin 層、Pods 層與 Xcode 層的責任邊界就會足夠清楚,多數故障都能在一次流水線內完成定位。
常見問題
實機 Framework 可以直接用於 Apple Silicon 模擬器嗎?
不可以。兩者即使同為 arm64,目標平台識別仍不同,必須分別建構 iosArm64 與 iosSimulatorArm64 產物。
Kotlin Multiplatform CI 應該快取哪些內容?
優先快取 Gradle 下載內容與 Kotlin/Native 工具鏈,並將鎖定檔、Gradle Wrapper、Xcode 和 Java 版本納入快取鍵。
將建置工作交給獨享雲端 Mac
選擇 VMOrbit M4 或 VMOrbit M4 Pro,並依團隊所在位置選擇節點。實際可用性與交付資訊以控制台即時回傳結果為準。