1. 项目概述为什么我们需要一个统一的自动化打包方案在游戏开发团队里打包这件事说大不大说小不小但绝对是个“脏活累活”。我经历过太多这样的场景策划要一个测试包程序说“等会儿我本地在跑测试”运营要一个渠道包程序又得手动改一堆参数一不留神就出错更别提iOS、Android、Windows三个平台每个平台的打包流程、证书配置、环境依赖都截然不同。手动操作不仅效率低下而且极易出错一个签名问题就能卡住整个团队半天。这就是为什么我们需要一个像“Unity项目多平台打包实战用Jenkins参数化构建管理iOS/Android/Windows版本”这样的自动化解决方案。这个方案的核心就是利用Jenkins这个老牌但极其强大的持续集成/持续部署CI/CD工具结合Unity的批处理模式Batchmode将原本需要人工干预、重复性高的打包工作变成一键触发、参数可控、结果可追溯的自动化流水线。它解决的不仅仅是“打包”这个动作更是团队协作、版本管理、环境一致性和开发效率的问题。想象一下美术、策划、测试同学不再需要打扰程序自己就能在Jenkins页面上点几下选择平台、版本号、构建模式然后去喝杯咖啡回来就能拿到热乎的安装包这能省下多少沟通成本和等待时间。对于中小型团队或者独立开发者来说搭建这样一套系统听起来可能有点“重”但实际投入产出比极高。一次搭建长期受益。它尤其适合需要频繁出包比如每日构建、多分支并行开发、多平台发布手游、PC端游、主机移植以及对构建质量有严格要求的项目。接下来我将以一个实战者的角度拆解如何从零开始搭建并配置这套自动化打包体系分享我踩过的坑和总结出的最佳实践。2. 环境准备与核心工具选型在动手写第一行脚本之前把地基打牢至关重要。这里的环境准备不仅仅是安装软件更是理解每个组件的作用和它们之间的协作关系。一个稳定的环境是自动化流程能够7x24小时可靠运行的前提。2.1 核心工具栈解析我们的工具栈主要围绕三个核心Unity、Jenkins和各个平台的SDK/NDK。Unity引擎与环境这是我们的“生产车间”。你需要一个稳定的Unity版本比如长期支持版LTS例如2022.3.x。关键在于你必须通过Unity Hub安装对应平台的支持模块。打Windows包需要“Windows Build Support (IL2CPP)”打Android包需要“Android Build Support”打iOS包则必须在macOS机器上安装“iOS Build Support”。很多人会忽略IL2CPP模块如果你的项目使用了HybridCLR原xLua热更框架的C#版本或者追求更好的性能与安全性IL2CPP是必须的。记住Unity的安装路径特别是Unity可执行文件Unity.exe或Unity.app/Contents/MacOS/Unity是我们后续脚本调用的关键务必记下。Jenkins服务端这是我们的“自动化指挥中心”。我强烈建议使用.war包方式部署而不是系统安装包因为它最通用也最干净。你需要一个Java运行环境注意Jenkins新版本已不再支持JDK 8最低要求是JDK 11我推荐使用JDK 17或21以获得更好的兼容性和性能。将JDK的bin目录添加到系统环境变量PATH中这是后续命令行启动的基础。平台特定工具链Android需要Android SDK和NDK。最简单的方式是通过Unity Hub安装Android模块时勾选它会自动配置好。但你也可以手动下载并指定路径。确保在Unity的Preferences - External Tools中正确设置SDK、NDK、JDK的路径。NDK的版本需要与Unity版本要求匹配不匹配会导致编译失败。iOS (仅限macOS)需要完整的Xcode以及Xcode Command Line Tools。在终端执行xcode-select --install即可安装命令行工具。此外你还需要准备好苹果开发者证书.p12文件和对应的描述文件.mobileprovision这是打包IPA的“通行证”。版本控制系统虽然Jenkins支持SVN但Git是目前绝对的主流。你需要确保打包服务器能够访问你的代码仓库GitHub、GitLab、Gitee等并且拥有拉取代码的权限配置SSH Key或账号密码。2.2 Jenkins部署实战与避坑指南部署Jenkins听起来简单但细节决定成败。这里以Windows服务器为例MacOS类似。首先在一个你希望放置Jenkins工作空间的目录比如D:\Jenkins下打开命令行。使用以下命令启动注意编码设置避免中文乱码# 在PowerShell中 $env:JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8 java -jar jenkins.war --httpPort8080--httpPort8080是指定端口如果8080被占用可以换成其他端口。注意第一次启动后命令行会打印一个初始管理员密码形如**************************************************。务必复制保存浏览器访问http://服务器IP:8080输入此密码才能进入初始化向导。在初始化向导中选择“安装推荐的插件”。这会安装包括Git、Pipeline在内的基础插件节省大量时间。创建完管理员账户后会进入Jenkins URL设置页面。这里是一个关键点如果你希望局域网内其他机器也能访问这台Jenkins必须将http://localhost:8080或http://127.0.0.1:8080改为你的服务器内网IP例如http://192.168.1.100:8080。否则只有本机可以访问。进入主界面后我们还需要安装几个对Unity自动化打包至关重要的插件。进入系统管理 - 插件管理 - 可选插件Pipeline 流水线插件核心中的核心我们所有的构建逻辑都将用Pipeline脚本编写。Job DSL 允许我们用Groovy脚本批量创建和管理Job这是实现“一套脚本管理所有平台任务”的关键。Active Choices Plug-in 提供动态参数如根据Git分支列表动态选择让参数化构建更强大、更友好。Authorize Project 为了安全地运行Groovy脚本特别是Job DSL我们需要在沙盒Sandbox模式下运行这个插件是前提。Subversion 如果你的项目还在用SVN就需要它。安装完插件后需要重启Jenkins。重启后进行一个关键安全配置进入系统管理 - 安全 - 安全找到“构建过程的访问控制”需要Authorize Project插件选择“以特定用户运行构建过程”并填入你系统的管理员用户名。这解决了后续DSL脚本运行时权限不足的问题。最后设置一个全局环境变量告诉Jenkins我们的Unity在哪。进入系统管理 - 系统配置找到“全局属性”添加一个环境变量。例如名称:UNITY_PATH值:C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exe(Windows) 或/Applications/Unity/Hub/Editor/2022.3.20f1/Unity.app/Contents/MacOS/Unity(macOS)至此我们的“指挥中心”就基本搭建完毕了。3. 参数化构建设计让打包变得灵活可控参数化构建是Jenkins Pipeline的灵魂它把固定的打包命令变成了一个可配置的“表单”。我们的目标是任何团队成员即使不懂命令行也能通过网页上的几个下拉框和输入框打出他想要的包。3.1 核心参数定义一个典型的Unity多平台打包Job应该包含以下参数。我会在Groovy Pipeline脚本的parameters块中定义它们properties([ parameters([ // 1. 选择构建平台 choice(name: PLATFORM, choices: [Android, iOS, Windows], description: 选择目标平台), // 2. 版本号控制 string(name: VERSION, defaultValue: 1.0.0, description: 应用版本号 (如: 1.0.0)), string(name: BUILD_NUMBER, defaultValue: ${BUILD_ID}, description: 构建号通常用Jenkins BUILD_ID), // 3. 代码版本控制 gitParameter(name: BRANCH, type: PT_BRANCH, description: 选择要构建的Git分支), // 4. 构建模式与配置 choice(name: BUILD_TARGET, choices: [Development, Release], description: 构建目标), booleanParam(name: DEVELOPMENT_BUILD, defaultValue: false, description: 是否启用Development Build允许调试), choice(name: SCRIPTING_BACKEND, choices: [Mono, IL2CPP], description: 脚本后端), // 5. 输出控制 string(name: OUTPUT_PATH, defaultValue: D:\\BuildOutput\\${JOB_NAME}, description: 打包输出路径), // 6. iOS特有参数 choice(name: IOS_SIGNING_TYPE, choices: [app-store, development, enterprise, ad-hoc], description: iOS签名类型, condition: PLATFORM iOS), booleanParam(name: IOS_USE_RESIGN, defaultValue: false, description: 是否使用重签名加速仅对非第一个签名包生效, condition: PLATFORM iOS) ]) ])参数设计思路解析PLATFORM 最核心的选择决定了后续所有流程的分支。VERSION和BUILD_NUMBER 用于生成最终包体的文件名如MyGame_1.0.0_125.apk。BUILD_NUMBER通常绑定${BUILD_ID}确保每次构建唯一。BRANCH 使用gitParameter插件实现动态分支列表构建时可以从仓库拉取的所有分支中选择非常灵活。BUILD_TARGET和DEVELOPMENT_BUILD 控制Unity的BuildOptions。Development模式包含调试符号方便测试Release模式则进行更多优化。SCRIPTING_BACKEND Mono兼容性好IL2CPP性能高且更安全。这是一个重要的性能与兼容性权衡点。OUTPUT_PATH 我强烈建议使用一个网络共享路径或固定的大容量磁盘路径方便所有人访问构建产物。${JOB_NAME}变量可以自动区分不同任务。IOS_SIGNING_TYPE和IOS_USE_RESIGN iOS打包的复杂性所在。签名类型必须与你的证书匹配。重签名加速是一个高级技巧先打一个基础包如App Store包然后通过重签名快速生成其他类型Dev、Ad-Hoc的包节省大量编译时间。3.2 使用Active Choices实现动态参数上面的condition属性展示了简单的条件显示。但有时我们需要更复杂的联动。例如当PLATFORM选择Android时我们希望多一个Android Build System的选择Gradle或Internal。这需要Active Choices Reactive Parameter插件。script { // 这个参数会在UI中动态生成 activeChoiceReactiveParam(name: ANDROID_BUILD_SYSTEM, description: Android构建系统, choiceType: PT_SINGLE_SELECT, groovyScript: if (PLATFORM.equals(Android)) { return [Gradle, Internal] } else { return [N/A] } , referencedParameters: [PLATFORM] // 声明依赖的参数 ) }这样只有当用户选择了Android平台这个下拉框才会出现并给出有效选项。这种动态性极大地提升了配置界面的友好度和准确性。4. Pipeline流水线核心脚本编写参数设计好了接下来就是实现打包逻辑的Pipeline脚本。我们将采用Declarative Pipeline语法它结构更清晰。整个Pipeline会被封装在一个Jenkinsfile中存放在项目仓库的根目录实现“配置即代码”。4.1 Pipeline主体结构一个完整的Pipeline通常包含以下阶段stagespipeline { agent any // 可以在任何有标签的agent上运行 parameters { // 这里放入3.1节定义的所有参数 } environment { // 定义环境变量如从Jenkins全局配置中读取Unity路径 UNITY_EXE ${env.UNITY_PATH} // 引用之前设置的全局变量 WORKSPACE ${env.WORKSPACE} // Jenkins自动提供的 workspace 路径 BUILD_OUTPUT ${params.OUTPUT_PATH} } stages { stage(代码拉取) { steps { // 根据参数BRANCH拉取代码 git branch: ${params.BRANCH}, url: 你的Git仓库地址, credentialsId: 你的凭据ID } } stage(环境检查与准备) { steps { script { // 检查Unity可执行文件是否存在 if (!fileExists(UNITY_EXE)) { error Unity executable not found at: ${UNITY_EXE} } // 根据平台检查特定环境如Android SDK路径 if (params.PLATFORM Android) { // 可以读取Unity的偏好设置文件或直接检查环境变量 def androidSdkPath ... // 获取SDK路径的逻辑 if (!fileExists(androidSdkPath)) { error Android SDK not found at: ${androidSdkPath} } } } } } stage(Unity构建) { steps { script { // 核心构建逻辑调用一个共享库函数 buildUnityProject() } } } stage(后处理与归档) { steps { script { // 重命名输出文件添加版本信息 // 将构建产物APK/IPA/EXE归档到Jenkins archiveArtifacts artifacts: **/*.apk, **/*.ipa, **/*.exe, fingerprint: true // 也可以上传到内部文件服务器或分发平台 } } } } post { always { // 无论成功失败都执行例如清理临时文件 cleanWs() // 清理工作空间 } success { // 构建成功后的通知如发送钉钉/企业微信消息 dingTalkTalker.sendText(构建成功: ${env.JOB_NAME} - ${params.VERSION}) } failure { // 构建失败后的通知 dingTalkTalker.sendText(构建失败: ${env.JOB_NAME} - 请检查日志) } } }4.2 核心构建函数buildUnityProject()详解这是整个Pipeline最核心的部分它需要处理不同平台的差异。我们将其写在一个共享库Shared Library中或者在script块内定义为一个大型函数。def buildUnityProject() { def unityArgs [] // 1. 基础参数 unityArgs [ -batchmode, // 批处理模式无图形界面 -nographics, // 不加载图形设备服务器环境必备 -quit, // 执行完毕后退出Unity -logFile, ${WORKSPACE}/unity_build.log, // 指定日志文件 -projectPath, ${WORKSPACE} // 项目路径 ] // 2. 根据平台和参数设置构建目标和方法 switch(params.PLATFORM) { case Android: unityArgs [-buildTarget, Android] // 设置Android特有参数 def exportPath ${BUILD_OUTPUT}/Android unityArgs [-executeMethod, Editor.BuildTool.BuildAndroid] // 调用自定义编辑器方法 unityArgs [-exportPath, exportPath] unityArgs [-versionName, ${params.VERSION}] unityArgs [-versionCode, ${params.BUILD_NUMBER}] if (params.SCRIPTING_BACKEND IL2CPP) { unityArgs [-scriptingBackend, il2cpp] } break case iOS: unityArgs [-buildTarget, iOS] def exportPath ${BUILD_OUTPUT}/iOS unityArgs [-executeMethod, Editor.BuildTool.BuildiOS] unityArgs [-exportPath, exportPath] // iOS签名信息通过外部配置文件或参数传入 unityArgs [-signingType, ${params.IOS_SIGNING_TYPE}] break case Windows: unityArgs [-buildTarget, StandaloneWindows64] def exportPath ${BUILD_OUTPUT}/Windows unityArgs [-executeMethod, Editor.BuildTool.BuildWindows] unityArgs [-exportPath, exportPath] break default: error Unsupported platform: ${params.PLATFORM} } // 3. 构建模式参数 if (params.DEVELOPMENT_BUILD.toBoolean()) { unityArgs [-developmentBuild, true] } // 4. 执行Unity命令 def cmd \${UNITY_EXE}\ ${unityArgs.join( )} echo 执行命令: ${cmd} def exitCode bat(script: cmd, returnStatus: true) // Windows用batMac/Linux用sh // 注意bat返回0表示成功非0表示失败 // 5. 检查日志和退出码 if (exitCode ! 0) { // 读取Unity日志将最后100行输出到Jenkins控制台方便排查 def logContent readFile ${WORKSPACE}/unity_build.log def lines logContent.readLines() def errorSnippet lines.size() 100 ? lines[-100..-1].join(\n) : logContent echo Unity构建失败日志末尾\n${errorSnippet} error Unity构建过程失败退出码: ${exitCode} } else { echo Unity构建成功完成。 } }关键点解析-executeMethod 这是关键。我们不在命令行里直接写BuildPipeline.BuildPlayer而是调用一个自定义的C#编辑器静态方法。这样做的好处是我们可以在这个方法里编写更复杂、更灵活的构建逻辑比如处理资源、修改PlayerSettings、处理不同渠道等。这个Editor.BuildTool类需要放在项目的Assets/Editor目录下。日志管理 一定要指定-logFile并将日志内容在失败时打印出来。Unity在批处理模式下的错误信息只会输出到这个文件控制台看不到具体错误。退出码检查 Unity命令执行后必须检查其退出码%ERRORLEVEL%on Windows,$?on bash。即使命令行执行了Unity内部构建失败也可能返回非零码。4.3 自定义编辑器构建脚本示例下面是一个简化的Editor/BuildTool.cs示例展示如何在C#端接收命令行参数并执行构建using UnityEditor; using System; using System.IO; public static class BuildTool { public static void BuildAndroid() { // 从命令行参数获取值 string exportPath GetArg(-exportPath); string versionName GetArg(-versionName); string versionCode GetArg(-versionCode); string scriptingBackend GetArg(-scriptingBackend); // 配置PlayerSettings PlayerSettings.applicationIdentifier com.yourcompany.yourgame; PlayerSettings.bundleVersion versionName; PlayerSettings.Android.bundleVersionCode int.Parse(versionCode); if (scriptingBackend il2cpp) { PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP); } // 定义构建选项 BuildOptions options BuildOptions.None; if (EditorUserBuildSettings.development) { options | BuildOptions.Development; options | BuildOptions.AllowDebugging; } // 执行构建 string[] scenes { Assets/Scenes/Main.unity }; BuildPipeline.BuildPlayer(scenes, Path.Combine(exportPath, YourGame.apk), BuildTarget.Android, options); } // 类似地编写 BuildiOS() 和 BuildWindows() 方法 public static void BuildiOS() { /* ... */ } public static void BuildWindows() { /* ... */ } // 辅助方法从命令行参数中获取值 private static string GetArg(string name) { var args Environment.GetCommandLineArgs(); for (int i 0; i args.Length; i) { if (args[i] name args.Length i 1) { return args[i 1]; } } return null; } }通过这种方式我们将平台相关的复杂配置从脆弱的命令行字符串中剥离出来放到了强类型的C#代码中更易于维护和调试。5. iOS自动化打包的深水区与证书管理iOS打包是三个平台中最复杂的一环核心难点在于代码签名和证书管理。自动化流程必须安全、可靠地处理.p12证书和.mobileprovision描述文件。5.1 证书的安全存储与使用绝对不要将证书和描述文件硬编码在脚本或放入代码仓库。Jenkins提供了Credentials功能来安全地存储这些敏感信息。添加证书凭证 进入系统管理 - Manage Credentials在全局域添加Secret file类型的凭据上传你的.p12文件。Jenkins会为其生成一个唯一的credentialsId。在Pipeline中使用 在Pipeline的environment块或steps中使用withCredentials绑定凭据将其临时解密并存储到工作空间的一个临时位置供打包脚本使用。stage(iOS 签名准备) { steps { script { withCredentials([file(credentialsId: ios-signing-cert, variable: P12_FILE), file(credentialsId: ios-provision-profile, variable: MOBILEPROVISION_FILE)]) { // 此时P12_FILE和MOBILEPROVISION_FILE是临时文件路径 sh cp ${P12_FILE} ${WORKSPACE}/temp/cert.p12 sh cp ${MOBILEPROVISION_FILE} ${WORKSPACE}/temp/profile.mobileprovision // 将临时文件路径传递给Unity构建方法或后续的xcodebuild命令 env.IOS_P12_PATH ${WORKSPACE}/temp/cert.p12 env.IOS_PROVISION_PATH ${WORKSPACE}/temp/profile.mobileprovision } } } }5.2 从Xcode工程到IPA自动化流程Unity的BuildPipeline.BuildPlayer在iOS平台下产出的是一个Xcode工程而不是直接的.ipa文件。因此完整的iOS自动化打包包含两个阶段Unity导出Xcode工程。使用xcodebuild和xcrun命令编译并打包成IPA。我们可以在自定义的BuildTool.BuildiOS()方法中只完成第一步然后在Jenkins Pipeline中调用Shell脚本完成第二步。也可以借助一些第三方插件或脚本如xcodebuild的archive和exportArchive命令来整合。一个简化的第二阶段Pipelinestage可能如下stage(生成IPA) { when { expression { params.PLATFORM iOS } } steps { script { dir(${WORKSPACE}/Build/iOS) { // 进入Unity导出的Xcode工程目录 // 1. 解锁钥匙串并导入证书 (MacOS Jenkins Agent上需要) sh security unlock-keychain -p ${KEYCHAIN_PASSWORD} ${HOME}/Library/Keychains/login.keychain-db sh security import ${env.IOS_P12_PATH} -k ${HOME}/Library/Keychains/login.keychain-db -P ${P12_PASSWORD} -T /usr/bin/codesign // 2. 将描述文件复制到正确位置 sh cp ${env.IOS_PROVISION_PATH} ~/Library/MobileDevice/Provisioning\\ Profiles/ // 3. 使用xcodebuild archive sh xcodebuild -workspace Unity-iPhone.xcworkspace -scheme Unity-iPhone -configuration Release -archivePath build/Unity-iPhone.xcarchive archive // 4. 使用xcodebuild -exportArchive 导出IPA // 需要创建一个ExportOptions.plist文件来配置签名方式 sh xcodebuild -exportArchive -archivePath build/Unity-iPhone.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath . } // 5. 将生成的IPA文件移动到最终输出目录 sh cp ${WORKSPACE}/Build/iOS/Unity-iPhone.ipa ${BUILD_OUTPUT}/ } } }重要提示 上述步骤需要在装有Xcode的macOS Jenkins Agent节点上运行。KEYCHAIN_PASSWORD和P12_PASSWORD也需要通过Jenkins Credentials管理。ExportOptions.plist文件需要根据你的签名类型App Store, Ad-Hoc等预先配置好模板。5.3 重签名加速技巧这是提升iOS多包构建效率的“黑科技”。原理是第一次完整编译生成一个未签名或带有某种签名的.xcarchive或.app。后续需要其他签名类型的包时不再重新编译而是对这个已有的产物进行重签名。stage(iOS 重签名加速) { when { allOf { expression { params.PLATFORM iOS } expression { params.IOS_USE_RESIGN.toBoolean() } } } steps { script { // 假设基础包如app-store签名的.app已经存在在某个路径 def baseAppPath ${env.BUILD_OUTPUT}/BaseApp/YourGame.app // 使用新的描述文件和证书进行重签名 sh /usr/bin/codesign --force --sign ${SIGNING_IDENTITY} --entitlements entitlements.plist --timestampnone ${baseAppPath} // 然后重新打包成IPA sh /usr/bin/xcrun -sdk iphoneos PackageApplication -v ${baseAppPath} -o ${env.BUILD_OUTPUT}/YourGame_AdHoc.ipa --embed ${env.IOS_PROVISION_PATH} } } }这可以将后续包的构建时间从十几二十分钟缩短到一两分钟在需要频繁出测试包时非常有用。6. Android与Windows平台的构建要点相比iOSAndroid和Windows的自动化构建要直接许多但也有一些需要注意的细节。6.1 Android构建的变体与渠道Android平台经常需要为不同渠道应用商店出不同的包可能涉及不同的包名、应用图标、启动图或SDK初始化配置。我们可以在自定义构建方法中通过命令行参数来指定渠道。// 在Jenkins参数中增加一个渠道选择 choice(name: ANDROID_CHANNEL, choices: [googleplay, huawei, xiaomi, official], description: Android发布渠道) // 在buildUnityProject函数中传递给Unity if (params.PLATFORM Android) { unityArgs [-androidChannel, ${params.ANDROID_CHANNEL}] }在C#的BuildAndroid方法中读取这个参数并据此修改PlayerSettings或执行特定的预处理脚本比如替换assets中的渠道配置文件。关于Android App Bundle (AAB) 如果需要上架Google Play你可能需要打AAB包。Unity 2018.3支持直接构建AAB。在BuildPlayer时将BuildOptions设置为BuildOptions.BuildAppBundle即可。注意AAB不能直接安装需要上传到Play Console后由Google生成APK。6.2 Windows构建单EXE与数据分离Unity打Windows包有两种输出格式包含数据文件夹的EXE 一个.exe文件加一个同名的_Data文件夹。这是默认选项。单文件EXE 将所有数据打包进一个独立的.exe文件便于分发。在构建时可以通过BuildOptions尝试启用BuildOptions.CompressWithLz4HC等进行压缩。对于单文件打包Unity官方并未直接提供选项但社区有一些后处理脚本或工具如il2cpp工具链中的UnityLinker可以尝试不过稳定性和兼容性需要自行测试。更常见的做法是使用第三方安装包制作工具如Inno Setup, NSIS将EXE和Data文件夹打包成一个安装程序。Windows构建的另一要点是图形API。如果项目使用了Vulkan等API需要在PlayerSettings中正确设置。这些设置也应该在自定义构建脚本中根据参数或项目配置来设定。7. 使用Job DSL实现任务批量管理与维护当你的项目有多个开发分支如main,develop,feature/xxx每个分支又需要打三个平台的包时手动在Jenkins界面上创建几十个Job是灾难性的。Job DSL插件就是来解决这个问题的。7.1 DSL脚本基础Job DSL允许你用Groovy脚本描述Job的配置。你可以创建一个“种子Job”这个Job的唯一任务就是执行一段DSL脚本这段脚本会像程序一样批量创建或更新其他真正的构建Job。创建一个新的Pipeline类型的“种子Job”在Pipeline脚本中写入// create_jobs.groovy def branches [main, develop, release/1.0] def platforms [Android, iOS, Windows] branches.each { branch - platforms.each { platform - def jobName Build-${branch}-${platform}.replaceAll(/, _) // 替换斜杠因为Job名不允许 pipelineJob(jobName) { definition { cpsScm { scm { git { remote { url(https://github.com/your-company/your-unity-project.git) credentials(your-git-credential-id) } branch(branch) } } scriptPath(Jenkinsfile) // 指向仓库根目录的Jenkinsfile } } parameters { // 这里可以定义该Job的默认参数覆盖Jenkinsfile中的defaultValue stringParam(PLATFORM, platform, 目标平台) stringParam(BRANCH, branch, 构建分支) // ... 其他参数 } } } }运行这个“种子Job”它就会自动创建出Build-main-Android,Build-main-iOS, ...Build-release_1.0-Windows等9个Job。7.2 维护与更新当你的构建流程需要更新时比如修改了Jenkinsfile或者想增加一个公共参数你只需要更新“种子Job”中的DSL脚本然后再次运行它。Job DSL插件会计算差异并自动更新所有由它管理的Job配置。这实现了“基础设施即代码”极大提升了多任务管理的效率和一致性。8. 常见问题排查与实战心得即使流程设计得再完美在实际部署和运行中也会遇到各种问题。这里记录一些我踩过的坑和解决方案。8.1 Unity构建失败排查清单日志是唯一真理 构建失败第一时间查看unity_build.log文件。搜索error、exception、failed等关键词。常见的错误有许可证问题Unity license is not activated。需要在服务器上以无头模式激活Unity许可证。使用-batchmode -quit -logFile -manualLicenseFile或-serial参数。脚本编译错误 项目代码本身有错。需要在打包前确保主分支代码是编译通过的。资源缺失 引用了不存在的资源或场景。检查构建场景列表。SDK/NDK路径错误 Android构建失败常见原因。确保Unity Preferences中的路径正确且Jenkins进程有权限访问。批处理模式下的交互弹窗 有些编辑器脚本可能会弹出对话框如“是否确认”这会导致批处理模式卡住。确保所有自动化流程涉及的编辑器代码都避免使用EditorUtility.DisplayDialog改用EditorUtility.DisplayDialogComplex或直接静默执行。路径与权限问题 Jenkins服务默认可能以SYSTEMWindows或jenkinsLinux用户运行其对某些目录如Program Files可能没有写权限。确保OUTPUT_PATH设置在Jenkins用户有读写权限的位置。Unity项目路径中也不要包含中文或特殊字符。8.2 Jenkins Pipeline执行问题sh/bat命令找不到 在Pipeline中执行系统命令时如果命令不在环境变量PATH中需要使用全路径。例如在Windows上使用C:\\Program Files\\Unity\\...\\Unity.exe。文件操作权限 Pipeline步骤中删除或移动文件可能因权限不足失败。可以尝试在命令前加上powershell或使用bat的force选项。流水线被意外终止 长时间构建尤其是首次导入大型Unity项目可能超过Jenkins默认的超时时间。可以在stage或整个script块外使用timeout包装。stage(长时间构建) { options { timeout(time: 2, unit: HOURS) } steps { script { buildUnityProject() } } }8.3 环境一致性与依赖管理这是保证自动化构建稳定性的基石。使用Docker高级 最彻底的解决方案。为Unity构建创建一个Docker镜像里面包含了指定版本的Unity、JDK、SDK等所有依赖。Jenkins Agent只需要能运行Docker即可。这确保了在任何机器上构建环境完全一致。不过Unity在Docker中的授权和无头模式运行需要额外配置。专用打包机 更实际的做法是准备一台或多台专用的物理机或虚拟机作为打包机。在这台机器上固定所有软件的版本并做好系统镜像备份。Jenkins将这些机器作为固定标签的Agent节点构建任务指定在这些节点上运行。依赖缓存 Unity项目的Library文件夹很大每次清空重拉会非常耗时。可以利用Jenkins的stash和unstash功能在构建间缓存Library文件夹但要注意如果项目配置或Unity版本升级需要清理缓存。或者使用更高级的缓存插件只缓存Asset Pipeline的中间产物。8.4 我的几点实战心得从小处着手迭代演进 不要试图一次性搭建完美的全平台流水线。可以先从最简单的Android开发包自动化开始跑通代码拉取-Unity构建-产出APK这个核心链路。然后再逐步加入参数化、iOS打包、Windows打包、通知、归档等特性。版本化一切Jenkinsfile、DSL脚本、自定义编辑器构建脚本、证书配置文件等全部纳入Git版本控制。这样任何更改都可追溯也方便回滚。构建状态可视化 除了邮件和即时通讯通知可以在办公室放一个显示器用Jenkins的插件如Build Monitor View展示所有关键构建Job的状态红/绿/黄。让团队对构建健康度一目了然。定期清理 设置Jenkins Job的“丢弃旧的构建”策略自动清理历史构建记录和产物防止磁盘被撑爆。构建产物最好自动上传到文件服务器或云存储并设置保留策略。文档化 为团队编写简单的使用文档如何触发构建、参数如何选择、产物在哪里找。降低非技术人员的使用门槛才能真正发挥自动化的价值。搭建这样一套系统初期会花费一些时间和精力但一旦运转起来它将成为团队研发流程中不可或缺的稳定基石将开发者从重复劳动中解放出来更专注于创造性的开发工作。每一次成功的自动构建都是对这项投资的最佳回报。
Unity多平台自动化打包实战:基于Jenkins的参数化CI/CD流水线设计
发布时间:2026/7/15 5:17:40
1. 项目概述为什么我们需要一个统一的自动化打包方案在游戏开发团队里打包这件事说大不大说小不小但绝对是个“脏活累活”。我经历过太多这样的场景策划要一个测试包程序说“等会儿我本地在跑测试”运营要一个渠道包程序又得手动改一堆参数一不留神就出错更别提iOS、Android、Windows三个平台每个平台的打包流程、证书配置、环境依赖都截然不同。手动操作不仅效率低下而且极易出错一个签名问题就能卡住整个团队半天。这就是为什么我们需要一个像“Unity项目多平台打包实战用Jenkins参数化构建管理iOS/Android/Windows版本”这样的自动化解决方案。这个方案的核心就是利用Jenkins这个老牌但极其强大的持续集成/持续部署CI/CD工具结合Unity的批处理模式Batchmode将原本需要人工干预、重复性高的打包工作变成一键触发、参数可控、结果可追溯的自动化流水线。它解决的不仅仅是“打包”这个动作更是团队协作、版本管理、环境一致性和开发效率的问题。想象一下美术、策划、测试同学不再需要打扰程序自己就能在Jenkins页面上点几下选择平台、版本号、构建模式然后去喝杯咖啡回来就能拿到热乎的安装包这能省下多少沟通成本和等待时间。对于中小型团队或者独立开发者来说搭建这样一套系统听起来可能有点“重”但实际投入产出比极高。一次搭建长期受益。它尤其适合需要频繁出包比如每日构建、多分支并行开发、多平台发布手游、PC端游、主机移植以及对构建质量有严格要求的项目。接下来我将以一个实战者的角度拆解如何从零开始搭建并配置这套自动化打包体系分享我踩过的坑和总结出的最佳实践。2. 环境准备与核心工具选型在动手写第一行脚本之前把地基打牢至关重要。这里的环境准备不仅仅是安装软件更是理解每个组件的作用和它们之间的协作关系。一个稳定的环境是自动化流程能够7x24小时可靠运行的前提。2.1 核心工具栈解析我们的工具栈主要围绕三个核心Unity、Jenkins和各个平台的SDK/NDK。Unity引擎与环境这是我们的“生产车间”。你需要一个稳定的Unity版本比如长期支持版LTS例如2022.3.x。关键在于你必须通过Unity Hub安装对应平台的支持模块。打Windows包需要“Windows Build Support (IL2CPP)”打Android包需要“Android Build Support”打iOS包则必须在macOS机器上安装“iOS Build Support”。很多人会忽略IL2CPP模块如果你的项目使用了HybridCLR原xLua热更框架的C#版本或者追求更好的性能与安全性IL2CPP是必须的。记住Unity的安装路径特别是Unity可执行文件Unity.exe或Unity.app/Contents/MacOS/Unity是我们后续脚本调用的关键务必记下。Jenkins服务端这是我们的“自动化指挥中心”。我强烈建议使用.war包方式部署而不是系统安装包因为它最通用也最干净。你需要一个Java运行环境注意Jenkins新版本已不再支持JDK 8最低要求是JDK 11我推荐使用JDK 17或21以获得更好的兼容性和性能。将JDK的bin目录添加到系统环境变量PATH中这是后续命令行启动的基础。平台特定工具链Android需要Android SDK和NDK。最简单的方式是通过Unity Hub安装Android模块时勾选它会自动配置好。但你也可以手动下载并指定路径。确保在Unity的Preferences - External Tools中正确设置SDK、NDK、JDK的路径。NDK的版本需要与Unity版本要求匹配不匹配会导致编译失败。iOS (仅限macOS)需要完整的Xcode以及Xcode Command Line Tools。在终端执行xcode-select --install即可安装命令行工具。此外你还需要准备好苹果开发者证书.p12文件和对应的描述文件.mobileprovision这是打包IPA的“通行证”。版本控制系统虽然Jenkins支持SVN但Git是目前绝对的主流。你需要确保打包服务器能够访问你的代码仓库GitHub、GitLab、Gitee等并且拥有拉取代码的权限配置SSH Key或账号密码。2.2 Jenkins部署实战与避坑指南部署Jenkins听起来简单但细节决定成败。这里以Windows服务器为例MacOS类似。首先在一个你希望放置Jenkins工作空间的目录比如D:\Jenkins下打开命令行。使用以下命令启动注意编码设置避免中文乱码# 在PowerShell中 $env:JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8 java -jar jenkins.war --httpPort8080--httpPort8080是指定端口如果8080被占用可以换成其他端口。注意第一次启动后命令行会打印一个初始管理员密码形如**************************************************。务必复制保存浏览器访问http://服务器IP:8080输入此密码才能进入初始化向导。在初始化向导中选择“安装推荐的插件”。这会安装包括Git、Pipeline在内的基础插件节省大量时间。创建完管理员账户后会进入Jenkins URL设置页面。这里是一个关键点如果你希望局域网内其他机器也能访问这台Jenkins必须将http://localhost:8080或http://127.0.0.1:8080改为你的服务器内网IP例如http://192.168.1.100:8080。否则只有本机可以访问。进入主界面后我们还需要安装几个对Unity自动化打包至关重要的插件。进入系统管理 - 插件管理 - 可选插件Pipeline 流水线插件核心中的核心我们所有的构建逻辑都将用Pipeline脚本编写。Job DSL 允许我们用Groovy脚本批量创建和管理Job这是实现“一套脚本管理所有平台任务”的关键。Active Choices Plug-in 提供动态参数如根据Git分支列表动态选择让参数化构建更强大、更友好。Authorize Project 为了安全地运行Groovy脚本特别是Job DSL我们需要在沙盒Sandbox模式下运行这个插件是前提。Subversion 如果你的项目还在用SVN就需要它。安装完插件后需要重启Jenkins。重启后进行一个关键安全配置进入系统管理 - 安全 - 安全找到“构建过程的访问控制”需要Authorize Project插件选择“以特定用户运行构建过程”并填入你系统的管理员用户名。这解决了后续DSL脚本运行时权限不足的问题。最后设置一个全局环境变量告诉Jenkins我们的Unity在哪。进入系统管理 - 系统配置找到“全局属性”添加一个环境变量。例如名称:UNITY_PATH值:C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exe(Windows) 或/Applications/Unity/Hub/Editor/2022.3.20f1/Unity.app/Contents/MacOS/Unity(macOS)至此我们的“指挥中心”就基本搭建完毕了。3. 参数化构建设计让打包变得灵活可控参数化构建是Jenkins Pipeline的灵魂它把固定的打包命令变成了一个可配置的“表单”。我们的目标是任何团队成员即使不懂命令行也能通过网页上的几个下拉框和输入框打出他想要的包。3.1 核心参数定义一个典型的Unity多平台打包Job应该包含以下参数。我会在Groovy Pipeline脚本的parameters块中定义它们properties([ parameters([ // 1. 选择构建平台 choice(name: PLATFORM, choices: [Android, iOS, Windows], description: 选择目标平台), // 2. 版本号控制 string(name: VERSION, defaultValue: 1.0.0, description: 应用版本号 (如: 1.0.0)), string(name: BUILD_NUMBER, defaultValue: ${BUILD_ID}, description: 构建号通常用Jenkins BUILD_ID), // 3. 代码版本控制 gitParameter(name: BRANCH, type: PT_BRANCH, description: 选择要构建的Git分支), // 4. 构建模式与配置 choice(name: BUILD_TARGET, choices: [Development, Release], description: 构建目标), booleanParam(name: DEVELOPMENT_BUILD, defaultValue: false, description: 是否启用Development Build允许调试), choice(name: SCRIPTING_BACKEND, choices: [Mono, IL2CPP], description: 脚本后端), // 5. 输出控制 string(name: OUTPUT_PATH, defaultValue: D:\\BuildOutput\\${JOB_NAME}, description: 打包输出路径), // 6. iOS特有参数 choice(name: IOS_SIGNING_TYPE, choices: [app-store, development, enterprise, ad-hoc], description: iOS签名类型, condition: PLATFORM iOS), booleanParam(name: IOS_USE_RESIGN, defaultValue: false, description: 是否使用重签名加速仅对非第一个签名包生效, condition: PLATFORM iOS) ]) ])参数设计思路解析PLATFORM 最核心的选择决定了后续所有流程的分支。VERSION和BUILD_NUMBER 用于生成最终包体的文件名如MyGame_1.0.0_125.apk。BUILD_NUMBER通常绑定${BUILD_ID}确保每次构建唯一。BRANCH 使用gitParameter插件实现动态分支列表构建时可以从仓库拉取的所有分支中选择非常灵活。BUILD_TARGET和DEVELOPMENT_BUILD 控制Unity的BuildOptions。Development模式包含调试符号方便测试Release模式则进行更多优化。SCRIPTING_BACKEND Mono兼容性好IL2CPP性能高且更安全。这是一个重要的性能与兼容性权衡点。OUTPUT_PATH 我强烈建议使用一个网络共享路径或固定的大容量磁盘路径方便所有人访问构建产物。${JOB_NAME}变量可以自动区分不同任务。IOS_SIGNING_TYPE和IOS_USE_RESIGN iOS打包的复杂性所在。签名类型必须与你的证书匹配。重签名加速是一个高级技巧先打一个基础包如App Store包然后通过重签名快速生成其他类型Dev、Ad-Hoc的包节省大量编译时间。3.2 使用Active Choices实现动态参数上面的condition属性展示了简单的条件显示。但有时我们需要更复杂的联动。例如当PLATFORM选择Android时我们希望多一个Android Build System的选择Gradle或Internal。这需要Active Choices Reactive Parameter插件。script { // 这个参数会在UI中动态生成 activeChoiceReactiveParam(name: ANDROID_BUILD_SYSTEM, description: Android构建系统, choiceType: PT_SINGLE_SELECT, groovyScript: if (PLATFORM.equals(Android)) { return [Gradle, Internal] } else { return [N/A] } , referencedParameters: [PLATFORM] // 声明依赖的参数 ) }这样只有当用户选择了Android平台这个下拉框才会出现并给出有效选项。这种动态性极大地提升了配置界面的友好度和准确性。4. Pipeline流水线核心脚本编写参数设计好了接下来就是实现打包逻辑的Pipeline脚本。我们将采用Declarative Pipeline语法它结构更清晰。整个Pipeline会被封装在一个Jenkinsfile中存放在项目仓库的根目录实现“配置即代码”。4.1 Pipeline主体结构一个完整的Pipeline通常包含以下阶段stagespipeline { agent any // 可以在任何有标签的agent上运行 parameters { // 这里放入3.1节定义的所有参数 } environment { // 定义环境变量如从Jenkins全局配置中读取Unity路径 UNITY_EXE ${env.UNITY_PATH} // 引用之前设置的全局变量 WORKSPACE ${env.WORKSPACE} // Jenkins自动提供的 workspace 路径 BUILD_OUTPUT ${params.OUTPUT_PATH} } stages { stage(代码拉取) { steps { // 根据参数BRANCH拉取代码 git branch: ${params.BRANCH}, url: 你的Git仓库地址, credentialsId: 你的凭据ID } } stage(环境检查与准备) { steps { script { // 检查Unity可执行文件是否存在 if (!fileExists(UNITY_EXE)) { error Unity executable not found at: ${UNITY_EXE} } // 根据平台检查特定环境如Android SDK路径 if (params.PLATFORM Android) { // 可以读取Unity的偏好设置文件或直接检查环境变量 def androidSdkPath ... // 获取SDK路径的逻辑 if (!fileExists(androidSdkPath)) { error Android SDK not found at: ${androidSdkPath} } } } } } stage(Unity构建) { steps { script { // 核心构建逻辑调用一个共享库函数 buildUnityProject() } } } stage(后处理与归档) { steps { script { // 重命名输出文件添加版本信息 // 将构建产物APK/IPA/EXE归档到Jenkins archiveArtifacts artifacts: **/*.apk, **/*.ipa, **/*.exe, fingerprint: true // 也可以上传到内部文件服务器或分发平台 } } } } post { always { // 无论成功失败都执行例如清理临时文件 cleanWs() // 清理工作空间 } success { // 构建成功后的通知如发送钉钉/企业微信消息 dingTalkTalker.sendText(构建成功: ${env.JOB_NAME} - ${params.VERSION}) } failure { // 构建失败后的通知 dingTalkTalker.sendText(构建失败: ${env.JOB_NAME} - 请检查日志) } } }4.2 核心构建函数buildUnityProject()详解这是整个Pipeline最核心的部分它需要处理不同平台的差异。我们将其写在一个共享库Shared Library中或者在script块内定义为一个大型函数。def buildUnityProject() { def unityArgs [] // 1. 基础参数 unityArgs [ -batchmode, // 批处理模式无图形界面 -nographics, // 不加载图形设备服务器环境必备 -quit, // 执行完毕后退出Unity -logFile, ${WORKSPACE}/unity_build.log, // 指定日志文件 -projectPath, ${WORKSPACE} // 项目路径 ] // 2. 根据平台和参数设置构建目标和方法 switch(params.PLATFORM) { case Android: unityArgs [-buildTarget, Android] // 设置Android特有参数 def exportPath ${BUILD_OUTPUT}/Android unityArgs [-executeMethod, Editor.BuildTool.BuildAndroid] // 调用自定义编辑器方法 unityArgs [-exportPath, exportPath] unityArgs [-versionName, ${params.VERSION}] unityArgs [-versionCode, ${params.BUILD_NUMBER}] if (params.SCRIPTING_BACKEND IL2CPP) { unityArgs [-scriptingBackend, il2cpp] } break case iOS: unityArgs [-buildTarget, iOS] def exportPath ${BUILD_OUTPUT}/iOS unityArgs [-executeMethod, Editor.BuildTool.BuildiOS] unityArgs [-exportPath, exportPath] // iOS签名信息通过外部配置文件或参数传入 unityArgs [-signingType, ${params.IOS_SIGNING_TYPE}] break case Windows: unityArgs [-buildTarget, StandaloneWindows64] def exportPath ${BUILD_OUTPUT}/Windows unityArgs [-executeMethod, Editor.BuildTool.BuildWindows] unityArgs [-exportPath, exportPath] break default: error Unsupported platform: ${params.PLATFORM} } // 3. 构建模式参数 if (params.DEVELOPMENT_BUILD.toBoolean()) { unityArgs [-developmentBuild, true] } // 4. 执行Unity命令 def cmd \${UNITY_EXE}\ ${unityArgs.join( )} echo 执行命令: ${cmd} def exitCode bat(script: cmd, returnStatus: true) // Windows用batMac/Linux用sh // 注意bat返回0表示成功非0表示失败 // 5. 检查日志和退出码 if (exitCode ! 0) { // 读取Unity日志将最后100行输出到Jenkins控制台方便排查 def logContent readFile ${WORKSPACE}/unity_build.log def lines logContent.readLines() def errorSnippet lines.size() 100 ? lines[-100..-1].join(\n) : logContent echo Unity构建失败日志末尾\n${errorSnippet} error Unity构建过程失败退出码: ${exitCode} } else { echo Unity构建成功完成。 } }关键点解析-executeMethod 这是关键。我们不在命令行里直接写BuildPipeline.BuildPlayer而是调用一个自定义的C#编辑器静态方法。这样做的好处是我们可以在这个方法里编写更复杂、更灵活的构建逻辑比如处理资源、修改PlayerSettings、处理不同渠道等。这个Editor.BuildTool类需要放在项目的Assets/Editor目录下。日志管理 一定要指定-logFile并将日志内容在失败时打印出来。Unity在批处理模式下的错误信息只会输出到这个文件控制台看不到具体错误。退出码检查 Unity命令执行后必须检查其退出码%ERRORLEVEL%on Windows,$?on bash。即使命令行执行了Unity内部构建失败也可能返回非零码。4.3 自定义编辑器构建脚本示例下面是一个简化的Editor/BuildTool.cs示例展示如何在C#端接收命令行参数并执行构建using UnityEditor; using System; using System.IO; public static class BuildTool { public static void BuildAndroid() { // 从命令行参数获取值 string exportPath GetArg(-exportPath); string versionName GetArg(-versionName); string versionCode GetArg(-versionCode); string scriptingBackend GetArg(-scriptingBackend); // 配置PlayerSettings PlayerSettings.applicationIdentifier com.yourcompany.yourgame; PlayerSettings.bundleVersion versionName; PlayerSettings.Android.bundleVersionCode int.Parse(versionCode); if (scriptingBackend il2cpp) { PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP); } // 定义构建选项 BuildOptions options BuildOptions.None; if (EditorUserBuildSettings.development) { options | BuildOptions.Development; options | BuildOptions.AllowDebugging; } // 执行构建 string[] scenes { Assets/Scenes/Main.unity }; BuildPipeline.BuildPlayer(scenes, Path.Combine(exportPath, YourGame.apk), BuildTarget.Android, options); } // 类似地编写 BuildiOS() 和 BuildWindows() 方法 public static void BuildiOS() { /* ... */ } public static void BuildWindows() { /* ... */ } // 辅助方法从命令行参数中获取值 private static string GetArg(string name) { var args Environment.GetCommandLineArgs(); for (int i 0; i args.Length; i) { if (args[i] name args.Length i 1) { return args[i 1]; } } return null; } }通过这种方式我们将平台相关的复杂配置从脆弱的命令行字符串中剥离出来放到了强类型的C#代码中更易于维护和调试。5. iOS自动化打包的深水区与证书管理iOS打包是三个平台中最复杂的一环核心难点在于代码签名和证书管理。自动化流程必须安全、可靠地处理.p12证书和.mobileprovision描述文件。5.1 证书的安全存储与使用绝对不要将证书和描述文件硬编码在脚本或放入代码仓库。Jenkins提供了Credentials功能来安全地存储这些敏感信息。添加证书凭证 进入系统管理 - Manage Credentials在全局域添加Secret file类型的凭据上传你的.p12文件。Jenkins会为其生成一个唯一的credentialsId。在Pipeline中使用 在Pipeline的environment块或steps中使用withCredentials绑定凭据将其临时解密并存储到工作空间的一个临时位置供打包脚本使用。stage(iOS 签名准备) { steps { script { withCredentials([file(credentialsId: ios-signing-cert, variable: P12_FILE), file(credentialsId: ios-provision-profile, variable: MOBILEPROVISION_FILE)]) { // 此时P12_FILE和MOBILEPROVISION_FILE是临时文件路径 sh cp ${P12_FILE} ${WORKSPACE}/temp/cert.p12 sh cp ${MOBILEPROVISION_FILE} ${WORKSPACE}/temp/profile.mobileprovision // 将临时文件路径传递给Unity构建方法或后续的xcodebuild命令 env.IOS_P12_PATH ${WORKSPACE}/temp/cert.p12 env.IOS_PROVISION_PATH ${WORKSPACE}/temp/profile.mobileprovision } } } }5.2 从Xcode工程到IPA自动化流程Unity的BuildPipeline.BuildPlayer在iOS平台下产出的是一个Xcode工程而不是直接的.ipa文件。因此完整的iOS自动化打包包含两个阶段Unity导出Xcode工程。使用xcodebuild和xcrun命令编译并打包成IPA。我们可以在自定义的BuildTool.BuildiOS()方法中只完成第一步然后在Jenkins Pipeline中调用Shell脚本完成第二步。也可以借助一些第三方插件或脚本如xcodebuild的archive和exportArchive命令来整合。一个简化的第二阶段Pipelinestage可能如下stage(生成IPA) { when { expression { params.PLATFORM iOS } } steps { script { dir(${WORKSPACE}/Build/iOS) { // 进入Unity导出的Xcode工程目录 // 1. 解锁钥匙串并导入证书 (MacOS Jenkins Agent上需要) sh security unlock-keychain -p ${KEYCHAIN_PASSWORD} ${HOME}/Library/Keychains/login.keychain-db sh security import ${env.IOS_P12_PATH} -k ${HOME}/Library/Keychains/login.keychain-db -P ${P12_PASSWORD} -T /usr/bin/codesign // 2. 将描述文件复制到正确位置 sh cp ${env.IOS_PROVISION_PATH} ~/Library/MobileDevice/Provisioning\\ Profiles/ // 3. 使用xcodebuild archive sh xcodebuild -workspace Unity-iPhone.xcworkspace -scheme Unity-iPhone -configuration Release -archivePath build/Unity-iPhone.xcarchive archive // 4. 使用xcodebuild -exportArchive 导出IPA // 需要创建一个ExportOptions.plist文件来配置签名方式 sh xcodebuild -exportArchive -archivePath build/Unity-iPhone.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath . } // 5. 将生成的IPA文件移动到最终输出目录 sh cp ${WORKSPACE}/Build/iOS/Unity-iPhone.ipa ${BUILD_OUTPUT}/ } } }重要提示 上述步骤需要在装有Xcode的macOS Jenkins Agent节点上运行。KEYCHAIN_PASSWORD和P12_PASSWORD也需要通过Jenkins Credentials管理。ExportOptions.plist文件需要根据你的签名类型App Store, Ad-Hoc等预先配置好模板。5.3 重签名加速技巧这是提升iOS多包构建效率的“黑科技”。原理是第一次完整编译生成一个未签名或带有某种签名的.xcarchive或.app。后续需要其他签名类型的包时不再重新编译而是对这个已有的产物进行重签名。stage(iOS 重签名加速) { when { allOf { expression { params.PLATFORM iOS } expression { params.IOS_USE_RESIGN.toBoolean() } } } steps { script { // 假设基础包如app-store签名的.app已经存在在某个路径 def baseAppPath ${env.BUILD_OUTPUT}/BaseApp/YourGame.app // 使用新的描述文件和证书进行重签名 sh /usr/bin/codesign --force --sign ${SIGNING_IDENTITY} --entitlements entitlements.plist --timestampnone ${baseAppPath} // 然后重新打包成IPA sh /usr/bin/xcrun -sdk iphoneos PackageApplication -v ${baseAppPath} -o ${env.BUILD_OUTPUT}/YourGame_AdHoc.ipa --embed ${env.IOS_PROVISION_PATH} } } }这可以将后续包的构建时间从十几二十分钟缩短到一两分钟在需要频繁出测试包时非常有用。6. Android与Windows平台的构建要点相比iOSAndroid和Windows的自动化构建要直接许多但也有一些需要注意的细节。6.1 Android构建的变体与渠道Android平台经常需要为不同渠道应用商店出不同的包可能涉及不同的包名、应用图标、启动图或SDK初始化配置。我们可以在自定义构建方法中通过命令行参数来指定渠道。// 在Jenkins参数中增加一个渠道选择 choice(name: ANDROID_CHANNEL, choices: [googleplay, huawei, xiaomi, official], description: Android发布渠道) // 在buildUnityProject函数中传递给Unity if (params.PLATFORM Android) { unityArgs [-androidChannel, ${params.ANDROID_CHANNEL}] }在C#的BuildAndroid方法中读取这个参数并据此修改PlayerSettings或执行特定的预处理脚本比如替换assets中的渠道配置文件。关于Android App Bundle (AAB) 如果需要上架Google Play你可能需要打AAB包。Unity 2018.3支持直接构建AAB。在BuildPlayer时将BuildOptions设置为BuildOptions.BuildAppBundle即可。注意AAB不能直接安装需要上传到Play Console后由Google生成APK。6.2 Windows构建单EXE与数据分离Unity打Windows包有两种输出格式包含数据文件夹的EXE 一个.exe文件加一个同名的_Data文件夹。这是默认选项。单文件EXE 将所有数据打包进一个独立的.exe文件便于分发。在构建时可以通过BuildOptions尝试启用BuildOptions.CompressWithLz4HC等进行压缩。对于单文件打包Unity官方并未直接提供选项但社区有一些后处理脚本或工具如il2cpp工具链中的UnityLinker可以尝试不过稳定性和兼容性需要自行测试。更常见的做法是使用第三方安装包制作工具如Inno Setup, NSIS将EXE和Data文件夹打包成一个安装程序。Windows构建的另一要点是图形API。如果项目使用了Vulkan等API需要在PlayerSettings中正确设置。这些设置也应该在自定义构建脚本中根据参数或项目配置来设定。7. 使用Job DSL实现任务批量管理与维护当你的项目有多个开发分支如main,develop,feature/xxx每个分支又需要打三个平台的包时手动在Jenkins界面上创建几十个Job是灾难性的。Job DSL插件就是来解决这个问题的。7.1 DSL脚本基础Job DSL允许你用Groovy脚本描述Job的配置。你可以创建一个“种子Job”这个Job的唯一任务就是执行一段DSL脚本这段脚本会像程序一样批量创建或更新其他真正的构建Job。创建一个新的Pipeline类型的“种子Job”在Pipeline脚本中写入// create_jobs.groovy def branches [main, develop, release/1.0] def platforms [Android, iOS, Windows] branches.each { branch - platforms.each { platform - def jobName Build-${branch}-${platform}.replaceAll(/, _) // 替换斜杠因为Job名不允许 pipelineJob(jobName) { definition { cpsScm { scm { git { remote { url(https://github.com/your-company/your-unity-project.git) credentials(your-git-credential-id) } branch(branch) } } scriptPath(Jenkinsfile) // 指向仓库根目录的Jenkinsfile } } parameters { // 这里可以定义该Job的默认参数覆盖Jenkinsfile中的defaultValue stringParam(PLATFORM, platform, 目标平台) stringParam(BRANCH, branch, 构建分支) // ... 其他参数 } } } }运行这个“种子Job”它就会自动创建出Build-main-Android,Build-main-iOS, ...Build-release_1.0-Windows等9个Job。7.2 维护与更新当你的构建流程需要更新时比如修改了Jenkinsfile或者想增加一个公共参数你只需要更新“种子Job”中的DSL脚本然后再次运行它。Job DSL插件会计算差异并自动更新所有由它管理的Job配置。这实现了“基础设施即代码”极大提升了多任务管理的效率和一致性。8. 常见问题排查与实战心得即使流程设计得再完美在实际部署和运行中也会遇到各种问题。这里记录一些我踩过的坑和解决方案。8.1 Unity构建失败排查清单日志是唯一真理 构建失败第一时间查看unity_build.log文件。搜索error、exception、failed等关键词。常见的错误有许可证问题Unity license is not activated。需要在服务器上以无头模式激活Unity许可证。使用-batchmode -quit -logFile -manualLicenseFile或-serial参数。脚本编译错误 项目代码本身有错。需要在打包前确保主分支代码是编译通过的。资源缺失 引用了不存在的资源或场景。检查构建场景列表。SDK/NDK路径错误 Android构建失败常见原因。确保Unity Preferences中的路径正确且Jenkins进程有权限访问。批处理模式下的交互弹窗 有些编辑器脚本可能会弹出对话框如“是否确认”这会导致批处理模式卡住。确保所有自动化流程涉及的编辑器代码都避免使用EditorUtility.DisplayDialog改用EditorUtility.DisplayDialogComplex或直接静默执行。路径与权限问题 Jenkins服务默认可能以SYSTEMWindows或jenkinsLinux用户运行其对某些目录如Program Files可能没有写权限。确保OUTPUT_PATH设置在Jenkins用户有读写权限的位置。Unity项目路径中也不要包含中文或特殊字符。8.2 Jenkins Pipeline执行问题sh/bat命令找不到 在Pipeline中执行系统命令时如果命令不在环境变量PATH中需要使用全路径。例如在Windows上使用C:\\Program Files\\Unity\\...\\Unity.exe。文件操作权限 Pipeline步骤中删除或移动文件可能因权限不足失败。可以尝试在命令前加上powershell或使用bat的force选项。流水线被意外终止 长时间构建尤其是首次导入大型Unity项目可能超过Jenkins默认的超时时间。可以在stage或整个script块外使用timeout包装。stage(长时间构建) { options { timeout(time: 2, unit: HOURS) } steps { script { buildUnityProject() } } }8.3 环境一致性与依赖管理这是保证自动化构建稳定性的基石。使用Docker高级 最彻底的解决方案。为Unity构建创建一个Docker镜像里面包含了指定版本的Unity、JDK、SDK等所有依赖。Jenkins Agent只需要能运行Docker即可。这确保了在任何机器上构建环境完全一致。不过Unity在Docker中的授权和无头模式运行需要额外配置。专用打包机 更实际的做法是准备一台或多台专用的物理机或虚拟机作为打包机。在这台机器上固定所有软件的版本并做好系统镜像备份。Jenkins将这些机器作为固定标签的Agent节点构建任务指定在这些节点上运行。依赖缓存 Unity项目的Library文件夹很大每次清空重拉会非常耗时。可以利用Jenkins的stash和unstash功能在构建间缓存Library文件夹但要注意如果项目配置或Unity版本升级需要清理缓存。或者使用更高级的缓存插件只缓存Asset Pipeline的中间产物。8.4 我的几点实战心得从小处着手迭代演进 不要试图一次性搭建完美的全平台流水线。可以先从最简单的Android开发包自动化开始跑通代码拉取-Unity构建-产出APK这个核心链路。然后再逐步加入参数化、iOS打包、Windows打包、通知、归档等特性。版本化一切Jenkinsfile、DSL脚本、自定义编辑器构建脚本、证书配置文件等全部纳入Git版本控制。这样任何更改都可追溯也方便回滚。构建状态可视化 除了邮件和即时通讯通知可以在办公室放一个显示器用Jenkins的插件如Build Monitor View展示所有关键构建Job的状态红/绿/黄。让团队对构建健康度一目了然。定期清理 设置Jenkins Job的“丢弃旧的构建”策略自动清理历史构建记录和产物防止磁盘被撑爆。构建产物最好自动上传到文件服务器或云存储并设置保留策略。文档化 为团队编写简单的使用文档如何触发构建、参数如何选择、产物在哪里找。降低非技术人员的使用门槛才能真正发挥自动化的价值。搭建这样一套系统初期会花费一些时间和精力但一旦运转起来它将成为团队研发流程中不可或缺的稳定基石将开发者从重复劳动中解放出来更专注于创造性的开发工作。每一次成功的自动构建都是对这项投资的最佳回报。