Unity移动端CI/CD实战:Jenkins+Fastlane双平台自动打包方案
1. 项目概述与核心价值最近在项目里把Unity的iOS和Android双平台自动打包流程给彻底跑通了用的是Jenkins Fastlane这套组合拳。说实话从手动点Xcode、等Unity Build、再到处理各种证书和上传到现在的代码一提交测试包就自动飞到内测群这个转变带来的效率提升和解放感是实实在在的。很多Unity团队可能还在用半自动化的脚本或者只做了单平台的CI但其实双平台的流程整合起来核心思路是相通的关键在于理清每个平台的“脾气”和找到合适的工具链。这篇文章我就把自己从零搭建这套CI/CD流水线的完整过程、踩过的坑以及最终沉淀下来的稳定方案做个详细的复盘和分享。无论你是独立开发者还是团队中的TA或主程这套方案都能帮你把重复的打包工作自动化把精力更多地还给创意和开发本身。简单来说这套方案的目标是在Jenkins这个自动化引擎上驱动Unity进行跨平台编译并利用Fastlane这个“瑞士军刀”处理各平台尤其是iOS后续繁琐的签名、打包、上传等任务最终实现一键或提交触发全自动产出可测试的安装包。它解决的不仅仅是“自动编译”更是打通了从代码到可分发产物的“最后一公里”特别是对于证书管理复杂、发布流程繁琐的移动平台。2. 整体架构设计与工具选型在开始敲命令之前我们先从顶层看看整个系统是如何协作的。一个健壮的CI/CD流水线清晰的分工和可靠的工具选型是成功的一半。2.1 核心组件角色解析我们的架构主要包含以下几个核心角色它们像流水线上的工人各司其职版本控制仓库 (GitLab/GitHub等)这是流水线的源头。任何触发自动化流程的行为如推送代码到特定分支、打Tag都从这里开始。它存储了所有的项目代码、Unity工程以及后续我们会提到的Fastlane配置。CI/CD 服务器 (Jenkins)这是流水线的大脑和调度中心。Jenkins负责监听版本仓库的变动触发构建任务。它的核心工作是拉取代码从Git仓库获取最新版本的工程。执行构建脚本调用我们写好的Shell脚本或Jenkins Pipeline脚本这个脚本会去驱动Unity和Fastlane。管理环境确保构建机器上安装了正确版本的Unity、SDK、NDK、Fastlane等所有依赖。归档产物与通知收集构建生成的APK/IPA文件、日志并可以通过邮件、钉钉、Slack等方式通知构建结果。构建引擎 (Unity)这是核心的“生产车间”。Jenkins通过命令行Unity.exe或/Applications/Unity/Hub/Editor/版本号/Unity.app/Contents/MacOS/Unity以无界面的批处理模式-batchmode调用Unity执行我们预先编写好的Editor脚本完成实际的编译、打包资源、生成Xcode工程或Gradle工程的工作。后处理与发布工具 (Fastlane)这是平台相关的“精加工车间”。Unity产出的是“半成品”对于Android产出的是一个未签名的APK或者一个完整的Gradle工程。Fastlane的gradleAction可以很方便地调用Gradle进行编译和签名。对于iOS产出的是一个Xcode工程.xcodeproj或.xcworkspace。后续的代码签名、打包成IPA、甚至上传到TestFlight或App Store这些iOS生态里特有的、极其繁琐的步骤正是Fastlane大显身手的地方。它用Ruby脚本封装了xcodebuild、altool等命令提供了像gym打包、match证书管理、pilot上传TestFlight这样高度封装的Action。密钥与证书管理这是安全核心尤其是iOS。我们不会把敏感的证书和描述文件.p12,.mobileprovision直接放在代码库里。推荐使用Fastlane Match这是最佳实践。它将证书和描述文件加密后存储在一个私有的Git仓库中团队成员通过一个密码来同步确保了团队间证书的一致性和安全性。Jenkins Credentials可以将签名所需的Keystore密码、API密钥等存储在Jenkins的凭据管理中在Pipeline中以安全的方式引用。2.2 为什么是 Jenkins Fastlane市面上CI/CD工具很多比如GitLab CI、GitHub Actions、Azure DevOps等。选择JenkinsFastlane主要是基于以下几点考量Jenkins的灵活性与控制力Jenkins是自托管的对构建环境有绝对的控制权。对于需要特定版本Unity、特定Xcode版本的复杂环境自己维护的Jenkins节点更可靠。它的插件生态极其丰富几乎可以和任何工具集成。虽然需要一些初始配置但一旦配好稳定性极高。Fastlane对移动端的专注与强大Fastlane是专门为移动应用自动化而生的。它抽象了iOS和Android平台最令人头痛的那些流程代码签名、截图、发布等提供了声明式的简洁语法。它的match工具解决了iOS团队开发的证书地狱问题。很多功能用纯Shell脚本实现非常复杂且脆弱而Fastlane可能只需要几行配置。组合的威力Jenkins负责通用的CI调度、环境管理和触发而Fastlane负责平台特定的、复杂的发布流程。两者分工明确边界清晰。Jenkins Pipeline的Jenkinsfile可以清晰地编排“调用Unity编译” - “调用Fastlane处理平台任务” - “归档产物”的整个流程。注意如果你的团队完全在云端且项目相对标准使用GitHub Actions或GitLab CI搭配Fastlane也是极好的选择配置可能更简单。但本文方案的优势在于环境可控适合对构建环境有定制化需求如特定内网依赖、多个Unity版本并行的团队。3. 环境准备与基础配置工欲善其事必先利其器。在Jenkins上跑通这套流程首先需要一台配置得当的构建机器通常是Mac因为需要编译iOS。这里我以一台Mac Mini作为Jenkins主节点和构建节点为例。3.1 构建机器环境搭建构建机器需要安装以下所有依赖Unity Unity Hub通过Unity Hub安装项目所需的特定版本Unity Editor。记住安装路径后续在Jenkins中需要指定。Android开发环境Java JDKUnity Android构建需要JDK建议安装OpenJDK 8或11并配置JAVA_HOME环境变量。Android SDK NDK通过Android Studio下载或直接使用命令行工具安装。必须配置ANDROID_HOME指向SDK根目录和ANDROID_NDK_HOME环境变量。Unity构建时会检查这些路径。iOS开发环境Xcode安装项目所需的Xcode版本并确保通过xcode-select -s命令选中了正确的版本。Xcode Command Line Tools执行xcode-select --install安装。Fastlane使用RubyGems安装是最简单的方式。sudo gem install fastlane -NV。确保安装成功且版本较新。Jenkins Agent配置如果Jenkins主节点在其他机器需要将这台Mac配置为Jenkins的Agent节点。通常需要安装Java并通过“Launch agent via SSH”或“Launch agent via Java Web Start”方式连接。确保Agent用户有权限执行上述所有命令尤其是Unity和Xcode相关命令。3.2 Jenkins基础配置与关键插件在Jenkins管理界面中需要完成以下关键配置安装必要插件Git plugin 从Git仓库拉取代码。Pipeline 支持使用Jenkinsfile定义流水线这是现代Jenkins的核心。Credentials Binding Plugin 安全地在Pipeline中注入密码、密钥等凭据。Workspace Cleanup Plugin 构建前后清理工作空间避免残留文件干扰。可选Email Extension Plugin 定制化构建通知邮件。配置全局工具路径 在“系统管理” - “全局工具配置”中指定各工具的路径。虽然我们更多在Shell脚本中用绝对路径但这里配置可以方便一些插件调用。Git 指定git可执行文件路径。JDK 配置名称和JAVA_HOME。通常不需要为Unity、Fastlane单独配置我们会在脚本中写死路径或通过环境变量调用。管理凭据 在“凭据”系统中安全地添加以下信息类型为“Secret text”或“Username with password”APP_STORE_CONNECT_API_KEY Fastlane上传到App Store Connect所需的API密钥JSON内容。MATCH_PASSWORD Fastlane Match加密仓库的密码。ANDROID_KEYSTORE_PASSWORD Android签名密钥库的密码。Git仓库的访问密钥SSH Key或账号密码。3.3 Unity项目准备为了让Unity能够被命令行驱动并执行我们想要的构建操作我们需要在Unity项目内创建一个Editor脚本。这个脚本将包含实际的构建逻辑。在项目的Assets/Editor/目录下创建一个脚本例如BuildScript.csusing System; using System.Linq; using UnityEditor; using UnityEngine; using System.Collections.Generic; public static class BuildScript { // 从命令行参数获取构建目标 public static string GetBuildTargetArg() { string[] args Environment.GetCommandLineArgs(); for (int i 0; i args.Length; i) { if (args[i] -buildTarget) { return args[i 1]; } } return Android; // 默认目标 } [MenuItem(Build/CI Build All)] public static void BuildAll() { // 实际CI中我们通过命令行调用不从这里执行 Debug.Log(Use command line arguments to build.); } // 这个函数将被命令行直接调用 public static void PerformBuild() { string buildTargetStr GetBuildTargetArg(); BuildTarget buildTarget BuildTarget.Android; string extension .apk; string subfolder Android; if (buildTargetStr.Equals(iOS, StringComparison.OrdinalIgnoreCase)) { buildTarget BuildTarget.iOS; extension ; // iOS输出是目录 subfolder iOS; } else if (buildTargetStr.Equals(Android, StringComparison.OrdinalIgnoreCase)) { buildTarget BuildTarget.Android; extension .apk; subfolder Android; } else { throw new Exception($Unsupported build target: {buildTargetStr}); } // 定义输出路径 string buildPath $Build/{subfolder}; string productName PlayerSettings.productName.Replace( , ); string finalOutputPath System.IO.Path.Combine(buildPath, productName extension); // 创建构建选项 BuildPlayerOptions buildPlayerOptions new BuildPlayerOptions(); buildPlayerOptions.scenes EditorBuildSettings.scenes.Where(s s.enabled).Select(s s.path).ToArray(); buildPlayerOptions.locationPathName finalOutputPath; buildPlayerOptions.target buildTarget; buildPlayerOptions.options BuildOptions.None; // 执行构建 BuildPipeline.BuildPlayer(buildPlayerOptions); Debug.Log($Build completed for {buildTarget}. Output: {finalOutputPath}); } }这个脚本定义了一个PerformBuild方法它会根据命令行传入的-buildTarget参数如Android或iOS来决定构建目标并将产物输出到项目根目录的Build/Android或Build/iOS文件夹下。实操心得在Editor脚本中一定要处理好路径问题。建议使用System.IO.Path.Combine来拼接路径以保证跨平台兼容性。另外构建前可以通过PlayerSettings接口动态设置Bundle Identifier、版本号等这些参数可以从Jenkins通过命令行参数传入实现完全动态化配置。4. Fastlane 配置详解Fastlane的配置是平台相关流程自动化的核心我们分别为iOS和Android创建Fastfile。4.1 iOS Fastlane 配置 (fastlane/Fastfile)iOS的流程最为复杂主要涉及证书管理、打包和上传。# fastlane/Fastfile default_platform(:ios) platform :ios do desc 构建并打包iOS Ad-hoc版本 lane :build_ad_hoc do # 1. 同步证书和描述文件使用match # 假设你的match配置在单独的Matchfile中这里直接调用 match( type: adhoc, readonly: true # 重要在CI环境中使用只读模式避免创建新证书 ) # 2. 增加构建号可以从环境变量获取如Jenkins的BUILD_NUMBER increment_build_number( build_number: ENV[BUILD_NUMBER] || 1 ) # 3. 编译并打包IPA gym( scheme: YourUnity-iOS, # 你的Xcode scheme名称Unity默认生成的是“Unity-iOS” workspace: ./Build/iOS/Unity-iOS.xcworkspace, # Unity生成的xcworkspace路径 export_method: ad-hoc, output_directory: ./fastlane_builds, # 输出目录 output_name: YourApp_#{ENV[BUILD_NUMBER]}.ipa, clean: true, silent: true, # 减少日志输出 suppress_xcode_output: true ) # 4. 上传到分发平台例如TestFlight # pilot( # ipa: ./fastlane_builds/YourApp_#{ENV[BUILD_NUMBER]}.ipa, # skip_waiting_for_build_processing: true # 不等待处理完成 # ) # 5. 或者上传到内部部署的分发平台如蒲公英、Fir # 这里以调用curl上传到自定义服务器为例 # sh(curl -F file./fastlane_builds/YourApp_#{ENV[BUILD_NUMBER]}.ipa -F _api_keyYOUR_API_KEY https://www.pgyer.com/apiv2/app/upload) end end对应的Matchfile用于配置证书管理# fastlane/Matchfile git_url(gityour-private-repo.com:your-org/certificates.git) # 存放加密证书的私有Git仓库 storage_mode(git) type(adhoc) # 默认类型可在lane中覆盖 app_identifier([com.yourcompany.yourapp]) # 你的Bundle ID username(your-apple-idemail.com) # App Store Connect账号 team_id(YOUR_TEAM_ID) # 开发者团队ID注意事项match的readonly: true在CI环境中至关重要。这确保Fastlane只会从证书仓库拉取现有的证书和描述文件而不会尝试创建新的这通常需要人工在Apple Developer后台审批。创建证书的操作应该在本地开发机上由开发者手动执行一次。4.2 Android Fastlane 配置Android的流程相对简单因为签名密钥通常由团队保管且打包过程更直接。# fastlane/Fastfile (追加android平台配置) platform :android do desc 构建并签名Android APK lane :build_release do # 1. 调用Gradle进行构建和签名 # 假设Unity导出的是Gradle工程且已在gradle.properties或环境变量中配置了签名信息 gradle( task: assembleRelease, project_dir: ./Build/Android/ # Unity导出的Android工程目录 ) # 2. 找到生成的APK文件路径可能因Unity版本和设置略有不同 apk_path ./Build/Android/build/outputs/apk/release/unity-release.apk # 3. 可选对齐APK使用zipalign # android_align_apk( # apk_path: apk_path # ) # 4. 重命名并移动APK到指定目录 destination ./fastlane_builds/YourApp_#{ENV[BUILD_NUMBER]}.apk FileUtils.mv(apk_path, destination) if File.exist?(apk_path) # 5. 上传到分发平台 # 例如上传到蒲公英 # pgyer( # api_key: ENV[PGYER_API_KEY], # user_key: ENV[PGYER_USER_KEY], # apk: destination # ) end endAndroid签名配置建议将签名密钥库.keystore或.jks文件放在安全的地方不提交到代码库在CI环境中通过环境变量或Jenkins凭据注入其路径和密码。在Unity的Player Settings中配置Android Keystore时可以留空然后在导出的Gradle工程的gradle.properties文件中动态配置或者在Jenkins的构建脚本中通过-D参数传递。5. Jenkins Pipeline 核心脚本这是将所有部分串联起来的“总指挥”。我们在项目根目录创建一个Jenkinsfile它定义了整个构建流水线的阶段。// Jenkinsfile (Declarative Pipeline) pipeline { agent any // 指定在哪个Jenkins节点上运行可以是标签 environment { // 定义关键路径和环境变量 UNITY_PATH /Applications/Unity/Hub/Editor/2022.3.20f1/Unity.app/Contents/MacOS/Unity // 你的Unity路径 PROJECT_PATH ${WORKSPACE} // Jenkins工作空间即项目目录 BUILD_NUMBER ${env.BUILD_ID} // 使用Jenkins构建号 // 从Jenkins凭据中读取敏感信息 MATCH_PASSWORD credentials(fastlane-match-password) APP_STORE_API_KEY credentials(app-store-connect-api-key) ANDROID_KEYSTORE_PASS credentials(android-keystore-password) } stages { stage(Checkout) { steps { // 拉取项目代码 git branch: main, url: gityour-repo.com:your-project.git } } stage(Unity Build iOS) { when { // 可以设置触发条件例如分支是release/ios时 expression { params.PLATFORM iOS || params.PLATFORM ALL } } steps { script { echo Starting iOS Build with Unity... // 调用Unity进行构建-quit参数让Unity构建完成后退出 sh ${UNITY_PATH} -batchmode \ -projectPath ${PROJECT_PATH} \ -executeMethod BuildScript.PerformBuild \ -buildTarget iOS \ -logFile ${WORKSPACE}/unity_ios.log \ -quit // 检查Unity日志中是否有错误 sh grep -i error\\|exception ${WORKSPACE}/unity_ios.log | head -20 } } post { always { // 无论成功失败都归档Unity的日志文件便于排查 archiveArtifacts artifacts: unity_ios.log, allowEmptyArchive: true } } } stage(Fastlane iOS) { when { expression { params.PLATFORM iOS || params.PLATFORM ALL } } steps { script { dir(./Build/iOS) { // 进入Unity生成的Xcode工程目录执行Fastlane sh BUNDLE_IDENTIFIERcom.yourcompany.yourapp \ BUILD_NUMBER${BUILD_NUMBER} \ FASTLANE_PASSWORD${MATCH_PASSWORD} \ fastlane ios build_ad_hoc } } } post { success { // 构建成功归档IPA包 archiveArtifacts artifacts: fastlane_builds/*.ipa, allowEmptyArchive: false } } } stage(Unity Build Android) { when { expression { params.PLATFORM Android || params.PLATFORM ALL } } steps { script { echo Starting Android Build with Unity... sh ${UNITY_PATH} -batchmode \ -projectPath ${PROJECT_PATH} \ -executeMethod BuildScript.PerformBuild \ -buildTarget Android \ -logFile ${WORKSPACE}/unity_android.log \ -quit sh grep -i error\\|exception ${WORKSPACE}/unity_android.log | head -20 } } post { always { archiveArtifacts artifacts: unity_android.log, allowEmptyArchive: true } } } stage(Fastlane Android) { when { expression { params.PLATFORM Android || params.PLATFORM ALL } } steps { script { dir(./Build/Android) { // 设置Android签名所需的环境变量 sh export ANDROID_KEYSTORE_PASSWORD${ANDROID_KEYSTORE_PASS} \ BUILD_NUMBER${BUILD_NUMBER} \ fastlane android build_release } } } post { success { archiveArtifacts artifacts: fastlane_builds/*.apk, allowEmptyArchive: false } } } } post { always { echo Pipeline finished. Cleaning up... // 可以在这里添加清理工作空间的步骤 } success { echo Build succeeded! // 可以在这里触发通知如邮件、钉钉、Slack // emailext body: 构建 #${BUILD_NUMBER} 成功\n 查看详情: ${BUILD_URL}, subject: Unity CI Build Success, to: teamexample.com } failure { echo Build failed! // 失败通知 // emailext body: 构建 #${BUILD_NUMBER} 失败\n 查看日志: ${BUILD_URL}, subject: Unity CI Build Failed, to: teamexample.com } } }这个Jenkinsfile定义了一个清晰的流水线检出代码 - 根据参数选择构建iOS和/或Android - 分别调用Unity编译 - 调用Fastlane进行后续处理 - 归档产物和日志。它使用了Declarative Pipeline语法结构清晰易读。6. 常见问题与排查技巧实录在实际搭建和运行过程中你几乎一定会遇到各种问题。下面是我踩过的一些坑和对应的解决方案。6.1 Unity 构建相关问题问题1Unity批处理模式构建失败日志无明确错误。现象Jenkins任务失败查看Unity日志末尾只有Aborting batchmode due to failure:但没有具体错误。排查Unity在批处理模式下某些错误如脚本编译错误、缺失依赖可能不会直接打印在最后。需要仔细查看完整的日志文件我们上面已经归档了unity_ios.log。技巧在Jenkins的Shell命令中构建完成后立即用grep过滤日志中的“error”和“exception”关键字并输出前几行能快速定位问题。另外可以尝试先在本地命令行用相同的参数执行Unity构建看是否能复现。问题2构建出的Xcode工程无法编译提示签名错误或头文件找不到。原因Unity导出的Xcode工程可能依赖一些特定的系统库或框架或者签名设置Team、Bundle Identifier与Fastlanematch提供的描述文件不匹配。解决确保Unity Player Settings中的Bundle Identifier、Version、Build Number设置正确且与Fastlane配置或传入的参数一致。检查Unity导出的Xcode工程中Signing Capabilities是否设置为自动管理签名。如果是Fastlane的gym可能会覆盖它。一个更稳妥的做法是在Unity中禁用自动签名Player Settings - iOS - Other Settings - Signing Team ID留空取消勾选Automatically Sign完全由Fastlanematch来控制。确保构建机器上安装的Xcode版本支持项目的Target iOS版本。6.2 Fastlane 与证书问题问题3match在CI上失败提示找不到证书或权限不足。原因readonly模式找不到对应Bundle Identifier和类型的证书或者访问私有证书仓库的SSH密钥没有配置在Jenkins Agent上。解决首次设置先在本地开发机上用有足够权限的Apple Developer账号执行一次fastlane match init和fastlane match adhoc或appstore将证书和描述文件生成并加密上传到私有Git仓库。CI配置确保Jenkins Agent运行的用户有访问那个私有证书仓库的SSH密钥。可以将密钥添加到该用户的~/.ssh/目录并在Jenkins的“SSH Agent Plugin”或直接配置SSH Agent。验证可以在Jenkins Agent上手动切换到构建用户尝试执行fastlane match命令看是否能成功拉取证书。问题4gym构建成功但打包成IPA失败。现象xcodebuild编译通过但gym报错常见错误如Exporting the archive failed。排查gym会生成详细的日志通常在~/Library/Logs/gym/目录下。查看这些日志错误信息会更具体。常见原因描述文件不匹配描述文件包含的证书、设备或能力Capability与项目设置不符。确保match使用的类型如adhoc与你想要导出的方法export_method一致。Xcode版本问题某些Xcode版本有已知的打包bug。尝试升级或降级Xcode到稳定版本。临时文件干扰尝试在gym参数中添加clean: true并确保output_directory是干净的。6.3 Jenkins 与环境问题问题5Jenkins Pipeline中环境变量不生效。原因Shell脚本的环境与Jenkins Pipeline的环境可能不同。在sh块中直接使用${ENV_VAR}可能获取不到Pipelineenvironment块定义的变量。解决在sh脚本中使用双引号包裹整个命令字符串并直接引用环境变量名如$BUILD_NUMBER因为我们在environment块中已经定义了。对于从credentials绑定的变量它们通常作为环境变量注入在sh块中可以直接用$MATCH_PASSWORD访问。问题6构建时间过长或偶尔超时。优化缓存利用Jenkins的cache功能或插件缓存Unity的Library文件夹尤其是Library/BuildCache、依赖包如UPM packages等。这能极大缩短后续构建的清理和导入时间。增量构建对于开发分支的频繁提交可以考虑只构建发生变化的场景或使用智能的构建脚本但这在Unity中实现较复杂。更实际的是优化构建脚本本身避免不必要的操作。专用构建节点为CI/CD准备一台性能强劲的专用机器避免与其他任务争抢资源。并行构建如果资源充足可以在Jenkins Pipeline中使用parallel阶段让iOS和Android的Unity构建同时进行。6.4 通用调试技巧“本地复现”原则任何在CI上出现的问题首先尝试在构建机器上使用相同的用户、相同的命令、在相同的工作目录下手动执行一遍。这能立刻区分是环境问题、权限问题还是脚本逻辑问题。日志是生命线确保所有关键步骤Unity构建、Fastlane操作都输出日志到文件并在Jenkins中归档。为不同的构建目标iOS/Android和阶段Unity/Fastlane使用不同的日志文件方便排查。参数化构建在Jenkins任务中启用“参数化构建”添加一个CHOICE参数PLATFORM选项为ALLiOSAndroid。这样可以在手动触发时灵活选择构建平台方便测试。分阶段启用不要试图一次性搭建完美的全流程。建议的步骤是1) 先在本地命令行跑通Unity构建。2) 然后在本地跑通Fastlane流程。3) 接着在Jenkins上只跑Unity构建阶段。4) 最后将Fastlane集成进去。每一步都确保稳定后再进入下一步。搭建这样一套完整的CI/CD流水线初期确实需要投入不少时间但一旦稳定运行它所带来的自动化、标准化和可靠性提升对团队开发节奏和产品质量的保障是长期且显著的。每次看到提交代码后自动开始构建的提示以及最终自动出现在测试群里的安装包都会觉得这些折腾是值得的。