Flutter 号称一套代码跑遍全平台,但 iOS 这条线有个例外——flutter build ipa 底层调的是 xcodebuild,而 Xcode 只有 macOS 版。所以编译打包这一步必须在 macOS 上跑,本地的 Mac 或者云端的 macOS 构建机,二选一。
我一开始以为这意味着整条发布链路都得搬到 Mac 上,后来才发现完全不必。Bundle ID 注册、证书签发、描述文件生成、IPA 提交、装机测试,这些一个都不需要 Mac,我现在用开心上架(AppUploader)在 Windows 上处理,Mac 那边只负责跑一条构建命令。
下面把我实际的分工写清楚。
先说结论:哪一步真绕不开 Mac
| 环节 | 我实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
flutter build ipa | 这一步要 macOS——我走 Codemagic 的 macOS Runner |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说清楚一点:我用的工具本身是跨平台的,网页版在 macOS、Windows、Linux 的浏览器里都能开,桌面客户端也有 macOS 版。上表里说"不需要 Mac"不等于"只能在 Windows 用"——只是这一步不再被 Xcode、钥匙串访问、Transporter 这些 macOS 专属工具卡住而已,用 Mac 的人拿它一样省事。
第一步:确认 Bundle Identifier
Flutter 工程里 iOS 的标识在 ios/Runner.xcodeproj 的 PRODUCT_BUNDLE_IDENTIFIER 里。如果当初是用 flutter create --org com.yourcompany 建的工程,这个值已经生成好了,直接沿用就行。
不管怎么定,记住这个字符串,后面注册 Bundle ID 和绑定描述文件都要用同一个,三处必须完全一致。
第二步:在 Windows 上把签名材料备齐
这三件事在一个界面里连着做完,我每次新项目大概花五分钟。
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填上一步那个值。创建后改不了,所以别随手填。用到推送、Sign in with Apple、App Groups 这些能力的,在这个 Bundle ID 的「权限」页面勾上再保存。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定刚注册的 Bundle ID,填名称和邮箱,设一个 .p12 保护密码。
这个密码要当密钥保管——服务器不保存它,也没有找回入口,忘了只能吊销证书重签一张,然后依赖它的描述文件全都要重建。我现在一律丢密码管理器里。
顺手把「使用开心上架服务同步证书」勾上,换机器的时候能直接下载同一张证书,不用满世界找 .p12 在谁电脑上。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
CSR 和私钥都在本机生成,不用开钥匙串,也不用手敲 OpenSSL 那一串命令。
第三步:把材料装到 macOS 构建机上
不管构建机是自己的 Mac 还是 CI Runner,动作都一样:
# 把 .p12 导进钥匙串
security import dist.p12 -k ~/Library/Keychains/build.keychain-db -P "你的p12密码" -T /usr/bin/codesign
# 描述文件放到 Xcode 认识的目录
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
在 CI 上我是把这两个文件用 base64 编码存进加密变量,构建时解码落盘再执行上面两条命令,跑完删掉临时文件。别直接把 .p12 提交进仓库。
第四步:构建 IPA
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
ExportOptions.plist 用来指定用哪份描述文件、导出什么类型的包。手动签名的话大致长这样:
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>你的TeamID</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.yourcompany.appname</key>
<string>你的描述文件名称</string>
</dict>
</dict>
这里有个我卡过很久的细节:provisioningProfiles 里的 key 是 Bundle ID,value 要填描述文件的名称——就是你在开心上架创建描述文件时填的那个名字,不是文件名,也不是 UUID。写错了 xcodebuild 会报找不到 profile,但错误信息完全不提示你是名称写错了。
另外 method 的取值在不同 Xcode 版本里改过(早期是 app-store),照你构建机上那个 Xcode 的文档来。
产物在 build/ios/ipa/*.ipa。
CI 上我建议一律把自动签名关掉、signingStyle 写 manual。开着自动签名的话 Xcode 会自己跑去 Apple 那边申请证书,同类型证书一共就 3 个名额,几轮构建下来就占满了,然后你就得回去挨个吊销。
第五步:把 IPA 传上去
拿到 IPA 之后 Xcode Organizer 和 Transporter 都可以不用了。我一般先在开心上架的「文件&提交」页面把包打开看一眼——Bundle ID、描述文件类型、证书有效期,确认了再点「提交 App Store」,日志实时滚,失败原因直接写在里面。
接流水线就用命令行,Runner 是 Windows 或 Linux 也能跑:
appuploader-cli upload -f build/ios/ipa/app.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码,这个我一开始搞混过。团队场景更推荐 API 密钥,属于团队、能单独吊销:
appuploader-cli upload -f app.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
还有个小提醒:pubspec.yaml 里 version: 1.0.0+3 加号后面那个是 build number,每次上传都得比上次大,不然会被顶回来。
第六步:发内测包
「设备」页面点「注册设备」,二维码发出去让测试的人扫,UDID 自动上报(一年 100 台,免费账号 3 台)。设备齐了回描述文件那边点「重建」,自动用当前有效的证书和设备重新生成一份 Ad Hoc 描述文件。
然后把 ExportOptions.plist 的 method 换成 ad-hoc、描述文件换成新的,重新 flutter build ipa。最后在「文件&提交」页面生成二维码,测试的人扫码无线装。
Flutter 这条线我现在的状态是:日常开发全在 Windows,需要出包的时候推一下 CI,几分钟后拿 IPA,在 Windows 上传掉。真正需要 macOS 的只有那条 flutter build ipa,而且它跑在别人的机器上。
证书、描述文件和上传我用的是开心上架网页版打开就能用。如果你也在 Windows 上写 Flutter,卡在签名材料这步的话可以照着走一遍。