CMake依赖管理
Git Submodule 管理依赖
由于 C++ 自身的特殊性,要想实现跨平台代码,只能使用源码分发(也有类似于 Conan 的 C++ 依赖解决方案,但是对于一些冷门的库可能就不支持,还是得自己写)。
最简单的办法就是通过 git 的子模块将其添加到项目中,这种依赖管理方式确保可以访问到源代码,其缺点也很明显,我们在下载库的时候需要通过下载依赖库(也就是子模块),如果子模块较大可能下载起来比较耗时。
前面一个笔记中也提到了,要想删除添加的 git submodule 十分麻烦,没有一个简单的命令可以直接删除,需要手动调整需要文件才能删除干净。
FetchContent 模块添加依赖
而 FetchContent 是 CMake 提供的一种新式依赖管理方案(在 3.11 版本首次出现),其在 CMake 的配置过程中自动下载依赖项并配置(或者由用户自行配置)。这样可以保持我们的代码库整洁。
使用时首先引入这个模块
1 | include(FetchContent) |
主要包含下面三个函数
FetchContent_Declare
:用来指定依赖库的下载路径(git 仓库地址或者 URL 链接,其会自动解压缩)FetchContent_MakeAvailable
:自动配置依赖库(相当于调用了add_subdirectory
)FetchContent_Populate
:手动配置依赖库,这个函数会帮我们配置好一系列变量,然后由使用者自行管理如何编译)其会设置一些常用的变量
<lowercaseName>_SOURCE_DIR
,指定的库源代码地址<lowercaseName>_BINARY_DIR
,指定库的编译输出地址
当我们使用的时候就可以通过
add_subdirectory
进行添加,也可以手写CMakeLists.txt
来手动添加库
其中有点需要注意的就是 FetchContent_MakeAvailable
需要在所有的 FetchContent_Declare
声明完成后再写,这样可以确保我们依赖加载完全了,因为一个依赖有可能使用
FetchContent
添加另外一个依赖(隐式依赖)
FetchContent — CMake 3.27.4 Documentation
Projects should aim to declare the details of all dependencies they might use before they call
FetchContent_MakeAvailable()
for any of them. This ensures that if any of the dependencies are also sub-dependencies of one or more of the others, the main project still controls the details that will be used (because it will declare them first before the dependencies get a chance to).
实际使用
下面给出一个实际案例:
我们需要添加四个依赖:fmt
,glfw
,imgui
,glad
对于前三个,我们都可以使用 FetchContent
来添加,由于
glad
是使用网站生成的,我们需要手动将其将其添加到源代码库中。
fmt
库的添加代码
1 | FetchContent_Declare( |
这里我们通过 GIT_REPOSITORY
指定了待下载库的 git
仓库连接,同时通过 GIT_TAG
指定我们需要的版本(也可使用
commit 号),最后的 GIT_SHALLOW
表示我们在
git clone
时设置
--depth 1
,这样可以确保我们仅下载需要的部分,而不下载完整的
git 仓库,减少下载时间。
由于 fmt
库本身比较简单,且是一个 header-only
的库,我们直接通过 FetchContent_MakeAvailable
将其添加到代码中即可,当我们需要使用该库时直接链接即可,例如
1 | add_executable(test_fmt main.cpp) |
glfw
库的添加代码
1 | FetchContent_Declare( |
由于 glfw 库包含一堆 tests 和 examples
,我们可能并不想生成这些文件,为此我们可以通过
FetchContent_Populate
来手动配置 glfw 的编译。
我们在添加 glfw 编译目标之前通过参数 GLFW_BUILD_TESTS
来取消生成 tests,然后将其添加到代码库中,最终由于 glfw
没有提供别名输出(glfw::glfw
,可以用来防止打错名字,例如我们直接错写成
glfv
时,没有这个依赖库 cmake 也不会报错,但是写成
glfw::glfv
时没有这个依赖库 cmake
会报错,提示我们找不到这个依赖库),我们手动为其添加一个别名。
imgui
库的添加代码
1 | FetchContent_Populate(imgui) |
对于 imgui 而言稍微复杂一点,因为其并没有提供开箱即用的
CMakeLists.txt
供我们使用,我们需要手动配置使用哪些文件进行编译,为此我们写了一个小函数
add_imgui
来辅助配置,完整代码如下
1 | function(add_imgui) |
函数 add_imgui
通过提供 imgui
源代码目录以及一些配置项来简化配置流程,其默认使用 glfw + opengl
作为渲染后端。
然后对于 glad 库(用来加载 OpenGL 函数),其配置代码也十分简单
1 | # 配置为动态链接库 |
ExternalProject 添加依赖
这个也是 CMake 中下载依赖的一种方式,其早于 FetchContent
出现,可以提供和 FetchContent
类似的功能,主要通过一个巨大的函数进行依赖配置
1 | ExternalProject_Add |
我们可以在其中指定编译的命令等,也可以使用
ExternalProject_Add_Step
来手动管理依赖的编译步骤,不过这种方式个人感觉没有
FetchContent_Declare
和 FetchContent_Populate
使用来的方便,就不过多介绍了,网上也有很多相关的教程。
杂项
在 Windows 下使用动态链接库时可能会因为找不到 dll
库的位置而无法启动应用,我们需要手动将动态链接库(例如
glad.dll
)拷贝到可执行环境目录下,才可以正常运行,但是每次手动拷贝太麻烦了,cmake
也可以自动帮我们完成这一任务。
(图片太大可能看不清字,写的是:由于找不到 glad.dll,无法继续执行代码,重新安装程序可能会解决此问题。)
比较坑的点在于 Windows 下使用 VSCode 完全看不到这个错误提示,而在 Visual Studio 中就可以看到,恶心坏了
解决方案有很多:
- 强制所有编译输出到同一个目录下,这样就可以确保一定能找到了(个人感觉比较丑陋,只能算一个 workaround)
- 找到该应用所有的依赖库,将其拷贝到可执行文件输出路径下(关键在于如何找到可执行文件的所有依赖库呢?)
这个问题肯定不止我们一个人碰到过,cmake 给我们提供好了这个变量
1 | # %target-name% 对应的所有运行时依赖 |
以及
1 | # %target-name% 的可执行文件输出路径 |
这样我们可以通过 add_custom_command
来进行自动拷贝了,完整代码如下
1 | add_custom_command(TARGET test_imgui POST_BUILD |
这一段代码来源于 cmake 官方文档(看来是需要这个功能的人太多了)
当然,这个功能要求 cmake 的版本比较新,需要在 cmake 版本高于
3.21,记得更新 cmake_minimum_required
1 | cmake_minimum_required(VERSION 3.21) |