更现代化的 CMake
你疑似有点太现代化了。这里选择 CMake 3.9 版本,是 2017 年 7 月发布的,也就是 C++17 进入国际标准草案阶段之后 4 个月。
参考文献:
1. 基本
一些基本用法
1.1. TL;DR
1 | # 先看看选项的情况,后面还可以接 A(advanced), H(human-readable) |
-S表示 source 目录-L让 CMake 列出 cache variable 的情况-B表示 build 目录--build表示进行构建。加上-j N可以并行编译( CMake 3.12+ )--install后接 build 目录表示安装这个 project
据说这样写是异类,正常人一般
cd进./build之后进行操作,不需要带上那些指定./build为目录的 flag
1.2. 通用的选项
CMAKE_BUILD_TYPE顾名思义,没指定的话 CMake 不会给底层工具链传相关 flag , i.e. 具体行为由底层工具链决定CMAKE_INSTALL_PREFIX顾名思义,默认为/usr/local,非 root 安装可以设置成~/.localBUILD_SHARED_LIBS在add_library时没指定STATIC或SHARED时会读取这个选项,如果是ON则会生成动态库,否则默认为静态BUILD_TESTING约定俗成表示启用测试,貌似add_test后默认启用
1.3. 建议
- 别用作用于全局的函数 e.g.
link_directories,include_directories - 别乱给
PUBLIC的东西加上莫名其妙的 buff e.g.-Wall,把这些上给PRIVATE的东西 - 别用
GLOB,用了之后不重新生成项目的话底层工具感知不到项目的变化 - 尽量链接到 target ,不要直接链接某个库
- 链接时尽量加上
PRIVATE/PUBLIC
2. CMake 也是一种编程语言
CMake 怎么不算编程语言?(振声)
2.1. 变量
foo bar
2.1.1. 局部变量
顾名思义有作用域
1 | set(VARIABLE_NAME "variable value") |
2.1.2. Cache 变量
一般用于命令行,比如通过 -D 定义的变量,比如 CMAKE_BUILD_TYPE
1 | set(VARIABLE_NAME "variable value" CACHE STRING "variable description") |
可选的变量类型有 BOOL, FILEPATH, PATH, STRING 和 INTERNAL 。其中 BOOL 比较常见,所以有一个更简洁的写法:
1 | option(OPTION_NAME "option description" OFF) |
可以在
<build-directory>/CMakeCache.txt看到所有 cache 变量
这样写不会覆盖从命令行传进来的值(如果覆盖的话,处理不当会导致命令行参数不生效),在后面加上 FORCE 可以进行覆盖,比如覆盖命令行的 CMAKE_BUILD_TYPE :
1 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "the type of build." FORCE) |
2.1.3. 环境变量
一般来说不建议写入环境变量
1 | message("$ENV{SHELL}") |
2.1.4. Property
一般作用于比如 TARGET, INSTALL 或 TEST 之类的东西,大多数由 CMAKE_ 开头的变量初始化,比如 CXX_STANDARD 这个 property 由 CMAKE_CXX_STANDARD 变量初始化。比如设置使用 C++17 :
1 | set_property(TARGET target1 target2 target3 |
这种可以给多个东西设置一个 property ,还有一些特化版本可以给某一个特定类型的东西设置多个目标,比如给 TARGET :
1 | set_target_properties(target_name PROPERTIES |
2.2. 流程控制
1 | if(variable) |
上面直接写了 variable 而不是 ${variable} 是由于历史原因, CMake 会自动展开 if 里面的变量,但这会导致歧义,比如:
1 | set(foo "114514") |
这里 ${bar} 会被展开为 foo ,语句变为 if(foo) ,而这里 foo 也是一个变量, if 是否应该继续展开? CMake 3.1 版本之后有了一个规则,双引号包起来的变量不会再被展开,所以可以这样写:
1 | if("${bar}") |
当然少不了逻辑运算符:
- 一元:
NOT,TARGET,EXISTS(file),DEFINED, … - 二元:
STREQUAL,AND,OR,MATCHES(regexp),VERSION_LESS,VERSION_LESS_EQUAL(CMake 3.7+), … - 括号用来调整优先级
2.3. 生成表达式
上面提到的东西基本都是 CMake 生成项目时进行的工作,而生成表达式可以在编译时执行一些逻辑操作,语法与变量展开类似,为 $<...> ,比如:
1 | target_include_directories(foo PRIVATE /opt/include/$<CXX_COMPILER_ID>) |
编译 foo 时会根据底层工具的不同选择不同的目录,比如使用 GNU GCC 时会选择 /opt/include/GNU ,使用 LLVM Clang 时会选择 /opt/include/Clang 。再比如:
1 | target_compile_definitions(foo PRIVATE |
如果编译器版本小于 4.2.0 的话,编译 foo 时 OLD_COMPILER 宏会被定义
2.4. 宏和函数
宏与函数的唯一区别就是宏没有作用域一说,里面的变量在宏外依然可以访问,而函数需要使用 set 配合 PARENT_SCOPE 把变量传给父作用域
1 | function(my_func) |
可以定义命名参数:
1 | function(my_func param1) |
还有两个特殊变量,比如 my_func(foo bar) 调用:
ARGV表示所有参数,为my_func,foo,barARGN表示匹配后(这里除了函数名my_func外,把foo匹配给了param1)剩下的参数,为bar
CMake 3.5 后内置了 cmake_parse_arguments (之前版本可以引入 CMakeParseArguments 模块后使用),语法为:
1 | function(my_func) |
- 第一行
FOO表示为后面定义的变量加上FOO_前缀,比如第二行的OPTION_1实际名为FOO_OPTION_1 - 第二行
OPTION_1;OPTION_2表示匹配BOOL类型变量,比如在这个位置成功匹配到OPTION_1,则FOO_OPTION_1为TRUE - 第三行
ARG_1;ARG_2表示匹配含有单个值的变量,比如在这个位置成功匹配到ARG_1 value1,则FOO_ARG_1为value1 - 第四行
ARGS表示匹配含有多个值的变量,比如在这个位置成功匹配到ARGS foo bar,则ARGS为foo;bar - 第五行
${ARGN}把函数名以外的所有参数传给cmake_parse_arguments
举个例子:
1 | my_func(OPTION_2 ARG_1 value1 ARGS 114 514) |
my_func 内变量的情况:
FOO_OPTION_1为FALSEFOO_OPTION_2为TRUEFOO_ARG_1为value1FOO_ARG_2未定义FOO_ARGS为114;514
未被成功匹配的所有变量会被装进 FOO_UNPARSED_ARGUMENTS 。如果不想写 OPTION_1;OPTION_2, ARG_1;ARG_2 这样带有分号的多个变量可以利用变量展开,比如这样可以实现同样的效果:
1 | function(my_func) |
2.5. 与其他文件交互
我觉得这些设计有点蹩脚
2.5.1. 输出文件
configure_file 可以做一些替换,生成比如头文件:
1 | // version.h.in |
使用 configure_file :
1 | # CMakeLists.txt |
生成项目时会生成 version.h (具体版本号随便填的):
1 | // version.h |
configure_file 会替换 ${VAR} 和 @VAR@ 两种模式,可以 configure_file(... ... @ONLY) 使它只对 @VAR@ 进行替换,忽略 ${VAR} 格式。还有一种写法:
1 | // foo.h.in |
如果在 CMake 里,变量 FOO 被定义了,则生成的 foo.h 为:
1 | // foo.h |
否则为:
1 | // foo.h |
当然 #cmakedefine VAR_NAME 后面还可以接 @VAR_NAME@ ,生成后变为 #define VAR_NAME value 或 /* #undef VAR_NAME */ 。如果喜欢 #define VAR_NAME 0/#define VAR_NAME 1 的格式还可以用另一种写法:
1 | // foo.h.in |
如果在 CMake 里,变量 FOO 被定义了,则生成的 foo.h 为:
1 | // foo.h |
否则为:
1 | // foo.h |
2.5.2. 读取文件
比如让 CMake 读取头文件,自动填写 project 里的版本号:
1 | # 在 version.h 里找到符合 `#define FOO_VER ".+"` 的一行,放进 `version_line` 里 |
2.6. 调用其他程序
可以用 execute_process 配合 find_program 调用其他程序,比如模拟 ls -a | sort -i :
1 | find_program(ls_exe ls) # 把 ls 的绝对路径放进 `ls_exe` |
上面指定了多个 COMMAND ,这些 COMMAND 会依次被管道线串起来。
除此之外,还有 add_custom_command 可以实现更精细的控制,比如项目需要在编译时生成某个文件:
1 | add_custom_command( |
add_custom_command 后,如果某个 target 的源文件里包含了 OUTPUT 指定的文件,则这个 COMMAND 就会被执行一次。如果多个可能并行编译的 target 都以这个文件为源文件,则建议使用 add_custom_target 把这个文件包装成 target ,然后再作为 DEPENDS 放进可能并行编译的 target 里
3. 组织项目文件
我的个人口味
应该像这样:
1 | . |
cmake 目录下可以放一些 CMake 模块,这样可以让 CMake 读取 cmake 目录:
1 | set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) |
下面的代码可以拒绝在包含 CMakeLists.txt 的目录下生成/编译项目:
1 | file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" cmake_file) |
4. 给项目加 Buff
比如 C++ 标准,编译器自己的扩展
4.1. 半官方的设置默认编译模式的方式
CMake 如果没有被指定 CMAKE_BUILD_TYPE 的话具体编译成什么由底层工具决定,但他们的官方网站 Kitware.com 写了一篇设置默认编译模式的文章。大概是这样:
1 | # 用 `default_build_type` 保存默认的编译模式,这里为 `Debug` |
4.2. C++17
更加现代化的设置方式为( CMake 3.8+ ):
1 | # 设置 C++ 标准为 C++17 |
target_compile_features 允许更细粒度的编译器特性的控制。如果版本低于 3.8 ,可以这样:
1 | set_target_properties( |
这里如果没启用 CXX_STANDARD_REQUIRED ,当底层编译器不支持 C++17 时, CMake 不会报错,而是继续执行,并给 foo 设置一个最接近的 C++ 标准。虽然这样也可以设置 C++ 标准,但没有 target_compile_features 一样的 PUBLIC, PRIVATE 控制
不要手动设置
-std=c++11之类的显式 flag
4.3. PIC (Position Independent Code)
CMake 对应变量为 POSITION_INDEPENDENT_CODE ,由命令行参数 CMAKE_POSITION_INDEPENDENT_CODE 初始化。对于 SHARED 或 MODULE 的库 target , POSITION_INDEPENDENT_CODE 默认为 ON ,否则为 OFF ,所以一般不需要手动指定,如果一定要指定的话可以这样:
1 | # 全局启用 |
4.4. 链接外部的库
比如 -ldl 一般用来引入 dlopen 和 dlclose , CMake 提供了 CMAKE_DL_LIBS :
1 | target_link_libraries(foo PRIVATE ${CMAKE_DL_LIBS}) |
别的库可以先 find_package ,比如链接 json-c :
1 | find_package(json-c REQUIRED CONFIG) |
最基本的为:
1 | find_package(package_name) |
4.4.1. 查找模式
具体有两种模式:
- Module 模式:这个模式下 CMake 会去
CMAKE_MODULE_PATH找Find<PackageName>.cmake,这种一般是 CMake 或库的用户提供的, i.e. 非官方的。 - Config 模式:这个模式下 CMake 会找
<package_name>-config[-version].cmake或<PackageName>Config[Version].cmake,查找的目录更加细致,这种一般由库提供, i.e. 官方的。
没有指定模式的情况下 CMake 会先使用 Module 模式,失败后 fallback 到 Config 模式。如果需要指定,可以:
1 | find_package(package_name MODULE) # 仅使用 Module 模式,不 fallback 到 Config 模式 |
4.4.2. 如果没找到
默认情况下 CMake 会输出一些警告,继续完成项目的生成,还可以加上 QUIET 或 REQUIRED 来禁用这些警告或停止下一步操作:
1 | find_package(package_name QUIET) # 没找到也不会输出警告,项目会照常生成 |
4.5. 链接时优化
也就是 GCC 中的 -flto 选项, CMake 称其为 IPO (Interprocedural Optimization) 。 CMake 3.9+ 添加了 INTERPROCEDURAL_OPTIMIZATION 用于设置链接时优化,其值由 CMAKE_INTERPROCEDURAL_OPTIMIZATION 初始化,可以像这样启用:
1 | set_target_properties(foo PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) |
但如果编译器不支持 IPO , CMake 会输出错误信息并异常退出,可以配合 CheckIPOSupported 模块解决这个问题:
1 | include(CheckIPOSupported) |
4.6. 使用 CCache
可以把类似的程序(包装一个编译指令)放进 <LANG>_COMPILER_LAUNCHER 里作为 target 的 PROPERTY ,其值由 CMAKE_<LANG>_COMPILER_LAUNCHER 初始化,比如对 C++ 全局使用 CCache :
1 | find_program(CCACHE ccache) # 找到 ccache ,把绝对路径放进 `CCACHE` 里 |