☰
CMake引入第三方库全攻略:以EnTT为例,从原理到实操
2026/9/28 12:40:17 网站建设 项目流程

很多刚接触 CMake 的 C++ 开发者,都会卡在同一个地方:业务代码写得挺顺,一提到引入第三方库,要么在find_package的报错里绕不出来,要么把整个库源码塞进仓库、换台机器就编译不过。我这些年把 vcpkg、Conan、FetchContent、add_subdirectory 这几条路都实际走过之后,才真正想明白:CMake 引入第三方库的本质,不是背命令,而是搞清楚“依赖从哪里来、构建时怎么被发现、使用要求怎么传给最终目标”。这篇文章就以 EnTT 库为具体例子,把主流引入方式从原理到实操完整过一遍,适合正在学 CMake、想把依赖管理做得干净的新手,也适合已经有多个项目、想统一管理策略的中级 C++ 开发者。

有人会问 CMake 和 Makefile 到底啥区别。简单说,Makefile 是给 make 这类工具执行的构建脚本,而 CMake 本身不编译代码,它是"生成构建系统"的工具,可以根据你的描述生成 Makefile、Ninja 构建文件、Visual Studio 工程等。所以"引入第三方库"这件事发生在 CMake 配置阶段:把依赖信息收集好,再交给具体的构建后端去执行。这个认识一旦建立,后面看所有方案都会顺很多。

1. 引入第三方库:先把思路理清楚

1.1 四种主流方式,选型先看这张表

先把话放前面:CMake 不是一个包管理器,它不负责去网上下载库。它的职责是把源码、依赖、编译选项组织成一棵 target 依赖树,然后交给后端执行。第三方库引入本质上是两件事——让编译能找到头文件,让链接能找到库文件。对于纯头文件库,第二件事可以省略,但思路完全一致。

我在实际项目中见过的引入方式,归纳起来就四种,各有各的适用场景:

引入方式核心动作依赖获取时机适合场景
FetchContent配置时拉取源码,原地变成子项目参与构建配置阶段(需要网络)想要"克隆即编",依赖不多的小项目
find_package在系统路径、vcpkg、Conan 等环境里找已安装库配置阶段(可离线)团队已统一使用包管理器
add_subdirectory把第三方库源码放进仓库或 submodule,作为子目录源码随仓库走需要改库源码、内网离线构建
手动写路径直接指定头文件目录和库文件路径无临时验证,不建议进正经项目

很多新人纠结"哪个最好",其实没有最好,只有匹配场景。比如公司 CI 部署在内网、访问不了 GitHub,那 FetchContent 就得靠本地文件 URL 或者干脆退化成 add_subdirectory。又比如团队全员已经用 vcpkg 管理依赖,那就别再折腾 FetchContent,统一走 find_package 反而省心。选型的关键是看你的依赖来源是否可控、网络环境是否稳定、是否需要经常修改库源码。

1.2 核心概念:target、INTERFACE 与"头文件即库"

先补一个基础概念,后面实操会反复用到。现代 CMake 的核心单元是 target,也就是add_library、add_executable创建出来的"目标"。target 身上可以挂一堆属性:头文件路径、宏定义、链接库、编译选项。你写target_link_libraries(app PRIVATE EnTT::EnTT)的本质,是把 EnTT 这个 target 携带的使用要求(比如 include 路径)传递给 app。这个传递是"按需"的,而不是像老式include_directories那样全局污染。

纯头文件库在 CMake 里有个特殊表示方式:add_library(EnTT INTERFACE)。INTERFACE 库没有构建产物,它只是一个属性包,专门用来传递 include 目录、编译选项这类"使用要求"。你在target_link_libraries里链接它,实际上不是在链接二进制,而是把这个属性包里的头文件路径并到自己的编译命令里。这个设计非常巧妙,理解了它,以后看任何头文件库的 CMake 代码都不会犯晕。

另外要注意,头文件库"不需要链接"不代表引入成本为零。编译选项、C++ 标准、宏定义这些使用要求依然要正确传递。比如一个库要求 C++17,你的项目还停在 C++14,配置阶段可能风平浪静,编译阶段却疯狂报模板错误,而且报错文本往往是天书级别的。这就是为什么我后面会专门把"编译器标准"放进排查清单。

2. EnTT 是什么样的库:为什么拿它当模板

2.1 EnTT 是什么:ECS、纯头文件、只需头文件路径

EnTT 是 skypjack(Michele Caini)开源的 C++ ECS 框架。ECS 是游戏开发里很流行的一种数据组织方式:Entity 是对象的唯一 ID,Component 是纯数据(位置、速度、血量这类),System 是处理这些数据的逻辑。EnTT 在游戏社区里知名度很高,迭代活跃,Github 上的 star 数量也说明它的认可度。

对这次的主题来说,EnTT 最重要的两个特性:一是纯头文件库,对外只需要#include <entt/entt.hpp>一个入口;二是相对宽松的许可证和零第三方依赖,拿来写示例不会有任何版权和环境上的顾虑。因为纯头文件,你用 CMake 引入它时不需要操心.a、.lib、.so、.dll这些链接产物的路径,只需要保证编译器找得到头文件。这个特性让它成为学习第三方库引入的最佳实验对象——如果连它都搞不定,说明你对 CMake 的理解还有盲区;如果搞定了,再去处理需要链接的动态库,无非是额外多配一步库文件路径而已。

2.2 引入 EnTT 前必须知道的两件事

第一,EnTT 要求 C++17 起步。这也是它给很多新手挖的第一个坑:有人复制了一个 C++11 的旧项目直接加 EnTT,结果编译期报出一堆看不懂的模板错误,还以为是库本身有问题。实际上只要把标准切到 C++17,问题立刻消失。引入 EnTT 之后,第一步就是确认对应 target 的CXX_STANDARD不低于 17。

第二,EnTT::EnTT这个 target 是它的官方"出口"。不管你是用 FetchContent 拉源码、用 add_subdirectory 加目录,还是用 find_package 找安装好的包,最终在 CMake 里链接的通常都是同一个名字的 target。这个设计对使用者非常友好:只要你的 CMakeLists 里写的是target_link_libraries(demo PRIVATE EnTT::EnTT),那么未来切换依赖引入方式时,你的业务代码基本不用改,变的只是上面几行"获取依赖"的写法。

3. 四种引入方式实测:从 FetchContent 到手工路径

3.1 方式一:FetchContent,最推荐的默认方案

如果你的项目依赖不多、团队规模不大,我个人建议把 FetchContent 当作默认首选。它的思路是在配置阶段把库源码下载到本地构建目录,然后当作子项目直接参与构建。看起来像 add_subdirectory,但又省去了手动 clone 源码、更新版本的操作。一个最小可用的 CMakeLists 长这样:

cmake_minimum_required(VERSION 3.14) project(entt_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( entt GIT_REPOSITORY https://github.com/skypjack/entt.git GIT_TAG v3.12.2 ) FetchContent_MakeAvailable(entt) add_executable(demo src/main.cpp) target_link_libraries(demo PRIVATE EnTT::EnTT)

FetchContent_Declare负责登记依赖的名字、来源和版本,FetchContent_MakeAvailable负责真正下载并把库加入当前构建。这里有两个细节值得说:一是GIT_TAG建议固定到具体的 release 标签而不是master,否则今天能编过、明天可能就编不过,可复现性完全失控;二是FetchContent_MakeAvailable需要 CMake 3.14 以上,如果公司环境还在老版本,要么升级 CMake,要么退回FetchContent_GetProperties和FetchContent_Populate这两步手写的老写法。

如果你的构建环境没装 Git,或者不想依赖 Git 拉取,还可以改用 URL 方式直接下载官方 release 包:

FetchContent_Declare( entt URL https://github.com/skypjack/entt/archive/refs/tags/v3.12.2.zip )

这种方式不需要 Git,只要网络能访问到下载地址即可。我实测下来,URL 方式对 CI 环境更友好,因为少了 Git 克隆时的各种 checkout 分支问题。

3.2 方式二:find_package 加 vcpkg / Conan

等依赖数量多起来,很多团队会引入包管理器。vcpkg 是微软维护的 C++ 包管理器,装 EnTT 只需一条命令:vcpkg install entt。安装完之后,在 CMake 里用 find_package 查找:

find_package(EnTT CONFIG REQUIRED) add_executable(demo src/main.cpp) target_link_libraries(demo PRIVATE EnTT::EnTT)

用 vcpkg 时,配置命令需要指定 toolchain 文件,这个通常在 vcpkg 目录下的scripts/buildsystems/vcpkg.cmake。配合 CMake 的CMAKE_TOOLCHAIN_FILE参数或者 vcpkg 的 manifest 模式,整个过程可以做得非常自动化。如果依赖装到了非标准路径,find_package 找不到,大多数时候是CMAKE_PREFIX_PATH没指对:

list(APPEND CMAKE_PREFIX_PATH "/path/to/your/installed/libs")

Conan 的使用逻辑类似,只是换成了 Conan 的 generator 机制。这一类方案的优点是把"依赖从哪来"的问题从 CMake 里剥离出来,交给专门的包管理器去处理;缺点是每个开发者的机器上都得先把依赖装好,刚拉下项目时多了一步环境准备。

3.3 方式三:add_subdirectory 与 vendored 源码

有些场景下,你需要把第三方库源码直接放到自己的仓库里,或者用 git submodule 管理。这种"vendor"模式在游戏公司、嵌入式项目里非常常见,原因是构建环境完全可控,不依赖外部网络。写法也简单:

add_subdirectory(third_party/entt) target_link_libraries(demo PRIVATE EnTT::EnTT)

关键点在于third_party/entt目录下得有 EnTT 自己的 CMakeLists.txt,并且它对外提供的 target 名叫EnTT::EnTT。用 submodule 的话,克隆仓库之后要先执行git submodule update --init --recursive,这个步骤经常被人遗忘,导致全新环境编译时报"目录不存在"。我建议在 README 里把这个命令写成一行,并把git submodule update --init --recursive写进团队的初始化脚本。

另外,把 EnTT 作为子目录加入构建时,它自带的一些可选项可能会把测试目标也带进来。如果你发现构建列表里多了很多测试目标,可以在 add_subdirectory 之前先关掉测试选项,具体变量名以你拉取的版本 README 为准,常见的是set(ENTT_BUILD_TESTING OFF CACHE BOOL "" FORCE)这种写法。

3.4 方式四:include_directories 的"能跑但别学"

最后一种是我最不推荐、但必须提一下的方式:

include_directories(third_party/entt/src)

EnTT 的头文件放在src/entt/entt.hpp,所以直接指定third_party/entt/src就能让#include <entt/entt.hpp>找到。这种写法确实最快,但对稍微大一点的项目就是灾难:include_directories是目录级命令,会把这个路径塞给当前目录下所有 target,一旦项目里出现同名头文件、多个版本共存,编译错误会变得极难排查。现代 CMake 的规范做法是"target 级传递":让头文件路径跟着 target 走,用target_link_libraries按需传播。这个方式只适合临时验证一个库能不能用,千万别把它写进长期维护的工程里。

4. 完整实操:从空目录到第一个 ECS demo

4.1 目录与 CMakeLists 最小模板

说了这么多理论,不如直接跑一个例子。先建一个干净的项目结构:

entt_demo/ ├── CMakeLists.txt └── src/ └── main.cpp

CMakeLists.txt 就用 3.1 里那个 FetchContent 模板,这里我贴一份可以原样复制的完整版:

cmake_minimum_required(VERSION 3.14) project(entt_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( entt GIT_REPOSITORY https://github.com/skypjack/entt.git GIT_TAG v3.12.2 ) FetchContent_MakeAvailable(entt) add_executable(entt_demo src/main.cpp) target_link_libraries(entt_demo PRIVATE EnTT::EnTT)

main.cpp 写一个最简单的 ECS 例子:一个实体有位置(Position)和速度(Velocity),我们遍历所有同时拥有这两个组件的实体,更新位置。这个逻辑在 EnTT 里非常直观:

#include <entt/entt.hpp> #include <iostream> struct Position { float x{0.0f}; float y{0.0f}; }; struct Velocity { float dx{0.0f}; float dy{0.0f}; }; int main() { entt::registry registry; auto entity1 = registry.create(); registry.emplace<Position>(entity1, 1.0f, 2.0f); registry.emplace<Velocity>(entity1, 0.5f, 0.25f); auto entity2 = registry.create(); registry.emplace<Position>(entity2, 10.0f, 20.0f); auto view = registry.view<Position, Velocity>(); for (auto entity : view) { auto &pos = view.get<Position>(entity); auto &vel = view.get<Velocity>(entity); pos.x += vel.dx; pos.y += vel.dy; std::cout << "entity " << entity << " now at " << pos.x << ", " << pos.y << std::endl; } return 0; }

这里registry.view<Position, Velocity>()只会返回同时拥有两个组件的实体,所以 entity2 因为没有速度组件,不会被遍历到。输出里应该能看到 entity1 的位置从 (1.0, 2.0) 变成了 (1.5, 2.25)。这个例子虽然简单,但足以验证 EnTT 的头文件路径、C++ 标准、模板实例化全部正常。

4.2 配置、编译、运行三部曲

在项目根目录执行:

cmake -S . -B build cmake --build build ./build/entt_demo

-S . -B build指定源码目录和构建目录,这是 CMake 3.13 之后推荐的做法,不要在源码目录里直接跑 cmake 生成一堆文件。如果你用 VS Code 的 CMake Tools 插件,底部状态栏那个 Configure 按钮触发的就是第一步「配置阶段」,Build 按钮触发的就是第二部「编译阶段」。很多新人点完 Configure 看到输出一堆信息就以为是在编译,其实配置阶段只是生成了构建文件,真正的编译发生在下一步,这个区分一定要建立起来。

如果你是 Windows 上用 Visual Studio 工具集,编译命令行要带上构建配置:cmake --build build --config Release。这是最容易踩的小坑:明明配置成功了,执行cmake --build build却提示找不到目标,多半是因为你没告诉它构建 Debug 还是 Release。

4.3 验证 EnTT 头文件路径是否生效

有时候编译通过了,但你自己心里没底:EnTT 的头文件到底是从哪个路径被找到的?我常用的验证方法有两个。一是打开 CMake 的编译命令导出功能,重新配置一次工程:

cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

然后去build/compile_commands.json里搜entt_demo,看它的command字段里有没有-I参数指向上次拉取下来的 EnTT 源码目录。这个方法对任何库都适用,排查 include 路径问题非常高效。

第二个方法更直接:把#include <entt/entt.hpp>临时改成#include "entt/entt.hpp",如果编译立刻失败,说明之前的尖括号写法确实依赖编译器按照-I路径搜索头文件,路径传递是生效的。验证完记得改回去。

5. 常见报错排查速查表与避坑心得

5.1 高频报错逐条拆解

下面这些错误,我在不同环境里都亲历过。整理成速查表,你可以直接对照排查:

报错 / 现象原因解决办法
Unknown CMake command "FetchContent_MakeAvailable"CMake 版本低于 3.14升级 CMake,或改用 FetchContent_Populate 老写法
配置时Failed to perform the checkout类似报错网络不通、Git 未安装或仓库访问失败改用 URL 方式下载 release 包;内网环境放本地源码包
fatal error: entt/entt.hpp: No such file or directory没链接 EnTT target,或 include 路径没传过来检查target_link_libraries是否在add_executable之后,且名字是EnTT::EnTT
编译报大量模板错误、涉及std::invoke之类编译器标准低于 C++17set(CMAKE_CXX_STANDARD 17),并确认编译器本身支持
find_package(EnTT ...) could not find EnTT依赖没安装,或 CMAKE_PREFIX_PATH 没指对确认 vcpkg/Conan 是否安装成功;用list(APPEND CMAKE_PREFIX_PATH ...)指向安装目录
CMake Error at .../CMakeDetermineCompilerID.cmake:9系统里没装 C++ 编译器,或 CC/CXX 环境变量没设置安装编译器(如 g++、cl),或者打开 Visual Studio 开发者环境再跑 cmake
同名头文件冲突、编译顺序飘忽不定用了全局include_directories,污染了所有 target改为 target 级传递,用target_link_libraries传播 include 目录

有一个排查思路值得养成习惯:看到错误先分清阶段。配置阶段(Configure)报错,多半是 find_package、FetchContent、路径、版本问题;编译阶段(Build)报错,多半是 C++ 标准、头文件冲突、宏定义问题;链接阶段(Link)报错,才是库文件路径、符号缺失问题。EnTT 是纯头文件库,你基本不会遇到第三类错误,一旦遇到,反而要回头检查自己是不是把某个库编成了静态库模式却忘了链接。

5.2 PRIVATE、PUBLIC、INTERFACE 别随手写

target_link_libraries(demo PRIVATE EnTT::EnTT)里的PRIVATE是什么含义?它告诉 CMake:EnTT 只是 demo 这个可执行程序的内部实现依赖,不需要传给依赖 demo 的其他人。对于顶层可执行文件,PRIVATE 完全正确,也是我推荐的默认值。

但如果你是写一个库给别人用,就要动脑子了:你的库对外暴露的公开头文件里有没有#include <entt/entt.hpp>?如果有,说明 EnTT 的类型会出现在你库的公开接口里,那就要用PUBLIC,让下游用户也能看到 EnTT 的头文件;如果你的公开头文件不涉及 EnTT,只是.cpp里用了,那还是PRIVATE。还有一个INTERFACE:只在头文件用、实现里不用,这个用得少,但需要知道它存在。这个取舍直接影响下游是否能编译通过,很多人因为随手写了 PRIVATE,导致自己库的调用方报"找不到 entt/entt.hpp",本质就是传播层级写错了。

5.3 三个提升体验的 CMake 小技巧

分享几个我实际用下来很舒服的小技巧。第一,FetchContent 支持本地源码覆盖。如果你不想每次都从网上拉,可以在配置时传一个变量:cmake -S . -B build -DFETCHCONTENT_SOURCE_DIR_ENTT=/path/to/local/entt。这个变量存在时,FetchContent 会直接用本地目录而不再访问网络,非常适合内网开发和本地调试 EnTT 源码。

第二,离线环境或者不想反复检查更新时,可以设置FETCHCONTENT_UPDATES_DISCONNECTED=ON。它告诉 CMake:只要本地缓存里有这个依赖,就别再去查 Git 更新了。配置速度会快不少,也能避免每次构建都去访问远程仓库。

第三,固定版本一定要养成习惯。我们做软件构建,最怕的是"昨天还能跑,今天拉了个新依赖就挂"。GIT_TAG v3.12.2这种写法就相当于给依赖上了保险,把版本选择权握在自己手里。依赖升级应该是一个主动决策,而不是被动接受。

最后说一点个人体会。我自己的默认方案是 FetchContent,因为它让一个新克隆下来的仓库在最少步骤下跑起来,这对小团队和开源项目特别友好;但在公司内网 CI 里,我已经把好几个依赖切成了 vendor 加 add_subdirectory 的模式,因为不依赖外网、可控性更强。方

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询