iOS 自动打包:Jenkins、GitLab CI 在 Windows、Linux 上自动打包 iOS
最后更新
iOS 自动打包可以放在现有的 Windows 或 Linux 服务器上:装好 KXBuild 后接进 Jenkins 或 GitLab CI,证书和密码放进 CI 的密钥管理,流水线里一条 kxbuild build -c Release --export app-store 出签好名的 ipa,构建号取 CI 的递增编号,再用 iTMSTransporter 自动上传 TestFlight。原有的 xcodebuild 脚本照常可用。
iOS 自动打包的构建机怎么准备?
在构建机上按安装页装好 KXBuild 并登录账号,然后新开一个终端运行体检,全部通过即可。Linux 用安装页的 install.sh 一条命令安装。Windows 服务器选「直接在 Windows 安装」,这样 Jenkins 和 GitLab Runner 启动的 PowerShell 能直接调用 kxbuild。
1kxbuild doctor
安装脚本会把安装目录写进当前用户的环境变量。Jenkins 节点和 GitLab Runner 要用同一个用户运行,否则流水线里找不到 kxbuild。以服务方式运行的,在服务属性里把登录身份改成这个用户;做不到的,在节点或 Runner 的环境变量里加上 KXBUILD_HOME 和它下面的 bin 目录。流水线第一步运行一次 kxbuild version,能打印版本号就说明路径没问题。
再给构建机设置一个环境变量 KXBUILD_NO_UPDATE_CHECK=1,关掉新版本提示和后台检查。升级放在维护时间手动执行 kxbuild doctor,不要写进每次构建,免得两次构建用了不同版本的工具链。目录和环境变量的完整说明见目录与环境变量。
Windows 构建机把 Jenkins 工作区或 Runner 的 builds 目录放在短路径上,例如 D:\ci,React Native 这类依赖层级很深的工程超过 260 字符会报错。
证书和描述文件怎么交给流水线?
签名要三样东西,发布证书 .p12、它的密码、App Store 描述文件。证书和密码不要提交进仓库,放进 CI 系统的密钥管理里,构建时临时写进一个证书目录,用构建设置 CODE_SIGN_KEYCHAIN 指向它,构建结束后删除。描述文件本身不含私钥,也可以直接提交在仓库里。
| 名称 | Jenkins 凭据类型 | GitLab 变量类型 | 内容 |
|---|---|---|---|
| IOS_P12 | Secret file | File | Apple Distribution 证书 .p12 |
| IOS_P12_PASSWORD | Secret text | Variable,勾选 Masked 和 Protected | .p12 的密码 |
| IOS_PROFILE | Secret file | File | App Store 类型的 .mobileprovision |
临时证书目录里的结构和证书与签名一致,.p12 和描述文件放在一起,password.properties 每行写一条「文件名=密码」,文件名必须和 .p12 完全一致。
专用构建机只给一个团队用的话,也可以把证书一次性放进构建机的 KXBUILD_HOME\keychain,所有工程共用,流水线里就不用处理证书。代价是任何能在这台机器上跑流水线的人都能拿到私钥。
生成 .p12 和描述文件的方法见证书与签名。
流水线里用什么命令打包?
流水线里的出包命令用 kxbuild build,参数含义见 kxbuild 命令参考。
1kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa -D CODE_SIGN_KEYCHAIN="D:/ci/myapp/ci-keychain"
--derivedDataDir ./out把构建输出固定在工作区里,--clean每次从干净目录开始。--skip-package-updates让 Swift Package 严格按 Package.resolved 的版本解析、不访问远程仓库,Package.resolved 要提交进仓库。--ipa指定 ipa 的位置,旁边会同时生成 MyApp.AppStoreInfo.plist,上传 App Store Connect 时用得到。--export app-store用证书目录里的 Apple Distribution 证书和 App Store 描述文件签名,不管工程 Release 配置里设的是哪种证书,得到的 ipa 可以直接上传 App Store Connect。
命令成功退出码为 0,失败为非 0,Jenkins 和 GitLab 据此判断构建是否失败。想在脚本里解析结果可以加 --json,每行输出一条 JSON 事件;排查构建失败时去掉它,错误详情只在普通输出里。
用 CocoaPods 的工程,构建前先在工程目录运行 pod install,构建 .xcworkspace,见 CocoaPods。
Flutter、React Native、Expo、Kotlin Multiplatform 工程在构建前先运行一次带工程目录的体检,它会检查框架环境,并准备好 iOS 端的工程,之后再按对应技术栈页的方法安装依赖、构建 ios 目录下的工程。
1kxbuild doctor ./my_flutter_app
已有的 xcodebuild 脚本
构建机上的 KXBuild 自带和 macOS 同名的 xcodebuild 和 xcrun,原来在 Mac 上跑的构建脚本、框架自带的构建脚本调用它们时不用改。
支持 build、clean、archive 等动作和 -workspace、-scheme、-configuration、-sdk、-derivedDataPath 以及 KEY=VALUE 覆盖等常用参数,完整列表见 xcodebuild / xcrun 兼容。
1xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphoneos -derivedDataPath ./out
导出 ipa 的 -exportArchive 也照常可用,按 ExportOptions.plist 的 method 用证书目录里的证书和描述文件签名。也可以把「archive 再 exportArchive」的两步换成一条 kxbuild build -c Release --export app-store,直接得到签好名的 ipa。
构建号怎么自动递增?
App Store Connect 要求同一版本下每次上传的构建号(CFBundleVersion)比上一次大。流水线里用 -D 覆盖 CURRENT_PROJECT_VERSION,取 CI 自带的递增编号,不用改工程文件,详见临时覆盖构建设置。
1kxbuild build ./MyApp.xcodeproj -c Release --export app-store -D MARKETING_VERSION=1.2.0 -D CURRENT_PROJECT_VERSION=45
Jenkins 里用 BUILD_NUMBER,GitLab 里用 CI_PIPELINE_IID(项目内递增的流水线编号)。版本号 MARKETING_VERSION 可以从 git tag 取,也可以留在工程里手动改。
这种写法要求 Info.plist 的 CFBundleVersion 写成 $(CURRENT_PROJECT_VERSION)、CFBundleShortVersionString 写成 $(MARKETING_VERSION),Xcode 13 之后新建的工程默认如此。老工程里写死数字的,用属性列表编辑器改一次并提交。
Jenkins 怎么配置 iOS 自动打包?
下面两个 Jenkinsfile 分别用于 Windows 节点和 Linux 节点,流程相同。先在 Jenkins 的凭据管理里按上面的表创建三条凭据,ID 分别为 ios-p12、ios-p12-password、ios-profile。
Windows 节点
1pipeline {
2 agent { label 'ios-windows' }
3 environment {
4 KXBUILD_NO_UPDATE_CHECK = '1'
5 }
6 stages {
7 stage('Build') {
8 steps {
9 withCredentials([
10 file(credentialsId: 'ios-p12', variable: 'IOS_P12'),
11 string(credentialsId: 'ios-p12-password', variable: 'IOS_P12_PASSWORD'),
12 file(credentialsId: 'ios-profile', variable: 'IOS_PROFILE')
13 ]) {
14 powershell '''
15 kxbuild version
16 $kc = Join-Path $env:WORKSPACE 'ci-keychain'
17 New-Item -ItemType Directory -Force $kc | Out-Null
18 Copy-Item $env:IOS_P12 "$kc/distribution.p12"
19 Copy-Item $env:IOS_PROFILE "$kc/MyApp_AppStore.mobileprovision"
20 "distribution.p12=$env:IOS_P12_PASSWORD" | Set-Content -Encoding ascii "$kc/password.properties"
21 kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa -D CODE_SIGN_KEYCHAIN="$kc" -D CURRENT_PROJECT_VERSION=$env:BUILD_NUMBER
22 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
23 '''
24 }
25 }
26 }
27 }
28 post {
29 success {
30 archiveArtifacts artifacts: 'out/*.ipa, out/*.AppStoreInfo.plist', fingerprint: true
31 }
32 always {
33 powershell 'Remove-Item -Recurse -Force ci-keychain -ErrorAction SilentlyContinue'
34 }
35 }
36}
脚本最后检查 $LASTEXITCODE,保证 kxbuild 失败时这一步也标为失败。
Linux 节点
1pipeline {
2 agent { label 'ios-linux' }
3 environment {
4 KXBUILD_NO_UPDATE_CHECK = '1'
5 }
6 stages {
7 stage('Build') {
8 steps {
9 withCredentials([
10 file(credentialsId: 'ios-p12', variable: 'IOS_P12'),
11 string(credentialsId: 'ios-p12-password', variable: 'IOS_P12_PASSWORD'),
12 file(credentialsId: 'ios-profile', variable: 'IOS_PROFILE')
13 ]) {
14 sh '''
15 set -e
16 kxbuild version
17 KC="$WORKSPACE/ci-keychain"
18 mkdir -p "$KC"
19 cp "$IOS_P12" "$KC/distribution.p12"
20 cp "$IOS_PROFILE" "$KC/MyApp_AppStore.mobileprovision"
21 printf 'distribution.p12=%s\\n' "$IOS_P12_PASSWORD" > "$KC/password.properties"
22 kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa -D CODE_SIGN_KEYCHAIN="$KC" -D CURRENT_PROJECT_VERSION="$BUILD_NUMBER"
23 '''
24 }
25 }
26 }
27 }
28 post {
29 success {
30 archiveArtifacts artifacts: 'out/*.ipa, out/*.AppStoreInfo.plist', fingerprint: true
31 }
32 always {
33 sh 'rm -rf ci-keychain'
34 }
35 }
36}
脚本用 ''' 单引号字符串,密码由 shell 展开,不经过 Groovy 插值,Jenkins 不会因此提示泄露风险。printf 里的 \\n 在 Groovy 字符串中写成两个反斜杠,传给 shell 的是 \n。
GitLab CI 怎么配置 iOS 自动打包?
在 GitLab 项目的 Settings → CI/CD → Variables 按上面的表添加三个变量。File 类型的变量在作业里展开为一个临时文件的路径。Runner 用 shell 执行器,注册时打上 ios-linux 或 ios-windows 标签。
1stages:
2 - build
3
4variables:
5 KXBUILD_NO_UPDATE_CHECK: "1"
6
7build-ios:
8 stage: build
9 tags: [ios-linux]
10 rules:
11 - if: $CI_COMMIT_TAG
12 - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
13 script:
14 - kxbuild version
15 - export KC="$CI_PROJECT_DIR/ci-keychain"
16 - mkdir -p "$KC"
17 - cp "$IOS_P12" "$KC/distribution.p12"
18 - cp "$IOS_PROFILE" "$KC/MyApp_AppStore.mobileprovision"
19 - printf 'distribution.p12=%s\n' "$IOS_P12_PASSWORD" > "$KC/password.properties"
20 - kxbuild build ./MyApp.xcworkspace -s MyApp -c Release --export app-store --clean --derivedDataDir ./out --skip-package-updates --ipa ./out/MyApp.ipa -D CODE_SIGN_KEYCHAIN="$KC" -D CURRENT_PROJECT_VERSION="$CI_PIPELINE_IID"
21 after_script:
22 - rm -rf "$CI_PROJECT_DIR/ci-keychain"
23 artifacts:
24 paths:
25 - out/*.ipa
26 - out/*.AppStoreInfo.plist
27 expire_in: 30 days
Windows 上的 Runner 执行器 shell 设为 powershell 或 pwsh,script 部分换成 PowerShell 写法,和上面 Jenkins Windows 示例中 powershell 块里的命令相同,变量用 $env:IOS_P12 这种形式读取,构建号用 $env:CI_PIPELINE_IID。
GitLab 的 Masked 变量要求值至少 8 个字符,.p12 密码太短时设不上 Masked,建议导出 .p12 时就用足够长的密码。勾选 Protected 的变量只在受保护的分支和 tag 上可用,用 tag 触发打包时要在 Settings → Repository 中把对应的 tag 设为受保护。
ipa 产物怎么归档?
上面两个示例都把 out/*.ipa 和 out/*.AppStoreInfo.plist 作为构建产物保存,在 Jenkins 的构建页或 GitLab 的作业页可以直接下载。ipa 签好了名,可以直接上传。
给测试人员直接安装的包,把 --export app-store 换成 --export ad-hoc,临时证书目录里放 Ad Hoc 描述文件,见发布与上架中的「不上架,直接分发」。一次流水线同时出 App Store 包和 Ad Hoc 包时,两份描述文件都放进临时证书目录,分两次构建,用 --ipa 放到不同文件名。
怎么自动上传到 TestFlight?
手动上传用 Appuploader 或 Transporter,见发布与上架。要在流水线里自动上传,可以用 Apple 的命令行版 Transporter(iTMSTransporter),它有 Windows 和 Linux 版,在 App Store Connect 帮助页的 Transporter 部分下载,装在构建机上。
认证用 App Store Connect API 密钥,不需要账号密码和两步验证。在 App Store Connect 的「用户和访问 → 集成 → App Store Connect API」生成一个团队密钥,角色选 App 管理,记下 Key ID 和 Issuer ID,下载 AuthKey_.p8。.p8 只能下载一次,和 .p12 一样存进 CI 的 Secret file,上传前复制到工作目录的 private_keys 子目录。
1mkdir -p private_keys
2cp "$ASC_KEY_FILE" "private_keys/AuthKey_${ASC_KEY_ID}.p8"
3iTMSTransporter -m upload -assetFile out/MyApp.ipa -assetDescription out/MyApp.AppStoreInfo.plist -apiKey "$ASC_KEY_ID" -apiIssuer "$ASC_ISSUER_ID"
-assetDescription 用的就是构建时 ipa 旁边生成的 AppStoreInfo.plist。App Store Connect 里要先建好这个 App 的记录,Bundle ID 和包中一致。上传成功后 Apple 需要处理几分钟到几十分钟,构建才会出现在 TestFlight 页。
GitHub Actions 怎么用?
GitHub 托管的 Windows 和 Linux runner 每次都是全新环境,工具链需要重新下载。用 GitHub Actions 时建议在自己的构建机上注册自托管 runner,按本文第一节准备好,workflow 里的步骤和上面 GitLab 示例的 script 相同,证书和密码放在仓库的 Secrets 里。无论用哪种 CI,iOS 自动打包的核心都是同一条 kxbuild build 命令加临时证书目录。
常见问题
原来的 xcodebuild 构建脚本要改吗?
不用改。build、clean、archive、-exportArchive 和 -workspace、-scheme 等常用参数照常可用,证书和描述文件放进证书目录即可;想简化,可以把「archive 再 exportArchive」换成一条 kxbuild build -c Release --export app-store,完整参数见 xcodebuild / xcrun 兼容。
流水线里提示找不到 kxbuild 命令怎么办?
让 Jenkins 节点或 GitLab Runner 用安装 KXBuild 的那个用户运行。安装脚本只把路径写进当前用户的环境变量,做不到时在节点或 Runner 的环境变量里加上 KXBUILD_HOME 和它下面的 bin 目录。
自动上传 TestFlight 需要账号密码和两步验证吗?
不需要。用 App Store Connect API 密钥认证,记下 Key ID、Issuer ID 并保存 .p8 文件,iTMSTransporter 上传时传入即可。.p8 只能下载一次,要存进 CI 的 Secret file。
GitLab 的 .p12 密码变量为什么勾不上 Masked?
密码少于 8 个字符。GitLab 的 Masked 变量要求值至少 8 个字符,重新导出 .p12 时设一个足够长的密码。