UE5:基于Zen Server的Unreal Engine构建产物发布机制分析

基于Zen Server的Unreal Engine构建产物发布机制分析

摘要

本文试图理解nreal Engine自动化构建系统(BuildGraph)中与Zen Server集成的构建产物发布机制。通过对BuildGraph脚本的XML定义及其底层C#实现(ZenExportSnapshotTask)的分析,了解该机制的核心工作流程、技术优势,并明确了其在数据持久化方面的具体行为。

1. 引言

本文聚焦于BuildGraph系统中名为ZenExportSnapshot的节点,对其工作原理进行详细解析。

2. Zen Server发布机制的核心组件

该发布机制主要由两部分协同工作:BuildGraph中的XML声明式节点,以及由该节点调用的后端C#任务实现。

2.1 BuildGraph XML声明

BuildGraph脚本(如 BuildAndTestProject.xml)中,通过<ZenExportSnapshot>节点来调用发布功能。其关键参数配置如下:

1
2
3
4
5
6
7
8
<ZenExportSnapshot 
Project="$(TargetProject)"
Platform="$(CookPlatform)"
DestinationStorageType="Zen"
DestinationZenHost="$(UE-ZenPublishHost)"
DestinationIdentifier="$(SnapshotIdentifier)"
SnapshotDescriptorFile="$(SnapshotLocalDir)/$(StagedPlatformFolder)/$(SnapshotFilenamePrefix)-zen.json"
/>
  • -Set:PublishZenSnapshot=true: 命令行参数,作为激活该功能的主要开关。
  • UE-ZenPublishHost: 环境变量,用于指定目标Zen Server的地址。
  • <ZenExportSnapshot>节点属性:
    • Project, Platform: 定义了发布内容的基本信息。
    • DestinationStorageType="Zen": 明确指定存储后端为Zen Server。
    • DestinationZenHost: 引用UE-ZenPublishHost环境变量,作为数据上传的目标。
    • DestinationIdentifier: 为本次发布提供一个唯一的标识符,通常结合分支名和变更列表(Changelist)生成。
    • SnapshotDescriptorFile: 指定生成的快照描述符JSON文件的输出路径。

2.2 后端实现:ZenExportSnapshotTask

该XML节点在执行时会实例化并运行ZenExportSnapshotTask类。此类的核心职责是作为调度器,收集参数并调用外部命令行工具zen.exe来完成实际工作。

3. 工作流程深度解析 (结合源码)

ZenExportSnapshotTask的执行流程确保了构建数据的完整上传。

3.1 数据源定位

任务首先在本地的烘焙输出目录 (Saved/Cooked/<Platform>) 中查找 ue.projectstore 文件。该JSON文件是在Cook阶段由引擎生成的,记录了本次烘焙操作的数据在本地Zen Server实例中的存储信息,包括项目ID (ProjectId) 和操作日志ID (OplogId)。

源码佐证 (ZenExportSnapshotTask.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... 遍历每个平台 ...
FileReference projectStoreFile = FileReference.Combine(platformCookedDirectory, "ue.projectstore");
ProjectStoreData? parsedProjectStore = null;

// 尝试加载并解析 ue.projectstore 文件
if (TryLoadJson(projectStoreFile, out parsedProjectStore) && (parsedProjectStore != null) && (parsedProjectStore.ZenServer != null))
{
ExportSourceData newExportSource = new ExportSourceData();
// 从文件中提取本地Zen Server的连接信息和关键ID
newExportSource._isLocalHost = parsedProjectStore.ZenServer.IsLocalHost;
newExportSource._hostName = parsedProjectStore.ZenServer.HostName;
newExportSource._hostPort = parsedProjectStore.ZenServer.HostPort;
newExportSource._projectId = parsedProjectStore.ZenServer.ProjectId;
newExportSource._oplogId = parsedProjectStore.ZenServer.OplogId; // <-- 定位数据的关键句柄
newExportSource._targetPlatform = platform;
// ...
exportSources.Add(newExportSource);
}

此段代码清晰地展示了任务如何通过解析ue.projectstore文件来获取源数据的确切位置和标识。

3.2 构建命令行

任务基于ue.projectstore提供的信息和XML节点传入的参数,构建一个针对zen.exe的命令行调用。其核心命令为 oplog-export

源码佐证 (ZenExportSnapshotTask.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化核心命令
StringBuilder oplogExportCommandline = new StringBuilder();
oplogExportCommandline.Append("oplog-export --embedloosefiles");

// ...
case SnapshotStorageType.Zen:
// ...
// 添加目标Zen Server参数
oplogExportCommandline.AppendFormat(" --zen {0}", _parameters.DestinationZenHost);

// ... 遍历每个数据源 ...
// 定义源Zen Server地址
string hostUrlArg = string.Format("--hosturl http://{0}:{1}", exportSource._isLocalHost ? "localhost" : exportSource._hostName, exportSource._hostPort);

// ...
// 组装完整的命令行字符串
exportSingleSourceCommandline.AppendFormat(" {0} --target-project {1} --target-oplog {2} {3} {4}",
hostUrlArg, // --hosturl (源)
projectName, // --target-project (目标项目)
destinationOplog, // --target-oplog (目标Oplog ID)
exportSource._projectId,// (源项目ID)
exportSource._oplogId); // (源Oplog ID)

这段代码展示了如何动态构建一个完整的 oplog-export 命令,它精确地定义了数据同步的源与目标。

3.3 执行数据同步

通过TryExportOplogCommand函数调用zen.exe执行上一步构建的命令。此过程涉及实际的网络传输。

源码佐证 (ZenExportSnapshotTask.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static bool TryExportOplogCommand(string app, string commandLine)
{
int attemptLimit = 2; // 包含重试机制,表明这是一个可能失败的网络操作
int attempt = 0;
while (attempt < attemptLimit)
{
// TryRunAndLogWithoutSpew 内部调用 CommandUtils.RunAndLog 执行外部进程
if (TryRunAndLogWithoutSpew(app, commandLine, false))
{
return true; // 命令成功执行
}
Logger.LogWarning("Attempt {AttemptNum} of exporting the oplog failed, {Action}...",
attempt + 1, attempt < (attemptLimit - 1) ? "retrying" : "abandoning");
attempt = attempt + 1;
}
return false; // 命令最终失败
}

// ... 在主流程中调用
if (_parameters.SkipExport || TryExportOplogCommand(zenExe.FullName, exportSingleSourceCommandline.ToString()))
{
successfullyExportedSources.Add(exportSource);
}

zen.exe oplog-export进程负责执行实际的数据传输。其内部利用Zen Server的CAS特性:

  • 数据块化 (Chunking): oplog-export首先获取源Oplog引用的所有数据块(Chunks)的哈希列表。
  • 差异比对 (Diffing): 它将这个哈希列表与目标Zen Server进行比对,识别出目标服务器上缺失的数据块。
  • 增量上传 (Incremental Upload): 仅将目标服务器上不存在的数据块从源服务器拉取并上传。构建的实际二进制数据在此阶段被可靠地传输

3.4 生成快照描述符 (Snapshot Descriptor)

仅在oplog-export命令成功执行后,任务才会生成JSON格式的快照描述符文件。

源码佐证 (ZenExportSnapshotTask.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在所有导出尝试完成后
if ((_parameters.SnapshotDescriptorFile != null) && successfullyExportedSources.Any())
{
// ...
// 创建一个JsonWriter来写入文件
using (JsonWriter writer = new JsonWriter(_parameters.SnapshotDescriptorFile))
{
writer.WriteObjectStart();
writer.WriteArrayStart("snapshots");

// 遍历所有成功导出的源,并为其写入描述符条目
foreach (ExportSourceData exportSource in successfullyExportedSources)
{
WriteExportSource(writer, destinationStorageType, exportSource, exportNames[exportIndex]);
exportIndex = exportIndex + 1;
}

writer.WriteArrayEnd();
writer.WriteObjectEnd();
}
}

此段代码的执行条件是successfullyExportedSources.Any(),即必须至少有一个数据源被成功导出,这再次确认了快照文件的生成是在数据同步之后的步骤。

4. 结论

BuildGraph中的ZenExportSnapshot功能并非一个简单的元数据快照工具,而是一个完整的、基于CAS模型的数据发布与同步机制。它的执行流程保证了构建产物的二进制数据被增量、高效地上传至目标Zen Server。生成的快照描述符文件可以被视为数据成功入库的凭证或“提货单”。

该机制的核心优势在于:

  • 存储效率: 通过CAS模型,极大地减少了存储相似构建版本时的空间冗余。
  • 网络效率: 增量上传的特性显著降低了CI/CD流程中的网络带宽消耗。
  • 可靠性: 流程设计确保了只有在数据完全同步后才会生成可供下游使用的快照文件。

因此,在Unreal Engine的自动化流程中,Zen Server不仅可作为开发时的派生数据缓存(Derived Data Cache, DDC),更可作为存储和分发构建产物的可靠后端,是实现高效CI/CD流程的关键基础设施。


UE5:基于Zen Server的Unreal Engine构建产物发布机制分析
http://muchenhen.com/posts/10849/
作者
木尘痕
发布于
2025年9月9日
许可协议