怎么在 Windows 上用 Flutter 打包 iOS 应用并上架 App Store?

48 阅读6分钟

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 StoreWindows 上传
装到 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,卡在签名材料这步的话可以照着走一遍。