UE4.27.2 构建系统 UnrealBuildTool 的调试分析
2025-05-20 22:58:09

模块以及目标类型

不再赘述

构建 UBT,创建 VS 工程

GenerateProjectFiles.bat 调用 Engine\Build\BatchFiles\GenerateProjectFiles.bat

该脚本前面的部分是准备编译环境,比如找到 MSBuild,然后调用 MSBuild 构建 UnrealBuildTool

UnrealBuildTool.csproj 是提前写好的,所以可以直接编译

然后就是调用 UnrealBuildTool 来构建 VS 工程。于是开始看 UnrealBuildTool 代码。

如何调试 UnrealBuildTool 生成项目

Engine\Build\BatchFiles\GenerateProjectFiles.bat 里面写得很清楚了,就是用这个命令行参数

1
-ProjectFiles

UnrealBuildTool 入口函数根据参数选择创建 ToolMode 调用 Execute 虚函数。不同的 mode 对应不同的构建行为

显然 GenerateProjectFilesMode 类对应构建 VS 工程的逻辑。

我想知道自己创建出来的模块是怎么让 UE 识别到的,于是还是要看看他是怎么生成项目文件的

生成项目时,自定义模块如何被识别

查看 GenerateProjectFilesMode 类的 Execute 实现

可以看到他对于不同平台有不同的项目生成器 Generator,最终调用 Generator.GenerateProjectFiles

点进来看 Generator.GenerateProjectFiles,前面的配置就不说了

然后是他寻找 game, target 和 module 的部分

1
2
3
4
5
6
7
8
9
10
11
12
// Build the list of games to generate projects for
List<FileReference> AllGameProjects = FindGameProjects();

// Find all of the target files. This will filter out any modules or targets that don't
// belong to platforms we're generating project files for.
List<FileReference> AllTargetFiles = DiscoverTargets(AllGameProjects);

...

// Find all of the module files. This will filter out any modules or targets that don't belong to platforms
// we're generating project files for.
List<FileReference> AllModuleFiles = DiscoverModules(AllGameProjects);

之后就是把找到的这些目标添加到工程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ProjectFile EngineProject;
ProjectFile EnterpriseProject;
List<ProjectFile> GameProjects;
List<ProjectFile> ModProjects;
Dictionary<FileReference, ProjectFile> ProgramProjects;
{
// Setup buildable projects for all targets
AddProjectsForAllTargets( PlatformProjectGenerators, AllGameProjects, AllTargetFiles, Arguments, out EngineProject, out EnterpriseProject, out GameProjects, out ProgramProjects );

// Add projects for mods
AddProjectsForMods(GameProjects, out ModProjects);

// Add all game projects and game config files
AddAllGameProjects(GameProjects, SupportedPlatformNames, RootFolder);

...
}

...

// Setup "stub" projects for all modules
AddProjectsForAllModules(AllGameProjects, ProgramProjects, ModProjects, AllModuleFiles, bGatherThirdPartySource);

如何调试 UnrealBuildTool 构建项目

已经配置好 VS 项目后,直接在 VS 工程中点击调试,在输出窗口中可以看到构建命令

比如我这里的构建命令是

1
<path to engine>\Engine\Build\BatchFiles\Build.bat -Target="<project name>Editor Win64 Development -Project=\"E:\Unreal Projects\<project name>\<project name>.uproject\"" -Target="ShaderCompileWorker Win64 Development -Quiet" -WaitMutex -FromMsBuild

去看一下 <path to engine>\Engine\Build\BatchFiles\Build.bat 就可以看到,他只是 UnrealBuildTool.exe 的简单包装

所以完全可以复制粘贴后面的命令行参数,以调试 UnrealBuildTool 项目

UnrealBuildTool 构建模式的逻辑

显然,BuildMode 类对应构建逻辑。

Execute 内主要功能是收集构建目标,进行多目标构建。每个目标可以是远端构建或本地构建。

然后它调用 Build 开始对具体目标开始构建。

具体到本地构建 public static void Build(List<TargetDescriptor> TargetDescriptors, BuildConfiguration BuildConfiguration, ISourceFileWorkingSet WorkingSet, BuildOptions Options, FileReference WriteOutdatedActionsFile, bool bSkipPreBuildTargets = false),每个不能被跳过构建的目标,都生成 makefile

再下一层 static void Build(TargetMakefile[] Makefiles, List<TargetDescriptor> TargetDescriptors, BuildConfiguration BuildConfiguration, ISourceFileWorkingSet WorkingSet, BuildOptions Options, FileReference WriteOutdatedActionsFile) 对确认要构建的,具有 makefile 的目标进行处理。

这里面,除了一些构建特性的支持,比如热重载,暂时跳过不看,剩下的主题是引入了一个 Action 的概念。每一项构建一个 ActionAction 之间有依赖关系,是一个图。ActionGraph.ExecuteActions 对这个图结构发起构建。

这个图结构发起构建的逻辑是,根据构建配置创建不同类型的 Executor,然后调用 Executor.ExecuteActionsAction 列表发起构建

在我的调试中,它创建的是 ParallelExecutor

Executor 类里面对输入的 Action 列表遍历,转为 BuildAction 列表

单纯从他创建的过程来看,BuildAction 相较于 Action 主要是多了个序号

然后根据 BuildAction.Inner.PrerequisiteItems 创建依赖关系

最终从 BuildAction 列表遍历,创建 BuildAction 队列

BuildAction 队列处理构建。这时可以根据依赖关系对 BuildAction 排序

排序之后就可以对每一个 BuildAction 创建构建线程

1
2
3
4
5
6
7
8
BuildAction Action = QueuedActions[QueuedActions.Count - 1];
QueuedActions.RemoveAt(QueuedActions.Count - 1);

Thread ExecutingThread = new Thread(() => { ExecuteAction(ProcessGroup, Action, CompletedActions, CompletedEvent); });
ExecutingThread.Name = String.Format("Build:{0}", Action.Inner.StatusDescription);
ExecutingThread.Start();

ExecutingActions.Add(Action, ExecutingThread);

之后就是怎么收集这些构建线程的事情了。

这些线程工作完了,UBT 就结束了。于是步进调试到 ParallelExecutor.ExecuteAction

其中的核心是

1
ManagedProcess Process = new ManagedProcess(ProcessGroup, Action.Inner.CommandPath.FullName, Action.Inner.CommandArguments, Action.Inner.WorkingDirectory.FullName, null, null, ProcessPriorityClass.BelowNormal)

可见,他也是最终调用一个可执行文件,然后传入各种文本作为构建参数

我调试时,Action.Inner.CommandPath.FullName

1
<path to engine>\Engine\Build\Windows\cl-filter\cl-filter.exe

这个 cl-filter 甚至不在 Engine 的 VS 项目和 Project 的 VS 项目目录里,酷

知道他是 cl 的包装,用来实现一些目的就够了

Action.Inner.CommandArguments

1
-dependencies="E:\Unreal Projects\<project name>\Intermediate\Build\Win64\UE4Editor\Development\<project name>\MyClass.cpp.txt" -compiler="E:\software\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\HostX64\x64\cl.exe" -- "E:\software\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\HostX64\x64\cl.exe"  @"E:\Unreal Projects\<project name>\Intermediate\Build\Win64\UE4Editor\Development\<project name>\MyClass.cpp.obj.response" /showIncludes

打开看这个 response 文件,可以看到他应该是为了传入编译选项、include 路径、输入输出目录

前面的部分的节选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/bigobj
/fp:fast
/Zp8
/we4456
/we4458
/we4459
/wd4463
/we4668
/wd4244
/wd4838
/I .
/I "E:\Unreal Projects\<project name>\Source"
/I Runtime
/I Runtime\TraceLog\Public
/I Runtime\Core\Public
/I ..\Intermediate\Build\Win64\UE4Editor\Inc\CoreUObject
/I Runtime\CoreUObject\Public
/I ..\Intermediate\Build\Win64\UE4Editor\Inc\Engine
/I Runtime\Engine\Classes
/I Runtime\Engine\Public
/I ..\Intermediate\Build\Win64\UE4Editor\Inc\NetCore
/I Runtime\Net

后面的部分的节选

1
2
3
4
5
6
7
8
9
10
11
12
13
/I "C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt"
/I "C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\shared"
/I "C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\um"
/I "C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\winrt"
/FI"<path to engine>\Engine\Intermediate\Build\Win64\UE4Editor\Development\Engine\SharedPCH.Engine.NonOptimized.ShadowErrors.h"
/Yu"<path to engine>\Engine\Intermediate\Build\Win64\UE4Editor\Development\Engine\SharedPCH.Engine.NonOptimized.ShadowErrors.h"
/Fp"<path to engine>\Engine\Intermediate\Build\Win64\UE4Editor\Development\Engine\SharedPCH.Engine.NonOptimized.ShadowErrors.h.pch"
"E:\Unreal Projects\<project name>\Source\<project name>\MyClass.cpp"
/FI"E:\Unreal Projects\<project name>\Intermediate\Build\Win64\UE4Editor\DebugGame\<project name>\Definitions.<project name>.h"
/Fo"E:\Unreal Projects\<project name>\Intermediate\Build\Win64\UE4Editor\DebugGame\<project name>\MyClass.cpp.obj"
/TP
/GR-
/W4

到此就差不多了。剩下的,比如 cl 干了什么、cl-filter 干了什么、response 文件是怎么生成的、读 response 之后怎么构建的,应该就太细节了,我没有去调试

虽然我没有调试,但是看了一些博客。cl-filter 这个包装,主要是为了解决,增量构建时,如何找到某次代码修改涉及到的所有的依赖项的问题。