iOS 生态有个不成比例的堵点:写代码、打包、签名都能分工,唯独「把 .ipa 交给苹果」这一步,很多人只能在一台特定电脑上完成。本文记录我们怎么照着 Apple 公开文档把 Build Upload API 在 Windows 上跑通,以及一路上真实踩到的坑——包括 uploadOperations 不能自己拼、签名类型不对一定被拒、进度 100% 不等于 TestFlight 能看到构建。文章后半段是我们把它做成免安装小工具的过程和数据流向说明。
一、堵点到底有多具体
一个很小的团队,两个客户端、一个后端、一台当构建机用的 Windows。Unity 出 iOS 包的过程是:Windows 上打资源、把工程同步过去,Mac 上 Xcode Archive 导出.ipa。到这里一切顺利。然后要把它送进 App Store Connect——而 Mac 这时候往往正在被人用来复现一个只在真机上出现的 bug。
再往上还有几种更常见的组合:
- 公司只给预算买 Windows,iOS 构建靠外包或远程 Mac,交付物就是一个
.ipa文件; - 做 Flutter / React Native / Unity 的跨端团队,CI runner 是 Windows;
- 个人开发者从安卓转过来,手上只有一台游戏本;
- 需要频繁给不同客户传不同包的外包工作室,排队传包的人比打包的人还多。
传统答案只有两条。第一条是 Apple 自家的iTMSTransporter:需要 macOS、需要 Java、需要 Apple ID + app 专用密码,而且 Apple 已经停止公开下载这个工具,网上流传的是各种「从旧 Xcode 里挖出来」的路子。第二条是第三方中转:把包上传到别人的服务器,由服务器替你交给苹果——这一步在你的账号之外多了一个持有你完整构建产物的地方,包体大小、内部资源、埋点字符串都在里面。
而 Apple 其实早就把这条路开成了公开接口。
二、Apple 真正提供的是四步握手
1. 用 Bundle ID 找到 App 的记录号。GET /v1/apps?filter[bundleId]=com.xxx.app。.ipa里的 Bundle ID 就是它的身份,不需要你额外登记任何对照表。查不到通常意味着这个 Bundle ID 还没在「我的 App」里建出来,或者这把密钥没有该 App 的权限。
2. 申请一个上传单。POST /v1/buildUploads,带上bundleId、CFBundleShortVersionString、CFBundleVersion(就是 build 号)和platform。这一步苹果只是「预留」,还没碰到你的字节
3. 换成一个真正能写的地址。POST /v1/buildUploadFiles挂到上一步的上传单上,苹果在返回体里给出assetToken和一份uploadOperations清单——每一项包含method: PUT、一个带签名的url、offset和length。这一步是全部坑的集中地,单独拎到第三节讲。
4. 提交并等结论。
字节全部写完后PATCH /v1/buildUploadFiles/{id},把uploaded置成true,同时在属性里带上整包的checksum(MD5)和分片校验;然后轮询GET /v1/buildUploads/{id}直到状态落地。
把这四步串起来,Windows、Linux、树莓派都能传包。所谓「iOS 上不了非 Mac 系统」是工具生态造成的错觉,不是协议层的限制。
四、把上面这些收进一个 7 MB 的单文件
跑通之后,我们把这条链路做成了一个小工具,WinIPAUploader,Windows 10 / 11 x64,单文件约 7 MB,免安装、不带 Java、不需要任何运行环境。压缩包里两个可执行文件其实是同一个程序:WinIPAUploader.exe双击出窗口,winipa.exe带参数就是命令行。
几个设计上的选择,说一下理由:
不再有第二个账号体系。你的苹果Issuer ID(发行者 ID)就是登录账号,Apple 让你下载的那个.p8就是凭证。点一次「登录」= 把密钥交给服务器保管 + 请苹果当场确认 + 起算试用,一次完成。新用户不用注册、不用填手机号、不用等审核,新开发者账号第一次绑定成功后自动开始7 天试用。换一台电脑只要重新填这两样,授权跟着 Issuer ID 走。
窗口只做三件事。拖包、确认「会交付给哪个 App」、点开始上传。App 名字是工具拿 Bundle ID 去 ASC 查回来给你确认的,不用手填。五个阶段依次是确认 App → 计算校验 → 分片传输 → 提交 → Apple 处理;任何报错都用中文写清「怎么办」,下面一行保留原始技术信息,找客服时直接复制。
命令行才是给流程用的。
winipa.exe check D:\ipa\shop.ipa--json# 机器可读体检结果,适合做 CI 门禁winipa.exe upload D:\ipa\shop.ipa --no-wait# 传完就退出,不等苹果的结论winipa.exe status<buildUploadId>--ops# 只读状态和分片清单,出问题用它取证--dry-run只走检查和计算、不真的上传;--workers 8调并发;--quiet只打印结果;--timeout 30m设等待上限。要批量就自己写循环——它把上传做成了一次一个明确退出码的进程,配合计划任务或者 Jenkins / GitLab Runner 里的 Windows agent 正好。这里说清楚边界:窗口里没有「一键批量」按钮,批量是脚本层的事。
升级不静默安装。打开窗口会问一次最新版本,有新版本时那行变成可点击的「有新版本 x.y.z」,点一下只复制下载地址,程序不会自己下载执行。
五、数据流向(这段建议别删)
这是别人问我们最多的一节,写清楚比写漂亮有用:
- 你的
.ipa字节不经过我们的服务器。分片是直接 PUT 到苹果在uploadOperations里给的那个上传地址,我们的服务器没有能力、也没有位置去接你的构建产物。-.p8密钥正文由服务器保管,用于替你签苹果凭证。客户端只在你点「登录」或「换一把密钥并重新验证」那一次读文件,内容通过 HTTPS 交给服务端;服务端存的是 AES-256-GCM 密文,本机config.json只记录文件路径。 - 苹果下发的临时凭证只在内存里,十五分钟就过期,不落盘;本机日志
window.log里不会出现.p8正文或苹果凭证。 machineId是安装标识,不参与授权判定。它是第一次运行时随机生成的win-<24位hex>,不含硬件序列号,只用来把客服工单和这台电脑对上。
明说这一层是有代价的:它意味着你信任我们保管密钥。替代方案(凭证全在本机签、包体走第三方中转)各自有各自的风险面,我们把取舍摊开而不是含糊过去,愿意接受这个方案才用。
六、和 iTMSTransporter 的对照
| iTMSTransporter | WinIPAUploader | |
|---|---|---|
| 需要 Mac / Xcode | 是 | 否 |
| 需要 Java | 是 | 否 |
| 需要安装 | 是(且 Apple 已停止公开下载) | 否,单文件 |
| 需要 Apple ID + app 专用密码 | 是 | 否,只用 API 密钥 |
| 要额外记一套账号密码 | 是 | 否,账号就是你的 Issuer ID |
| 包体是否经过别人服务器 | 不经过 | 不经过(直连苹果上传地址) |
| 上传协议 | Apple 私有协议 | Apple 公开文档里的 Build Upload API |
| 传前体检 | 依赖 macOS 侧信息 | 内置,Windows 上直接查签名类型和 Bundle ID |
| 需要授权 | 否(自己承担全部风险) | 是,到期即拿不到上传凭证 |
它不覆盖的部分也说清楚:只管「把构建交给苹果」这一件事,元数据、截图、隐私标签、提审填表、TestFlight 分组,仍然在 App Store Connect 网页里做。