build-ios-binary について
build-ios-binary は、拡張機能のネイティブソースから、ラップランタイム用のフラットな iOS .framework をビルドするスキルです。Xcode を使う Mac の開発者向けで、ソースとは別にステージングし、React は実行時にホストから提供される形で弱リンクします。
主な機能
- 01デバイス用のフラットな .framework をビルド
- 02正規の iOS ソースを変更せずにステージング
- 03ソースとマニフェストの整合性を確認
- 04固定された React Native 依存関係の React ヘッダーを使用
- 05署名とプラグイン組み立ては別工程
活用シーン
- 01Power Apps 拡張機能の iOS バイナリ作成
- 02ネイティブモジュールのメソッドとマニフェストの照合
- 03後続のプラグイン組み立てに向けたフレームワーク準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill build-ios-binaryインストール後、Claude Code で/build-ios-binaryを入力して読み込まれたか確認します。
詳細
/build-ios-binary
拡張機能の iOS ソース(ios/RCT<Pascal>Module.{h,m})を、ラップランタイムが読み込むフラットな <Pascal>Plugin.framework/(デバイス用スライス)に変換します。/build-android-binary の iOS 版です。ソースを入力し、ビルド済みのフレームワークを出力します。
.xcframeworkではなく、フラットな.frameworkを同梱してください。 ラップのパイプラインはios/<Name>.frameworkを想定しており、.xcframeworkの中までは検索しません。同梱すると "Framework '.framework' not found in plugin." というエラーが発生します。このスキルはデバイス用アーカイブのみをビルドし、フラットな.frameworkを取り出します。詳細はppmplugin-format §5bを参照してください。
Mac 専用です。 Xcode が必要です。Linux/Windows ではこのスキルはブロックされます。Android 専用のコントリビューターは iOS 用スライスを生成できません。
既知の制限事項 — こちらをお読みください。 唯一の難所は React-Core です。フレームワークは、コントロールリポジトリで固定されている
react-nativeの devDep に含まれる React のヘッダー(#import <React/RCTBridgeModule.h>)を使ってコンパイルしますが、React は埋め込みません。ラップホストが実行時に提供します(弱いリンク)。重要な依存関係は RN のバージョン固定(0.79.7)で、ラップホストに同梱されている RN と一致させる必要があります。正確な弱いリンクのフラグとヘッダー検索パスの設定は、稼働中のラップホストに対してまだ検証されていません。ここが主なリスク領域です。React のシンボル/ヘッダーに関するエラーはコードのバグではなく、RN のバージョン固定がホストの RN と食い違っていることを意味します。
shared/ppmplugin-format.md §5b(iOS バイナリ要件)と、naming-conventions.md の iOS の行をお読みください。
このスキルでは行わないこと
manifest.jsonは作成しません(先に/generate-ppmplugin-manifestを実行してください。このスキルはクロスチェックとフレームワーク/moduleClass の名前のために、そのファイルを読み込みます)。.ppmpluginは zip 化しません。これは/assemble-ppmpluginが行います。- Android はビルドしません。これは
/build-android-binaryが行います。 - 正規の
ios/や podspec は変更しません。調整はすべて、使い捨てのコピーであるppmplugin/staging/ios-build/の中で行います。 - フレームワークには署名しません。ラップのパイプラインがパッケージ化時に署名します(
BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NOが指定されている場合)。署名用スキルは不要です。
ステップ 1 — 共有ドキュメント + 前提条件ブロックを読む
shared/shared-instructions.mdとshared/ppmplugin-format.mdを読みます。ppmplugin/staging/manifest.jsonを読みます。存在しない場合は、NEEDS_CONTEXT: manifest.json missing — run /generate-ppmplugin-manifest firstを出力して STOP します。entrypoints.iosがない場合は、NEEDS_CONTEXT: manifest targets Android-only — re-run /generate-ppmplugin-manifest and choose iOS/Bothを出力して STOP します。.extension-state.mdの## ppmplugin (third-party controls)ブロックを読みます。既存を置き換えるゲート:ppmplugin/staging/ios/<framework>.frameworkがすでに存在する場合は、それ(およびそのビルドタイムスタンプ)を提示し、AskUserQuestionで確認します — 既存を保持(ステップ 6 へスキップ)か 置き換え(再ビルド) か。記録されたビルド以降にソースとマニフェストが変更されていなければデフォルトは保持、そうでなければ置き換えです(保持は古い状態であることに注意)。ppmplugin-format §1の置き換えルールに従います。- 前提条件チェックを実行し、表示ブロックを出力します(shared-instructions §9.2)。ポリシー: 解決する、先送りしない(パスでツールを特定し、安全なインストールを提案 + 実行します):
| チェック | 確認内容 | 自動修正 |
|---|---|---|
| macOS | uname = Darwin | 修正不可 — Linux/Windows では BLOCKED: /build-ios-binary is Mac-only (iOS needs Xcode) を出力して STOP |
| Xcode 16+(26.2+ を推奨) | xcodebuild -version | 自動修正不可 — 出力: App Store / xcodes から Xcode をインストールしてください; 存在しない場合は STOP |
| React-Core ヘッダーが存在する | node_modules/react-native/React/Base/RCTBridgeModule.h が存在する(コントロールリポジトリ内の react-native devDep) | devDep がインストールされていない場合: リポジトリで pnpm install |
| CocoaPods(オプション — フォールバックパスのみ) | pod --version | 推奨されるヘッダーのみのビルド(ステップ 2)には不要です。Podfile パスにフォールバックする場合にのみ必要です; 確認のうえ brew install cocoapods |
ステップ 2 — React-Core ソース(自己完結)+ 設定の確認
フレームワークはコンパイル時に React のヘッダーを必要とします。ソースはコントロールリポジトリ自身の node_modules/react-native です — ピン留めされた react-native devDep(pen-input では 0.79.7)で、React ヘッダー(React/Base/RCTBridgeModule.h)が同梱されています。ビルドはコントロールリポジトリ内で完全に自己完結しています。devDep がインストールされていない場合は pnpm install を実行します; 本当に存在しない場合にのみ、ユーザーに react-native のパスを尋ねます。
推奨: ヘッダーのみの React(CocoaPods なし)。 ステージングコピー内のフラットなインクルードディレクトリに React ヘッダーを集約し、フレームワークターゲットをそこに向けます — Pod チェーンよりもクリーンで高速であり、最近の Xcode で問題になる CocoaPods/React-Codegen の失敗を回避できます(ppmplugin-format §5b を参照):
mkdir -p ppmplugin/staging/ios-build/include/React
# copy the RN headers the module imports (Base + the Libraries it uses):
cp node_modules/react-native/React/Base/*.h ppmplugin/staging/ios-build/include/React/
cp -R node_modules/react-native/React/Base node_modules/react-native/Libraries ppmplugin/staging/ios-build/include/React/ 2>/dev/null || true
次にフレームワークターゲットに設定します: HEADER_SEARCH_PATHS = $(inherited) $(SRCROOT)/include および OTHER_LDFLAGS = -undefined dynamic_lookup(React のシンボルを弱リンクします — ホストが実行時に提供します)。-undefined dynamic_lookup は Apple によって非推奨とされています(現在は動作しますが、将来の Xcode で壊れる可能性があります — 既知の注意点)。
フォールバック: CocoaPods — ヘッダーのみのパスで不十分な場合にのみ使用します。devDep を参照する Podfile(pod 'React-Core', :path => '<repo>/node_modules/react-native')は CocoaPods にヘッダーマップを提供しますが、最近の Xcode/RN-0.79 では post_install パッチ(boost URL、Yoga _pt、RCT-Folly clockid_t、boost std::unary_function)が必要になり、それでも React-Codegen のサンドボックス障害に遭遇する可能性があります — すべて ppmplugin-format §5b に記載されています。
いずれの場合も重要なのは、ピン留めされた RN バージョン(0.79.7)が、ラップホストが実行時に提供する RN と一致することです(React は弱リンクされ、ホストが提供します)。検出された RN バージョンを出力し、ビルド前に AskUserQuestion でソース + バージョンを確認 / 上書きするよう求めます。次のように明確に述べます: 「React は実行時にラップホストによって提供されます; ビルド時またはロード時の React シンボル/ヘッダーエラーは、RN ピンがホストの RN バージョンから乖離していることを意味します。」
ステップ 3 — 単独の Xcode フレームワークプロジェクトをステージングする(正規の ios/ は変更しない)
Android のビルドと同様に、使い捨てのコピーを使用して、エンジニアが保守するソースには一切手を加えません。
-
コピー:
ios/*.{h,m}→ppmplugin/staging/ios-build/Sources/(実行ごとに新規作成します)。 -
アンブレラヘッダーを生成する:
ppmplugin/staging/ios-build/<Pascal>Plugin.hを作成し、Public ヘッダーとして設定します(ビルドを適切に保つためのものであり、実行時には必須ではありません。ラッププレーヤーはバイナリをdlopenし、NSClassFromStringを使用します。モジュールをimportすることはないため、モジュールマップがなくてもフレームワークは正常に読み込まれます。アンブレラヘッダーはDEFINES_MODULEの警告を抑制するだけです。クリーンビルドのため、生成してください)。最小限の内容:#import <Foundation/Foundation.h> FOUNDATION_EXPORT double <Pascal>PluginVersionNumber; FOUNDATION_EXPORT const unsigned char <Pascal>PluginVersionString[]; -
単独の Xcode フレームワークプロジェクトを生成する:
ppmplugin/staging/ios-build/<Pascal>Plugin.xcodeprojを作成します。プロダクト名/ターゲット名は<Pascal>Plugin(=entrypoints.ios.framework)とし、コピーしたソースをコンパイルして、アンブレラヘッダーを Public ヘッダーとして設定します。正規の podspec を基準にします。- リンクするシステムフレームワーク:podspec の
s.frameworksを使用します(例:pen-input →PencilKit, UIKit, CoreGraphics, QuartzCore)。 - デプロイメントターゲット:
s.platformを使用します(例::ios, "14.0")。ただし、サポート対象の PAM シェルの最低要件である iOS 16.0 以上に設定します。 - xcodeproj-gem の落とし穴(
xcodeprojgem を使用して生成する場合):グループにすでにパスが設定されている(例:Sources)場合、ファイル参照はファイル名のみで追加します。new_group('Sources','Sources')とnew_reference("Sources/<file>")を組み合わせると、パスがSources/Sources/<file>になり、ファイルが見つからなくなります。毎回再導出するのではなく、動作確認済みのgen_project.rbを固定してください。
- リンクするシステムフレームワーク:podspec の
-
ビルド設定を行う(それぞれ、文書化されているラップ時/デバイス上の失敗に対応します。詳細は
ppmplugin-format §5bを参照してください)。設定 値 Mach-O Type Dynamic Library(Static ではありません) Dead Code Stripping NO( dlopen後にホストが読み取る+moduleNameクラスメソッドとRCT_EXPORT_METHODメタデータを保持します)Defines Module YES Enable Bitcode NO HEADER_SEARCH_PATHS$(inherited) $(SRCROOT)/include(ステップ 2 のヘッダーのみの React)OTHER_LDFLAGS-undefined dynamic_lookup(React を弱リンクし、実行時にホストが提供します)BUILD_LIBRARY_FOR_DISTRIBUTIONYES SKIP_INSTALLNO GENERATE_INFOPLIST_FILEYES(必須 — Info.plist がないフレームワークは、ラップの codesign ステップで拒否されます) INFOPLIST_KEY_CFBundleDisplayName<Pascal>PluginMARKETING_VERSION<manifest のバージョン>CURRENT_PROJECT_VERSION1PRODUCT_BUNDLE_IDENTIFIERcom.powerapps.<lowername>
正規の ios/ と podspec は変更しません。適用したステージング時の調整内容を報告してください。
ステップ 4 — マニフェストへの適合を検証する
ビルドする前に、ソースがマニフェストと一致することを確認します(Android の DexClassLoader アサーションに相当する iOS 側の検証です)。
ios/RCT<Pascal>Module.hのクラス名 ==manifest.entrypoints.ios.moduleClass。.mのクラス名 ==manifest.entrypoints.ios.moduleClassであり、+ (NSString *)moduleNameがmanifest.receivers[].nativeModuleを返すこと。- 各
RCT_EXPORT_METHOD(<name>:…)の名前がmanifest.receivers[].methodsに含まれていること。 - モジュールクラスは引数なしの
[cls new]でインスタンス化されます — ランタイムはフレームワークの読み込み後にmoduleClassを解決し、[cls new]でインスタンスを構築します。引数を取るカスタムの指定イニシャライザーがあると、インスタンス化に失敗し、プラグインは読み込み時にスキップされます。.m に- (instancetype)initWith…が宣言されていて、通常のinitがない場合は、BLOCKED: <moduleClass> must support no-arg initializationで停止します。 - 各
RCT_EXPORT_METHODは、最初のパラメーターとしてNSDictionary *requestを受け取ります(その後に resolver/rejecter が続きます)— ラッププロキシは PCF のargs: [request]を位置引数として展開します。request を複数の位置引数に展開するメソッドではデータを受け取れません。その場合は警告を示します(getStatusのような引数なし操作で resolver/rejecter のみを受け取るのは問題ありません — ppmplugin-format §2)。
不一致がある場合、マニフェストとバイナリが一致していないため、デバイス上で呼び出しがモジュールに届きません。壊れた組み合わせをビルドせず、具体的な不一致を示して停止します。
ステップ 5 — フラットな .framework をビルドする(デバイス用スライスのみ)
実際に実行し、ストリーミングログではなくアーティファクトのパスを示します。デバイス用スライスのみをアーカイブし(ラップ CI が必要としているのは .xcframework ではなくフラットな <Name>.framework です — §5b を参照)、その後フレームワークを直接コピーします。-create-xcframework の手順はありません。
cd ppmplugin/staging/ios-build
# device archive only:
xcodebuild archive -project <Pascal>Plugin.xcodeproj -scheme <Pascal>Plugin \
-destination "generic/platform=iOS" \
-archivePath build/<Pascal>Plugin-device.xcarchive \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
# copy the flat framework out (NO xcframework wrapper):
rm -rf ../ios/<Pascal>Plugin.framework
cp -R build/<Pascal>Plugin-device.xcarchive/Products/Library/Frameworks/<Pascal>Plugin.framework ../ios/<Pascal>Plugin.framework
strip -x ../ios/<Pascal>Plugin.framework/<Pascal>Plugin # optional, if size matters
(ヘッダーのみではなく CocoaPods を使ってビルドした場合は、.xcodeproj ではなく .xcworkspace のスキームをアーカイブします。それ以外は同じで、デバイス用のみを対象とし、フラットなフレームワークをコピーします。)
失敗した場合は、失敗した手順と最も関連性の高い xcodebuild のエラー行を出力し、BLOCKED: xcodebuild failed — <line> で停止します。スタンドアロンでよくある失敗とその原因は ppmplugin-format §5b に記載されています。多くは React のリンク設定(ステップ 2)に起因します。リンク時にシンボルが見つからない場合は React が弱リンクされていない(-undefined dynamic_lookup がない)、<React/…> が見つからない場合は HEADER_SEARCH_PATHS が間違っている、シンボルが 一致しない 場合は RN のバージョン固定がホストの RN と一致していません。これらは拡張コードのバグではなく、React/RN バージョンの依存関係の問題として示します。
フレームワークを検証します。 ppmplugin/staging/ios/<Pascal>Plugin.framework/ には、ランタイムで必須の次の 2 つの項目が含まれている必要があります。
<Pascal>Plugin— Mach-O バイナリです。名前は正確に<Pascal>Pluginでなければなりません(プレーヤーはFrameworks/<Pascal>Plugin.framework/<Pascal>Pluginをdlopenするため、名前が異なるバイナリは読み込まれません)。必須です。Info.plist— 必須です(これがないフレームワークバンドルは無効で、ラップ時の codesign 手順で失敗します)。
いずれかが欠けている場合は、BLOCKED: framework incomplete — missing <X> と §5b の修正方法(GENERATE_INFOPLIST_FILE=YES/バイナリ名 == フレームワーク名)を示して停止します。アンブレラヘッダーと Modules/module.modulemap はビルドの衛生管理に関するものです。ない場合はその旨を記録しますが、これらを理由に停止してはいけません(dlopen ローダーはこれらを使用しません — §5b)。
ステップ 6 — 報告と次のステップ
.extension-state.md の ## ppmplugin (third-party controls) ブロックを更新します:iOS framework: built <ISO timestamp> (<Pascal>Plugin.framework, device slice)。.framework のパス(umbrella + module.modulemap + Info.plist を含むことも記載)を含むフェンス付きの概要を出力し、その後 AskUserQuestion を使って次のステップを提示します(shared-instructions §9.1 — 選択されたスキルを Skill ツールで呼び出してください。説明するのではなく、実行してください):
- Run /assemble-ppmplugin(推奨 — マニフェストとバイナリを
.ppmpluginに zip します) - Run /build-android-binary(Both バンドル用の Android スライスも必要な場合)
- Stay — I'll inspect the framework first
.framework のパスを添えて DONE を返すか、失敗した xcodebuild の行(多くの場合、React のリンク処理/ホストとの RN バージョン不一致が原因で、拡張機能のコード修正では解決しません)を添えて BLOCKED を返します。