Zenn
🛠️

別のPCで生成されたKMPのxcframeworkをXcodeでデバッグする

2025/02/05に公開
2

Kotlin Multiplatform(以下KMP)を利用している際に、XcodeからKotlinのファイルに対してBreakpointを貼ってデバッグをしたいケースがあるかと思います。

xcode-kotlinを利用することで、一般的なKMPのプロジェクトにおいては容易にXcodeからデバッグができるようになります。

しかし、担当しているプロジェクトではXcode上でBreakpointを貼ってアプリを起動すると、起動したタイミングでBreakpointが無効になってしまってデバッグができない現象が発生していました。

本記事では、その課題の原因の説明と解決方法について記載しています。

問題となったiOSとKMPのプロジェクト構成

KMPのプラグインを利用してプロジェクトを生成した場合は、プロジェクト内にiOSプロジェクトが生成されます。そして、Run Scriptで./gradlew :somemodule:embedAndSignAppleFrameworkForXcodeを実行して、ローカルでFrameworkを都度ビルドする構成になります。
上記の構成であれば、ドキュメント通りにxcode-kotlinを利用することで、XcodeからKotlinファイルのデバッグができるようになります。

担当しているプロジェクトでは、KMPのRepositoryとiOSのRepositoryが別になっている構成となっており、KMPのRepositoryからCIで生成したxcframeworkをSPM等でiOSのRepositoryに取り込んで利用しています。

Breakpointが無効になってしまう原因

xcode-kotlinのリポジトリにnot working with xcFramework #116というissueが2024年9月末が作成されていました。

Hello,

  1. the documentation and installation steps and screenshots are too old specially after Xcode 16
  2. I did not how to add group if we are using only xcFramework , it is not clear how to ad group to the project root Pro1.xcodeproj

単にxcframeworkでxcode-kotlinがうまく動かずにデバッグができないというissueも見えますがが、開発者からの返答コメントが重要な内容となっていました。

As long as your XCFramework is built on the same machine your sources live on, it'll work the same as a regular framework. If you're building the code on a different machine, that usually means the source paths are different and you'll have to manually map files using an LLDB command. That's out of scope for Xcode-Kotlin at this time.
ref. https://github.com/touchlab/xcode-kotlin/issues/116#issuecomment-2368896127

デバッグするマシンと同一のマシンでビルドされたxcframework(もしくはframework)でなければsource pathが違ってしまうので、LLDBのコマンドでファイルをマッピングしないとデバッグができないという内容でした。

別な開発者からの返答コメントにも重ねて説明がありました。

If your XCFramework is not built on your machine, you won't be able to debug it. That's not a Kotlin-specific issue. We published a post about KMP and teams related to this: https://touchlab.co/kmp-teams-use-source
ref. https://github.com/touchlab/xcode-kotlin/issues/116#issuecomment-2368911721

コメントにある2024年7月末にリリースされた記事を読み進めて行くと、上記に関する内容が詳しく記載されています。

So, if you publish an XCFramework binary built by CI, Xcode will look for source files at the path which the CI server used when the XCFramework was built.

In summary: you need to build the Kotlin locally, and point Xcode at that same local path.
ref. https://touchlab.co/kmp-teams-use-source#browsing-and-debugging

There is a workaround
To be precise, you can debug code built on a different path. It’s too much trouble to be practical, however. In short, you need to provide a path mapping override in the framework metadata. You’ll also need to configure the CI build to use a “known path”. Then the iOS dev needs to clone the Kotlin source, of course checking that the dependency version matches the source code tag/version, then edit the framework metadata to the new local path, etc.

As an alternative, you can just build from source code locally. It’s easier.
ref. https://touchlab.co/kmp-teams-use-source#there-is-a-workaround

つまり、担当しているプロジェクトではCIでビルドしたxcframeworkをiOSに取り込んで利用しているため、Xcodeが見ているsource pathとxcframeworkがビルドされたsource pathが異なっていることでxcode-kotlinを使ってもデバッグができなかったのです。

具体的な解決方法

ファイルのマッピングを変更することでデバッグできるようになるとのことでしたが、具体的な方法はどうのようになるでしょうか?

xcframeworkのビルド時のpathを確認する

Shared.xcframeworkがCI上で生成されたKMPの成果物だとします。
おそらく、xcframework内のディレクトリ構造は以下のようになっているかと思います。

Shared.xcframework/
 ├ ios-arm64/
 │  ├ Shared.framework/
 │  │  └ Shared
 │  └ dSYMs/
 │     └ Shared.framework.dSYM
 └ ios-arm64-simulator/

上記のディレクトリ内にあるBinaryを指定してlldbを起動し、Debug Symbolも指定します。
そして、framework内にある任意の関数(グローバル関数だとわかりやすい)に対してimage lookupを実行します。

lldb --batch
  -o "add-dsym Shared.xcframework/ios-arm64/dSYMs/Shared.framework.dSYM" \
  -o "image lookup -vn getPlatform" \
  Shared.xcframework/ios-arm64/Shared.framework/Shared

出力結果のLineEntyがCI上でビルドされた際のsource pathになっているので、そこに記載されているKMPのプロジェクトまでの絶対パスを利用します。

source pathを置き換える

iOSプロジェクトのルートに、.lldbinit-KMP(命名はこれでなくても問題ない)を生成し、
以下の内容を追記します。

settings set target.source-map ${CI上のKMPのディレクトリまでのPath} ${ローカルのKMPのディレクトリまでのPath}

上記の設定をした後にXcodeのSchemeから、LLDB init Fileとして.lldbinit-KMPを指定します。

そして、iOSプロジェクト内に追加しているKotlinファイルの参照に対してBreakpointを貼ってアプリをビルドすると、無事デバッグができるようになっているかと思います。

シェルスクリプトでセットアップできるようにする

以下のような、xcframeworkのsource pathを取得して.lldbinit-Xcodeに書き込むスクリプトを実装しておくと便利です。

#!/bin/bash

set -euo pipefail

KMP_ROOT_DIR=$1 # ローカルにあるKMPのプロジェクトまでのパス
FRAMEWORK_NAME=$2 # xcfarameworkの名前
KMP_FUNC=$3 # KMPで定義されている関数の名前 (グローバル関数だとわかりやすい)
KMP_FUNC_DEF_PATH=$4 # 上記で指定している関数が定義されているファイルのパス

if [ -z "${KMP_ROOT_DIR}" ]; then
    echo "`KMP_ROOT_DIR` is empty."
    exit 1
fi

if [ -z "${FRAMEWORK_NAME}" ]; then
    echo "`FRAMEWORK_NAME` is empty."
    exit 2
fi

if [ -z "${KMP_FUNC}" ]; then
    echo "`KMP_FUNC` is empty."
    exit 3
fi

if [ -z "${KMP_FUNC_DEF_PATH}" ]; then
    echo "`KMP_FUNC_DEF_PATH` is empty."
    exit 4
fi

# xcode-kotlinがインストールされているかを確認
if which xcode-kotlin &> /dev/null; then
    echo "xcode-kotlin already installed"
else
    echo "xcode-kotlin is not installed. seealso: [https://touchlab.co/xcodekotlin#installation]"
    exit 5
fi

# xcode-kotlinのセットアップ
xcode-kotlin install
xcode-kotlin sync

PROJECT_DIR="$(git rev-parse --show-toplevel)"
TEMP_DIR=$(mktemp -d -p "${PROJECT_DIR}")

trap "rm -rf $TEMP_DIR" EXIT

ARM64_DST="${TEMP_DIR}/${FRAMEWORK_NAME}.xcframework/ios-arm64"

# lldbを使ってCIでビルドされた際のpathを取得
CI_KMP_ROOT_DIR=$(lldb --batch \
  -o "add-dsym ${ARM64_DST}/dSYMs/${FRAMEWORK_NAME}.framework.dSYM" \
  -o "image lookup -vn ${KMP_FUNC}" \
  "${ARM64_DST}/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" \
  | grep -oE "/([^[:space:]])+${KMP_FUNC_DEF_PATH}" \
  | sed -E "s|(/.*)${KMP_FUNC_DEF_PATH}|\1|")

if [ -n "${KMP_FUNC_DEF_PATH}" ]; then
    echo "Source paths found: ${KMP_FUNC_DEF_PATH}"
else
    echo "Can not find source paths..."
    exit 6
fi

LLDBINIT_FILE="${PROJECT_DIR}/.lldbinit-KMP"
if [ -e "${LLDBINIT_FILE}" ]; then
    echo "${LLDBINIT_FILE} found."
else
    echo "Can not find ${LLDBINIT_FILE}. Will create it."
    touch "${LLDBINIT_FILE}"
fi

# 必要があれば、./.lldbinit-KMPにsource-mapを書き換える設定を追加する
TARGET_SOURCE="settings set target.source-map ${CI_KMP_ROOT_DIR} ${KMP_ROOT_DIR}"
MATCH_RESULT=$(grep "${TARGET_SOURCE}" "${LLDBINIT_FILE}" || true)
if [ -n "${MATCH_RESULT}" ]; then
    echo "xcode-kotlin settings already completed."
else
    echo "Write xcode-kotlin settings to ${LLDBINIT_FILE}"
    echo "${TARGET_SOURCE}" >> "${LLDBINIT_FILE}"
    echo "Completed!"
fi

追記

本記事をXにて投稿したところ、@giginetさんから本記事に関連するWWDCのセッションを教えていただきました。
https://x.com/giginet/status/1886934357434360165

5:36あたりから、本記事で記載している内容をよりわかりやすい形で説明されています。
https://developer.apple.com/jp/videos/play/wwdc2022/110370/

2

Discussion

ログインするとコメントできます