mcpField
B
build-android-binary

build-android-binary

github.com/microsoft开发1.1k72 次安装今天更新
代码仓库

关于 build-android-binary

面向扩展工程师的构建技能,可将 Android 源码编译为 wrap 运行时加载的 DEX 二进制文件。它在临时暂存副本中构建,不改动维护中的 `android/` 源码。

主要功能

  1. 01构建 Android DEX 二进制文件
  2. 02使用 `android/` 的暂存副本
  3. 03将 Gradle wrapper 固定为 8.13
  4. 04检查 JDK、Android SDK 和构建要求

适用场景

  1. 01从 Android 源码生成 `Plugin.dex`
  2. 02修改 Android 模块代码后重新构建
  3. 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 — 阅读共享文档和前置检查说明

  1. 阅读 shared/shared-instructions.md 和 shared/ppmplugin-format.md。
  2. 阅读 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。选择替换后,继续重新构建。重复构建始终安全——此确认步骤只是为了尊重你可能有意生成的产物。
  3. 运行前置检查并打印可见区块(共享说明 §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 中使用该路径:

  1. command -v d8 / where d8.bat — 如果位于 PATH 上,则使用它。
  2. 否则搜索 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;如果没有,则选择最高版本。
  3. 将绝对路径记录为 $D8,并通过备注标记检查通过:✓ d8 found at <path> (will invoke by absolute path)。不要求修改 PATH。
  4. 只有在任何位置都找不到 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(这会损害真实源代码,只为满足一次性构建),不如从副本构建。

  1. 复制 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-android AAR 是针对 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()。

  2. 添加 settings.gradle,在副本中声明该库为其自身的根项目:

    rootProject.name = "<lower>plugin"
    
  3. 添加 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=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
    

    distributionSha256Sum 是必需项,不可省略。首次调用 ./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。

  4. 在暂存副本中生成 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):

  1. 从暂存副本组装 release AAR — 根据 shared-instructions §5 调用适用于对应操作系统的包装程序:

    cd ppmplugin/staging/android-build && ./gradlew :assembleRelease       # macOS / Linux
    
    cd 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 保持一致。

  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.dex
    

    Windows(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.dex
    

    d8 发出的 Type com.facebook.react.* was not found 形式的警告是预期且无害的(RN 是 compileOnly)——请明确说明这一点,以免用户将其误认为错误。d8 出现任何错误(非警告)→ 停止并显示该行。

  3. 验证 ppmplugin/staging/android/<Pascal>Plugin.dex 存在且非空。

  4. 可选 — 原生 .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。

评论

支持 Markdown