关于 build-android-binary
面向扩展工程师的构建技能,可将 Android 源码编译为 wrap 运行时加载的 DEX 二进制文件。它在临时暂存副本中构建,不改动维护中的 `android/` 源码。
主要功能
- 01构建 Android DEX 二进制文件
- 02使用 `android/` 的暂存副本
- 03将 Gradle wrapper 固定为 8.13
- 04检查 JDK、Android SDK 和构建要求
适用场景
- 01从 Android 源码生成 `Plugin.dex`
- 02修改 Android 模块代码后重新构建
- 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任务。不要将它与/assemble-ppmplugin混淆,后者会打包最终的 bundle——这是不同的层级。此技能生成二进制文件;后者生成bundle。
**跨平台:**可在 macOS、Linux 和 Windows 上运行——Android 路径没有仅限 Mac 的步骤(JDK 17、Gradle 和 Android SDK
d8在这三个平台上都可用)。根据共享说明 §5,以下每条 shell 命令均提供 bash 和 PowerShell 两种形式。
阅读 shared/ppmplugin-format.md §5——此技能会执行其中的 Android 二进制文件要求。
此技能不会做什么
- 不会编写
manifest.json(请先运行/generate-ppmplugin-manifest——此技能会读取它进行交叉检查)。 - 不会打包
.ppmplugin——这由/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)部分。如果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(提供安装选项;由用户确认) | | Gradle(仅用于引导) |gradle --version— 任何版本均可;它只用于生成固定版本的 wrapper(步骤 2.3)。系统 Gradle 不用于构建,因此不要在此检查其版本。 | 如果缺失,根据操作系统安装 Gradle | | Android SDK Build-Tools 35 +d8| 定位d8,不要求它位于 PATH 上(见下文) | 如果在任何位置找到,检查通过。只有完全找不到d8时 → 自动修复需确认:<sdkmanager> "build-tools;35.0.0"。 | | Android 平台 35 |<sdkmanager> --list_installed包含platforms;android-35,或$ANDROID_HOME/platforms/android-35/存在 | **确认后自动修复:**运行安装(见下文)。 |
定位 d8(不要仅仅因为它不在 PATH 上就阻止操作)。 安装了 Android Studio 的用户通常已经安装了 d8,但它很少位于 PATH 上。将其解析为绝对路径,并在步骤 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,并通过备注标记检查通过:✓ d8 found at <path> (will invoke by absolute path)。不要求修改 PATH。 - 只有在任何位置都找不到
d8时才阻止操作 → 提供通过<sdkmanager> "build-tools;35.0.0"安装的选项(以相同方式解析<sdkmanager>— 搜索$ANDROID_HOME/cmdline-tools/*/bin/sdkmanager和$ANDROID_HOME/tools/bin/sdkmanager,不要只搜索 PATH)。
安装平台 35(确认后自动修复)。 平台 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"
之后重新验证,再继续。只有用户拒绝或安装失败时才停止。
为何必须是平台 35: 独立构建使用 compileSdk 35 编译(步骤 2),因为 RN 0.79 的 react-android AAR 是基于 compileSdk 35 构建的,并且 AGP 强制要求使用者的 compileSdk ≥ 35。因此需要 platforms;android-35 和 AGP 8.x(7.x 无法针对 android-35 编译)。所以 35 是最低版本,而不是“34 或更高”。
步骤 2 — 准备独立构建副本(保持规范的 android/ 原样)
仓库的 android/ 是一个裸库模块,旨在由托管宿主构建的 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,使其不会被打包——运行时由 wrap shell 提供 RN。如果必须改写,请说明:规范源文件正在偏离,应由/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" -
添加 Gradle wrapper,并固定为 RN 0.79 搭配 AGP 8.8.2 使用的版本。 RN 0.79 将 AGP 8.8.2 与 Gradle 8.13 搭配使用。wrapper 必须固定为 8.13(AGP 8.8 需要 Gradle 8.10.2 或更高版本;较旧的 Gradle 会失败)。
首选方案——写入预生成的 wrapper,跳过引导步骤。 直接在暂存副本中写入
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前验证 wrapper JAR。gradlew会执行gradle-wrapper.jar,因此它需要与发行版受到同等处理:只能从可信来源复制,例如控件自身已提交的android/gradle/wrapper/,或经过验证的 Gradle 安装,并根据https://downloads.gradle.org/distributions/gradle-8.13-wrapper.jar.sha256发布的官方 Gradle 8.13 wrapper 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,使 wrapper 任务无需评估任何内容;生成 wrapper 后再恢复: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(固定版本的 wrapper),绝不调用系统的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 约束
如果预期的 Android 模块 / ReactPackage .kt 文件不存在于 manifest.entrypoints.android.packageClass 所指示的位置,则停止,并报告 NEEDS_CONTEXT: Android module structure missing — see shared/repo-layout.md,而不是任由 Gradle 以晦涩的方式报错(manifest 技能的结构预检通常会先发现此问题;如果该检查被跳过,此检查就是后备措施)。
构建前,验证源代码是否符合运行时加载约束(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)必须有一个公开的无参构造函数。 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都必须只接受一个ReadableMap请求参数(然后是Promise)——wrap 代理会按位置展开 PCF 的args: [request],因此将请求展开为多个位置参数的方法无法收到其数据。如果某个@ReactMethod的签名不符合(request: ReadableMap, promise: Promise)的形式,请发出警告(对于getStatus()→(promise: Promise)这类无参操作,这可能是有意为之;但含多个位置参数的方法几乎总是分发契约错误——参见 ppmplugin-format §2)。 - 不得导入 SDK 时代的符号——模块源代码不得包含
INativeExtension、INativeOperation、INativeExtensionContext、sendAsync、handleMessageAsync、HermesBytecodeLoader中的任何内容,也不得包含powerapps-native-extension导入。此软件包仅包含原生代码;wrap 主机会直接分发到NativeModules.<nativeModule>.<method>(参见 ppmplugin-format §2 — 运行时分发契约),因此此处出现 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。 - 暂存副本
ppmplugin/staging/android-build/已在第 2 步重新复制,因此其中的任何build/目录都已被删除。规范目录中的android/build/(来自先前的主机构建)现在无关紧要——我们绝不会在该目录中构建。
第 5 步 — 构建 AAR → DEX
实际运行构建,并展示输出位置,而不是流式日志(shared-instructions §9):
-
从暂存副本组装 release AAR — 根据 shared-instructions §5 调用适用于对应操作系统的包装程序:
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>停止(根据实际试运行经验:展示真实错误,不要吞掉错误)。常见的独立构建失败及其修复方法已列在ppmplugin-format.md §5中(compileSdk 太低 → 缺少平台 35;React 无法解析 → 坐标错误)。还需识别Cannot add extension with name 'kotlin'→ 副本应用的 Kotlin 插件与其 AGP 冲突(AGP 9.x 已内置 Kotlin;手动应用kotlin-android会导致冲突)。此处较少见,因为我们固定使用 AGP 8.8.2(需要显式应用kotlin-android),并使用控制项自己的 gradle 进行构建——但如果出现此问题,请使副本应用的 Kotlin 插件与 AGP 8.8.2 保持一致。 -
提取
classes.jar并编译为 DEX — 根据 shared-instructions §5 适配操作系统(macOS/Linux 使用d8,Windows 使用d8.bat;Windows SDK 的build-tools/<ver>/文件夹必须位于 PATH 中):macOS / Linux(bash): 使用 第 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.dexd8发出的Type com.facebook.react.* was not found形式的警告是预期且无害的(RN 是compileOnly)——请明确说明这一点,以免用户将其误认为错误。d8出现任何错误(非警告)→ 停止并显示该行。 -
验证
ppmplugin/staging/android/<Pascal>Plugin.dex存在且非空。 -
可选 — 原生
.so库。 如果模块有 JNI 依赖,release AAR 会将其放在jni/<abi>/*.so下。将这些库提取到ppmplugin/staging/android/lib/<abi>/*.so(<abi>∈arm64-v8a、armeabi-v7a、x86、x86_64),这样/assemble-ppmplugin就会将其打包到android/lib/下——封装注入器会将每个 ABI 目录复制到 APK 的lib/<abi>/中(ppmplugin-format §5)。使用jar tf <module>-release.aar | grep '^jni/'检查;如果没有结果(常见情况——纯 Kotlin 控制项),则跳过此步骤。
第 6 步 — 报告 + 后续步骤
更新 .extension-state.md 中的 ## ppmplugin (third-party controls) 区块:Android DEX: built <ISO timestamp> (<dex name>)。打印包含 DEX 路径和大小的围栏摘要,然后通过 AskUserQuestion 提供后续选项(shared-instructions §9.1):
- 运行 /assemble-ppmplugin(推荐的下一步——将清单 + DEX 打包到
.ppmplugin中) - 重新运行 /generate-ppmplugin-manifest(如果方法列表或名称已更改)
- 暂不运行 — 我先检查 DEX
用户选择 Run /… 选项时,必须在同一轮中立即通过 Skill 工具调用该技能(子技能调用,shared-instructions §8 + §9.1「执行,不要描述」)。不要输出“准备好后运行”之类的指令然后停止。只有选择“暂不运行”时才结束运行。
返回 DONE 并附上 DEX 路径;如果 @ReactMethod 数量与已记录的操作数不一致(清单技能的第 1 步会标记这一情况),则返回 DONE_WITH_CONCERNS。