尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

UE5.6 + Xcode 26 集成gtest:绕开三个坑

发布时间:2026/9/29 18:00:59

资讯中心
01
ARTICLE

UE5.6 + Xcode 26 集成gtest:绕开三个坑

UE5.6 + Xcode 26 集成gtest:绕开三个坑
先说个结论免得你翻了半天不知道值不值得看这个报错最后拆开其实是三个不相干的坑叠在了一起。老版 gtest 在 Xcode 26 的 libc 里找不到tr1/tupleCMake 缓存把libgtest.a编成了 x86_64外加误把带main()的libgtest_main.a链接进了 UE 模块。如果你也在 UE5.6 Xcode26 上做插件开发想给插件的纯 C 逻辑补上 gtest 单测这篇记录应该能让你少折腾一整个下午。我的项目背景不复杂一个资源批处理插件负责导入检查、缩略图生成、命名规范校验这些事。逻辑层刻意不依赖 UE 的反射和 UObject方便在纯 C 环境下做单元测试。团队里已经有几套 gtest 用例了所以插件这边也顺手选了 gtest。但 UE5.6 在 macOS 上的默认工具链就是 Xcode 26编译器一更新老版本 gtest 的兼容问题马上就暴露出来了。下面按我实际踩坑的顺序写每一步的报错、排查、修复都能直接复现。1. 为什么偏偏 UE5.6 Xcode 26 gtest 这个组合容易翻车1.1 需求背景与集成方式选型我的插件模块分成两层引擎相关部分放在Editor模块里负责 UI 和编辑器扩展核心算法和工具函数放在独立的运行时模块里尽量不碰引擎数据类型。后者是我最想保护的部分测试用例多、迭代快必须有一套能快速运行的单元测试框架。gtest 是 C 社区里最稳妥的选择资料多、社区活跃、参数化测试和死亡测试都够用。问题从来不在 gtest 本身而在怎么把它接进 UE 的构建体系。1.2 为什么不直接用引擎自带的 GoogleTest 模块UE 引擎源码里确实带了 GoogleTest位置在Engine/Source/ThirdParty/GoogleTest。如果你装的是源码版引擎能直接看到头文件和一部分构建脚本。但我最终还是放弃了它原因有两个。第一引擎自带的 gtest 版本通常比较旧和团队的用例风格不一定匹配而且你没法轻易换版本——引擎目录是只读的改坏了影响面太大。第二引擎自带版本在模块化编译时和 UE 自己的宏、编译选项混在一起出问题时很难判断是 gtest 的问题还是引擎 ThirdParty 集成的问题。所以我选了最传统的做法从 GitHub 拉一份 googletest 源码放到插件自己的ThirdParty/GoogleTest目录下用 CMake 独立编译出静态库再通过Build.cs链接进插件。这样 gtest 版本的主动权完全在自己手里出了任何编译问题也能单独排查不会牵扯到整个 UE 工程的构建链。1.3 整体集成路线整个集成分四步下载 googletest 源码放到Plugins/MyPlugin/ThirdParty/GoogleTest/。用 CMake 在 macOS 上编出libgtest.a和libgmock.a拷到lib/Mac/目录。在插件的Build.cs里加上头文件路径和静态库路径。写一个独立的测试 runner提供入口在编辑器里触发RUN_ALL_TESTS()。看起来挺常规的。但真正动手的时候前三步每一步都给我甩了个编译报错而且报错信息一个比一个隐蔽。2. 第一道坎tr1/tuple file not found 与 libc 的断代2.1 报错现场编译插件模块时Xcode 的输出窗口里冒出来这样一段In file included from ThirdParty/GoogleTest/src/googletest/src/gtest-port.cc:38: ThirdParty/GoogleTest/src/googletest/include/gtest/internal/gtest-port.h:123:12: fatal error: tr1/tuple file not found第一反应当然是 include 路径配错了。我检查了PublicSystemIncludePaths确认gtest和gmock的头文件目录都在。再一看tr1/tuple这个头文件根本不是 gtest 项目自带的它应该是编译器工具链的系统头文件。也就是说问题出在编译器能搜到哪些头文件。2.2 排查不是路径问题是工具链里根本没有这个头我先用预处理器确认了一遍免得自己怀疑错方向。在终端里把 gtest 源码文件跑一遍看它到底有没有可能找到tr1/tupleclang -stdc17 -arch arm64 \ -isysroot $(xcrun --sdk macosx --show-sdk-path) \ -I ThirdParty/GoogleTest/include \ -dM -E ThirdParty/GoogleTest/src/googletest/src/gtest-port.cc | grep GTEST_HAS_TR1_TUPLE输出里能看到宏被默认开启#define GTEST_HAS_TR1_TUPLE 1然后我在 Xcode 26 的 SDK 目录里直接搜tr1find $(xcrun --sdk macosx --show-sdk-path) -path *c*tr1* 2/dev/null结果一条记录都没有。到这里才确认这不是项目路径问题是 Xcode 26 这套工具链里tr1头文件已经彻底从 libc 中消失了。2.3 根因老 gtest 对 GNU 兼容编译器的判断太粗糙老版本 gtest我用的 1.8.1在gtest-port.h里有一段比较古老的兼容逻辑。当年写这段代码时GCC 4.x 配套的是 libstdctr1/tuple是真实存在的。所以gtest-port.h只要看到编译器定义__GNUC__就默认std::tr1::tuple可用于是直接 includetr1/tuple。问题在于 Apple Clang 虽然也叫 clang但它为了兼容性同样会定义__GNUC__。Xcode 老早切换到了 libctr1目录在系统头文件里本来就名存实亡Xcode 26 的新 SDK 更是干净利落地移除了这一套。于是老 gtest 的判断逻辑就彻底失效了它以为存在于系统中的头文件实际上一行都不剩。2.4 两个修复方向我分别试了一遍临时方案是在编译 gtest 源码时手动把GTEST_HAS_TR1_TUPLE定为 0cmake -S . -B build \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_OSX_ARCHITECTURESarm64 \ -DCMAKE_CXX_FLAGS-DGTEST_HAS_TR1_TUPLE0 \ -Dgtest_build_testsOFF \ -Dgtest_build_samplesOFF这个宏一旦置 0老 gtest 就不会再去 includetr1/tuple而是走 C11 的tuple路径。我的插件本来就是 C17 工程完全不在乎这层兼容。但临时方案治标不治本。gtest 的旧源码里不止一处这种过时假设后面很可能还会冒出别的兼容问题。所以我最终选择了升级 googletest 到 1.14 版本新版源码已经完全删掉了 tr1 相关代码。这个决定在后面省了很多事。这里有个我实际踩到的小坑升级时只替换了include/gtest目录忘了替换include/gmock目录结果 gtest 新版头文件和 gmock 旧版头文件混在一起编译时直接报出类成员声明与实现不一致这种莫名其妙的错误。所以换版本务必整个仓库一起换别只换一半。3. 第二道坎CMake 缓存把 libgtest.a 编成了 x86_643.1 链接器无情的架构报错gtest 源码本身编译通过之后我把它接进 UE 的Build.cs里继续编译插件。这次链接阶段直接挂了ld: in /Users/xx/Plugins/MyPlugin/ThirdParty/GoogleTest/lib/Mac/libgtest.a, building for macOS-arm64 but attempting to link with file built for macOS-x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)看到这句话我第一反应是这台机器明明是 Apple Silicon终端uname -m输出也是arm64CMake 怎么可能编出 x86_64 的库3.2 用 lipo 和 nm 验证架构先用lipo直接看静态库的实际架构lipo -info ThirdParty/GoogleTest/lib/Mac/libgtest.a输出让我有点意外Non-fat file: libgtest.a is architecture: x86_64再用nm看看库里的符号是否正常nm libgtest.a | grep testing::UnitTest::GetInstance符号是有的但这段符号属于 x86_64 切片对 arm64 的 UE 可执行文件来说毫无意义。3.3 罪魁祸首埋藏在 CMakeCache.txt 里的历史架构设置排查 CMake 配置时我在build/CMakeCache.txt里看到一行CMAKE_OSX_ARCHITECTURES:STRINGx86_64这是非常典型的历史遗留。很早之前我在一台 Intel Mac 上用同一个源码目录配过 gtest后来把整个项目迁到了 Apple Silicon 机器上为了方便直接用命令行继续编译。但 CMake 的缓存机制会优先信任已有缓存里的架构设置不会每次都根据当前机器重新推断。于是 CMake 就照旧编出了 x86_64 的库。这种情况在 Windows 上很少发生因为 Windows 下架构切换没有 macOS 这么普遍在 macOS 上却很容易踩中尤其是多人协作项目里源码目录或者build目录被提交到 Git换机器后 CMake 缓存就成了隐形的定时炸弹。3.4 正确的做法显式指定架构并清掉旧缓存修复方式很简单但有一个关键动作先删掉build目录让 CMake 重新生成缓存。命令行里显式指定目标架构rm -rf build cmake -S . -B build \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_OSX_ARCHITECTURESarm64 \ -DCMAKE_OSX_DEPLOYMENT_TARGET14.0 \ -Dgtest_build_testsOFF \ -Dgtest_build_samplesOFF \ -Dgmock_build_testsOFF cmake --build build -j8重点就是两个参数CMAKE_OSX_ARCHITECTURES指定arm64和CMAKE_OSX_DEPLOYMENT_TARGET指定 macOS 最低版本。UE5.6 在 Apple Silicon 上跑的是 arm64 的 UnrealEditor插件最终链接进主程序时所有静态库的切片必须和主程序架构一致。这里的CMAKE_OSX_DEPLOYMENT_TARGET也建议和 UE5.6 的DefaultTargetSettings对齐省得以后出现 ABI 层面的诡异问题。如果你需要在 Intel 和 Apple Silicon 之间分发同一个 gtest 库也可以编两个切片的库再合并lipo -create libgtest_arm64.a libgtest_x86_64.a -output libgtest_universal.a但 UE 插件通常只需要本机架构我最终只保留了 arm64 版本。fat 库虽然通用却没来由地把二进制体积翻了一倍。4. 第三道坎静态库里的 main() 和你的 main() 撞了车4.1 又一个新的链接错误架构问题解决后gtest 库能编过了也进得了链接列表。但 UE 工程编译到最后一步链接器又抛了一个新错误ld: duplicate symbol _main in .../GTestRunner.cpp.o and .../ThirdParty/GoogleTest/lib/Mac/libgtest_main.a(gtest_main.cc.o)哦这个报错就非常直白了链接器发现_main符号被定义了两次。一次来自我自己的GTestRunner.cpp另一次来自libgtest_main.a里的gtest_main.cc。4.2 为什么静态库里会冒出一个 maingoogletest 的 CMake 默认会构建两个使用层级的库一个是libgtest.a只包含测试框架本身另一个是libgtest_main.a额外提供一个标准的main()函数方便你写最小测试工程。它的设计初衷是好的你只需要写测试用例链接gtest_main之后就能直接生成可执行文件。但 UE 有自己的入口和模块体系插件的定位是动态库不需要也无法拥有最终程序的main。我早期为了快速验证某个用例把libgtest_main.a也顺手加到了Build.cs的库列表里又保留了自己的 runner于是两个main在最终链接阶段正面撞上。确认一下库里的符号很简单nm libgtest_main.a | grep _main$输出0000000000000000 (__TEXT,__text) _main没跑里面确实住着一个main。4.3 修复不让 gtest 来抢 UE 的入口修复的方式取决于你想要哪种运行方式。如果你想完全让 gtest 独立跑一个测试可执行文件那libgtest_main.a是你最好的朋友这种情况完全不需要 UE 参与CMake 构建出来的测试 target 直接跑。但如果你想在 UE5.6 的编辑器环境里跑测试正确做法是链接libgtest.a和libgmock.a绝不链接libgtest_main.a同时由你自己提供测试入口。我最终在 CMake 配置里顺手关掉了 gtest_main 的构建从源头避免以后再手滑-Dgtest_build_mainOFF这样库里根本不会生成libgtest_main.a链接阶段也不会再有第二个_main出现。4.4 一个更容易踩的变体把 gtest 源码直接拖进 UE 模块还有一种做法是把 gtest 的.cc文件直接放进 UE 模块里编译而不是链接静态库。如果你只是想把 gtest 用在独立测试 target 上这样做的确方便但一旦放进了 UE 模块除了main()冲突还会遇到 UE 宏和编译选项对第三方源码的干扰排查起来比链接静态库复杂得多。我的建议很明确gtest 永远作为一个独立的第三方库编译好UE 模块里只保留include路径和链接路径。这样 gtest 的编译参数是独立的不会受 UE 的-Werror、异常设置、RTTI 开关这些选项影响遇到问题也容易单独复现。5. 跑通之后的完整工程结构与入口设计5.1 最终目录形态所有折腾结束后我的 ThirdParty 目录长这样Plugins/MyPlugin/ThirdParty/GoogleTest/ ├── include/ │ ├── gtest/ │ └── gmock/ ├── lib/Mac/ │ ├── libgtest.a │ └── libgmock.a └── src/ # googletest 源码只用于重新编译库不参与 UE 模块编译src目录纯粹留作备份和二次编译UE 模块构建时不会碰它。5.2 Build.cs 的关键片段UE 侧只需要把 include 和 library 加进去using System.IO; using UnrealBuildTool; public class MyPlugin : ModuleRules { public MyPlugin(ReadOnlyTargetRules Target) : base(Target) { PCHUsage ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine }); string GoogleTestRoot Path.Combine(ModuleDirectory, ../../ThirdParty/GoogleTest); PublicSystemIncludePaths.Add(Path.Combine(GoogleTestRoot, include)); if (Target.Platform UnrealTargetPlatform.Mac) { PublicAdditionalLibraries.Add(Path.Combine(GoogleTestRoot, lib/Mac/libgtest.a)); PublicAdditionalLibraries.Add(Path.Combine(GoogleTestRoot, lib/Mac/libgmock.a)); } } }注意PublicSystemIncludePaths和PublicIncludePaths在 UE5.6 里有细微区别。前者是给系统级第三方头文件用的比较推荐后者在老项目里也用得多但新增代码就别再用了。5.3 测试入口用控制台命令而不是 main既然不链接gtest_main就得自己提供一个触发入口。我把它放进了插件模块的StartupModule里注册一条控制台命令#include Modules/ModuleManager.h #include Misc/ConsoleCommand.h #include gtest/gtest.h class FMyPluginModule : public IModuleInterface { public: virtual void StartupModule() override { GTestCommand MakeUniqueFAutoConsoleCommand( TEXT(MyPlugin.RunAllGTest), TEXT(Run all registered GoogleTest cases.), FConsoleCommandWithArgsDelegate::CreateLambda( [](const TArrayFString) { int Argc 1; char Arg0[] MyPlugin; char* Argv[] { Arg0, nullptr }; testing::InitGoogleTest(Argc, Argv); const int Result RUN_ALL_TESTS(); UE_LOG(LogTemp, Log, TEXT(GTEST_RESULT%d), Result); })); } virtual void ShutdownModule() override { GTestCommand.Reset(); } private: TUniquePtrFAutoConsoleCommand GTestCommand; }; IMPLEMENT_MODULE(FMyPluginModule, MyPlugin)在编辑器里打开控制台输入MyPlugin.RunAllGTest测试就会跑起来结果统一打到LogTemp里。这个方案的好处是不需要单独的测试进程也不依赖main直接在编辑器环境里完成单元测试。5.4 一个容易忽略的问题测试库别进正式包我这里把 gtest 链接放进了公共模块依赖但实际生产项目建议给它单独做隔离。更稳妥的做法是建一个独立的测试 target 或者在Build.cs里判断Target.Configuration只在Development或者DebugGame配置下启用 gtest。否则正式打包时你会把 gtest 的符号和测试代码一起带进交付包一个追求精简的线上包不该背这口锅。6. 复盘一张速查表和几条长记性的经验6.1 排错速查表把这次的三个报错整理成一张表下次遇到能直接对号入座报错特征根因方向快速检查手段修复建议tr1/tuple file not found老 gtest 头文件与新版 libc 不兼容find $(xcrun --sdk macosx --show-sdk-path) -path *tr1*升级 googletest 到 1.14或编译时定义GTEST_HAS_TR1_TUPLE0building for macOS-arm64 but attempting to link with file built for macOS-x86_64第三方静态库架构与主程序不一致lipo -info libgtest.a清除 CMakeCache显式指定-DCMAKE_OSX_ARCHITECTURESarm64duplicate symbol _main同时链接了gtest_main并自定义了入口nm libgtest_main.a | grep _main$不链接libgtest_main.a或-Dgtest_build_mainOFF升级后出现类成员声明不一致类报错gtest 与 gmock 头文件版本混搭对比include/gtest与include/gmock的版本整个 googletest 仓库一起替换不要只换一半6.2 几条长记性的经验第一第三方库的编译要尽量脱离 UE 构建体系独立完成。gtest 这种开源库在 CMake 环境下是非常成熟的项目一旦和 UE 的构建规则搅在一起报错信息会被各种宏展开和隐式路径污染排查成本成倍上升。第二CMake 缓存是最容易忽视的隐形设置。换机器、换架构之后先删掉build目录再说。很多诡异的问题不是代码写错了而是缓存里存了上个环境的状态。第三链接告警一定别忽略。duplicate symbol这类错误虽然名字叫重复符号但不代表你的工程必须重构。先看一下符号来自哪个库很多时候只是链接列表里多了一个不该出现的库。第四新版 gtest 的代价没有想象中大。我最初执着于在旧版本 1.8.1 上打补丁后来换了 1.14 才发现新版对 Xcode 26 的支持非常干净代码里那些老旧的tr1兼容层早就被删掉了。如果你的项目不需要旧版 gtest 的特殊依赖直接升级是最省时间的。最后分享一个小的实操习惯我每次在 UE 里集成第三方库都会先用 CMake 单独编一个完全不依赖 UE 的最小可执行测试用它跑一遍 gtest 自带的基础用例。这一步能确保库本身是健康的之后再接入 UE 时如果还有问题问题几乎可以断定出在我的集成方式上而不是出在库的编译上。这次整个排查能三小时收工这个习惯帮了不少忙。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。