build-android-binary について
拡張機能のエンジニア向けに、Android ソースを wrap ランタイムが読み込む DEX バイナリへ変換するビルドスキルです。使い捨てのステージングコピーでビルドし、管理対象の `android/` ソースは変更しません。
主な機能
- 01Android DEX バイナリのビルド
- 02`android/` のステージングコピーを使用
- 03Gradle wrapper を 8.13 に固定
- 04JDK、Android SDK、ビルド要件の確認
活用シーン
- 01Android ソースから `Plugin.dex` を生成
- 02Android モジュール変更後の再ビルド
- 03独立ビルド用の変更を正規ソースから分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill build-android-binaryインストール後、Claude Code で/build-android-binaryを入力して読み込まれたか確認します。
詳細
/build-android-binary
Android ソース(android/.../<Pascal>Module.kt + <Pascal>Package.kt)を、ラップランタイムが実行時に DexClassLoader 経由で読み込む DEX バイナリに変換します。.ppmplugin を生成する、ツールチェーンに依存する重量級の手順です。ソースコードを入力すると、実行可能な <Pascal>Plugin.dex が出力されます。
命名に関する注意:このスキルは内部で Gradle の
assembleReleaseタスクを実行します。最終バンドルを zip にする/assemble-ppmpluginと混同しないでください。処理する階層が異なります。このスキルは バイナリを生成し、もう一方は バンドルを生成します。
クロスプラットフォーム: macOS、Linux、Windows で実行できます。Android の手順に Mac 専用のステップはありません(JDK 17、Gradle、Android SDK の
d8はいずれの OS でも利用できます)。以下の各シェルコマンドは、共通手順 §5 に従い、bash 版と PowerShell 版を記載しています。
shared/ppmplugin-format.md §5 を読み、このスキルが適用する Android バイナリ要件を確認してください。
このスキルで行わないこと
manifest.jsonは作成しません(先に/generate-ppmplugin-manifestを実行してください。このスキルは照合のためにファイルを読み込みます)。.ppmpluginは zip にしません(その処理は/assemble-ppmpluginが行います)。- iOS はビルドしません(その処理は
/build-ios-binaryが行います。Mac 専用です)。 - 正式な
android/は変更しません。単独ビルド用の調整はすべて、ppmplugin/staging/android-build/の使い捨てコピーに対して行います(ステップ 2)。エンジニアが保守するソースには一切手を加えません。
ステップ 1 — 共通ドキュメントと前提条件のセクションを読む
shared/shared-instructions.mdとshared/ppmplugin-format.mdを読みます。ppmplugin/staging/manifest.jsonを読みます。ファイルが存在しない場合は、NEEDS_CONTEXT: manifest.json missing — run /generate-ppmplugin-manifest firstを表示して停止します(ビルドではこのファイルとの照合を行います)。 2b..extension-state.mdの## ppmplugin (third-party controls)ブロックを読みます。すでに DEX が存在する場合(ppmplugin/staging/android/<dex>)、黙って上書きしないでください。ビルド時刻とともにその DEX を提示し、置き換えるかどうかをAskUserQuestionで確認してください。- 記録された
Android DEX: builtの時刻以降、ソースとmanifest.jsonが変更されていない場合 → **既存を保持(再利用)**を推奨[デフォルト]とし、**置き換え(再ビルド)**も選択肢として提示します。 - ソースまたは manifest が変更されている場合 → **置き換え(再ビルド)**を推奨[デフォルト]とし、既存を保持も選択肢として提示します(注:古いため、現在のソースとは一致しません)。 保持を選んだ場合は、ステップ 6(報告)に進み、既存の DEX を使用します。置き換えを選んだ場合は、再ビルドを続けます。ビルドは常に安全に繰り返し実行できます。この確認は、意図的に作成したアーティファクトを尊重するためのものです。
- 記録された
- 前提条件のチェックを実行し、表示用ブロックを出力します(共通手順 §9.2)。方針:先送りではなく、解決する。 各チェックでは、まず標準のインストール先(PATH だけでなく)からツールを探すか、安全なインストールを実行するかを提案し、
yesが選ばれた場合はインストールを実行します(§1.5 の自動修正フロー)。人間の対応なしには本当に解決できない要件(権限の付与など)がある場合に限り、チェックをハードブロックします。「見つかったが PATH にない」はブロック理由にはなりません。絶対パスを使用してください。チェック項目: | 確認項目 | 確認方法 | 自動修正 | |---|---|---| | JDK 17 |java -version(17以上) | shared-instructions §1.5 に従って OS に応じたインストールを提示します(ユーザーの確認後に実行) | | Gradle(ブートストラップのみ) |gradle --version— バージョンは問いません。固定されたラッパーを生成するためだけに使います(Step 2.3)。システムの Gradle はビルドに使わないため、ここではバージョンを問題として指摘しないでください。 | 見つからない場合は、OS に応じて Gradle をインストール | | Android SDK Build-Tools 35 +d8|d8の場所を特定してください。PATH 上にある必要はありません(以下を参照)。 | どこかで見つかれば PASS。d8がまったく存在しない場合に限り、確認後に<sdkmanager> "build-tools;35.0.0"を自動修正として実行します。 | | Android platform 35 |<sdkmanager> --list_installedにplatforms;android-35が含まれている、または$ANDROID_HOME/platforms/android-35/が存在する | 確認後に自動修正: インストールを実行します(以下を参照)。 |
d8 の場所の特定(PATH 上にないことだけを理由にブロックしないでください)。 Android Studio を使っているユーザーには d8 がインストールされていますが、PATH 上にあることはほとんどありません。絶対パスを特定し、Step 5 ではそのパスを使用してください:
command -v d8/where d8.bat— PATH 上にある場合は、それを使用します。- それ以外の場合は、SDK のルート(
$ANDROID_HOME、なければ$ANDROID_SDK_ROOT、それもなければ mac のデフォルト~/Library/Android/sdk、win のデフォルト%LOCALAPPDATA%\Android\Sdk)を検索します。build-tools/35*/d8(mac/linux)またはbuild-tools\35*\d8.bat(win)を選択します。35.x の build-tools を優先し、なければ利用可能な中で最も新しいものを使用します。 - 絶対パスを
$D8として記録し、次の注記を付けてチェックを PASS にします:✓ d8 found at <path> (will invoke by absolute path)。PATH の編集は求めないでください。 d8がどこにも存在しない場合に限り BLOCK とし、<sdkmanager> "build-tools;35.0.0"によるインストールを提示します(<sdkmanager>も同じ方法で特定します — PATH だけでなく、$ANDROID_HOME/cmdline-tools/*/bin/sdkmanagerと$ANDROID_HOME/tools/bin/sdkmanagerを検索します)。
platform 35 のインストール(確認後に自動修正)。 platform 35 のチェックに失敗した場合は、インストールの実行を提示し、ユーザーの yes を待ってください(shared-instructions §1.5 — コマンドを表示するだけでなく、実行してください):
# if sdkmanager is missing entirely (mac):
brew install --cask android-commandlinetools
# then (yes | … auto-accepts the SDK licenses):
yes | <sdkmanager> "platforms;android-35"
インストール後に再確認してから続行してください。ユーザーが拒否した場合、またはインストールに失敗した場合に限り、STOP してください。
platform 35 を指定する理由: スタンドアロンビルドは compileSdk 35 を使ってコンパイルします(Step 2)。これは RN 0.79 の react-android AAR が compileSdk 35 に対してビルドされており、AGP が利用側に compileSdk ≥ 35 を強制するためです。そのため、platforms;android-35 と AGP 8.x が必要です(7.x では android-35 に対してコンパイルできません)。したがって、35 が最低要件であり、「34 以上」ではありません。
Step 2 — スタンドアロンビルド用のコピーを準備する(正規の android/ は変更しない)
リポジトリの android/ は、managed host build の Gradle で利用することを想定したベアなライブラリモジュールです。ホストから rootProject.ext の値(compileSdkVersion、minSdkVersion、kotlin_version。safeExtGet(...) で読み込みます)が提供され、さらに React Native がクラスパスに追加されることを前提としています。スタンドアロンでは、これらは存在せず、safeExtGet のフォールバックが適用され、React を解決できません。一時的なビルドのために正規の android/build.gradle を変更して本来のソースを劣化させるのではなく、コピーからビルドします。
-
android/をppmplugin/staging/android-build/にコピーします。毎回、新しいコピーを作成するために削除してからコピーし、正規の内容からずれないようにします。スタンドアロン向けの調整は、コピーにのみ適用します。 a. React Native を固定します。 ジェネレーターは現在compileOnly "com.facebook.react:react-android:<rnVersion>"を直接書き込むため、新しく生成されたコントロールではこれはすでに正しく、この手順は何も行いません — 確認して次に進みます。古いコントロールには従来のimplementation 'com.facebook.react:react-native:+'(またはそのcompileOnly版)が含まれています。コピー内のそれらをcompileOnly "com.facebook.react:react-android:<rnVersion>"に書き換えます(<rnVersion>はpackage.jsonの devDependencies から取得、例:0.79.7)。スタンドアロンでは、ホストは React を提供せず、最新の Android コーディネートはreact-androidです(mavenCentral()から解決されます)。compileOnlyにすることでバンドルされることはありません — RN は実行時に wrap シェルによって提供されます。書き換えが必要だった場合は、その旨を報告してください。正規のソースが乖離しており、/debug-extensionがそちらを修正すべきです。b. compileSdk を 35 に固定します。 スタンドアロンビルドには管理されたホストビルドの
rootProject.extがないため、safeExtGet('compileSdkVersion', …)はコントロール自身のフォールバックを使用します(古いコントロールはデフォルトで 33)— これは低すぎる可能性があります。RN 0.79 のreact-androidAAR は compileSdk 35 に対してビルドされており、AGP はcompileSdk < 35のモジュールがそれに対してコンパイルすることを拒否します。コピーのcompileSdkVersionを 35 に設定します(そしてminSdkVersionが ≥ 24(AAR の下限)であることを確認します)。これには AGP 8.x が必要です(7.x は android-35 に対してコンパイルできません)。ppmplugin-format.md §5を参照してください。targetSdkVersionがすでに ≥ 35 の場合はそのままにします。c.
repositoriesにgoogle()とmavenCentral()の両方が含まれていることを確認します。 -
コピーに
settings.gradleを追加し、ライブラリを独自のルートプロジェクトとして宣言します:rootProject.name = "<lower>plugin" -
RN 0.79 が AGP 8.8.2 で使用するバージョンに固定された Gradle ラッパーを追加します。 RN 0.79 は AGP 8.8.2 と Gradle 8.13 を組み合わせます。ラッパーは 8.13 に固定する必要があります(AGP 8.8 は Gradle 8.10.2 以上が必要です。それより古い Gradle では失敗します)。
推奨 — 事前生成されたラッパーを書き込み、ブートストラップをスキップします。
gradle/wrapper/gradle-wrapper.propertiesをステージングコピーに直接書き込み、ディストリビューション URL とそのチェックサムの両方、およびgradle-wrapper.jar+gradlew/gradlew.batスクリプトを固定します:distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78distributionSha256Sumは必須であり、省略可能ではありません。最初の./gradlewの呼び出しはそのアーカイブをダウンロードしてから実行します。HTTPS だけではホストを認証するだけで、バイト列は認証しません。ピン留めにより、Gradle はディストリビューションを検証し、不一致の場合は中止します — フェイルクローズドです。上記の値は Gradle がhttps://services.gradle.org/distributions/gradle-8.13-bin.zip.sha256で公開している SHA-256 です。これは 8.13 ピンの定数であるため、shared-instructions.md §0の Gradle バージョンが移動するたびにそのエンドポイントから再導出し、失敗するビルドを通すために手で編集しないでください。最初の
./gradlewの前にラッパー JAR を検証します。gradle-wrapper.jarはgradlewによって実行されるため、ディストリビューションと同じ扱いが必要です。信頼できるソースからのみコピーしてください — コントロール自身がコミットしたandroid/gradle/wrapper/、または検証済みの Gradle インストール — そしてhttps://downloads.gradle.org/distributions/gradle-8.13-wrapper.jar.sha256で公開されている公式の Gradle 8.13 ラッパー JAR SHA-256 に対して検証します。公式チェックサム(8.13):
81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e7433460610fecd7ae45fフェイルクローズド:
cd ppmplugin/staging/android-build EXPECTED_WRAPPER_SHA256=81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e7433460610fecd7ae45f ACTUAL=$(shasum -a 256 gradle/wrapper/gradle-wrapper.jar | cut -d' ' -f1) [ "$ACTUAL" = "$EXPECTED_WRAPPER_SHA256" ] || { echo "BLOCKED: gradle-wrapper.jar SHA-256 mismatch — expected $EXPECTED_WRAPPER_SHA256, got $ACTUAL"; exit 1; }どちらのアーティファクトも、ピン留めされていないサードパーティのミラーから取得しないでください。また、ビルドが「単なる使い捨てのステージングコピー」であるという理由でチェックを省略しないでください — ステージングコピーは同じマシン上で同じ権限で実行されます。
フォールバック —
gradle wrapperを実行する必要がある場合で、システムの Gradle が AGP 8.8.2 に対して古すぎる場合:build.gradleを一時的に脇に移動してラッパータスクが評価するものをなくし、ラッパーを生成してから復元します:cd ppmplugin/staging/android-build mv build.gradle build.gradle.tmp gradle wrapper --gradle-version 8.13 --distribution-type bin \ --gradle-distribution-sha256-sum 20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 mv build.gradle.tmp build.gradleいずれの場合も、
gradle/wrapper/gradle-wrapper.propertiesがgradle-8.13-bin.zipで終わり、かつdistributionSha256Sum行を含んでいることを確認します。ステップ 5 のビルドは常に./gradlew(ピン留めされたラッパー)を呼び出し、システムのgradleは決して呼び出しません。 -
ステージング用コピーに
gradle.propertiesを生成します(管理ホストのビルドではこれらの設定が環境から自動的に提供されますが、スタンドアロン環境にはありません)。android.useAndroidX=trueがないと、androidx 依存関係(appcompat、core-ktx)のリソースリンクに失敗します。Android ビルドを不安定な状態から一発で成功する状態に変える、唯一の設定です。android.useAndroidX=true android.enableJetifier=false org.gradle.jvmargs=-Xmx2048m
使い捨てのコピーへの編集に確認は不要です(正規のソースには手を加えません)。適用したスタンドアロン環境向けの調整(React の座標の置き換え、compileSdk 35、gradle.properties の生成)を報告するだけで、管理ホストのビルド環境との違いをユーザーに知らせます。
ステップ 3 — DexClassLoader の制約を検証する
manifest.entrypoints.android.packageClass が示す場所に、想定される Android モジュールまたは ReactPackage の .kt ファイルが存在しない場合は、Gradle の不可解なエラーに任せず、NEEDS_CONTEXT: Android module structure missing — see shared/repo-layout.md を表示して停止します(通常はマニフェストスキルの構造事前チェックで先に検出されますが、これはチェックが省略された場合の最後の確認です)。
ビルド前に、ソースが実行時の読み込み制約(ppmplugin-format §5)を満たしていることを確認します。Kotlin ファイルを読み、次の点を確認します。
- モジュール内のどこにも
@ReactModuleアノテーションがないこと。DexClassLoader から認識できない静的シンボルが必要です。存在する場合は、BLOCKED: remove @ReactModule annotation (incompatible with DexClassLoader)を表示して停止します。 getName()の戻り値がmanifest.jsonのreceivers[].nativeModuleと一致していること。- ReactPackage の FQN が
manifest.jsonのentrypoints.android.packageClassと一致していること。 ReactPackage(packageClass)に public な引数なしコンストラクターがあること。wrap ランタイムはgetDeclaredConstructor().newInstance()でインスタンス化します。クラスが引数付きコンストラクターしか宣言していない場合、ビルドエラーにはならず、プラグインは実行時に読み込みに失敗します(Loaded 0 plugin package(s))。<Pascal>Package.ktを読み、class <Pascal>Package : ReactPackageのような単純なクラス宣言であれば問題ありません(引数なしコンストラクターが暗黙的に用意されます)。class <Pascal>Package(...)のようにプライマリコンストラクターの引数リストがある場合は不可です。引数がある場合は、BLOCKED: ReactPackage <Pascal>Package must have a public no-arg constructorを表示して停止します。- 各
@ReactMethodの引数が、1 つのReadableMapリクエストパラメーター(その後にPromise)だけであること。wrap プロキシは PCF のargs: [request]を位置引数として展開するため、リクエストを複数の位置引数に展開するメソッドにはデータが渡されません。@ReactMethodのシグネチャーが(request: ReadableMap, promise: Promise)形式でない場合は、WARNING を表示します。getStatus()→(promise: Promise)のような引数なしの操作では意図的な場合もありますが、複数の位置引数を取るメソッドは、ほぼ確実にディスパッチ契約の誤りです(ppmplugin-format §2)。 - SDK 時代のインポートがないこと。モジュールのソースに
INativeExtension、INativeOperation、INativeExtensionContext、sendAsync、handleMessageAsync、HermesBytecodeLoader、powerapps-native-extensionのインポートが含まれていないこと。このバンドルはネイティブ専用です。wrap ホストはNativeModules.<nativeModule>.<method>に直接ディスパッチするため(ppmplugin-format §2 — Runtime dispatch contract)、SDK シンボルが含まれていれば混入しており、後続の/audit-ppmpluginの DEX スキャンで拒否されます。ソースの段階で検出します。存在する場合は、BLOCKED: SDK-era symbol '<sym>' in module source — the .ppmplugin is native-only (no INativeExtension/sendAsync layer)を表示して停止します。
ここに不一致がある場合、マニフェストとバイナリの内容が一致していないため、デバイス上で呼び出しがモジュールに届きません。壊れた組み合わせをビルドせず、具体的な不一致を示して停止します。
ステップ 4 — 事前クリーンアップ
古い出力が残っていると、紛らわしい競合が発生します。ビルド前に次の処理を行います。
- 以前の
ppmplugin/staging/android/<Pascal>Plugin.dexを削除します。 - ステップ 2 でステージング用コピーの
ppmplugin/staging/android-build/を新しく再コピーしているため、その中のbuild/はすでに削除されています。以前のホストビルドで作成された正規のandroid/build/は今回関係ありません。そこではビルドしません。
Step 5 — AAR をビルド → DEX
実際にビルドを実行し、ストリーミングログではなく出力先を示します(shared-instructions §9)。
-
ステージングコピーからリリース AAR をアセンブルします。shared-instructions §5 に従って OS 対応のラッパーを呼び出します。
cd ppmplugin/staging/android-build && ./gradlew :assembleRelease # macOS / Linuxcd ppmplugin\staging\android-build; .\gradlew.bat :assembleRelease # Windows出力先:
ppmplugin/staging/android-build/build/outputs/aar/<module>-release.aar。失敗した場合は、失敗したタスクと最も関連性の高い Gradle エラー行を表示し、BLOCKED: gradle assembleRelease failed — <line>で STOP します(dogfooding の教訓に従い、実際のエラーを表示し、握りつぶさないでください)。よくある単独ビルドの失敗とその修正方法は、ppmplugin-format.md §5に記載されています(compileSdk が低すぎる → platform 35 が不足、React の解決エラー → 座標指定が誤っている)。また、Cannot add extension with name 'kotlin'の場合も識別してください。コピー側での Kotlin プラグインの適用が AGP と競合しています(AGP 9.x には Kotlin がバンドルされているため、手動でのkotlin-android適用が衝突します)。AGP 8.8.2(明示的なkotlin-androidが必要)に固定し、control 自体の gradle でビルドするため、ここではまれですが、発生した場合はコピー側の Kotlin プラグイン適用を AGP 8.8.2 に合わせてください。 -
classes.jarを抽出して DEX にコンパイルします。shared-instructions §5 に従って OS に対応します(macOS/Linux ではd8、Windows ではd8.bat。Windows SDK のbuild-tools/<ver>/フォルダーを PATH に追加する必要があります)。macOS / Linux(bash): Step 1 で解決した 絶対パス
$D8を使ってd8を呼び出します(通常、PATH にはありません)。d8だけで実行できると仮定しないでください。mkdir -p ppmplugin/staging/android work=$(mktemp -d) cp ppmplugin/staging/android-build/build/outputs/aar/<module>-release.aar "$work/" ( cd "$work" && jar xf <module>-release.aar classes.jar && "$D8" --min-api 24 --output . classes.jar ) cp "$work/classes.dex" ppmplugin/staging/android/<Pascal>Plugin.dexWindows(PowerShell):
$D8は解決済みの…\build-tools\35.0.0\d8.batです。New-Item -ItemType Directory -Force ppmplugin\staging\android | Out-Null $work = New-Item -ItemType Directory -Force (Join-Path $env:TEMP "ppm-dex") Copy-Item ppmplugin\staging\android-build\build\outputs\aar\<module>-release.aar $work Push-Location $work jar xf <module>-release.aar classes.jar & $D8 --min-api 24 --output . classes.jar Pop-Location Copy-Item (Join-Path $work classes.dex) ppmplugin\staging\android\<Pascal>Plugin.dexType com.facebook.react.* was not found形式のd8警告は 想定内であり、問題ありません(RN はcompileOnlyです)。ユーザーがエラーと誤解しないよう、その旨を明示してください。d8の警告以外の エラー があれば、その行を示して STOP します。 -
ppmplugin/staging/android/<Pascal>Plugin.dexが存在し、空でないことを確認します。 -
任意 — ネイティブ
.soライブラリ。 モジュールに JNI 依存関係がある場合、リリース AAR にはjni/<abi>/*.soとして格納されています。それらをppmplugin/staging/android/lib/<abi>/*.soに抽出します(<abi>はarm64-v8a、armeabi-v7a、x86、x86_64のいずれか)。これにより、/assemble-ppmpluginがandroid/lib/以下に ZIP 化します。wrap injector は各 ABI ディレクトリを APK のlib/<abi>/にコピーします(ppmplugin-format §5)。jar tf <module>-release.aar | grep '^jni/'で確認してください。何もなければ(純粋な Kotlin 製 control である一般的なケース)、この手順は省略します。
Step 6 — 報告 + 次のステップ
.extension-state.md の ## ppmplugin (third-party controls) ブロックを更新し、Android DEX: built <ISO timestamp> (<dex name>) と記載します。DEX のパスとサイズを含むフェンス付きの要約を表示し、その後 AskUserQuestion で次のステップを提示します(shared-instructions §9.1)。
- Run /assemble-ppmplugin(次に推奨 — manifest + DEX を
.ppmpluginに ZIP 化します) - Re-run /generate-ppmplugin-manifest(メソッド一覧や名前が変更された場合)
- Stay — I'll inspect the DEX first(Stay — まず DEX を確認します)
ユーザーが Run /… オプションを選んだら、同じターン内に Skill ツールでそのスキルを直ちに呼び出してください(サブスキル呼び出し。shared-instructions §8 + §9.1 の「Execute, don't describe」に従います)。「準備ができたら実行してください」という指示を表示して終了してはいけません。「Stay」を選んだ場合にのみ実行を終了します。
DEX のパスを含めて DONE を返します。または、@ReactMethod の数が文書化された操作数と異なる場合(manifest スキルの Step 1 で検出されます)は DONE_WITH_CONCERNS を返します。