第一次编译 Godot 的人都会被 SCons 劝退一次:scons platform=linuxbsd target=editor -j8 这条命令背后没有 CMakeLists.txt,没有 Makefile,只有一个 3000 行的 SConstruct 和散落各目录的 SCsub。但用熟了会发现这套系统对"裁剪引擎"这件事的支持比 CMake 好——scons disable_3d=yes module_text_server_fb_enabled=no 就能编出一个砍掉 3D 和高级字体渲染的 2D 专用引擎,体积从 100MB 降到 30MB。这篇讲 SCons 在 Godot 里的具体组织方式,以及怎么用它做定制构建。
为什么是 SCons:Python 脚本的灵活性
Godot 选 SCons 的核心原因是构建逻辑需要编程。CMake 的 DSL 适合"声明依赖关系",但 Godot 的构建要干很多"声明"之外的事:
- 动态生成代码:
methods.py里的generate_disabled_classes()根据disable_3d=yes生成disabled_classes.gen.h,这个生成逻辑是几百行 Python; - 平台特定的工具链探测:macOS 构建要自动找 Xcode 路径、检查 SDK 版本、决定是否用 MoltenVK——这些在 CMake 里要写一堆
execute_process,在 SCons 里就是 Python 的subprocess; - 模块系统的条件编译:每个
modules/xxx/目录自带一个SCsub,里面用 Python 逻辑决定"这个模块要不要编译进来"——modules/jolt/SCsub里会检查module_jolt_enabled和平台能力。
SCons 的缺点是增量构建慢(全量 Python 解释器启动 + 依赖图构建要几秒),以及 IDE 集成不如 CMake(CLion/VSCode 对 CMake 的支持是一等公民)。Godot 的妥协是提供了 compiledb 工具(scons compiledb=yes 生成 compile_commands.json),让 clangd 能接管代码导航。
构建脚本的分层:SConstruct → SCsub
Godot 的构建不是单文件,而是分层的:
SConstruct # 顶层:解析命令行选项、初始化环境、调用各层 SCsub
├── core/SCsub # core 层:编译 core/**/*.cpp 成 libcore.a
├── servers/SCsub # servers 层:依赖 core,编译成 libservers.a
├── scene/SCsub # scene 层:依赖 core+servers,编译成 libscene.a
├── editor/SCsub # editor 层:只在 target=editor 时编译
├── platform/linuxbsd/SCsub # 平台层:链接所有库 + 平台特有源文件成可执行文件
└── modules/xxx/SCsub # 每个模块一个,被主 SConstruct 动态发现
SConstruct 的核心逻辑(简化):
# SConstruct
env = Environment(tools=custom_tools) # 创建构建环境
opts = Variables(customs_files) # 命令行选项解析
opts.Add('platform', 'Target platform', '')
opts.Add('target', 'editor|template_debug|template_release', 'editor')
opts.Add('disable_3d', 'Disable 3D nodes and servers', False)
# ... 200 多个选项
opts.Update(env)
# 递归处理子目录
SConscript('core/SCsub', exports='env')
SConscript('servers/SCsub', exports='env')
if env['target'] == 'editor':
SConscript('editor/SCsub', exports='env')
每个 SCsub 拿到 env 对象(包含编译器路径、flags、选项值),往里面加自己的源文件和编译选项。exports='env' 是 SCons 的机制,让子脚本共享同一个构建环境。
库的构建顺序有讲究。 core 最先编译(无依赖),servers 其次(依赖 core),scene 再其次(依赖前两者),platform 最后链接。这个顺序是单向依赖的构建系统体现——scene 的 SCsub 里 env.Prepend(LIBS=['servers', 'core']),反向不行。
模块开关:编译期裁剪的实现
每个模块目录下的 SCsub 决定自己要不要参与编译:
# modules/jolt/SCsub
Import('env')
if not env['module_jolt_enabled']:
Return() # 模块被禁用,不编译任何文件
env.add_source_files(env.modules_sources, '*.cpp')
env.Append(CPPDEFINES=['GDJOLT_ENABLED']) # 定义宏,供代码里 #ifdef 用
配合 SConstruct 里的选项定义:
opts.Add(BoolVariable('module_jolt_enabled', 'Enable Jolt physics module', True))
命令行 scons module_jolt_enabled=no 就能关掉这个模块。但光不编译还不够——代码里可能有 #include "modules/jolt/jolt_physics_server.h",所以模块系统还有第二层:头文件的条件包含。
modules/register_module_types.gen.cpp 是构建时生成的,内容类似:
// 自动生成,不要手改
void register_module_types() {
#ifdef MODULE_JOLT_ENABLED
jolt_register_types();
#endif
#ifdef MODULE_TEXT_SERVER_FB_ENABLED
text_server_fb_register_types();
#endif
// ...
}
这个生成逻辑在 methods.py 的 generate_module_registration_header() 里——扫描所有启用的模块,生成对应的 #ifdef 块。禁用模块时,不仅不编译它的 .cpp,连注册调用都被预处理掉,彻底从二进制里消失。
平台特定的构建逻辑
platform/macos/SCsub 里的 macOS 特定逻辑(简化):
Import('env')
if env['arch'] == 'arm64':
env.Append(CPPDEFINES=['__ARM_NEON__']) # Apple Silicon 的 NEON 支持
env.Append(LIBS=['Cocoa', 'Metal', 'QuartzCore']) # 系统框架
if env['vulkan']:
env.Append(LIBS=['MoltenVK']) # Vulkan 通过 MoltenVK
env.Append(LINKFLAGS=['-rpath', '@executable_path/../Frameworks']) # .app bundle 的库路径
平台层的职责是把前面编译出的静态库(libcore.a、libservers.a、libscene.a、libeditor.a)加上平台特有的源文件(os_macos.mm、godot_main_macos.mm)链接成最终的可执行文件或 .app bundle。交叉编译(Linux 上编 Windows 版用 MinGW)也是在这里切换工具链。
常用定制命令实战
几个实际用过的定制构建:
最小 2D 引擎(30MB vs 默认 100MB):
scons platform=linuxbsd target=template_release \
disable_3d=yes \
module_text_server_fb_enabled=no module_text_server_adv_enabled=no \
module_mbedtls_enabled=no \
lto=full
disable_3d=yes 会触发 methods.py 生成 disabled_classes.gen.h,把所有 Node3D 派生类从 ClassDB 注册表里剔除——编辑器里看不到 3D 节点,运行时 ClassDB::instantiate("Node3D") 返回 nullptr。
调试构建带 AddressSanitizer:
scons platform=linuxbsd target=editor \
dev_mode=yes \
use_asan=yes \
debug_symbols=yes
dev_mode=yes 会加 -Werror 和更多警告,use_asan=yes 链 ASan 运行时。编译出的二进制启动慢 2 倍,但内存错误第一时间暴露。
无编辑器、无 GDScript 的纯运行时(服务器用):
scons platform=linuxbsd target=template_release \
tools=no \
module_gdscript_enabled=no \
optimize=size
tools=no 裁掉整个 editor/ 目录,module_gdscript_enabled=no 让引擎只支持 C# 或 GDExtension。配合 optimize=size(-Os)能编出 15MB 的无头服务器。
构建流程的依赖图
调试构建问题的入口
编译错误排查的常用命令:
scons --debug=explain:显示为什么某个文件要重新编译(依赖变了还是命令变了);scons -c或rm -rf .sconsign.dblite:清缓存,解决"改了选项但没生效";verbose=yes:显示完整编译命令,能看到实际的编译器 flags;compiledb=yes:生成compile_commands.json,之后能用clangd或clang-tidy分析单个文件。
最常见的坑是选项改了但没重新生成代码——比如改了 disable_3d,但 disabled_classes.gen.h 没重新生成。这是因为生成逻辑只在 SConstruct 第一次运行时执行,增量构建不触发。解决方法是 touch SConstruct 强制全量重新配置。
本文代码引自 Godot 4.2.2-stable 的 SConstruct、methods.py、core/SCsub、modules/jolt/SCsub,均有删减。