BuildGraph
BuildGraph
BuildGraph 是 UE 提供的、以 XML 为脚本来自定义版本制作流程的工具。编译、拷贝、压缩这类功能都封装成了标签供脚本调用,搭配分布式构建机和共享存储,可以把整条出包流水线写成一份脚本跑起来。
几个特点:
- XML 语法,语法本身不复杂,难点在 UE 这套标签和属性的语义。
- 面向分布式构建系统设计:多台构建机协作,中间产物通过共享存储位置上传 / 下载。
- 官方文档:How to use BuildGraph、Accelerating your Unreal Engine builds with BuildGraph。
基本用法
入口命令格式:
RunUAT.bat BuildGraph -Script=<XMLPath> <Args>
一份最简单的脚本:
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.epicgames.com/BuildGraph ../Schema.xsd" >
<Agent Name="Default Agent" Type="Test">
<Node Name="Log Files 1">
<Log Message="Embedding source file information into PDB files..." />
<Log Message="Archive binaries:" Files="D:\Ue4.27"/>
</Node>
<Node Name="Log Files 2">
<Log Message="Embedding source file information into PDB files..." />
<Log Message="Archive binaries:" Files="D:\Ue4.27\*.*"/>
</Node>
</Agent>
</BuildGraph>
常用调用方式:
:: 相对路径,列出脚本里所有 target 不执行
RunUAT.bat BuildGraph -Script=Engine/Build/Graph/Examples/AllExamples.xml -ListOnly
:: 绝对路径
RunUAT.bat BuildGraph -Script=D:/test/test.xml -ListOnly
:: 运行一个 agent,里面的 node 会顺序执行
RunUAT.bat BuildGraph -Script=D:/test/test.xml -Target="Default Agent"
:: 运行单个 node
RunUAT.bat BuildGraph -Script=D:/test/test.xml -Target="Log Files 2"
-Target 指向某个 node 时,如果该阶段被标了依赖(required),BuildGraph 会把前置阶段也一并跑起来,保证这个阶段能正常执行。例如要跑 "Submit To Perforce for UGS" 阶段:
RunUAT.bat BuildGraph -Script=Engine/Build/Graph/Examples/BuildEditorAndTools.xml -Target="Submit To Perforce for UGS" -set:EditorTarget=MyProjectEditor -set:ArchiveStream=//depot/MyProject/dev-binaries -p4 -submit
核心概念
Graph 由这几类元素组成:
- Task:构建流程里的一个动作(编译、Cook 等)。
- Node:一组有序执行的 task,有命名,有输入输出。Node 可以依赖其它 node 先执行。
- Agent:一组在同一台机器上执行的 node(分布式构建时生效;本地构建时 Agent 不起作用,但必须写)。
- Trigger:需要人工介入后才执行的组的容器。
- Aggregate:把多个 node 和命名输出聚成一个名字,方便整体引用。
脚本语法
变量
所有 property 的值都是字符串——哪怕是数组和布尔值:
"String"
"123"
"true"
"false"
"A;B;C"
根元素 BuildGraph(必填)
命名空间是 http://www.epicgames.com/BuildGraph:
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.epicgames.com/BuildGraph ../Schema.xsd">
<!-- 子元素 -->
</BuildGraph>
可用子元素:Include、Option、EnvVar、Property、Regex、Macro、Agent、Trigger、Aggregate、Report、Badge、Label、Notify、Trace、Warning、Error、Expand、Do、Switch、ForEach。
Option
<Option> 定义可以从命令行设置的用户选项,只能出现在全局作用域:
<Option Name="EditorTarget" Restrict="[^ ]+" DefaultValue="UE4Editor" Description="Name of the editor target to be built"/>
<Option Name="UProjectPath" Restrict="[^ ]*" DefaultValue="" Description="Optional Uproject path to include in the arguments"/>
<Option Name="GameTargets" Restrict="[^ ]*" DefaultValue="" Description="Names of game targets to build separated by semicolons, eg. UE4Client;UE4Server"/>
<Option Name="TargetPlatforms" Restrict="[^ ]*" DefaultValue="Win64" Description="List of the target platforms to build for, separated by semicolons, eg. Win64;Win32;Android"/>
<Option Name="OutputDir" DefaultValue="$(RootDir)\LocalBuilds\$(EditorTarget)Binaries" Description="Output directory for compiled binaries"/>
<Option Name="Licensee" Restrict="true|false" DefaultValue="true" Description="Whether to treat the changelist number as being from a licensee Perforce server"/>
<Option Name="Versioned" Restrict="true|false" DefaultValue="$(IsBuildMachine)" Description="Whether to embed changelist number into binaries"/>
<Option Name="ArchiveStream" Restrict="(?://.*)?" DefaultValue="" Description="Stream path that will contain zip file of the compiled binaries, for use by UnrealGameSync"/>
<Option Name="ForceSubmit" Restrict="true|false" DefaultValue="false" Description="Forces the submit to happen even if another change has been submitted"/>
<Option Name="SymbolStorePath" DefaultValue="" Description="Output directory for symbol storage"/>
命令行用 -set:OptionName=Value 覆盖默认值。
Agent(必填)
<Agent> 定义一组顺序执行的 node 对 agent 的要求(执行期间不清理中间目录)。本地构建时这些要求被忽略,但仍必须写出来:
<Agent Name="XXX" Type="Test">
</Agent>
可用子元素:Property、Regex、EnvVar、Node、Trace、Label、Warning、Error、Expand、Do、Switch、ForEach。
Node
<Node> 是 BuildGraph 里最小的执行单元,有一组输入输出,内部是一串按序执行的 task:
<Agent Name="XXX" Type="Test">
<Node Name="Compile Tools Win64" Requires="Update Version Files" Produces="#ToolBinaries">
</Node>
<Node Name="Tag Output Files" Requires="#ToolBinaries;#EditorBinaries;$(GameBinaries)" Produces="#OutputFiles">
<Tag Files="#ToolBinaries;#EditorBinaries;$(GameBinaries)" Except=".../Intermediate/..." With="#OutputFiles"/>
</Node>
</Agent>
两个关键属性:
Requires="":本 node 执行需要的、由其它 node 产出的 node / aggregate / 标记文件集,分号分隔。Produces="":本 node 提供给其它 node 的标记文件集,分号分隔。
Node 内可用的预置 task 很多:AgeStore、Command、Commandlet、Compile、Cook、Copy、CsCompile、Delete、Log、ModifyConfig、ModifyJsonValue、Move、MsBuild、Notarize、PakFile、Rename、SanitizeReceipt、SetVersion、Sign、Spawn、Stage、Strip、Submit、SymStore、Tag、TagReceipt、Unzip、Wait、WriteTextFile、Zip,以及主机平台相关的 PGO / 内存调整 task(按平台用,这里不展开)。
Property
<Property> 设置属性值。同名属性在外层已声明过则覆盖,否则在当前作用域新建:
<!-- 设置 / 覆盖 -->
<Property Name="SubmissionDependencies" Value="#OutputFiles" />
<!-- 在原值上追加 -->
<Property Name="SubmissionDependencies" Value="$(SubmissionDependencies); Store Symbols" />
<!-- 带条件 -->
<Property Name="SubmissionDependencies" Value="$(SubmissionDependencies); Store Symbols" If="'$(SymbolStorePath)' != ''" />
ForEach
<Agent Name="XXX" Type="Test">
<Node Name="Print Vars">
<ForEach Name="TargetPlatform" Values="Win64;NX">
<Log Message="Writing symbols to $(TargetPlatform)"/>
</ForEach>
</Node>
<ForEach Name="TargetPlatform" Values="Win64;NX">
<Node Name="Print Vars $(TargetPlatform)"> <!-- 循环里包 Node -->
<Log Message="Writing symbols to $(TargetPlatform)"/>
</Node>
</ForEach>
<ForEach Name="TargetPlatform" Values="Win64+NX" Separator="+"> <!-- 自定义分隔符 -->
<Node Name="Print Vars $(TargetPlatform)">
<Log Message="Writing symbols to $(TargetPlatform)"/>
</Node>
</ForEach>
</Agent>
条件(Conditions)
条件由 atom 和 operator 组成,求值为 true / false:
- atom 可以是数字、字符串或标识符,会按使用它的 operator 强制转成对应类型。
- atom 可以用单引号、双引号包裹,也可以不包。
- 所有 atom 视为同一类型,不管怎么声明。
- 比较时大小写不敏感,
"True" == "true"。
注意 XML 里 < 和 > 必须转义成 < 和 >。
Filespec
...匹配任意内容,包括路径分隔符*匹配除路径分隔符外的任意内容(非递归)-排除匹配该 filespec 的文件;分号分隔多个条目
例:/Main/Engine/.../*.txt;-/Main/..../test.cs
完整 XML 示例
下面这组示例覆盖编译、聚合、带条件 Cook、拷贝 / 压缩 / 解压 / 删除等常见场景。工程名统一用 MyProject,P4 / NAS 路径都换成了占位形式。
编译工程(Compile Project)
<?xml version='1.0' ?>
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.epicgames.com/BuildGraph ../Schema.xsd" >
<Option Name="ProjectName" Restrict=".*" DefaultValue="MyProject" Description="Where the .uproject is" />
<Property Name="UProject" Value="$(ProjectName).uproject"/>
<Property Name="UProjectPathRelative" Value="$(RootDir)\Game\$(UProject)" />
<Property Name="EditorTarget" Value="$(ProjectName)Editor" />
<Property Name="EditorExe" Value="Engine\Binaries\Win64\UE4Editor-Cmd.exe" />
<Agent Name="Compile Editor" Type="CompileEditor">
<!-- 编译工具可执行文件 -->
<Node Name="Compile Tool executables">
<Compile Target="UnrealHeaderTool" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
<Compile Target="ShaderCompileWorker" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
<Compile Target="UnrealLightmass" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
<Compile Target="UnrealPak" Configuration="Development" Platform="Win64" Tag="#ToolBinaries"/>
<!--
XGEControlWorker.exe 是 ShaderCompileWorker.exe 的拷贝,在 post-build 步骤里生成,
见 \Engine\Source\Programs\ShaderCompileWorker\ShaderCompileWorker.Target.cs。
用 Incredibuild 编译 shader 时需要它。
-->
<Tag Files="$(RootDir)\Engine\Binaries\Win64\XGEControlWorker.exe" With="#ToolBinaries"/>
<Log Message="The following files are part of UnrealHeaderTool:" Files="#ToolBinaries"/>
</Node>
<!-- 编译 Windows 编辑器(后续 Cook 需要),强制工具先编 -->
<Node Name="Compile $(EditorTarget) Win64" Produces="#EditorBinaries" Requires="#ToolBinaries">
<Compile Target="$(EditorTarget)" Platform="Win64" Configuration="Development" Tag="#EditorBinaries" Arguments="-Project=$(UProjectPathRelative)"/>
</Node>
</Agent>
Aggregate
脚本里有很多任务、任务间没有依赖关系、但又都要执行时,可以用 Aggregate 聚到一起:
<Agent Name="Compile Editor" Type="CompileEditor">
<Node Name="UnrealHeaderTool">
<Compile Target="UnrealHeaderTool" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
</Node>
<Node Name="ShaderCompileWorker">
<Compile Target="ShaderCompileWorker" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
</Node>
<Node Name="UnrealLightmass">
<Compile Target="UnrealLightmass" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/>
</Node>
<Node Name="UnrealPak">
<Compile Target="UnrealPak" Configuration="Development" Platform="Win64" Tag="#ToolBinaries"/>
<Tag Files="$(RootDir)\Engine\Binaries\Win64\XGEControlWorker.exe" With="#ToolBinaries"/>
</Node>
<Node Name="Compile Win64" Produces="#EditorBinaries">
<Compile Target="$(EditorTarget)" Platform="Win64" Configuration="Development" Tag="#EditorBinaries" Arguments="-Project=$(UProjectPathRelative)"/>
</Node>
</Agent>
<Aggregate Name="BuildEditor" Requires="UnrealHeaderTool;ShaderCompileWorker;UnrealLightmass;UnrealPak;Compile Win64"/>
带条件 Cook(If...Do...)
<Option Name="TargetPlatform" DefaultValue="Win64" Description="Platforms we want to Build/Cook/Stage." />
<Option Name="SkipCook" Restrict="" DefaultValue="false" Description="If we want to skip the cooking" />
<Agent Name="Cook Game" Type="UEWindowsRunner">
<!-- 针对 Game target(非 Client)Cook -->
<Node Name="Cook Game $(TargetPlatform)" Requires="$(ToolBinaries)" Produces="#GameCookedContentFiles">
<Do If="!$(SkipCook)">
<!-- 主机平台(PS5/XSX/Switch)按需在这里加判断开启,具体平台名按项目实际填 -->
<Property Name="CookPlatform" Value="$(TargetPlatform)"
If="(('$(TargetPlatform)' == 'Win64'))" />
<!-- Name 就是 -run / -iterate 的值 -->
<Commandlet Name="cook" Project="$(UProjectPathRelative)" EditorExe="$(EditorExe)" Arguments="-targetplatform=$(CookPlatform) -Compressed" />
<!-- 另一种 Cook 写法 -->
<!-- Cook Project="Game/$(ProjectName).uproject" Platform="$(CookPlatform)" Arguments="-Compressed -cook -SkipCook" Tag="#GameCookedContentFiles" /-->
<Tag BaseDir="$(RootDir)\Game\Saved\Cooked\$(TargetPlatform)" Files="..." With="#GameCookedContentFiles" />
</Do>
</Node>
</Agent>
拷贝文件(Copy)
<Agent Name="Cook Game" Type="UEWindowsRunner">
<Node Name="Archive Content Files" Requires="Cook Game $(TargetPlatform);#GameCookedContentFiles">
<Do If="!$(SkipCook)">
<Property Name="CookedContentDir" Value="$(RootDir)\Game\Saved\Cooked\$(TargetPlatform)"/>
<Property Name="ArchiveCookedDirDir" Value="\\BuildShareHost\MyProjectBuilds\CookedContent"/>
<Copy Files="#GameCookedContentFiles" From="$(RootDir)\Game\Saved\Cooked\$(TargetPlatform)" To="$(ArchiveCookedDirDir)\$(Change)_$(TargetPlatform)"/>
</Do>
</Node>
</Agent>
压缩文件(Zip)
<Agent Name="Archive Content Files" Type="UEWindowsRunner">
<Node Name="Archive Content Files" Requires="Cook Game $(TargetPlatform);#GameCookedContentFiles">
<Do If="!$(SkipCook)">
<Property Name="CookedContentDir" Value="$(RootDir)\Game\Saved\Cooked\$(TargetPlatform)"/>
<Property Name="ArchiveFile" Value="\\BuildShareHost\MyProjectBuilds\CookedContent\$(Change)_$(TargetPlatform)_CookeContent.zip"/>
<Zip FromDir="$(CookedContentDir)" ZipFile="$(ArchiveFile)"/>
</Do>
</Node>
</Agent>
解压文件(Unzip)+ 调 BuildCookRun 出包
<Agent Name="Game Package" Type="UEWindowsRunner">
<Node Name="Build Game Package" Requires="#GameCookedContentFiles;#ToolBinaries">
<Property Name="BuildParameters" Value="-compile -build -Package -Stage -SkipCook -utf8output -compressed -noP4 -pak -unattended -stdlog" />
<Property Name="GameCommand" Value="-TargetPlatform=$(TargetPlatform) -ScriptsForProject=$(UProjectPathRelative) -project=$(UProjectPathRelative) -clientconfig=$(GameConfigurations) $(BuildParameters)"/>
<Log Message="BuildCookRun with arguments: $(GameCommand)"/>
<Command Name="BuildCookRun" Arguments="$(GameCommand)"/>
</Node>
</Agent>
</BuildGraph>
删除文件(Delete)
<Agent Name="Delete cache Files" Type="UEWindowsRunner">
<Node Name="Delete package Files">
<!-- 出新包前先删旧 package 文件 -->
<Delete Files="$(RootDir)/Game/Saved/Packages/.../*.*; $(RootDir)/Game/Saved/StageBuilds/.../*.*"/>
</Node>
</Agent>
常用预置 task 速查
| Task | 作用 | 示例 |
|---|---|---|
| AgeStore | 按修改时间删目录里的文件 | — |
| Command | 起一个 UAT 子进程跑指定命令 | — |
| Commandlet | 起编辑器跑一个 commandlet | — |
| Compile | 用 UBT 编译一个 target | <Compile Target="UnrealHeaderTool" Platform="Win64" Configuration="Development" Tag="#ToolBinaries"/> |
| Cook | 为某平台 Cook 一批地图 | — |
| Copy | 目录间拷文件 | — |
| CsCompile | 编译 C# 工程及其依赖 | <CsCompile Project="#UAT Projects" Configuration="Development" Platform="AnyCPU" Properties="EngineDir=$(RootDir)/Engine" Tag="#ArchiveFiles" EnumerateOnly="true"/> |
| Delete | 删一组文件 | <Delete Files="$(ArchiveDir)\..."/> |
| Log | 打印日志 | — |
| ModifyConfig | 改 config 文件 | — |
| Move | 目录间移文件 | — |
| MsBuild | 执行 MsBuild | — |
| PakFile | 从一组文件造 pak | — |
| Rename | 重命名文件 | — |
| SanitizeReceipt | 读 *.target 标记 build product / runtime 依赖 | — |
| SetVersion | 更新本地版本文件(Version.h / Build.version / Metadata.cs) | <SetVersion Change="$(Change)" Branch="$(EscapedBranch)" Licensee="$(Licensee)" If="$(Versioned)"/> |
| Sign | 用已安装证书给可执行文件签名 | — |
| Spawn | 起外部可执行文件并等其完成 | — |
| Stage | 按 receipt 把文件 stage 到输出目录 | — |
| Strip | 剥离 debug 信息 | — |
| Submit | 新建 changelist 并提交一组文件到 P4 stream | <Submit Description="[CL $(CodeChange)] Updated binaries" Files="$(ArchiveFile)" FileType="binary+FS32" Workspace="$(COMPUTERNAME)_ArchiveForUGS" Stream="$(ArchiveStream)" RootDir="$(ArchivePerforceDir)" Force="$(ForceSubmit)" If="'$(ArchiveStream)' != ''"/> |
| SymStore | 把符号存入符号库 | <SymStore Platform="Win64" Files="#OutputFiles" StoreDir="$(SymbolStorePath)" Product="UGSEditor" BuildVersion="$(CodeChange)"/> |
| Tag | 给一组文件打标记 | <Tag Files="Engine/Source/Programs/AutomationTool/..." Filter="*.csproj" With="#UAT Projects"/> |
| Unzip | 解压 zip | — |
| WriteTextFile | 写文本文件 | — |
| Zip | 压缩成 zip | <Zip FromDir="$(ArchiveStagingDir)" ZipFile="$(ArchiveFile)"/> |
分布式 BuildGraph
要把 BuildGraph 跑成分布式,几个前提:
- 至少 2 台构建机(系统不限)。
- 一个共享存储位置,要求大容量 + 高 I/O。
- 别用 AWS / Google Cloud 之类的云机器当构建机:中间目录得留在 workspace 上才快,否则要重编;磁盘 I/O 直接影响 C++ 编译速度;而且贵。
把脚本里某个 node 导出成 json,或上传产物到共享存储:
:: 把某个 node 导出成 json
RunUAT.bat BuildGraph -Script=D:/test/test.xml -Target="Log Files 2" -Export="D:\build.json"
:: 导出全部 node
RunUAT.bat BuildGraph -Script=D:/test/test.xml -SingleNode="Log Files 2" -Export="D:\build.json"
:: 上传产物到共享存储
:: 只跑单 node,若它依赖别的 node,会尝试从共享存储拉那些 node 的构建结果
RunUAT.bat BuildGraph -Script=D:/test/test.xml -SingleNode="Log Files 2" -SharedStorageDir=\\BuildShareHost\UEBuildShared -WriteToSharedStorage
:: 会把依赖的其它 node 也跑一遍,确保 target 能产出
RunUAT.bat BuildGraph -Script=D:/test/test.xml -Target="Log Files 2" -SharedStorageDir=\\BuildShareHost\UEBuildShared -WriteToSharedStorage
BuildEditorAndTools.xml:编译编辑器与工具二进制
引擎自带的 Engine/Build/Graph/Examples/BuildEditorAndTools.xml 演示了如何编译美术日常使用的编辑器 + 工具二进制、拷到 staging 目录分发,以及可选地提交到 Perforce。
命令行带 -buildmachine 时,build changelist 会写进引擎版本文件(Version.h、Build.version、Metadata.cs),让资产能正确版本化——防止用旧版编辑器打开新资产时丢数据。
三种典型用法:
:: 拷到 staging 目录
RunUAT.bat BuildGraph -Script=Engine/Build/Graph/Examples/BuildEditorAndTools.xml -Target="Copy To Staging Directory" -P4
:: 提交到 Perforce
RunUAT.bat BuildGraph -Script=Engine/Build/Graph/Examples/BuildEditorAndTools.xml -Target="Submit To Perforce" -P4 -Submit
:: 提交一份含二进制的 zip 给 UnrealGameSync 用
RunUAT.bat BuildGraph -Script=Engine/Build/Graph/Examples/BuildEditorAndTools.xml -Target="Submit To Perforce For UGS" -P4 -Submit
可配置的 option:
| Option | 说明 |
|---|---|
-set:EditorTarget=<TargetName> | 要编译的编辑器 target(默认 UE4Editor) |
-set:UProjectPath=<ProjectPath> | 传给编译参数的 -Project uproject 路径(默认无) |
-set:GameTargets=<TargetName> | 要编译的 game target(默认无) |
-set:TargetPlatforms=<A>;<B>;<C>... | 要编译的目标平台(默认 Win64) |
-set:OutputDir=<Path> | 编译产物输出目录(默认 $(RootDir)\LocalBuilds\$(EditorTarget)Binaries) |
-set:Licensee=true/false | 是否把 changelist 号标记为来自 licensee P4 服务器(避免和 Epic CL 号冲突) |
-set:Versioned=true/false | 是否把版本信息嵌进二进制(构建机默认 true) |
-set:ArchiveStream=<Path> | 存放二进制 zip 的 stream,供 UnrealGameSync 用 |
-set:ForceSubmit=true/false | 即使别人已提交也强制提交(以本地文件为准解决冲突) |
Copy To Staging Directory 阶段:编辑器和工具编译后的 dll、pdb 等文件,默认放在 LocalBuilds\$(EditorTarget)Binaries 目录下。