news 2026/10/19 5:54:58

Mac下用Jenkins+fastlane搭建iOS自动化打包实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mac下用Jenkins+fastlane搭建iOS自动化打包实践

1. 为什么我在 Mac 上折腾 Jenkins 和 fastlane

Mac 下搭建 Jenkins & fastlane,这个组合我是在 2021 年上半年正式跑通的。当时我用一个模拟项目 X 练手,这个项目结构跟普通 iOS App 一样,有若干 target、依赖了 CocoaPods,每周都要打一个 Ad Hoc 包丢给测试。最开始的时候,出包流程长这样:打开 Xcode,切到项目,点 Product 里的 Archive,等编译,再进 Organizer 里选导出方式,在导出向导里选 profile、填 ExportOptions,最后把 ipa 传到分发平台。这套动作如果只是偶尔做一次,其实还好,但频率一旦变成每周一次,甚至一天好几次,我就开始烦了。

更让人崩溃的是很多小坑:build number 会忘记递增,测试拿到包之后发现版本号没变,说不清当前打的是哪一版代码;临时要一个“带调试信息的包”,又得跑到工位上去手动操作。还有一次,我在改完代码后急着打一个包,结果 Archive 界面卡在 “Waiting for simulator” 上,折腾半小时才发现是模拟器进程没退出。这些事情单看都是小事,堆在一起就非常消耗耐心。于是我开始研究自动化,最后落地成了 Jenkins 负责调度、fastlane 负责打包的这套组合。

1.1 手动打包为什么不可持续

手动出包最大的问题不是慢,而是不可复现。你在自己电脑上能成功,不代表同事电脑上也能成功;你今天能成功,不代表换一台机器、换一个 Xcode 版本之后还能成功。签名、证书、描述文件、CocoaPods 版本、Ruby 版本、Xcode 命令行工具路径,任何一个环节不一致,都会让出包结果不同。

另外,手动流程特别依赖“人必须在电脑前”。Jenkins 的好处是它像一个放在后台的闹钟加看门人,到了时间或者收到代码推送通知,就自己开始拉代码、装依赖、跑打包脚本,最后把产物归档好。fastlane 则负责最重要的执行细节,比如读版本号、调用 Xcodebuild、处理签名、导出 ipa。两者分工明确,Jenkins 不关心包具体怎么打,fastlane 也不关心是谁用哪个 Job 触发它。

我当时从纯手动切到这套流程,最直观的感受是:每周打包这件事从“需要预留 40 分钟专心操作”变成了“点一下按钮,等通知”。而且因为每次构建都是新 clone 的代码、固定的脚本、固定的构建环境,最终产物的可信度高了很多,出问题也能从构建日志里直接找原因。

1.2 Jenkins 负责调度,fastlane 负责手艺

用生活里的例子类比,Jenkins 相当于一个无人值班的车间管理员,它负责开门、拉物料、记录上下班时间、保存成品;而 fastlane 是车间里最熟练的那个操作师傅,知道怎么调参数、怎么签名、怎么导出。管理员不需要会打螺丝,师傅也不用管考勤,两者通过命令行脚本配合就行。

在技术选型上,为什么是 Jenkins 而不是别的?因为 Jenkins 生态成熟、安装简单,有 Pipeline 插件之后可以把整个流程写成代码,方便入库和 review。fastlane 则恰好补上了移动端打包最繁琐的部分:它把 Xcodebuild 那一大串参数封装成 action,比如gym负责编译打包,match负责同步证书和描述文件,increment_build_number负责版本号递增。没有 fastlane 的话,Jenkins 不是不能做,但你需要自己写很多处理签名、导出选项的脚本,维护成本会高很多。

1.3 这套方案适合谁,不适合谁

我觉得这套组合适合这些场景:团队人数在几十人以内,有 iOS 或跨端项目,发版频率不低,需要快速出内测包;或者是个人开发者,希望在提交代码后自动打好一个可安装的包。它尤其适合那种“不放心别人电脑上的环境”的人,因为 Jenkins 作为构建机,环境由你自己统一控制。

不太适合的场景也有:项目一年只发一次版,那手工包也就够了,没必要维护构建机;如果团队已经用了成熟的 SaaS CI 服务,也不需要重复造轮子。另外要提醒,搭建 Jenkins 意味着你需要有一台长期开机的 Mac,而且这台机器不能随便休眠,否则 Job 会在半夜卡在 “agent is offline” 的状态。我当时的第一台构建机就是一台容易待机的旧 Mac,后来我干脆在系统设置里把休眠关掉,才消停。

2. 环境准备:从空 Mac 到 fastlane 能跑通的顺序

环境问题是最容易让人半途放弃的部分,因为安装顺序错了,后面处处别扭。我先按自己跑通的路径把顺序理一遍。

2.1 先把 Java 和 Ruby 两个“祖宗”理清楚

Jenkins 是 Java 应用,所以第一件事是装 JDK。2021 年那会,Jenkins LTS 稳定线推荐的是 Java 8 或 Java 11,我选了 OpenJDK 11。如果你用 Homebrew,安装很简单:

brew install openjdk@11

如果 macOS 没有自动把 OpenJDK 注册到系统 Java 目录,需要手动建一个符号链接:

sudo ln -sfn /usr/local/opt/openjdk@11/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-11.jdk

Apple Silicon 机器上路径会变成/opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk,细节以brew info openjdk@11的提示为准。装完以后一定要验证:

java -version

看到 openjdk version 11 才算过。注意系统里可能自带其他版本 Java,Jenkins 启动时会去找JAVA_HOME或者默认 Java 命令,最好把 PATH 指清楚。

Ruby 这块也要说。macOS Big Sur 自带 Ruby 2.6,fastlane 2021 年的版本依赖 Ruby 2.6 以上,直接装问题不大。我当时的做法是用 Homebrew 装 fastlane:

brew install fastlane

这种方式的好处是依赖由 Homebrew 统一管理,不会动不动就碰上系统自带 gem 的权限问题。如果希望严格锁定版本,更推荐在项目里放一个 Gemfile:

source "https://rubygems.org" gem "fastlane" gem "cocoapods"

然后执行bundle install。后面 Jenkins 构建时用bundle exec fastlane,这样换机器也保持同一套依赖。

2.2 安装 Jenkins:我为什么选了 pkg,而不是 brew services

Jenkins 在 Mac 上至少有两条路:官网下载.pkg安装包,或者用brew install jenkins-lts。我一开始选了 pkg,因为它会自动创建好/Users/Shared/Jenkins/Home目录和jenkins用户,相当于把 Jenkins 的数据目录固定下来,卸载和迁移都比较清晰。

安装完成后,Jenkins 默认跑在http://localhost:8080。第一次打开会要求输入解锁密码,密码在/Users/Shared/Jenkins/Home/secrets/initialAdminPassword里。我当时直接命令行读取:

cat /Users/Shared/Jenkins/Home/secrets/initialAdminPassword

然后把密码复制到网页里,再设置管理员账号。这一步没什么悬念,关键在插件选择。

另一条路brew install jenkins-lts是用当前用户启动服务,好处是后面打包时访问钥匙串里的证书会省很多事。坏处是服务脚本和数据目录没那么规整。如果你不熟悉 macOS 服务和系统权限处理,我建议从一开始就用 brew 方式,少踩一个钥匙串的坑。这个坑我在后面第 6 章会展开说。

2.3 安装 Xcode 命令行工具并指定版本

有了 Jenkins 和 fastlane 还不够,真正编译 iOS 包需要 Xcode。我当时的机器上装了 Xcode 12.5,系统是从 Xcode 12.4 升上来的。为了避免 xcode-select 指向错误,直接显式设置:

sudo xcode-select -s /Applications/Xcode_12.5.app/Contents/Developer xcodebuild -version

xcodebuild -version输出里应该能看到 Xcode 12.5 和对应的 Build version。这里有个容易忽略的点:只装 Command Line Tools 不够,xcodebuild虽然存在,但打包 iOS App 需要完整的 Xcode.app。

fastlane 在编译时依赖 Xcode 提供的工具链,如果你机器上装了多个 Xcode 版本,最好在 Fastfile 里通过环境变量固定住,而不是依赖全局的 xcode-select。我后面提供的 Jenkinsfile 也会用到类似思路。

2.4 2021 年我验证过的版本组合

给你一张供参考的表,这是当时跑通的组合,不一定是最新,但可以让心里有个底:

工具我用的版本备注
macOSBig Sur 11.xApple Silicon 刚出来,Intel 机器更省心
JDKOpenJDK 11Jenkins LTS 官方推荐的运行版本
Jenkins2.303.x LTS稳定线,插件兼容性好
Xcode12.512.4 也可以,关键是版本要固定
fastlane2.18x用 Gemfile 锁版本,别随便升
CocoaPods1.10.x与 Xcode 12.5 兼容性较好

如果你现在看这篇文章的时间点比 2021 晚很多,版本号肯定变了,但安装流程和踩坑思路基本一致。版本组合的核心原则是:不要在一台不常动的构建机上频繁升级 Xcode 和 fastlane,它们之间的兼容性问题比功能缺失更让人头疼。

3. Jenkins 初始化:账号、插件、全局工具链

装完 Jenkins 只是第一步,接下来把它配成适合 iOS 构建的形态才是正题。

3.1 解锁 Jenkins 和创建管理员账号

第一次访问 8080 端口,Jenkins 会让你输入管理员初始密码。拿到密码后,按页面提示创建一个管理员账号,之后所有配置都用这个账号登录。

有一点我吃了亏:创建账号时一定要勾选“设置为管理员”,不要只创建一个普通用户。否则后面配置插件、增加凭据、修改系统设置时,都会因为权限不够而到处碰壁。如果你已经建成了普通用户,可以在系统设置里的“Manage Users”中调整权限,但没必要绕这么一圈。

3.2 我在 iOS 流水线里实际用到的插件清单

插件这块我建议克制。第一次用 Jenkins 的人容易照着推荐插件一路安装,结果装了一大堆 Java、Maven、Docker 相关插件,真正用到的没几个,还增加了升级时出问题的概率。

我当时只装了这些:

插件用途为什么需要
Git拉取代码基础能力,不装不行
Pipeline支持流水线任务把构建流程写成 Jenkinsfile
Workspace Cleanup清理上次构建残留避免脏工作区导致打包失败
Build Timeout设置超时时间防止某个 stage 卡住占着构建机
Credentials Binding凭据注入把密码、API Key 做成环境变量,不进脚本

Blue Ocean 那个可视化界面装不装都行,我当时装了,但新鲜感过了之后还是主要看经典界面的日志。插件装好后,到“Manage Jenkins”里的“插件管理”确认一下版本和是否需要更新。插件不要盲目更新,尤其 Jenkins 正在用的时候,最好挑维护窗口更新。

3.3 JDK、Git、Xcode 的全局配置

进入“Manage Jenkins > Tools”,重点是配置 JDK。因为 JDK 在 Mac 上已经手动装好,我没有选择“自动安装”,而是填了JAVA_HOME路径。路径可以用java_home命令查:

/usr/libexec/java_home -v 11

输出会类似/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home,把这个填进去即可。

Git 那边的配置比较简单,填系统里的git路径,通常是/usr/local/bin/git或/usr/bin/git。不要在这里配 Xcode,因为 Xcode 的可选版本列表依赖插件,不如我们在构建脚本里用DEVELOPER_DIR来得直接和可靠。这也是我后来坚持“环境选择尽量进脚本,不依赖插件”的原因。

3.4 在让 Jenkins 签名前,先把钥匙串权限说清楚

真正开始跑 iOS 构建之前,有一件事必须先解决:签名。Jenkins 如果通过 pkg 安装,它以jenkins用户运行,默认没有你当前登录用户的钥匙串访问权限。第一次构建时,fastlane 调用 Xcode 签名,会报类似 “User interaction is not allowed” 或 “errSecInternalComponent” 的错误。

这个问题当时卡了我快半天。后来总结下来,解决思路有两种。

第一种,在每次构建前用命令解锁 Jenkins 用户自己的钥匙串。我当时的脚本是:

security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$HOME/Library/Keychains/login.keychain-db" security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$HOME/Library/Keychains/login.keychain-db"

第二条命令很关键,它让系统在命令行环境下访问证书时不会弹窗确认。如果不执行这一步,就算解锁成功,签名时依然可能卡住。

第二种,也是我后来换用 brew 方式跑 Jenkins 之后更省事的方法:让 Jenkins 直接以当前用户身份运行,这样它访问的就是你平时登录的钥匙串,证书、私钥都在,不需要额外维护一套 jenkins 用户的钥匙串。

两者没有绝对的对错。如果你已经用 pkg 装好 Jenkins,可以先用第一种;如果你还在环境安装阶段,我建议直接考虑改用 brew 方式。这个决定会直接决定你后面几小时是轻松还是暴躁。

4. fastlane 工程化:先把 lane 写得像部署脚本而不是魔法

fastlane 最好用的地方,是把“打包”这件琐事封装成一个个 lane。lane 写得清晰,整个 CI 才有灵魂。

4.1 初始化 fastlane 并规划 lane 名

在项目根目录执行:

fastlane init

这个命令会引导创建 Appfile 和 Fastfile。如果你不需要它自动探测 Apple ID,也可以手动创建目录和文件。我更推荐手动创建,理由是可以从一开始就规划好 lane 的命名和职责,而不是被默认模板带着走。

lane 命名我习惯用“动作 + 环境”的方式,比如build_adhoc、build_appstore、release_testflight。这样 Jenkins 的构建参数里写一个 lane 名,脚本里就调用对应的打包动作,逻辑很直观。

4.2 Fastfile 示例:取版本号、装依赖、打包

下面是我当时比较典型的一个 lane,注释写在代码里:

default_platform(:ios) platform :ios do desc "打 Ad Hoc 包" lane :build_adhoc do setup_ci if ENV["CI"] cocoapods increment_build_number( build_number: Time.now.strftime("%Y%m%d%H%M"), xcodeproj: "Example.xcodeproj" ) gym( scheme: "Example", clean: true, export_method: "ad-hoc", output_directory: "dist", output_name: "Example_adhoc.ipa", derived_data_path: "build/DerivedData" ) end end

几个细节说明一下。

setup_ci是 fastlane 在 CI 环境下推荐的 action,它会自动创建临时钥匙串,并设置好 match 需要的环境变量,避免签名时的 keychain 弹窗问题。因为 Jenkins 构建环境里没有 GUI,这一步对稳定性帮助很大。

cocoapods等价于执行pod install,但会根据 Gemfile 和当前 bundle 环境运行,比直接sh "pod install"更不容易出环境问题。

increment_build_number这里我用了时间戳格式,注意这个策略适合内测包,因为 iOS 要求 build number 递增。如果同一天打多个包,用这种格式仍然会递增。更稳妥的做法是直接调用increment_build_number不加参数,让 Xcode 自动 +1。如果你更在意可追溯,也可以把 Git 提交数作为 build number,团队习惯不同,选择一种保持一致性就好。

derived_data_path: "build/DerivedData"是我后来加的。默认 DerivedData 在系统目录里,多个 Job 同时构建时会互相踩踏,后面我会单独说。

4.3 签名策略:Match 仓库和手动证书各有什么坑

签名是 iOS CI 最容易混乱的地方。我自己经历了两个阶段。

第一阶段是手动证书。证书和描述文件都手工下载到构建机,fastlane 只负责调用。这个方法简单直接,适合单人维护一个项目。但坑也很明显:描述文件过期后,某天构建突然失败,你需要登录开发者后台重新下载;如果团队超过两个人,还会出现“谁的证书可以签名”的问题。

第二阶段是 match。match 会把证书和描述文件加密后存到一个 Git 仓库里,团队共享一份,配合MATCH_PASSWORD环境变量解密。好处是新人拉下来就能直接打包,不用手动装证书。坏处是 match 的私钥在仓库里,虽然加密了,但 Git 仓库的访问权限必须严格控制。

如果你决定用 match,在 Fastfile 里可以这样写:

match( git_url: "git@example.com:certificates/example-certificates.git", type: "adhoc", readonly: true )

readonly: true很重要,CI 上只需要读取证书和 profile,不需要修改仓库,避免并发构建时去写 Git 仓库造成冲突。

还有一个建议:在 CI 环境里,不要用 Apple ID 密码登录开发者后台,因为双重认证会把整个构建卡住。最好用 App Store Connect API Key。你可以在 Fastfile 里提前加载:

app_store_connect_api_key( key_id: ENV["ASC_API_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_filepath: ENV["ASC_KEY_PATH"] )

API Key 文件放到构建机的一个固定目录,权限收紧,不要提交到代码仓库。Jenkins 那边通过凭据管理把环境变量注入进去,securely enough.

4.4 本地先跑通 fastlane,再接 Jenkins

无论你怎么配置,接 Jenkins 之前都建议先在本地终端跑一次:

bundle exec fastlane build_adhoc

这一步能验证证书、描述文件、导出选项这些环节是否正常。如果本地都跑不过,进 Jenkins 只会更复杂。本地跑通之后,你再把注意力放在 Jenkins 的权限、钥匙串、环境变量上,出错时能快速判断是 fastlane 的问题还是 Jenkins 环境的问题。我见过不少人在 Jenkins 里来回改代码,后来发现同一个 lane 在终端里根本没跑通过,白白浪费了几个小时。

5. 用 Jenkins Pipeline 把整套流程串起来

fastlane 单独跑通只是完成了“手艺”的部分,现在要把 Jenkins 这个“车间”真正跑起来。

5.1 为什么不建议用“自由风格项目”把命令写在 Execute Shell 里

Jenkins 里有“自由风格项目”和“Pipeline 项目”两种常见形态。自由风格项目界面友好,适合快速验证,但构建步骤都散落在网页配置里,不进入版本控制。一旦需要迁移构建机或者团队 review 整个流程,就非常痛苦。

Pipeline 项目的核心价值是把构建流程写成一个 Jenkinsfile,放进代码仓库。任何改动都有记录,换机器只需要配置好环境,流程本身是跟着代码走的。所以我强烈建议用 Pipeline,而不是自由风格项目。第一次用 Pipeline,虽然要学习一点 Groovy 语法,但收益很快能体现出来。

5.2 一个能用的 Jenkinsfile 长什么样

我当时的 Jenkinsfile 简化后大概是这个结构:

pipeline { agent any options { timestamps() disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: "30")) } environment { DEVELOPER_DIR = "/Applications/Xcode_12.5.app/Contents/Developer" LANG = "en_US.UTF-8" } stages { stage("Checkout") { steps { checkout scm } } stage("Install Dependencies") { steps { sh "bundle install --path vendor/bundle" sh "bundle exec pod install" } } stage("Build with fastlane") { steps { withCredentials([ string(credentialsId: "match-password", variable: "MATCH_PASSWORD") ]) { sh "bundle exec fastlane build_adhoc" } } } } post { success { archiveArtifacts artifacts: "dist/*.ipa", fingerprint: true } failure { echo "Build failed, check the logs." } } }

options里我加了三样东西:timestamps()让日志每行带时间;disableConcurrentBuilds()禁止同一个 Job 并发跑,避免两个构建同时用同一个工作区;buildDiscarder只保留最近 30 次构建,防止磁盘被塞满。

environment里我固定了DEVELOPER_DIR指向 Xcode 12.5。这样即使机器上装了多个 Xcode,也不会因为全局 xcode-select 变了而选错工具链。

withCredentials是从 Jenkins 凭据里读取MATCH_PASSWORD,把它注入为环境变量,而不是把密码写死在脚本里。这个习惯很重要,不然 Jenkinsfile 一旦入库,密码也就泄露了。

5.3 触发方式和参数化构建

Pipeline 搭建好之后,可以在 Jenkins 页面配置触发方式。我建议至少保留两种入口。

第一种是手动参数化构建。在 Jenkinsfile 里加一段:

parameters { string(name: "BRANCH", defaultValue: "develop", description: "Git 分支") choice(name: "LANE", choices: ["build_adhoc", "build_appstore"], description: "选择 lane") }

然后在构建 stage 里把BRANCH参数传给 checkout,把LANE参数传给 fastlane:

stage("Checkout") { steps { checkout([$class: "GitSCM", branches: [[name: "${params.BRANCH}"]], userRemoteConfigs: [[url: "git@example.com:ios-app.git"]] ]) } } stage("Build") { steps { sh "bundle exec fastlane ${params.LANE}" } }

这样每次点 “Build with Parameters” 就可以任选分支和打包模式,非常方便。

第二种是代码推送后自动触发。如果你的 Git 托管平台可以配置 webhook,指向 Jenkins 的构建接口,那么 push 代码后 Jenkins 会自动跑。这个配置依赖具体平台的网络环境,我这里不展开,原理就是让平台在 push 事件发生时请求 Jenkins,Jenkins 收到请求后触发对应 Job。

如果暂时不想配 webhook,也可以简单用triggers { pollSCM("H/5 * * * *") }让 Jenkins 每 5 分钟检查一次代码变化,虽然不是实时,但也能满足大部分内测包需求。

5.4 产物管理和日志检查

构建成功之后,archiveArtifacts会把dist目录下的 ipa 文件归档到 Jenkins 里。这样你可以直接在 Job 页面上下载某个构建版本,不用去构建机上翻目录。

日志这块我的习惯是:构建失败时,先去 Jenkins 的“Console Output”里搜索fastlane输出的最下面部分。fastlane 的错误信息通常比 Xcode 报出来的更容易读,它会明确告诉你是在签名、编译还是导出阶段失败。如果你在构建脚本里加了--verbose,日志会更详细,排查效率会高很多。要注意日志文件会越积越大,结合buildDiscarder和 Jenkins 系统设置的磁盘监控,定期清理旧构建。

6. 2021 实操里最容易踩的坑,以及我怎么定位的

前面讲的都是顺利路径,下面这几个坑是我实际花时间解决过的,值得单独列出来。

6.1 签名第一次失败:User interaction is not allowed

症状是 fastlane 跑到了gym的签名步骤,控制台输出一堆编译成功的信息,最后抛出一个签名错误,提示 “User interaction is not allowed” 或 “errSecInternalComponent”。

我的排错过程是这样的。第一反应是证书没有导入。用 Jenkins 用户执行:

security find-identity -v -p codesigning

发现证书确实能查到,那就不是导入的问题。然后意识到是钥匙串访问权限。Jenkins 用户没有 GUI 会话,安全策略默认不允许它在没有用户交互的情况下访问私钥。解决方式就是前面 3.4 里的security set-key-partition-list命令。加完这条命令后,同样的 lane 再跑一次就通过了。

如果看到这个错误,先别急着重新生成证书,大概率不是证书问题,而是构建机权限问题。

6.2 Xcode 版本混装导致 fastlane 选错构建环境

Mac 上装多个 Xcode 很常见,特别是要调试新版本 iOS 时。问题在于,fastlane 在执行打包时会找当前激活的 Xcode 工具链,如果xcode-select指向的是旧版本,打包过程可能报出莫名其妙的错误,比如某些 SDK 找不到,或者 build setting 解析失败。

我当时的定位方法是先在构建机上手工跑:

xcode-select -p

如果发现不是预期路径,就在 Jenkinsfile 的environment块里固定:

DEVELOPER_DIR = "/Applications/Xcode_12.5.app/Contents/Developer"

这样每个 Job 都可以选择自己需要的 Xcode 版本,互不干扰。比在系统级改 xcode-select 要安全得多。

6.3 依赖安装时 CocoaPods 仓库更新太慢或偶发失败

CocoaPods 的pod install默认会去更新本地 spec 仓库,如果网络不稳定或者 spec 仓库很大,很容易在 Jenkins 构建时卡住,或者直接超时失败。

我的做法是把pod install改成不用每次更新仓库:

bundle exec pod install --repo-update=false

同时在第一次配置好 Podfile.lock 后,尽量让构建机保持本地仓库的稳定状态。如果你必须更新 pod 版本,就显式跑一次pod repo update,之后再切回普通构建。这能省下不少无意义的等待时间,尤其是高峰期网络波动的时候。

另一个建议是使用bundle install --path vendor/bundle,这样依赖都安装在项目工作区里,构建机全局环境不乱。缺点是目录会占体积,你可以在 Workspace Cleanup 插件里设置保留策略,避免反复下载。

6.4 多个 Job 并发导致 DerivedData 互相踩踏

当你有多个 iOS 项目,或者同一个项目配置了多个 Job,一旦并发构建,Xcode 默认的 DerivedData 路径是共享的。两个构建同时操作同一份 DerivedData,会出现一些很诡异的编译错误,比如 “Multiple commands produce” 或者 linker 报错。

我一开始不理解为什么同一个 lane 本地跑没问题,Jenkins 里却时不时失败。后来打开多个构建日志对比,发现报错都在编译阶段,且时间几乎重叠。解法就是在gym里指定独立的derived_data_path,同时在 Jenkinsfile 里加上disableConcurrentBuilds()。如果构建量大,可以考虑用不同的构建目录区分,但绝大多数小团队直接把并发禁掉就够了。

6.5 证书过期与描述文件失效的定时提醒

证书和描述文件是有生命周期的,尤其是 Ad Hoc 描述文件,过期之后构建就会在签名阶段报 “No profiles for ... were found”。这个问题并不是搭建时发现的,而是在某次赶版本时突然出现的。

后来我在 fastlane 里加了一个定期检查的 lane,配合 Jenkins 定时任务,每周跑一次:

lane :check_profiles do match( git_url: "git@example.com:certificates/example-certificates.git", type: "adhoc", readonly: true ) end

check_profiles 跑通了说明证书和描述文件都正常,跑不通就说明该续期了。与其等构建失败再处理,不如提前发现。

7. 半年用下来的优化建议

这套 Jenkins + fastlane 的组合稳定跑了半年之后,我有几个体会比较深,算是对前面内容的补充。

脚本和配置文件一定要纳入版本管理。Jenkinsfile、Fastfile、Gemfile、Matchfile 都要进 Git 仓库,不要只保存在构建机里。这样团队任何人改了流程,都能看到 diff,回滚也方便。

fastlane lane 的命名要保持动作语义。比如build_adhoc、build_appstore、release_testflight,别人一看就知道这个 lane 是干什么的。别用lane1、lane2这样的名字,写的时候爽,三个月后回来根本不知道那是干嘛的。

Jenkins 数据目录要备份。pkg 安装方式下,/Users/Shared/Jenkins/Home包含了所有 Job 配置、插件配置和凭据信息。我后来做了一个简单的定时压缩任务,把整个目录打包到外部存储,换机器时可以直接迁移。不要只在同一块磁盘上拷贝,因为如果机器彻底坏了,备份也一起没了。

凭据管理不要再偷懒。所有密码、API Key 尽量放到 Jenkins 的 Credentials 里,用withCredentials注入。不要写在 Fastfile 或者 shell 脚本里。我见过因为把密码写在脚本里,后来项目开源时差点泄露的情况,这个习惯要早一点建立。

关于构建机本身,如果条件允许,放在一台长期开机、不太会休眠的旧 Mac 上最合适。记得关闭自动更新、系统休眠和屏幕节能这类功能,不然半夜构建会因为机器休眠而失败,第二天早上才会发现。

最后再分享一个小技巧:搭好之后,把整个安装过程写成一份内部文档,记录你装过的工具、改过的路径、绕过哪些报错。这件事听起来麻烦,但换机器或者同事接手时,价值非常大。我自己就是靠这份记录,后来在另一台机器上重搭时,从零到跑通只花了一个下午,比第一次快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:54:21

artcraft:从审美到手工的实践框架,打通创意与执行

1. 从“artcraft”这个名字说起:它到底想解决什么问题第一次看到“artcraft”这个标题,我脑子里蹦出来的不是某个具体工具,而是一种很朴素的冲动——把“art”和“craft”这两个词捏在一起。art是艺术、是审美、是那种说不清道不明的“感觉对…

作者头像 李华
网站建设 2026/10/19 5:54:06

系统设计图不是画图,是构建可演进的系统表达语言

1. 项目概述:这不是画图,是构建可演进的系统表达语言“diagram-design”这个词组乍看像一个工具功能按钮,但在我过去十年带团队做技术方案设计、系统架构评审和跨职能协作的过程中,它早已不是“用Visio拖几个框”的代名词。它是一…

作者头像 李华
网站建设 2026/10/19 5:53:27

diagram-design:用结构化图形重建信息表达逻辑

1. 项目概述:这不是画图,是用结构化思维重建信息表达的底层逻辑“diagram-design”这个词组乍看像一个工具名或功能按钮,但在我带过的十几个跨领域协作项目里,它从来不是指“用Visio拖几个矩形框”,而是指一套完整的信…

作者头像 李华
网站建设 2026/10/19 5:52:38

从零构建图表设计引擎:架构、渲染与性能优化实战

去年年底我接了一个内部工具的重构任务,需求本身不复杂:在一个协作平台里,让用户通过拖拽画出架构图、流程图、组织架构图。麻烦的是,我和团队试了好几款开源图表工具,要么默认样式太死板,要么自定义能力有…

作者头像 李华
网站建设 2026/10/19 5:51:12

安卓学生签到系统开发实战:SSM后端与定位功能全解析

简介:基于安卓的学生签到系统是一套面向高校课堂考勤场景的完整项目源码,适合Android开发者、SSM后端学习者及毕业设计参考。项目覆盖扫码签到、数字签到、定位签到三种考勤方式,并支持签到记录修改与加课码选课功能,可满足日常教…

作者头像 李华
网站建设 2026/10/19 5:50:21

人工狼群算法优化FCM聚类:Matlab实现解决初始中心敏感问题

前阵子后台有人留言,问了一个特别典型的问题:Matlab里自带的 fcm 函数一行就能跑聚类,为什么还要专门写一套人工狼群算法去优化它?这个问题其实戳中了FCM最深的痛点——聚类结果对初始聚类中心非常敏感,随机给一组初值…

作者头像 李华