关于 build-ios-binary
build-ios-binary 用扩展的原生源代码构建供封装运行时使用的扁平 iOS .framework,面向使用 Xcode 的 Mac 开发者。它在独立暂存目录中构建,React 采用弱链接,由宿主在运行时提供。
主要功能
- 01构建仅含设备切片的扁平 .framework
- 02独立暂存,不修改规范 iOS 源码
- 03检查源代码与清单的一致性
- 04使用固定 React Native 依赖中的 React 头文件
- 05签名和插件组装由其他步骤处理
适用场景
- 01构建 Power 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/(设备切片)。这是 iOS 版的 /build-android-binary:输入源代码,输出预构建的 framework。
**请打包扁平
.framework,不要打包.xcframework。**包装流水线要求使用ios/<Name>.framework,不会深入查找.xcframework;打包后者会报错:"Framework '.framework' not found in plugin." 此技能只构建设备归档,并复制出扁平.framework。参见ppmplugin-format §5b。
**仅限 Mac。**需要 Xcode。在 Linux/Windows 上,此技能会阻止执行——仅开发 Android 的贡献者无法生成 iOS 切片。
已知限制——请阅读。唯一的难点是 React-Core:framework 使用控制仓库自身固定版本的
react-nativedevDep 中的 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——此技能会读取该文件,用于交叉检查以及获取 framework/moduleClass 名称)。 - 不会将
.ppmplugin打包成 zip——这由/assemble-ppmplugin完成。 - 不会构建 Android——这由
/build-android-binary完成。 - 不会修改规范的
ios/或 podspec——所有调整都在ppmplugin/staging/ios-build/下的临时副本中进行。 - 不会为 framework 签名——包装流水线会在打包时签名(使用
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。如果其中没有entrypoints.ios,立即停止并提示NEEDS_CONTEXT: manifest targets Android-only — re-run /generate-ppmplugin-manifest and choose iOS/Both。 - 阅读
.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) |
| Xcode 16+(建议使用 26.2+) | xcodebuild -version | 无法自动修复 — 打印:从 App Store / xcodes 安装 Xcode;如果缺失则立即停止 |
| 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 符号 — 宿主会在运行时提供这些符号)。Apple 已弃用 -undefined dynamic_lookup(目前可用,但未来的 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)必须与 wrap 宿主在运行时提供的 RN 版本匹配(React 使用弱链接,由宿主提供)。打印检测到的 RN 版本,并通过 AskUserQuestion 请用户确认 / 覆盖源和版本,然后再构建。明确说明:*"React 由 wrap 宿主在运行时提供;如果构建或加载时出现 React 符号/头文件错误,说明 RN 固定版本与宿主的 RN 版本不一致。"
第 3 步 — 暂存独立的 Xcode 框架项目(保持规范的 ios/ 原样)
与 Android 构建一样,请从临时副本着手,确保工程师维护的源文件不会被修改。
-
复制
ios/*.{h,m}→ppmplugin/staging/ios-build/Sources/(每次运行时重新创建)。 -
生成 umbrella header
ppmplugin/staging/ios-build/<Pascal>Plugin.h,并将其标记为 Public header(这是构建规范要求,并非运行时所需——包装播放器会对二进制执行dlopen并使用NSClassFromString,不会import模块,因此即使没有 module map,框架也能正常加载;umbrella 仅用于消除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),使用复制的源文件进行编译,并将 umbrella 设为 Public header。根据规范的 podspec 推导:- 要链接的系统框架:取自 podspec 的
s.frameworks(例如,pen-input →PencilKit, UIKit, CoreGraphics, QuartzCore)。 - 部署目标:取自
s.platform(例如:ios, "14.0"),但最低设为 PAM shell 支持的 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 类型 Dynamic Library(非 Static) 死代码剥离 NO(保留 +moduleName类方法和RCT_EXPORT_METHOD元数据,供宿主在dlopen后读取)定义模块 YES 启用 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 — 断言清单符合性
构建前,验证源代码与清单相符(相当于 iOS 版 Android DexClassLoader 断言):
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)— wrap 代理会按位置展开 PCF 的args: [request]。如果某个方法将 request 展开为多个位置参数,就无法接收到数据;请报告 WARNING(像getStatus这样只接收 resolver/rejecter 的无参操作则没问题 — ppmplugin-format §2)。
不匹配意味着清单与二进制不一致 — 设备上的调用将无法到达模块。停止并报告具体的不匹配项,而不是构建一个有问题的组合。
步骤 5 — 构建扁平 .framework(仅设备架构)
实际运行构建;显示产物路径,不要显示流式日志。仅归档 DEVICE 架构切片(wrap CI 需要的是扁平的 <Name>.framework,而不是 .xcframework — 见 §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 而非仅头文件方式构建,请归档 .xcworkspace scheme,而不是 .xcodeproj — 其余完全相同:仍然仅构建设备架构,仍然复制扁平框架。)
失败时,打印失败的步骤和最相关的一行 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/ 必须包含以下两个运行时必需项:
<Pascal>Plugin— Mach-O 二进制文件,名称必须恰好为<Pascal>Plugin(播放器会对Frameworks/<Pascal>Plugin.framework/<Pascal>Plugin调用dlopen;二进制文件名不同就无法加载)。必需。Info.plist— 必需(框架包缺少它即无效,并会导致 wrap codesign 步骤失败)。
如果缺少任一项,则停止并报告:BLOCKED: framework incomplete — missing <X>,并采用 §5b 中的修复方法(GENERATE_INFOPLIST_FILE=YES / 二进制文件名 == 框架名)。伞形头文件 + Modules/module.modulemap 属于构建规范 — 如果缺失,请注明,但不要因此阻止构建(dlopen 加载器不会使用它们 — §5b)。
第 6 步 — 报告 + 下一步
更新 .extension-state.md 中的 ## ppmplugin (第三方控件) 块:iOS framework: built <ISO timestamp> (<Pascal>Plugin.framework, device slice)。打印一段围栏摘要,其中包含 .framework 路径(并注明它包含 umbrella、module.modulemap 和 Info.plist),然后通过 AskUserQuestion 提供下一步(shared-instructions §9.1 — 通过 Skill 工具调用所选 skill;执行,而非描述):
- Run /assemble-ppmplugin(推荐 — 将清单和二进制文件打包为
.ppmplugin) - Run /build-android-binary(如果还需要 Android 切片来创建 Both bundle)
- Stay — 我会先检查 framework
返回 DONE 和 .framework 路径;如果失败,则返回 BLOCKED 和导致失败的 xcodebuild 行(最常见的原因是 React 链接问题或与宿主的 RN 版本不匹配——这不是扩展代码修复问题)。