我们组是 .NET 背景,从 Xamarin 一路迁到 MAUI,主力开发机全是 Windows。MAUI 对 Windows 开发者算友好的了——Visual Studio 就能写 iOS 界面、能调试,但真到出包这一步,编译和签名还是得由 Apple 的工具链完成,Windows 上的做法是 Pair to Mac,把编译任务通过网络甩给一台装了 Xcode 的 Mac。
那台 Mac 我们买了,但它现在基本就是个编译节点,没人登录去操作。证书、描述文件、上传这些活儿我用开心上架(AppUploader)在 Windows 上处理,不用为了签个名跑去开钥匙串。
先说结论:哪一步真绕不开 Mac
| 环节 | 我们实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
| 编译签名成 IPA | 要 macOS——Visual Studio 的 Pair to Mac 转交过去 |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说清楚一点:我用的工具本身是跨平台的,网页版在 macOS、Windows、Linux 的浏览器里都能开,桌面客户端也有 macOS 版。表里"不需要 Mac"是指这一步不再被 Xcode、钥匙串访问、Transporter 卡住,不是说它只能在 Windows 上用。
第一步:确认 Application ID
.csproj 里这几行:
<ApplicationId>com.yourcompany.appname</ApplicationId>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
ApplicationId 会变成 iOS 的 CFBundleIdentifier。ApplicationVersion 是 build number,每次上传都得比上次大——这个我们忘过好几次,传上去直接报版本号冲突,白等一轮。
从 Xamarin 迁过来的老工程要留意,Xamarin 时代很多手写在 Info.plist 里的配置项,MAUI 改成了 .csproj 属性,签名相关属性名以你用的 .NET 版本官方文档为准。
第二步:在 Windows 上把签名材料备齐
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填 ApplicationId 的值。创建后改不了。用到推送、iCloud、App Groups 的,在这个 Bundle ID 的「权限」页面勾上再保存。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书(对应 Apple Distribution),真机调试选开发签名证书(Apple Development)。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串的时候要输,服务器不保存,忘了只能吊销重签。
我建议勾上「使用开心上架服务同步证书」。我们团队三个人加一台构建机,全靠这个云备份下同一张证书,不用互相发文件。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
私钥和 CSR 都在本机生成后封进 .p12,不用在 Mac 上走一遍「钥匙串访问 → 从证书颁发机构请求证书」那套流程。
第三步:把材料装到构建机上
不管是 Pair to Mac 连的那台,还是 CI Runner,都是这两件事:
# 导入证书(含私钥)
security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign
# 描述文件放到 Xcode 认识的位置
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
Visual Studio 在 Pair to Mac 之后,iOS 打包选项里那两个下拉框读的就是这台 Mac 的钥匙串和描述文件目录。要是下拉框是空的,先确认 Mac 上开了远程登录、装了对应版本的 Xcode,两台机器在同一网络——这基本都是环境问题,跟证书没关系。
CI 上把两个文件用 base64 存进加密变量,构建时解码执行上面两条命令,跑完删掉临时文件。
第四步:发布出 IPA
在 .csproj 里指定手动签名用的证书和描述文件:
<PropertyGroup Condition="'$(Configuration)'=='Release' and $(TargetFramework.Contains('-ios'))">
<CodesignKey>Apple Distribution: Your Company (TEAMID)</CodesignKey>
<CodesignProvision>你的描述文件名称</CodesignProvision>
<ArchiveOnBuild>true</ArchiveOnBuild>
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
</PropertyGroup>
然后:
dotnet publish -f net9.0-ios -c Release
(目标框架名按你实际用的 .NET 版本替换。)
这两个属性值得说一下,我在这儿浪费过时间:
CodesignKey填的是证书的通用名称,就是钥匙串里显示的那一整串。找不到匹配项的话,通常是.p12没导进钥匙串,或者导进去的是不含私钥的.cer。CodesignProvision填的是描述文件的名称——你在开心上架创建描述文件时填的那个名字,不是文件名,也不是 UUID。
产物是个 .ipa,在 bin/Release/<tfm>/ios-arm64/publish/ 下面。
第五步:把 IPA 传上去
到这一步就不需要 Mac 了。我一般先在开心上架的「文件&提交」页面把包打开看看签名信息——Bundle ID、描述文件类型、证书有效期,确认了再点「提交 App Store」。
命令行版本,Windows/Linux 的 Runner 也能跑:
appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是登录密码。我们现在统一用 API 密钥了,它属于团队、能单独吊销,不会因为谁改了密码就断掉:
appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
第六步:发内测包
「设备」页面点「注册设备」,二维码发群里让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,把新设备包进一份 Ad Hoc 描述文件。
把 CodesignProvision 换成这份描述文件重新 publish,然后在「文件&提交」页面生成二维码让人扫码装。
写在最后
MAUI 这条线,那台 Mac 你可以完全当成黑盒——配好 Pair to Mac 之后就别再登录它了。日常真正需要人操作的证书维护、描述文件重建、包上传,在 Windows 上都能做完。
我用的是开心上架网页版,打开就能用。从 Xamarin 迁过来、被签名配置搞晕的话,可以照着上面理一遍。