CMake 学习笔记
为了写出更加专业规范的工程项目,有必要学习一下 CMake 工具。这里主要参考Modern CMake
, 一般指 CMake 3.4+ ,甚至是 CMake 3.21+.
本项目主要基于 Windows 平台进行讲解,并通过 VS 2017 来进行编译.
相关代码托管在 Github: https://github.com/siyouluo/learn_cmake
环境:
- Windows 10
- Visual Studio Community 2017
- CMake 3.18.5
安装
预定义变量 | 含义 |
---|---|
CMAKE_MAJOR_VERSION | cmake 主版本号 |
CMAKE_MINOR_VERSION | cmake 次版本号 |
CMAKE_C_FLAGS | 设置 C 编译选项 |
CMAKE_CXX_FLAGS | 设置 C++ 编译选项 |
PROJECT_SOURCE_DIR | 工程的根目录 |
PROJECT_BINARY_DIR | 运行 cmake 命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前 CMakeLists.txt 所在路径 |
CMAKE_CURRENT_BINARY_DIR | 目标文件编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标二进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
CMAKE_ROOT | cmake 安装目录 |
我的示例
通用编译流程
Configure & Generate
1
2
3> mkdir build
> cd build
> cmake ..在 build 目录下找到
${PROJECT_NAME}.sln
,双击打开,点击生成解决方案
进入生成的
*.exe
文件目录下,执行1
2> .\demo.exe 2 3
2 ^ 3 is 8如果要删除编译生成的 build 文件夹,可在 powershell 执行:
Remove-Item -Path build -Recurse
Demo1 - 基本框架
本项目仅包含一个 main.cpp
和一个 CMakeLists.txt
.
其中 main.cpp
的功能是读入两个浮点数 a,b
,计算a^b
并在终端输出.
本项目中演示 CMakeLists.txt
的最基础版本,如下.
1 | # cmake 语法关键字 -- 大 / 小写无关,一般用小写的 cmake_minimum_required |
Demo2 - 多文件编译
本项目包含三个代码文件 main.cpp my_power.cpp my_power.h
和一个 CMakeLists.txt
.
相比于 Demo1
,这里需要修改的只有add_executable
中添加源文件的方式,共有如下几种,选择任意一种即可.
手动列出所有文件
只需在编译目标后面列出所需的全部源文件和头文件即可.
1 | add_executable(demo main.cpp my_power.cpp my_power.h) |
请注意,这里添加头文件不是必须的,但是在用 cmake 配合 IDE 使用 (例如通过 cmake 配置并生成 visual studio 解决方案) 时,添加头文件列表可以显式地把自己的头文件放到一个 Header Files
分组内,如下是 VS 的 解决方案资源管理器
中显示的样子。如果 add_executable
没有添加头文件,那么用户自己的头文件 my_power.h
就会被放在 外部依赖项
里面。
1 | 解决方案 'Demo2'(3 个项目) |
自动列出目录下所有源文件
使用 aux_source_directory(<dir> <variable>)
将指定路径下的’cpp/cc/c’源文件全部绑定到 SRC_LIST,然后在添加目标时使用该变量.
1 | aux_source_directory(. SRC_LIST) |
注意这样不会添加头文件, 配置时终端输出如下, 其中 SRC_LIST 只有源文件,没有头文件.
1 | build> cmake .. |
使用通配符设置文件列表
通过 file(GLOB VAR "*.cpp")
来将所有 cpp
文件放到变量 VAR
中,然后在添加目标时使用该变量.
1 | file(GLOB ALL_SOURCES "*.cpp") # 查找指定目录下的所有.cpp, 并存放到指定变量名 ALL_SOURCES 中 |
在一条 file()
语句中可以添加多个匹配项,中间用空格隔开即可, 如: file(GLOB VAR "*.cpp" "*.cc" "*.h" "./FOLDER/*.cpp")
匹配方法:
file(GLOB SRC_LIST "*.cpp")
只匹配当前路径下的所有 cpp 文件file(GLOB_RECURSE SRC_LIST "*.cpp")
递归搜索所有路径下的所有 cpp 文件file(GLOB SRC_COMMON_LIST RELATIVE "common" "*.cpp")
在 common 目录下搜索 cpp 文件
在 Modern CMake 行为准则 中提到应该避免使用这种方法,因为在执行 cmake ..
之后如果新添加了源文件,该文件不会被编译系统感知到,也就不会被编译,除非重新再执行一次cmake ..
(虽然我觉得这不是件什么大不了的事).
文件模块化管理
一个良好的工程项目必然包含多个不同功能模块,同样在 cmake 中也可以将其按照功能的不同进行分组管理。
1 | file(GLOB MY_POWER_FILES "my_power.cpp" "my_power.h") # 将 my_power 模块的源文件和头文件, 设置到指定变量名 MY_POWER_FILES 中 |
如果希望在 解决方案资源管理器
也对文件按模块分组,那么可以添加 source_group
指令: source_group(common/math FILES ${MY_POWER_FILES})
, 效果如下:
1 | 解决方案 'Demo2'(3 个项目) |
注意: 在添加 source_group 函数并重新执行 cmake ..
后,VS 不会提示重新加载解决方案,需要关闭 VS 重新打开项目.
- [CMake 笔记] CMake 向解决方案添加源文件兼头文件
- CMAKE(3)—— aux_source_directory 包含目录下所有文件以及自动构建系统
- CMake » Documentation » cmake-commands(7) » aux_source_directory
- 初识 CMake,如何编写一个 CMake 工程(上)
- CMakeLists.txt 语法介绍与实例演练
Demo3 - 多目录模块化编译
本项目演示如何管理多个目录下的文件, 文件结构如下:
1 | Demo3 |
事实上,如果仅仅是简单的将文件分目录存放,那么 Demo2
中的方法也够用了.
本项目在子目录 math/
下也新建了一个 CMakeLists.txt
用来管理子目录下的文件,然后在上层目录下的 CMakeLists.txt
里面调用它. 注意:子路径下 CMakeLists.txt
中的变量名在上层 CMakeLists.txt
里面不再有效。
两个文件分别如下所示:
1 | # math/CMakeLists.txt |
1 | # CMakeLists.txt |
注意子目录下的 CMakeLists.txt
不需要也不可以指定 cmake 的版本要求和工程名.
子目录下的 CMakeLists.txt
将my_power.cpp/.h
编译成静态链接库,然后再将其链接到目标 demo
上. 由于是静态库,最后编译得到的目标 demo.exe
是可以单独运行的.
生成的 VS 解决方案资源管理器如下:
1 | 解决方案 'Demo3'(4 个项目) |
点击 生成解决方案
后得到如下编译输出:
1 | build |
Demo4 - 自定义编译选项
在一个大工程中可能包含多个不同的模块,有时可能希望选择其中一些模块进行编译,而另一些模块不编译,也就是对工程进行裁剪。或者工程中的两个模块是可以相互替代的,我们希望能让用户自己选择使用哪一个模块。
要实现这种功能需要考虑两个方面: CMakeLists.txt
和源代码.
- 在
CMakeLists.txt
中可以根据option()
参数来决定是否执行某些 cmake 语句.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15option(USE_MYMATH "use provided math implementation" ON) # 设置为 ON 或 OFF,默认为 OFF
message(STATUS "USE_MYMATH is ${USE_MYMATH}")
...
# 是否加入 math 目录下的模块
if (USE_MYMATH)
# include_directories("${PROJECT_SOURCE_DIR}/math")
add_subdirectory(math) # 编译其他文件夹的源代码
set(EXTRA_LIBS ${EXTRA_LIBS} my_power)
endif(USE_MYMATH)
# 编译当前目录下的文件
add_executable(demo main.cpp)
# 把其他目录下的静态、动态库链接进来
target_link_libraries(demo ${EXTRA_LIBS})
已定义 option 选项会缓存在 CMakeCache.txt 中, 除非在命令行通过 -D
参数显式地执行cmake .. -DUSE_MYMATH=OFF/ON
, 或者删除缓存文件,否则配置一次后就不再改变(即便修改CMakeLists.txt
).
- 当
USE_MYMATH
为OFF
时,编译目标没有链接到my_power
库,无法使用自己定义的函数,因此源码中必然需要进行相应的修改。为了更通用,cmake 提供了configure_file()
函数,可以在执行cmake ..
时根据option()
参数将一个config.h.in
文件转换为头文件config.h
.1
2
3
4
5
6
7
8
9# CMakeLists.txt
...
# 加入一个配置头文件 config.h.in,用于编译选项的设置,注意这个文件必须用户提前建立,否则编译错误 -- 找不到该文件
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
include_directories("${PROJECT_BINARY_DIR}")
...
注意, config.h.in
是自己编写的, 如下
1 | // 表示启用宏名 USE_MYMATH,而且会在 config.h 中自动加入对应代码 |
而 build/config.h
是 cmake 自动生成的,根据 USE_MYMATH
取值不同而不同
- 当
USE_MYMATH
为ON
:1
2// 表示启用宏名 USE_MYMATH,而且会在 config.h 中自动加入对应代码
- 当
USE_MYMATH
为OFF
:1
2// 表示启用宏名 USE_MYMATH,而且会在 config.h 中自动加入对应代码
/* #undef USE_MYMATH */
因此可以在 main.cpp
中根据是否定义了 USE_MYMATH
宏,来使用不同的代码参与运算.
注意在 main.cpp
中要 #include "config.h"
, 因此需要让编译器知道该头文件的位置,在CMakeLists.txt
中需要添加头文件路径include_directories("${PROJECT_BINARY_DIR}")
.
1 | // main.cpp |
Demo5 - 安装和测试
安装
- 设置安装目录
安装目录由CMAKE_INSTALL_PREFIX
指定,windows 下默认为CMAKE_INSTALL_PREFIX=C:/Program Files (x86)/${PROJECT_NAME}
要修改安装目录可以直接在 CMakeLists.txt
中通过 set()
指令实现:set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR}/install)
后续所有 install()
指令安装的目标路径都是相对于 CMAKE_INSTALL_PREFIX
的.
- 安装文件
所谓安装
实际上就是把编译生成的文件拷贝到安装目录中,整合成一个可以直接运行的软件包.
本例程文件结构如下:
1 | Demo5 |
首先安装子目录下的库
子目录math/
中有头文件,并且在CMakeLists.txt
中还设置了要生成库文件. 这两部分需要被安装到指定目录中去.
修改math/CMakeLists.txt
1
2
3
4
5
6
7# math/CMakeLists.txt
...
# 指定 my_power 库的安装路径
install(TARGETS my_power DESTINATION bin)
install(FILES my_power.h DESTINATION include)安装编译目标和相关头文件
修改CMakeLists.txt
1
2
3
4
5
6
7
8# CMakeLists.txt
...
# 指定 demo.exe 安装路径
install(TARGETS demo DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/config.h"
DESTINATION include)
执行 cmake ..
之后通过 VS 打开解决方案,可以看到解决方案资源管理器如下所示
1 | 解决方案 'Demo5'(5 个项目) |
其中 INSTALL
项目就是用于安装的, 在直接点击 生成解决方案
时会被跳过,输出如下:
1 | ... |
此时如果要安装,需要选中 INSTALL
项目,右键,点击 生成
, 输出如下, 表示相关文件已经被拷贝到了对应的目录中了:
1 | 1>------ 已启动生成: 项目: INSTALL, 配置: Debug Win32 ------ |
可以查看 install
目录如下:
1 | Demo5\install |
测试
为了验证代码正确性,可以事先给出几个样例,在生成目标后运行测试样例,判断其输出是否正确, 从而判断程序是否正确.CMake
提供了一个称为 CTest
的测试工具。我们要做的只是在项目根目录的 CMakeLists.txt
文件中调用一系列的 add_test()
命令.
要添加测试,首先在 CMakeLists.txt
中添加: enable_testing()
然后就可以通过 add_test(<name> <command> [<arg>...])
来添加测试. 其中 <name>
可以随便,但不要重复;<command>
是编译生成的目标文件, 后面 <arg>
给出可执行文件需要的参数.
- 测试程序是否可以运行:
add_test (test_run demo 5 2)
- 测试帮助信息是否可以正常提示
1
2
3add_test (test_usage demo) # 测试时不指定参数,main 函数中会检查发现参数个数不对,然后输出帮助信息
set_tests_properties (test_usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent") # 通过正则表达式检查程序输出是否正确 - 测试 5 的平方
1
2
3add_test (test_5_2 demo 5 2) # 测试时指定参数 5 和 2,程序会计算 5^2, 然后在终端输出
set_tests_properties (test_5_2
PROPERTIES PASS_REGULAR_EXPRESSION "is 25") # 通过正则表达式检查程序输出是否正确
如果要执行更多的测试样例,使用 add_test()
效率就太低了,可以通过编写宏来简化操作:
1 | # 定义一个宏,用来简化测试工作 |
执行 cmake ..
之后通过 VS 打开解决方案,可以看到解决方案资源管理器如下所示
1 | 解决方案 'Demo5'(6 个项目) |
其中 RUN_TESTS
项目就是用于测试的, 在直接点击 生成解决方案
时会被跳过,需要在生成解决方案之后单独生成一次,输出如下:
1 | 1>------ 已启动生成: 项目: RUN_TESTS, 配置: Debug Win32 ------ |
Demo6 - 添加版本号
1 | 版本格式:主版本号. 次版本号. 修订号,版本号递增规则如下: |
要在 CMakeLists.txt
中指定版本号只需在设置工程名时顺带用 VERSION
属性设置即可:
1 | project(Demo6 |
这里虽然只设置了一个属性VERSION
,但同时如下四个变量会被 cmake 自动进行设置.
1 | ${PROJECT_NAME}_VERSION |
可以通过 message()
来查看它们的值:
1 | # 查看程序版本是否设置正确 |
执行 cmake ..
时终端输出如下:
1 | PS <path>\Demo6\build> cmake .. |
如果希望在 *.cpp
源文件也能访问到这些变量,可以通过编写 Version.h.in
并用 configure_file()
指令实现, 此处略.
可参考如下资料:
Demo7 - 生成安装包(未实现)
本项目学习如何配置生成各种平台上的安装包,包括二进制安装包和源码安装包。为了完成这个任务,我们需要用到 CPack ,它同样也是由 CMake 提供的一个工具,专门用于打包。
首先在顶层的 CMakeLists.txt
文件尾部添加下面几行:
1 | # 构建一个 CPack 安装包 |
上面的代码做了以下几个工作:
- 导入 InstallRequiredSystemLibraries 模块,以便之后导入 CPack 模块;
- 设置一些 CPack 相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号;
- 导入 CPack 模块
执行 cmake ..
后用 vs 打开解决方案,点击生成解决方案,然后单独生成 INSTALL
项目,单独生成 PACKAGE
项目
1 | 解决方案 'Demo7'(7 个项目) |
终端输出如下, 似乎是没有安装NSIS
(该问题暂未解决, 先搁置):
1 | 1>------ 已启动生成: 项目: PACKAGE, 配置: Debug Win32 ------ |
参考:
Demo8 - 编写 FindXXX.cmake(待完善)
如果要在自己的项目中引用第三方库,可以通过 find_package(pkg-name)
来方便地实现,例如 OpenCV
和PCL
等常用库都提供了相应的 OpenCVConfig.cmake
和PCLConfig.cmake
, 调用者只需要让 CMake 能找到这类文件的路径即可方便地调用.
但有些小众的第三方库可能并没有提供这类文件,或者自己编写的某些简单的库没有提供相应的文件,为了通过 cmake 调用,可以编写一个 FindXXX.cmake
来帮助查找库文件,从而将库的查找与使用进行解耦。
本项目包含两个部分: export
和 import
. 其中export
部分负责编译生成库文件,而 import
部分通过 Findxxx.cmake
的形式来引用这些库.
生成库
使用库
当移动了 MyMath
库的路径之后,只需要修改 cmake/FindMyMath.cmake
中的 MyMath_ROOT_DIR
即可.
Demo9 - 为自己的库生成 XXXConfig.cmake
1 | D:\RESEARCH\MACHINEVISION\CODE\LEARN_CMAKE\CODES\DEMO9 |
- CMake Tutorial: Code
- Step 11: Adding Export Configuration
- CMakePackageConfigHelpers
- Modern CMake: Exporting and Installing
- 如何为 cmake 提供 package 以便于 find_package, 以及用 VCPKG 补充 CMake 实现快速下载集成
- CMake 库打包以及支持 find_package
Demo10 - 通过 CMake 管理 Qt GUI 项目
- Get started with CMake - doc.qt.io
- Build with CMake
- cmake-qt(7)
- CMake Introduction
- CMake 管理 C/C++ 工程的一点心得
- 在 Qt 项目中调用 OpenCV: 访问 usb 摄像头并实时绘制到 QLabel (CMake + VSCode)
- Using CMake with Qt 5
- Qt and CMake: The Past, the Present and the Future
参考
CMake 学习笔记