手上只有一台Windows打包机,项目却要出Linux版本——这个局面我在几个团队里都碰到过。运维不想再养一台Linux构建机,发行版升级还容易把老构建环境搞崩,最后能落地的方案基本只有一个:在Windows上装UE自带的Linux交叉编译工具链,用同一套RunUAT命令行把Linux包打出来。这条路本身不清奇,但它有一套自己的脾气:SDK版本对不上会直接报错退出,路径大小写混用要等到运行期才炸,产物拷到Linux上还会因为可执行权限丢了根本起不来。下面我把整条链路拆开讲,从工具链装在哪、Target怎么配,到RunUAT参数逐条说明,再到实际踩过的坑和排查顺序,尽量把每一步的"为什么"讲透。已经在Windows上跑通Windows包、准备加一个Linux平台输出的同学,或者打算把Linux打包接进流水线的人,都可以照着走一遍。
1. 先想清楚:为什么是交叉编译,而不是再开一台Linux构建机
1.1 交叉编译在UE里指的是什么
交叉编译这个词拆开看就是"在A平台上生成B平台能跑的程序"。放到UE的场景里,A是Windows,B是Linux x86_64或者Linux Arm64。你在Windows上敲一条命令行,UnrealBuildTool会调起一套专门为Linux目标编译的clang/lld,把C++源码编成ELF格式的可执行文件,再用Windows上的cooker把资源烘焙成Linux能读的pak,最后整包落到一个目录里。
这里有个概念必须先说清楚:UE的交叉编译工具链只覆盖运行时Target,也就是Game、Client、Server这几类。你不能指望用它在Windows上编出一个Linux版Editor。工具链里带的是目标的libc、libc++头文件和运行库,没有editor那套依赖,UBT在Target类型上就会把你挡回去。所以如果你的工作流需要"在Linux上开编辑器改关卡",这套方案帮不了你,得老老实实装Linux版引擎。但只要你的流程是"策划美术在Windows上做内容,CI出包",交叉编译就是最省事的那条路。
另一个容易被忽略的点是:UE的Linux工具链用的是clang + lld + 自带libc++,不是系统gcc。这是它敢跨平台的前提——目标侧的C++运行库是跟着工具链一起分发的,不依赖目标机装了什么版本的libstdc++。代价就是你得接受它挑clang版本,不能随便换成gcc-arm那一套自己搭的链子。
1.2 三种出Linux包的方案横向对比
我在不同团队里试过三种做法,各自的适用场景差别挺大,列个表更直观:
| 方案 | 首次搭建成本 | 构建速度 | 环境一致性 | 适用场景 |
|---|---|---|---|---|
| Windows交叉编译 | 低,装个工具链即可 | 中等,受限于Windows磁盘IO | 高,工具链随引擎版本锁定 | CI已在Windows,团队无Linux运维 |
| Linux原生构建 | 中,要装引擎源码版 | 高,Linux文件系统和并行IO更友好 | 中,容易受发行版升级影响 | 团队本身用Linux开发,或需要编辑器 |
| 容器化Linux构建 | 高,要维护镜像和GPU直通 | 高 | 高 | 大型团队,构建量大,要求可复现 |
交叉编译真正的优势不在速度,而在你不需要多一台机器、也不需要多一套运维知识。项目规模在两三百人天以内、一周出包几次的节奏,这条路完全够用。真正让它变痛苦的是构建量上来之后:UE的Linux链接阶段本身就是单线程的,Windows的NTFS在小文件读写上也确实不如ext4,出一次全量Shipping包一个多小时很常见。
1.3 哪些情况下我会劝你别走这条路
有几种场景我踩过之后会直接建议换方案。第一,项目里有大量依赖Linux原生扩展的插件,比如要调系统级的库或者自定义的so,交叉编译链里没有对应的sysroot包,你会陷在"补依赖"的循环里。第二,需要频繁出服务器包并且要做真实压力测试的,交叉编译出来的server包只能验证"能否启动",真实性能表现还是要在目标环境跑。第三,团队已经在用Linux做CI的,硬拐回Windows等于把已有的缓存和流水线全推翻,不划算。
判断标准其实很简单:如果你现在的痛点是"没机器",交叉编译;如果痛点是"构建太慢",那得解决机器和缓存,不是换平台能救的。
2. 工具链:装在哪、装哪个版本、UBT怎么找到它
2.1 工具链包的命名规则与版本对照
Epic把Linux交叉编译工具链单独发布,命名规则是v<序号>_clang-<版本>-<宿主基线>.zip。这个命名里三个信息都有用:序号是Epic自己的迭代号,clang版本决定你能用的C++特性,宿主基线(centos7或者后来的rockylinux8)决定它能编出兼容多老glibc的二进制。
几个常见引擎版本对应的工具链大致是这样,但以你本地引擎为准:
| 引擎版本 | 工具链包名 |
|---|---|
| UE 4.27 | v19_clang-11.0.1-centos7 |
| UE 5.0 - 5.2 | v20_clang-13.0.1-centos7 |
| UE 5.3 | v22_clang-16.0.6-centos7 |
| UE 5.4 及以后 | v23_clang-18.1.0-rockylinux8 |
权威来源不是这张表,是引擎源码里Engine/Source/Programs/UnrealBuildTool/Platform/Linux/LinuxPlatformSDK.cs中那个ExpectedSDKVersion常量。打开文件搜一下,写的是什么版本,你就去下什么版本,一个字符都不能差。工具链包从Epic的CDN拉,文件名就是包名加.zip。
解压位置建议统一放一个专门目录,比如C:\UnrealToolchains\,因为zip里自带一层同名的顶层目录,解压完路径就是C:\UnrealToolchains\v22_clang-16.0.6-centos7。别解压到引擎目录里,引擎升级的时候你的工具链就跟着一起被覆盖或者被清掉了,这坑我见过两次。
2.2 LINUX_MULTIARCH_ROOT与SDKVersion的双向对齐
UBT找工具链走的是环境变量LINUX_MULTIARCH_ROOT。设置成工具链顶层目录,结尾不要带反斜杠,路径里别带空格和中文。
setx LINUX_MULTIARCH_ROOT "C:\UnrealToolchains\v22_clang-16.0.6-centos7"setx写完要新开一个终端才生效,这点很坑——很多人设完在同一个cmd里继续跑,报"找不到SDK",然后开始怀疑人生。验证方法:
echo %LINUX_MULTIARCH_ROOT% dir %LINUX_MULTIARCH_ROOT%\x86_64-unknown-linux-gnu\bin另一边是引擎里的期望版本。位置在Engine/Config/Windows/WindowsEngine.ini的[/Script/LinuxTargetPlatform.LinuxTargetSettings]段下的SDKVersion。这个值和工具链包名必须一致。改了引擎里的这个值,UBT在启动时会先做一次校验,不一致直接报"SDK version mismatch",连编译都不会开始。
为什么会有这个双重校验?因为Epic要在引擎升级时强制你换工具链。clang大版本变了,libc++的ABI、默认的C++标准、链接行为都可能变,混着用会有各种诡异的运行期崩溃。所以这个校验看着烦,实际上是在帮你避一个很难查的坑。
2.3 装完先跑一个最小验证
别急着开大项目,先做两件事。
第一件,直接调工具链里的clang看目标三元组:
%LINUX_MULTIARCH_ROOT%\x86_64-unknown-linux-gnu\bin\clang++.exe --version输出里能看到Target: x86_64-unknown-linux-gnu就说明链子本身是完整的。如果这个目录里还带aarch64-unknown-linux-gnueabi,说明这个版本的工具链同时支持Linux Arm64,后面要用-platform=LinuxArm64的时候就不用再折腾了。
第二件,拿引擎自带的模板工程做一次最小打包,Development配置、不带pak,只要-build这一步能过就说明编译链通了:
Engine\Build\BatchFiles\RunUAT.bat BuildCookRun ^ -project="D:\Work\MyProject\MyProject.uproject" ^ -noP4 -platform=Linux -clientconfig=Development -build这一步的意义是把"环境问题"和"项目问题"隔离开。模板工程过不了,一定是你工具链或者引擎配置的问题;模板工程过了、自己的项目过不了,那就是项目侧有Windows专属的东西,去第3节找。
3. 项目侧要改的东西其实不多,但每一处都不能漏
3.1 Target.cs与Build.cs里的平台相关开关
UE的Target.cs里有个PlatformAllowList/PlatformDenyList,但实际操作中更常见的坑是bOverrideBuildEnvironment和自定义的LinkerSubsystem。如果你在Target里硬写了Windows的子系统或者附加链接参数,Linux构建会直接失败。检查方式就是全局搜一遍.Target.cs和.Build.cs里的Target.Platform == UnrealTargetPlatform.Win64这类判断,确认每个分支都有Linux的对应处理。
Build.cs里需要重点看的几个字段:
PublicAdditionalLibraries和PublicDelayLoadDLLs:Windows的.lib和.dll在Linux下无效,不能用同一个模块规则简单套。要么按平台分支给.a/.so,要么这个模块干脆不进Linux构建。RuntimeDependencies:这里列的文件会被原样拷进产物,路径大小写在Windows上不敏感、在Linux上敏感,见第5节。bEnableExceptions、bUseRTTI:跨平台能不能用异常和RTTI,得看你在Linux侧有没有对应的运行库支持,改动前先确认工具链里libc++是编了异常支持的。
我一般的做法是在Build.cs里先加一个平台判断,把Linux的依赖先留空,跑一次构建,UBT报什么缺什么,再一个个补。这比一次性把Windows那堆库名换成猜出来的Linux库名要快得多。
3.2 插件与第三方库的筛选
交叉编译最容易被插件卡住。判断一个插件能不能进Linux构建,看三点:它的Build.cs里有没有平台白名单;它依赖的第三方库有没有Linux预编译版本;它的二进制资源是不是只针对Windows烤的。
如果某个插件确实只服务Windows,最干净的做法是在打包命令行里把它排除,而不是去改插件源码:
-Platform=Linux -ClientConfig=Shipping -Cook -Build ^ -SkipCook -IterativeCooking排除插件的参数是-DisablePlugins=,多个用加号分隔:
-DisablePlugins=SomeWinOnlyPlugin+AnotherOne这个参数写在-build之前,UBT和cooker都会遵守。我踩过一次坑:只在cook阶段禁了插件,编译阶段没禁,结果编到一半报找不到模块。所以编译和cook两阶段用的是同一份清单,一次写全。
第三方库方面,能走源码的最省事——把源码编进模块里,跟着工具链走,不用担心目标机缺so。实在只能给预编译的,优先选静态库.a,其次是.so,并且确认它是用不高于你工具链基线glibc编出来的。用较新的glibc编的.so放到较老的发行版上会报GLIBC_2.xx not found,这个问题在交叉编译场景里特别隐蔽,因为你本机根本没有那个老发行版可以测。
3.3 默认RHI与Shader格式的提前配置
这一步不做,包能出来,但跑到目标机上大概率是黑屏或者直接崩。Linux桌面默认走Vulkan,你得保证Vulkan的shader被烤进pak里。
在DefaultEngine.ini里加:
[/Script/LinuxTargetPlatform.LinuxTargetSettings] +TargetedRHIs=SF_VULKAN_SM6 +TargetedRHIs=SF_VULKAN_SM5在编辑器里也有对应入口:Project Settings → Platforms → Linux → Targeted RHIs。两个都留是因为目标机驱动千奇百怪,某些老显卡只支持到SM5,只烤SM6的包在那台机器上就是黑屏。多烤一份shader会让cook时间变长,但对上线包来说是值得的。
同时确认Project Settings → Packaging里Use Pak File和Use IoStore都开着。IoStore在UE5里是默认,它把资源打成.utoc+.ucas,文件数大幅下降,启动更快,也顺带把"文件数量太多导致大小写问题暴露"的概率降下来。
4. RunUAT一次完整打包:参数逐条拆开看
4.1 BuildCookRun的必选参数
一条能用的Linux Shipping打包命令长这样,我把它拆成几行方便看:
Engine\Build\BatchFiles\RunUAT.bat BuildCookRun ^ -project="D:\Work\MyProject\MyProject.uproject" ^ -noP4 ^ -platform=Linux ^ -clientconfig=Shipping ^ -cook -allmaps -build -stage -pak -compressed ^ -compressionformats=Oodle ^ -unattended -nop4 -utf8output -nocompileeditor ^ -archive -archivedirectory="D:\Builds\MyProject"逐条解释几个容易搞混的:
-noP4和-nop4:前者告诉UAT不要碰Perforce,后者是给编辑器层的。两个都写没坏处,CI环境里尤其要写,不然UAT会尝试连P4,连不上就卡在那等超时。-platform=Linux:决定编译和cook的目标平台。注意它不决定-serverplatform,服务端要单独指定。-cook -allmaps:全量烤所有关卡。想增量就把-allmaps去掉,配合-iterate。-stage:把产物整理到Saved\StagedBuilds\Linux下,这一步会组装出MyProject.sh、MyProject\、Engine\三个顶层项。-pak:打pak。不加的话产物是一堆散文件,方便调试但启动慢。-archive -archivedirectory=:把staged结果复制一份到你指定的目录。建议永远加-archive,因为Saved\StagedBuilds下一次打包会被覆盖,CI里拿产物必须从archive目录取。-unattended:不弹任何对话框,CI必须加,否则报错时进程挂着等输入,流水线超时半小时才失败。-nocompileeditor:跳过编辑器模块编译,节省大量时间。
4.2 服务器包与客户端包分开出的写法
服务器包和客户端包往往要分开出,甚至只出服务端。这时候用-noclient或者-noserver控制:
Engine\Build\BatchFiles\RunUAT.bat BuildCookRun ^ -project="D:\Work\MyProject\MyProject.uproject" ^ -noP4 -platform=Linux -serverplatform=Linux ^ -clientconfig=Shipping -serverconfig=Shipping ^ -server -noclient ^ -cook -build -stage -pak -archive -archivedirectory="D:\Builds\Server"这里有个细节要注意:-platform是客户端平台,-serverplatform是服务端平台。如果你只出服务端,理论上-platform也要写,UAT用它来决定cook出来的资源格式。两个都写Linux最保险。
另外,服务端配置下Target.cs里的TargetType.Server会走一套不同的模块裁剪逻辑,很多只在客户端跑的系统(渲染、音频设备)会被排除。这意味着服务端能编过不代表客户端能编过,反过来也一样,CI里两条链路都要跑一遍。
4.3 Cooking卡住时先看这三处
痛点排行榜第一名是cook卡住。日志里刷一堆LogCook: Display: Cooked...之后突然长时间没输出,先按顺序看这三处:
第一,看Saved\Logs\Cook-*.log末尾有没有ShaderCompileWorker相关行。Vulkan shader的首次编译非常耗时,几千个材质变体编十几分钟很正常。加了+TargetedRHIs=SF_VULKAN_SM6和SM5之后时间会翻倍,这是预期行为,不是卡死。
第二,看是不是在等网络。项目里如果引用了不在本地的资源,或者有插件的Build.cs里写了RuntimeDependencies指向网络路径,cooker会尝试访问然后超时。把-verbose加上再跑一次,日志里能直接看到具体文件。
第三,看DDC。-ddc=参数不写的话走默认的本地DDC,路径在%LOCALAPPDATA%\UnrealEngine\Common\DerivedDataCache。如果DDC目录被清过或者权限有问题,cooker会从头全量重烤,表现同样是"卡住"。团队多人协作的话把DDC指到共享盘会好很多,但共享盘的IO要是跟不上,反而比本地慢,这个得实测。
5. 产物落地Linux之前,这几个坑基本都会遇到
5.1 从Windows拷到Linux,最先丢的是可执行权限和行尾
这是最经典的一个。你在Windows上打包成功,把Linux目录压缩,传到Linux服务器解压,然后执行./MyProject.sh,报Permission denied。原因是zip、tar打包在Windows侧不会保留Unix的可执行位。
修复就两条命令:
chmod +x *.sh chmod +x MyProject/Binaries/Linux/MyProject第二个chmod容易被忘,因为.sh脚本只是壳,真正启动的二进制是Binaries/Linux/下那个没有扩展名的文件,它没执行权限,脚本里那行"$DIR/MyProject/Binaries/Linux/MyProject"就会失败。
行尾问题更隐蔽。.sh脚本如果是CRLF行尾,Linux执行时报:
/bin/sh^M: bad interpreter: No such file or directory这个报错信息很有误导性,看着像找不到/bin/sh,其实是/bin/sh后面跟了个\r。批量修:
sed -i 's/\r$//' *.shCI里更省事的做法是,在归档那一步就用一条脚本把权限和行尾一起处理掉,别留给部署的人手工做。
5.2 路径大小写:Windows不报,Linux不认
完整排查链路是这样的。你先看到的现象是:包在Windows上开发跑得好好的,拷到Linux上启动后某个资源加载失败,日志里是LogStreaming: Warning: Failed to load file ...。
第一步,把日志里那个路径抄出来,去产物目录里ls一遍。如果ls能找到同名的但大小写不同,基本就确诊了。
第二步,回到源码里定位这个路径是从哪来的。常见的三个来源:Build.cs的RuntimeDependencies列表、C++里的#include、配置ini里写的相对路径。
第三步,为什么Windows上没报?因为NTFS不区分大小写,#include "myheader.h"在文件实际叫MyHeader.h的时候照样能找到,编译一点问题没有。UBT在Windows上也不会去校验大小写。等这个包跑到Linux上,区分大小写的文件系统一查,就找不到。
第四步,修法是在源码侧把大小写改成和磁盘文件完全一致。别指望用工具自动修,因为自动修没法判断是"文件名写错了"还是"引用名写错了",改错一边更麻烦。我一般用IDE的重命名重构功能改,这样引用会跟着一起更新。
第五步,验证方式是在Windows上做一次全量cook加-pak,然后在Linux上跑起来,走一遍所有关卡。别只测主菜单,测试关卡、存档读取、DLC资源这些边缘路径才是重灾区。
还有一个变体:.so文件的依赖名。如果你用了第三方.so,Linux加载器对文件名是严格区分的,libFoo.so和libfoo.so是两个东西。
5.3 非ASCII路径和空格
引擎路径、项目路径、工具链路径,这三个里面任何一个带中文或者空格,交叉编译都会出问题。原因不是UE不支持Unicode路径,是中间那层工具——clang的driver、链接脚本、shader编译器的临时文件处理——在处理非ASCII时会出各种意料之外的截断。
排查顺序很简单,看日志里第一条报错的文件路径,如果有??或者乱码,就是这个问题。解决方案就是把整条链路挪到纯ASCII路径下,比如D:\UE\Engine、D:\Work\MyProject、C:\UnrealToolchains。这不只是交叉编译的要求,Linux原生构建同样建议这么做。
5.4 体积:Debug符号和strip
第一次出Linux Shipping包的人往往会惊讶于产物的体积。Windows下Shipping包可能三四百兆,Linux这边一个二进制就奔着二百兆去。原因是Linux的Shipping配置默认仍然带调试符号。
瘦身用工具链自带的objcopy:
%LINUX_MULTIARCH_ROOT%\x86_64-unknown-linux-gnu\bin\objcopy.exe ^ --strip-debug MyProject MyProject.stripped用--strip-debug而不是--strip-all。区别在于:--strip-all会把.symtab也去掉,崩溃时拿到的堆栈就只剩地址了;--strip-debug只去掉.debug_*段,.symtab保留,符号化还能做。
操作建议:把原始二进制和strip后的都留着,原始的那个用于日后符号化崩溃堆栈。strip这一步能不能接进UAT?可以在-build之后加一个PostBuild步骤,或者干脆在CI的归档脚本里做。我第一次做的时候是手工strip的,第二次就写进脚本了,因为手工做一定会忘。
6. 在目标机上跑起来:依赖、启动脚本与自检清单
6.1 目标机需要哪些系统库
交叉编译出来的包不是完全自包含的。引擎自带的第三方so(SDL、OpenAL、PhysX这些)都在Engine/Binaries/ThirdParty/Linux/下被staged进产物了,但下面这些必须由目标机的系统提供:
- 图形相关:
libX11、libXcursor、libXrandr、libXi、libGL、libvulkan1、libfontconfig1 - 音频:
libasound2、libpulse0 - 基础运行库:
libc(glibc)、libstdc++、libm、libpthread、libdl、librt
在Debian系上一条命令能装齐:
sudo apt-get install -y libx11-6 libxcursor1 libxrandr2 libxi6 libgl1 \ libfontconfig1 libasound2 libpulse0 libvulkan1关键约束是glibc版本。工具链是以某个较老的glibc为基线编的,所以编出来的二进制在较新的发行版上一般能跑,反过来不行。目标机的glibc版本如果低于工具链基线,启动会直接报version 'GLIBC_2.xx' not found。确认方法:
ldd --version | head -1拿到版本号后和工具链的基线对一下。这也是为什么我一直建议在CI里保一台和目标环境一致的验证机,别等到客户那边才炸。
6.2 启动方式与常见启动失败信号
启动就走staged目录下的脚本:
cd /opt/myproject chmod +x MyProject.sh ./MyProject.sh -log加-log能直接在终端看到日志输出,排查阶段非常有用。几个典型失败信号和对应方向:
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
Permission denied | 少了可执行位 | chmod +x |
bad interpreter: ^M | 脚本是CRLF行尾 | sed -i 's/\r$//' |
error while loading shared libraries: libX11.so.6 | 系统缺库 | 按6.1安装 |
version 'GLIBC_2.xx' not found | 目标机glibc过老 | 换工具链基线或升级目标系统 |
| 启动后黑屏、有声音 | Vulkan shader缺失或驱动不支持 | 检查TargetedRHIs配置 |
| 启动即崩,无输出 | 崩溃处理器拦截了 | 找Saved/Crashes目录下的dump |
黑屏那个我要多提一句。表现是进程活着,能听到声音,窗口是黑的。九成是Vulkan shader没烤进去,或者目标机的Vulkan驱动版本太老。加-vulkan参数强制走Vulkan可以确认是不是RHI选择的问题。
6.3 上线前的自检清单
出包之后我会按这个清单过一遍,几分钟能省掉一轮沟通成本:
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| 可执行位 | ls -l *.sh MyProject/Binaries/Linux/MyProject | 有x权限 |
| 行尾格式 | file *.sh | 显示ASCII text |
| 依赖完整性 | ldd MyProject/Binaries/Linux/MyProject | grep "not found" | 无输出 |
| 启动冒烟 | ./MyProject.sh -log -nullrhi | 进程起来且日志正常 |
| 关卡加载 | 走一遍主要关卡 | 无Failed to load file |
| 存档读写 | 新建、读取、删除存档 | 均成功 |
| 分辨率与全屏 | 切换窗口模式和全屏 | 无异常 |
-nullrhi参数很实用,它跳过渲染初始化,用来验证"非图形部分是否正常"特别快。如果-nullrhi能起来、正常模式黑屏,那问题一定在渲染侧;如果-nullrhi都起不来,那是资源或运行库的问题,跟显卡无关。这个二分法能砍掉一半排查时间。
7. 接进流水线:参数化、增量与产物归档
7.1 把命令行收敛成一个可复用脚本
命令行参数一多就没人记得住,也没人敢改。我的做法是写一个.bat把变化的部分抽成变量:
@echo off set UE_ROOT=D:\UE\Engine set PROJECT=D:\Work\MyProject\MyProject.uproject set OUT=D:\Builds\MyProject\%BUILD_NUMBER% set TOOLCHAIN=C:\UnrealToolchains\v22_clang-16.0.6-centos7 set LINUX_MULTIARCH_ROOT=%TOOLCHAIN% set PATH=%TOOLCHAIN%\x86_64-unknown-linux-gnu\bin;%PATH% call "%UE_ROOT%\Build\BatchFiles\RunUAT.bat" BuildCookRun ^ -project="%PROJECT%" ^ -noP4 -platform=Linux -clientconfig=Shipping ^ -cook -build -stage -pak -compressed ^ -unattended -nop4 -utf8output -nocompileeditor ^ -archive -archivedirectory="%OUT%" if errorlevel 1 exit /b 1注意脚本里显式设了LINUX_MULTIARCH_ROOT和PATH。CI的agent进程环境变量经常和新开的交互式终端不一样,靠系统级环境变量有时候读不到,脚本里写死最稳。
7.2 增量Cook与DDC的配合
全量cook一次几十分钟,一天出五次包就是好几个小时。能优化的点有三个。
第一个是-iterate,它让cooker复用上一次的cook结果。适合"只改了几个资源"的场景。但要注意:改了引擎版本、改了shader格式配置、改了插件清单,都不能用-iterate,一定要全量。我有一次改了TargetedRHIs之后用增量,结果部分资源还是旧的shader,跑起来画面闪烁,查了大半天。
第二个是DDC共享。多人协作时把UE-SharedDataCachePath指向一个网络盘,能省掉每个人的重复烘焙。但如果构建机的网络IO比本地机械盘还慢,反而更差,这个必须实测。
第三个是只出变更平台。如果这次只改了一个默认值而两端都要出包,可以考虑先出Linux验证完再出Windows,不要无脑全平台全量。
7.3 产物命名与版本追溯
归档目录的命名带上构建号、平台、配置,一眼能看出是什么:
D:\Builds\MyProject\20240612-137\Linux\Shipping\同时在产物里放一个buildinfo.txt,记录引擎commit、项目commit、工具链版本、打包时间、打包机名。看起来是小事,但线上出问题要回溯"这个包到底是用哪版代码打的"时,没有这个文件你就得靠翻CI日志,翻不到就只能重打一个包去对齐。
这里有个细节:工具链版本一定要记。因为工具链升级会改变生成的二进制,同样的源码用不同工具链编出来的包,行为可能不一样。我之前查一个只在特定客户端上复现的崩溃,最后发现是两个包用了不同版本的工具链,一个带断言一个不带,白白多花了两天。
8. 我自己踩过之后总结的几条经验
工具链版本这件事,我的做法是把它和引擎版本绑定管理,在项目根目录放一个toolchain.txt写明当前用的包名,CI启动时先比对,不一致直接失败。这个检查只花两行脚本,但能挡住"某台构建机的环境变量被别人改过"这类最难查的问题。
第二件事是关于第一版验证顺序。我现在打Linux包的第一版一定是Development配置加-nopak,产物是一堆散文件,方便直接ls看目录结构、检查权限和大小写。等这个版本在目标机跑通了,再切Shipping加-pak。反过来做的话,一出问题你就得先解包再看,效率差很多。
第三件事,验证机不要只准备一台。至少准备一台较新的发行版和一台较老的发行版,因为glibc和Vulkan驱动这两个变量在真实用户环境里分布很广。交叉编译的最大风险就是"我只在自己这台机器上测过",一台验证机给不了你任何信心。
最后提一个容易被忽略的小技巧:./MyProject.sh脚本里能加自定义参数,比如指定日志目录、指定配置文件路径。如果你的部署脚本需要传环境相关的参数,改这个sh比改代码里读环境变量要灵活得多,而且它不进二进制,改完直接生效,不用重新打包。