CMake总结
[TOC]
对于稍微有规模的工程, Linux 下的 C++ 开发下, 构建编译工具 CMake 很受欢迎. 基于自己的实战总结以及学习所记录的笔记,现整理于下. CMake 内容非常丰富,这里只是冰山一角,但是对于入门而言,内容应该够用了.我会持续总结整理.
简介
CMake(Cross platform Make)是个一个开源的跨平台自动化建构(编译,测试和打包)系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录,多个应用程序与多个库.
CMake 是 meta-building, 也就是说并不直接建构出最终的软件,而是产生标准的建构文档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依一般的建构方式使用.CMake 的配置文件取名为CMakeLists.txt. 官网地址,logo如下:
C++ 还有一套 Google 开发的构建工具 Bazel, 两者的区别与特点可以参考文章.
GCC,make 和 CMake的关系
首先 GCC 与 make 都只是CMake 支持的编译器平台之一. 对于 Linux 下的 C++ 开发, GCC/make 比较常用. 这里简单总结一下 GCC,make 和 CMake的关系.
GCC 离编译最近,为了节省 GCC 一个个输入参数以及多文件系统的效率,使用 make 工具自动构建.make的语法还是较为繁琐,CMake 则用更加贴近用户的方式自动生成 make 文件,属于更上一级的构建工具(meta building).它们的关系如下:
功能概况
共 cmake,ctest 和 cpack 三个命令行工具分别负责构建,测试和打包
使用 cmake 一般流程为:
.
- configure: 配置阶段
- generate: 生成构建系统(buildsystem,比如 make 工具对应的 Makefile);
- build: 执行构建(比如 make ),生成目标文件;
- 执行测试,安装或打包,甚至参与到 CI 的过程中与其他工具集成,例如代码检查等(非必须).
CMake-GUI
CMake 还提供了 GUI 以便调试. 我更倾向于命令行交互,因此只把安装过程写出来,具体使用教程请参考官网教程.
Ubuntu 下:
1 | #依赖于qt |
主界面如下:
核心语法
CMake 的命令有不同类型,包括脚本命令,项目配置命令和测试命令,细节可以查看官网cmake-commands.
CMake语言在项目配置中组织为三种源文件类型:
- 目录:
CMakeLists.txt
,针对的是一个目录,描述如何针对目录树(Source tree)生成构建系统,会用到项目配置命令; - 脚本:
<script>.cmake
,是一个 CMake 语言的脚本文件,可使用cmake -P
直接执行,只能包含脚本命令,无法执行构建过程,更无法 define build targets or actions; - 模块:
<module>.cmake
,实现一些模块化的功能,可以被前面两者包含,比如include(CTest)
启用测试功能.
CMakeLists.txt
的主要构成元素(也就是变量指代的可操作的概念):
- 目录
- 源码文件
- 构建目标(可执行文件,库)
- 属性
- 作用域
- 缓存条目
- 模块(非必须)
- 测试项目(非必须)
- 安装文件(非必须)
注释与转义
#
:注释
\
:转义,例如空格\
,\n
换行
$<COMMA>
:,
$<SEMICOLON>
:;
命令对大小写不敏感(case-insensitive).
打印命令
message
命令
1 | message([<mode>] "message text" ...) |
mode
选项
- 空或者
NOTICE
:比较重要的信息,如前面演示中的格式. DEBUG
:调试信息,主要针对开发者.STATUS
:项目使用者可能比较关心的信息,比如提示当前使用的编译器.WARNING
:CMake 警告,不会打断进程.SEND_ERROR
:CMake 错误,会继续执行,但是会跳过生成构建系统.FATAL_ERROR
:CMake 致命错误,会终止进程.
变量
变量的 type
STRING 字符串(数字是特殊的字符串),也可以被视为只有一个元素的列表.
字符串列表(直接设置多个值,或者使用分号
;
隔开)布尔值
- 0,OFF,NO,FALSE,N,IGNORE,空字符串,NOTFOUND,及以”-NOTFOUND”结尾的字符串均视为
False
. - ON,YES,TRUE,Y,非0值以及所有非
Fasle
变量均被视为True
;
- 0,OFF,NO,FALSE,N,IGNORE,空字符串,NOTFOUND,及以”-NOTFOUND”结尾的字符串均视为
如果从作用域等实用性角度来分,又分为两种变量类型. Normal Variables 与 Cache Variables. 前者有严格规则限定的作用域(有些也有类似全局的作用域,例如顶层的目录里的变量),后者则一定是全局属性的.
变量的设置
set()
函数- 普通的 STRING
- set(MyString Text)
- set(MyString “Some Text”)
- set(MyString Some\ Text)
- set(MyStringWithVar “Some other Text: ${MyString}”)
- set(MyStringWithQuot “Some quote: "${MyStringWithVar}"“)
- STRING list
- set(MyList “a” “b” “c”)
- set(MyList a b c)
- set(MyList a;b;c)
- set(MyList ${MyList} “d”)
- 数字
- set(NUM 30)
- 布尔值
- set(FLAG ON)
- 普通的 STRING
string()
函数- string(APPEND MyStringWithContent “ ${MyString}”)
list()
函数- list(APPEND MyList “a” “b” “c”)
- list(APPEND MyList “d”)
APPEND
,往列表中添加元素;LENGTH
,获取列表元素个数;JOIN
,将列表元素用指定的分隔符连接起来;
unset()
函数,取消变量定义
考虑到目录/文件名里经常含有空格,建议对所有的 string / list 元素都用双引号括起来.提高维护效率.
虽然set()
也可以用来处理 list, 最好还是用list()
函数处理.
变量的引用
${<variable>}
,在if()
条件判断中可以简化为只用变量名<variable>
.
1 | set(MySourcesList "File.name" "File with Space.name") |
变量名通常全部用大写.
当变量的值中存在空格时,注意转义符,或者加上双引号.
Normal Variable
对于 Normal Variable, 又可以按照场景细分几个类型,例如:
环境变量
设置方法:set(ENV{<variable>} [<value>])
1 | # 注意引用格式 |
环境变量的变化不会 re-trigger 构建过程.
有时候 IDE 生成的环境变量与命令行里的不一致,这时候建议把环境变量转换成某个 cache varible.
内置变量
CMAKE_
,_CMAKE_
或者以下划线开头后面加上任意 CMake 命令的变量.例如下面的常用目录变量.
Variable | Info |
---|---|
CMAKE_SOURCE_DIR | The root source directory |
CMAKE_CURRENT_SOURCE_DIR | The current source directory if using sub-projects and directories. |
PROJECT_SOURCE_DIR | The source directory of the current cmake project. |
CMAKE_BINARY_DIR | The root binary / build directory. This is the directory where you ran the cmake command. 生成的二进制目标文件也默认在这个目录 |
CMAKE_CURRENT_BINARY_DIR | The build directory you are currently in. |
PROJECT_BINARY_DIR | The build directory for the current project. |
project()
enable_language()
一旦使用会确认很多内置变量,如果我们想自定义一些内置变量,要在使用这两个函数之前.
源文件变量
1 | set(SOURCES |
Normal Variable 的 scope
为了方便下面的解说先介绍 scope 的概念, 下面的变量都是同一个变量名字,但是使用地方不一样:
- parent:父 scope.
- child:子 scope.
访问
在设定的 CMakeLists.txt
里以及通过 add_subdirectory()
, include()
, macro()
function()
函数的调用处都可以访问.
修改add_subdirectory()
以及 function()
无法修改传入的变量. 可以理解为 C++ 语法中的 pass-by-value
.如果想要修改的话,需要通过set(... PARENT_SCOPE)
的形式修改. 实质上的原因是add_subdirectory()
以及 function()
会创建一个 child scope,并把 parent scope 中的两都拷贝到里面导致的.
对于 include()
macro()
则是 pass-by-reference
,可以修改传入的变量. 同样地,实质原因是include()
macro()
不会去创建一个 child scope,因此可以直接修改变量的值.
代码实例:
add_subdirectory()
1 | # parent CMakeLists.txt |
结果为:”Parent”
function()
1 | cmake_minimum_required(VERSION 3.2) |
结果为:”Parent”
include()
1 | # parent CMakeLists.txt |
结果为:”Child”
macro()
1 | cmake_minimum_required(VERSION 3.2) |
结果为:”Child”
上述概念的图解如下:
图片以及代码实例请参考链接.
考虑到数据安全性,我们应该优先选择
functions()
取代macros()
.以及add_subdirectory()
取代include()
, 尤其是对第三库而言,我们不要改变其内容应优先考虑将其复制过来再操作.当然本意就是要去修改的,可以无视此建议.
Cache Variables
这种变量的出现为了解决一个问题,例如 -D
等选项通过命令行输入给 CMake 的参数不能每次都手动输入一遍. CMake 的解决办法是把这种特殊需求的变量独立出来成为并且缓存到构建目录的根目录下的 CMakeCache.txt
中, 通过命令行输入的参数/变量在第二次会先读取 CMakeCache.txt
,如果想要更新参数的话, 可以再在命令行覆盖掉它(上述推测,仅仅为了引出概念,不是准确的). 同时通过命令行传入的参数一般都是全局的变量(例如 C 中的 main() 参数). 从这两种性质(缓存性,全局性)出发基本上可以理解 cache variables 的各种性质. 源型如下:
1 | set(<variable> <value> CACHE <type> <string for description> [FORCE]) |
要点:
- 如果想要屏蔽命令行的更新可以使用
FORCE
选项. - type:STRING, BOOL,PATH.
INTERNAL
=STRING FORCE
,同时该变量无法在 CMake GUI 里访问.- 使用命令行更新:
cmake -D var:type=value
,cmake -D var=value
,cmake -C CMakeInitialCache.cmake
. - 取消
unset(... CACHE)
- 引用 Cache 变量:
$CACHE{<varialbe>}
. - cache variable不仅可以用在自定义的变量上也可以用在内置变量上,例如
CMAKE_INSTALL_PREFIX
. - 遇到 normal variable 与 cache variable 重名的情况下, CMake 会在自己的 scope 里先找到 normal variable 然后使用其值忽略 cache variable,如果找不到才会使用 cache variable,例子如下:
1 | # parent CMakeLists.txt |
运行结果:
1 | 第一次在父目录缓存变量 MY_GLOBAL_VAR=111 |
可以看到第三行与第四行的输出区别.
由于
CMakeCache.txt
的存在导致就算我们没有输入要求的参数也可以往下编译导致编译结果是错的,但我们无意识地去用,存在风险,这也就是需要有时候构造前需要rm -rf *
情况构建环境的原因之一.
为了避免 cache variable 的上述属性,可以考虑set_property(GLOBAL PROPERTY ...)
set_property(GLOBAL APPEND PROPERTY ...)
设置 property 的形式来保证全局性但是不附加上述的 persistant 性. 关于 property 会在下面讲解.
对于find_XXX
函数如果找成功了,建议用 cache variable 保存下来,节省每次搜索时间.
字符串操作
字符串值生成
无缝拼接即可
1 | include_directories(/usr/include/$<CXX_COMPILER_ID>/) |
转换大小写
1 | $<LOWER_CASE:string> |
更多可以参考后面的生成器表达式.
Alias Target 变量别名
1 | add_library(hello::library ALIAS hello_library) |
可以使用hello_library的别名: hello::library.
变量引用 Variable References
${${<variable>}}
的形式取一个变量variable
的值, 并新命名一个变量.- 常用于
function/macro
函数的参数. - 可以从内向外嵌套:
${outer_${inner_variable}_variable}
. - 常见的例子为:环境变量
$ENV{<variable>}
, 缓存变量$CACHE{<variable>}
.
变量的嵌套替换
类似于 bash 中的字符串变量,我们可以在变量里引用变量组成新的变量,并且对它的引用还会深度递归到无法替换为止,例如set(CMAKE_${lang}_COMPILER ...)
.
但是要注意此种用法在if()
中的陷阱.
例如:当 CMAKE_CXX_COMPILER_ID
是 "MSVC"
, MSVC
是 "1"
时:if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
一定为 true, 因为它等效于 if("1" STREQUAL "1")
.if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
一定为 false, 因为它等效于 if("MSVC" STREQUAL "1")
.
因此使用 if(MSVC)
可以避免上述歧义.
cmake_policy(SET CMP0054 NEW)
CMake 3.1 起可以先约束:仅当if()
的参数不是大括号括起来时才会被解释成变量/关键字.
变量的预定义
当我们想预定义一个变量,先占个坑,后面有可能再赋值. CMake 提供了 option()
函数帮我们实现这一点.
option(<variable> "<help_text>" [value])
- 如果变量没被初始化, 变量值变成
OFF
(用于 if 判断). 如果被定义了则是被定义的值. - 也可以显式设定没被初始化的变量为
ON
.
1 | if(address) |
输出为
1 | NOT defined address! |
- 注意大括号后变量值会真的成为字符串
ON/OFF
而不是布尔值的ON/OFF
.1
2
3if(${address} STREQUAL "ON")
else()
endif()
变量的 debug
message()
函数打印- 检查
CMakeCache.txt
, 即便构建失败也会生成CMakeCache.txt
. - 使用
variable_watch()
函数观察. cmake --trace .....
选项打印更多信息.
Properties
Cmake 中还有一种存储信息的方式,就是用 property.它就像一个变量,但需要依附于其他 item,比如 directory 或 target.
设置 property
1 | # Sets one property on zero or more objects of a scope. |
第一种方式通用性更好,且可以指定多个 target/file/test 等.
第二种方式就是专门用来给 target 设置 property 的,它可以为单个 target 设置多个 property.
获取 property
1 | # Gets one property from one object in a scope. |
property 种类
大全可以参考官网链接.
分类如下:
- Properties of Global Scope
- Properties on Directories
- Properties on Targets
- Properties on Tests
- Properties on Source Files
- Properties on Cache Entries
- Properties on Installed Files
条件语句
条件表达式
三种形式:
$<condition:true_string>
:如果条件为真,则结果为true_string
,否则为空.$<IF:condition,str1,str2>
:如果条件为真,则结果为str1
,否则为str2
1 | if(expression) |
else和endif中的表达式是可以省略的.
使用小括号可以组合多个条件语句,比如:(cond1) AND (cond2 OR (cond3))
.
1 | set(EMPTY_STR "") |
判断条件
字符串(包括数值)比较,比如:
STREQUAL
,STRLESS
,STRGREATER
,EQUAL
,LESS
,GREATER
等;1
2
3$<STREQUAL:string1,string2>#判断字符串是否相等
$<EQUAL:value1,value2>#判断数值是否相等
$<IN_LIST:string,list>#判断string是否包含在list中,list使用分号分割布尔运算,
AND
,OR
,NOT
,BOOL
;1
2
3
4
5
6#如果字符串为空,0;不区分大小写的FALSE,OFF,N,NO,IGNORE,NOTFOUND
#或者区分大小写以-NOTFOUND结尾的字符串,则为0,否则为1
$<BOOL:string>
$<AND:conditions>
$<OR:conditions>
$<NOT:condition>路径判断,比如:
EXISTS
,IS_DIRECTORY
,IS_ABSOLUTE
等;变量判断
1
2
3
4$<TARGET_EXISTS:target>#判断目标是否存在
$<CONFIG:cfgs>#判断编译类型配置是否包含在cfgs列表(比如"release,debug")中;不区分大小写
$<PLATFORM_ID:platform_ids>#判断CMake定义的平台ID是否包含在platform_ids列表中
$<COMPILE_LANGUAGE:languages>#判断编译语言是否包含在languages列表中版本号判断
列表元素判断
1
$<CONFIG:cfgs>
if表达式可以用长表达式,优先级顺序如下:
1 | > EXISTS, COMMAND, DEFINED |
循环语句
foreach 循环
第一种形式
1
2
3
4
5foreach(loop_var arg1 arg2 ...)
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
endforeach(loop_var)注意endforeach(loop_var)的变量最好不要省略,因为foreach循环是依靠变量来跳出循环的.
第二种形式
1
foreach(loop_var RANGE total)
第三种形式
1
foreach(loop_var RANGE start stop [step])
第四种形式
1
2foreach(loop_var IN [LISTS [list1 [...]]]
[ITEMS [item1 [...]]])
while循环
1 | while(condition) |
注意endwhile中的条件最好不要省略.
在while和foreach循环中,取变量的值请用
${var}
.break
和continue
的用法基本与c一样,放心使用.
文件操作
读写,创建或复制文件和目录,计算文件hash,下载文件,压缩文件等等.
1 | file(GLOB_RECURSE ALL_SRC |
GLOB_RECURSE
表示执行递归查找,查找目录下所有符合指定正则表达式的文件.
执行系统命令
execute_process
命令可以执行一条或者顺序执行多条系统命令.
比如获取当前仓库最新提交的commit的commit id:
1 | execute_process(COMMAND bash "-c" "git rev-parse --short HEAD" OUTPUT_VARIABLE COMMIT_ID) |
查找依赖
查找第三方模块
这里的第三方模块包括第三方库,头文件,模块结构等. 一般而言第三方库,有比较完备的目录/依赖体系,会整理出一个针对 CMake 的接口. CMake 官方也会支持引入常用第三方库的接口. 对于用户, 只需要知道接口 find_package()
函数. 此函数会去2类目录(2种 mode)下查找:
Module mode:
CMAKE_MODULE_PATH
定义的目录下(如果不设置默认/usr/share/cmake_VERSION_NUMBER/Modules
, 如果设置了的话,搜索目录为上述目录+自定义的目录 )查找FindXXX.cmake
的脚本文件.
例如 Boost 库就有官方的支持接口,FindBoost.cmake
.对于更多模块可以查看,官网的链接:**cmake-modules**Config mode:
CMAKE_PREFIX_PATH
定义的目录以及系统的一些默认目录.查找XXXConfig.cmake
文件.
例如:CMake(3.15.0) 里的CMAKE_MODULE_PATH
没有 eigen(v3.2.92) 的官方支持,/usr/lib/cmake/eigen3
目录下安装有Eigen3Config.cmake
文件, 它再去调用UseEigen3.cmake
.
1 | /usr/lib/cmake/eigen3 $ tree |
Eigen3Config.cmake
文件内容如下,可以看到是一些目录/版本号的定义以及对 UseEigen3.cmake
的调用.
1 | set ( EIGEN3_FOUND 1 ) |
UseEigen3.cmake
的内容如下.
1 | add_definitions ( ${EIGEN3_DEFINITIONS} ) |
上面只是特定例子模块的构成,不同模块可能不同.
具体语法
1 | find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system) |
参数分别为:模块名字,模块版本,REQUIRED
强依赖(找不到报错中止),后面的两个为模块的子模块以及子模块的子模块.
导出的变量
CMake 规定了一些变量,可以供 find_package
后使用.
XXX_FOUND
,查找到与否. 例如上面例子中的set ( EIGEN3_FOUND 1 )
.XXX__INCLUDE_DIRS
,定位目录,例如上面例子中的set ( EIGEN3_INCLUDE_DIR "/usr/include/eigen3" )
.XXX_LIBRARY
,定位库的目录.
这些变量可以用来判断,或者进一步精细化的操作.
1 | if(Boost_FOUND) |
模块别名
从3.5 版本开始支持第三库的别名功能.例如, Boost 库.
Boost::boost -> header only libraries
Boost::system -> the boost system library.
Boost::filesystem -> filesystem library.
这样就可以与C++代码里的模块用法无缝对接:
1 | target_link_libraries( third_party_include |
include其他模块
include
命令将cmake文件或者模块加载并执行.它与 find_package()
的 module mode
通过类似的目录进行搜索.但是对于第三方的库,最好还是使用 find_package()
进行搜索,原因是通过 find_package()
搜索到的模块仍旧是 第三方模块 private
的.
1 | include(CPack) # 开启打包功能 |
查找库
通过find_library
在指定的路径和相关默认路径下查找指定名字的库,常用的格式如下:
1 | find_library (<VAR> name1 [path1 path2 ...]) |
查找源码文件
aux_source_directory(< dir > < variable >)
搜集所有在指定路径下的源文件的文件名,将输出结果列表储存在指定的变量中.这样可以避免手工罗列所有的实例.但是也有缺点,CMake 无法感知 dir 目录下有新的文件添加进来,这样不会在有新的源文件加进来时重新编译.
编译目标查询
这里的查询是指获取编译目标(通过add_executable()
,add_library()
命令生成的)相关的一些信息,包括:
$<TARGET_FILE:tgt>
:获取编译目标的文件路径$<TARGET_FILE_NAME:tgt>
:获取编译目标的文件名$<TARGET_FILE_BASE_NAME:tgt>
:获取编译目标的基础名字,也就是文件名去掉前缀和扩展名
配置输出
到文件
1 | file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<CONFIG:Debug>:-g;-O0>,$<PLATFORM_ID>\n") |
到命令行
1 | add_custom_target(gentest COMMAND ${CMAKE_COMMAND} -E echo "\"$<$<CONFIG:Debug>:-g;-O0>,$<PLATFORM_ID>\"") |
生成器表达式 generator-expressions
如其名字,生成器表达式主要用来方便地生成一些值,相当于 C++ 里的 lambda 函数.例如与其写多行的 if-else 语句, 生成器表达式只需要一行.
如前文所介绍,CMake 包含四个阶段. 其中 configuration 阶段无法使用生成器表达式, generation 与 build 阶段可以使用生成器表达式.
生成器表达式有2类, 根据生成的值是布尔值还是字符串划分.一般支持嵌套.官网生成器表达式大全.
布尔值生成表达式
- 逻辑符号
原型:$<condition:value-if-true>
1 | #如果为 debug 模式就把 debug.c 文件包含进 mylib 里参与构建.嵌套了下面的$<CONFIG:cfgs>. |
字符串比较
1
2
3
4
5
6
7
8$<STREQUAL:string1,string2>
$<EQUAL:value1,value2>
$<IN_LIST:string,list>
$<VERSION_LESS:v1,v2>
$<VERSION_GREATER:v1,v2>
$<VERSION_EQUAL:v1,v2>
$<VERSION_LESS_EQUAL:v1,v2>
$<VERSION_GREATER_EQUAL:v1,v2>Variable Queries 参数为变量
1 | $<TARGET_EXISTS:target> |
$<CONFIG>
表达式在 single-configuration generator 的情况下$<CONFIG>
=${CMAKE_BUILD_TYPE}
.但是在 Multi-configuration generator 时不相同, 因为可以在 build 阶段改变.详细介绍在后文.
字符串生成表达式
符号生成
1
2
3
4$<ANGLE-R> # ">"
$<COMMA> # ","
$<SEMICOLON> # ";"条件表达式
1
2$<condition:true_string>
$<IF:condition,true_string,false_string>字符串操作
1
2
3
4
5
6
7$<JOIN:list,string>
$<REMOVE_DUPLICATES:list> #3.15版本以上
$<FILTER:list,INCLUDE|EXCLUDE,regex> #3.15版本以上,支持正则表达
$<LOWER_CASE:string>
$<UPPER_CASE:string>
$<GENEX_EVAL:expr> #3.12版本以上
$<TARGET_GENEX_EVAL:tgt,expr> #3.12版本以上Variable Queries
1
2
3
4
5
6
7$<CONFIG>
$<CONFIGURATION>
$<PLATFORM_ID>
$<C_COMPILER_ID>
$<CXX_COMPILER_ID>
$<COMPILE_LANGUAGE> # 3.3版本以上
$<LINK_LANGUAGE> # 3.18版本以上Target-Dependent Queries 目标相关
1
2
3
4
5
6$<TARGET_NAME_IF_EXISTS:tgt> # 3.12版本以上
$<TARGET_FILE:tgt>
$<TARGET_FILE_BASE_NAME:tgt> # 3.15版本以上
$<TARGET_LINKER_FILE_PREFIX:tgt>
$<TARGET_SONAME_FILE:tgt>
$<TARGET_PROPERTY:tgt,prop>输出相关的Output-Related Expressions
1 | $<TARGET_NAME:...> # This is required if exporting targets to multiple dependent export sets. 不支持嵌套 |
一个例子:
1 | add_library(Foo ...) |
这里不会把所有项目一一列出,更多参考官网.
生成器表达式的调试
由于生成器表达式不会在 CMakeLists.txt
处理的过程中被计算,只在构建时计算.因此message()
函数无法打印出其值.
有2种方式打印出来.
add_custom_target
使用”虚拟” target1
add_custom_target(genexdebug COMMAND ${CMAKE_COMMAND} -E echo "$<...>")
- 使用文件名
1
file(GENERATE OUTPUT filename CONTENT "$<...>")
指定构建平台
除了 GCC 与 make, CMake 还支持其他很多平台,通过cmake --help
命令可以得到如下输出(v3.15):
1 | Generators |
可以大致分为3类:
- 纯命令行构建的 MakeFiles 类.
1
2
3
4
5
6
7
8Borland Makefiles
MSYS Makefiles
MinGW Makefiles
NMake Makefiles
NMake Makefiles JOM
Ninja
Unix Makefiles
Watcom WMake - IDE构建的配置文件类
1
2
3
4
5
6
7
8
9Visual Studio 6
Visual Studio 7
Visual Studio 7 .NET 2003
Visual Studio 8 2005
Visual Studio 9 2008
Visual Studio 10 2010
Visual Studio 11 2012
Visual Studio 12 2013
Xcode - 调用其他IDE构建的类(依赖于配置文件或者 MakeFiles)
1
2
3
4
5
6CodeBlocks
CodeLite
Eclipse CDT4
KDevelop3
Kate
Sublime Text 2
指定构建平台
1 | $ cmake .. -G Ninja |
Single-configuration generator 与 Multi-configuration generator
根据平台支持在 build 阶段修改 CMAKE_BUILD_TYPE
(构建类型)与否,将平台分为2种类型: Single-configuration generator 与 Multi-configuration generator.
Single-configuration generator 无法在 build 阶段修改构建类型, 代表的平台为 Unix Makefiles generator.
1 | cmake -H. -B_builds -DCMAKE_BUILD_TYPE=Debug |
Multi-configuration generator 可以在 build 阶段修改构建类型, 代表的平台为 Xcode ,Visual Studio.
1 | cmake -H. -B_builds -DCMAKE_CONFIGURATION_TYPES=Release;Debug -GXcode |
CMakeLists.txt基础
基础配置
设置项目版本和生成version.h
project
命令配置项目信息
1 | project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX) |
CMakeTemplate:项目名
VERSION
指定版本号,格式为main.minor.patch.tweak
,并且CMake会将对应的值分别赋值给以下变量(如果没有设置,则为空字符串):1
2
3
4
5PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH
PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK结合
configure_file
命令(后面详细介绍),可以配置自动生成版本头文件,将头文件版本号定义成对应的宏,或者定义成接口,方便在代码运行的时候了解当前的版本号.例如:1
configure_file(src/c/cmake_template_version.h.in "${PROJECT_SOURCE_DIR}/src/c/cmake_template_version.h")
假如
cmake_template_version.h.in
内容如下:1
2
3#define CMAKE_TEMPLATE_VERSION_MAJOR @CMakeTemplate_VERSION_MAJOR@
#define CMAKE_TEMPLATE_VERSION_MINOR @CMakeTemplate_VERSION_MINOR@
#define CMAKE_TEMPLATE_VERSION_PATCH @CMakeTemplate_VERSION_PATCH@执行cmake配置构建系统后,将会自动生成文件:
cmake_template_version.h
,其中@<var-name>@
将会被替换为对应的值:1
2
3#define CMAKE_TEMPLATE_VERSION_MAJOR 1
#define CMAKE_TEMPLATE_VERSION_MINOR 0
#define CMAKE_TEMPLATE_VERSION_PATCH 0
指定 CMake 版本
cmake_minimum_required(VERSION 3.5)
.
指定编程语言版本
1 | set(CMAKE_C_STANDARD 99) |
也可以通过配置编译选项实现,见下面.
配置编译选项
add_compile_options
命令可以为所有编译器配置编译选项(同时对多个编译器生效); 通过设置变量CMAKE_C_FLAGS
可以配置c编译器的编译选项; 而设置变量CMAKE_CXX_FLAGS
可配置针对c++编译器的编译选项. 比如:
1 | add_compile_options(-Wall -Wextra -pedantic -Werror) |
指定编译器/链接器
CMAKE_C_COMPILER
, CMAKE_CXX_COMPILER
,CMAKE_LINKER
.
-D
选项命令行传入
1 | cmake .. -DCMAKE_C_COMPILER=clang-3.6 -DCMAKE_CXX_COMPILER=clang++-3.6 |
命令行输入脚本化的例子:
1 |
|
配置编译类型
设置内置变量
CMAKE_BUILD_TYPE
:Debug
,Release
,RelWithDebInfo
,MinSizeRel
等,比如:
1 | set(CMAKE_BUILD_TYPE Debug) |
针对不同的编译类型设置不同的编译选项,比如对于Debug
版本,开启调试信息,不进行代码优化:
1 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0") |
对于Release
版本,不包含调试信息,优化等级设置为2:
1 | set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2") |
执行cmake
命令的时候通过参数-D
指定
1 | cmake -B build -DCMAKE_BUILD_TYPE=Debug |
添加全局宏定义
add_definitions
命令.
CMakeLists.txt
内容如下:
1 | option(TEST_DEBUG "option for debug" OFF) |
构建时使用命令行参数控制参数宏的开启与关闭.
1 | cmake -DTEST_DEBUG=1 .. #打开 |
源码中就可以使用 flag 对代码进行开关控制.
1 |
|
注意,在后期的版本中此命令被如下几个命令代替
- Use
add_compile_definitions()
to add preprocessor definitions. - Use
include_directories()
to add include directories. - Use
add_compile_options()
to add other options.
include目录
包含目录有如下几种命令:
通过命令include_directories
来设置头文件的搜索目录
target_include_directories()
目的:针对构建 target 指定所需的文件(含目录)
原型:
1 | target_include_directories(<target> [SYSTEM] [BEFORE] |
参数讲解
<target>
:需要经过add_executable()
或add_library()
添加过的,不能为IMPORTED
target.BEFORE
:指定的propety
是先加到 target 上再去执行构建. 如果不显示使用此参数, 执行到此步时再追加到 target 上.- 这里需要注意的是包含路径会成为主目录,也就是说 cpp 文件包含头文件要使用基于其主目录的相对路径.
如下目录:1
2
3
4
5
6
7
8
9$ tree
.
├── CMakeLists.txt
├── include
│ └── static
│ └── Hello.h
└── src
├── Hello.cpp
└── main.cppCMakeLists.txt
在1
2
3
4target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)Hello.cpp
中对头文件的引用格式如下:#include "static/Hello.h"
- 对同一个 target 多次执行此命令,将按照顺序添加文件以及属性.
aux_source_directory
命令
编译目标文件
编译目标(target)的类型一般有静态库,动态库(add_library()
)和可执行文件(add_executable()
). 这时编写CMakeLists.txt
主要包括两步:
- 编译:确定编译目标所需要的源文件
- 链接:确定链接的时候需要依赖的额外的库
编译库
1 | file(GLOB_RECURSE MATH_LIB_SRC |
- 使用
file
命令获取待编译的源码文件. add_library
命令编译STATIC
:静态库,SHARED
:动态库target_include_directories
:编译目标所依赖的文件/目录- 路径传递属性:如下
开放度的属性解读如下图:
一个 sub_object 的PRIVATE
的属性使得,再上一级的目标 super_object 无法使用 sub_object.
一个 sub_object 的INTERFACE
的属性使得,再上一级的目标 super_object 可以使用 sub_object,但是它的上一级 object 无法使用它.因此只是充当一个接口放在了 object 里. 实际应用的话, 这个属性是不是非常贴合 head-only 的库(仅头文件), 头文件本身不需要编译, 然而对于依赖以及更上一级的依赖而言需要引用头文件. 因此INTERFACE
的属性非常适合仅头文件的库.
一个 sub_object 的PUBLIC
的属性使得,再 super_object 以及 object 都使用 sub_object.
这里的 object 可以是target_include_directories
里的目录, 也可以是add_library
里的 target构建目标.
这些路径被添加到构建目标里后,当更高一层次的构建目标使用当前构建目标时,可以再次使用PRIVARE等关键字,组合过滤目录的暴露性.
1 | add_executable(main |
如上main对math使用了PRIVATE,也就意味着其他链接到main的目标无法访问math的目录了,即便math对目录的添加是public性质的.
编译可执行文件
1 | add_executable(demo src/c/main.c) |
add_executable
命令构建可执行文件(参数分别为可执行文件名, 源码文件名 list). 可以让 project 的名字与构建目标一致:add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries
命令来声明构建此可执行文件需要链接的库
构建类型
根据构建目标中含有 debug 信息的多少以及优化水平可以指定构建的类型.
- Release - 添加 -O3 -DNDEBUG flags
- Debug - 添加 -g flag
- MinSizeRel - 添加 -Os -DNDEBUG
- RelWithDebInfo - 添加 -O2 -g -DNDEBUG flags
命令行输入编译类型
1 | cmake .. -DCMAKE_BUILD_TYPE=Release |
1 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) |
当然也可以通过指定编译器选项例如CMAKE_CXX_FLAGS_DEBUG
变量直接手动指定.
通过 CMake 向工程源码传递参数
我们可以通过 configure_file()
函数把 CMake 的配置文件 NAME.h.in
转换成 NAME.h
头文件供指定目录下的源码进行访问, 由此可以实现编译时期进行调参的需求. 前文已经提及了生成版本号的用处.
1 | configure_file(<input> <output> |
实现的功能可以概括为:将 input 文件复制到 output 文件,并在输入文件内容中的变量,替换引用为 @VAR@ 或 ${VAR} 的变量值.每个变量引用将替换为该变量的当前值,如果未定义该变量,则为空字符串.
选项含义:
COPYONLY
:只拷贝文件,不进行任何的变量替换.这个选项在指定了NEWLINE_STYLE
选项时不能使用(无效).ESCAPE_QUOTES
:躲过任何的反斜杠(C风格)转义.@ONLY
:限制变量替换,让其只替换被@VAR@
引用的变量(那么${VAR}
格式的变量将不会被替换).这在配置${VAR}
语法的脚本时是非常有用的.NEWLINE_STYLE
style:指定输出文件中的新行格式.UNIX 和 LF 的新行是\n
,DOS 和 WIN32 和 CRLF 的新行格式是\r\n
.这个选项在指定了COPYONLY
选项时不能使用(无效).
简单的例子如下:
比如在 CMakeLists.txt 中定义了如下的变量:
1 | set(BUILD_Version 1) |
输入文件 temp.h.in 中为:
1 | #define BUILD_Version @BUILD_Version@ |
那么,在输出文件 temp.h 中就会被转化为:
1 |
一个具体的使用案例是有些时候,我们需要在项目中标明版本号,Git 的 hash 号,编译时间等信息,但是显然,对于 Git 的 hash 号,编译时间我们不想自己手动填写.现在提供一种途径,将这些信息写入到头文件中,再编译到so库文件或者可执行程序中.这样,就可以通过提供库文件的接口或者可执行程序的打印中得到这些值了.
安装和打包
安装库与可执行文件
- 通过
install
命令来说明需要安装的内容及目标路径 - 通过设置
CMAKE_INSTALL_PREFIX
变量说明安装的路径
有2种方式:set
命令指定,如下面是将默认的/usr/local/
改成当前构建的目录下面(注意此种方法要求设置在构建目标前).也可以1
2
3
4if( CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT )
message(STATUS "Setting default CMAKE_INSTALL_PREFIX path to ${CMAKE_BINARY_DIR}/install")
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE STRING "The path to use for make install" FORCE)
endif()cmake .. -DCMAKE_INSTALL_PREFIX=/install/location
在命令行输入. 3.15
往后的版本可以使用cmake --install --prefix <install-path>
覆盖指定安装路径.
1 | install(TARGETS math demo |
TARGETS
:指定需要安装的目标列表RUNTIME DESTINATION
,LIBRARY DESTINATION
,ARCHIVE DESTINATION
:分别指定应该安装到安装目录下个哪个子目录.ARCHIVE
一般是指静态库,LIBRARY
则是指共享库,在不同平台上,略有差异.如果是 DLL 库的话,需要如下指定安装目录1
2
3install (TARGETS library_name
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
例子
1 | # cmake -B cmake-build -DCMAKE_INSTALL_PREFIX=./output |
install命令详解
install
命令可以安装的目标类型:
- 构建目标
- 文件
- 程序
- 目录
- 执行自定义的脚本
- 执行自定义的命令
- EXPORT
1 | install(TARGETS <target>... [...]) |
常用共用关键字
DESTINATION:安装对象的目标安装路径,可以是绝对路径,也可以是相对路径,如果是相对路径,则认为是相对于
CMAKE_INSTALL_PREFIX
的.注意cpack
并不支持绝对路径,所以建议还是不要使用绝对路径.CONFIGURATIONS:为不同的配置设置不同的安装规则.
1
2
3
4
5
6
7#对Debug和Release两个配置不同的安装路径
install(TARGETS target
CONFIGURATIONS Debug
RUNTIME DESTINATION Debug/bin)
install(TARGETS target
CONFIGURATIONS Release
RUNTIME DESTINATION Release/bin)PERMISSIONS:设置安装目标的权限,接受的参数是一个权限关键字列表,比如:
1
2install(TARGETS target
RUNTIME PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
安装目录
完整命令格式:
1 | install(DIRECTORY dirs... |
参数 | 作用 |
---|---|
TYPE/DESTINATION | 二选一.使用TYPE 指定安装的目录中的文件类型,然后CMake会自动按照类型分配安装目录.DESTINATION 粗暴安装. |
FILES_MATCHING | 使用此参数表示必须要满足对应的模式或者正则的文件才能被安装 |
PATTERN/REGEX | PATTERN 表示文件名完全匹配才会被安装,而REGEX 则是通过正则表达式匹配目标安装文件(针对目标文件的全路径);在这两个表达式后面还可以加上EXCLUDE 表示反选,或者使用PERMISSIONS 指定匹配的目标文件的权限. |
DESTDIR
有时候我们担心直接安装到系统里有错,撤销的成本太高,我们可以用下面的语句把文件先都安装到${DESTDIR}/${CMAKE_INSTALL_PREFIX}
下面再做检查.
1 | make install DESTDIR=/tmp/stage |
安装文件
完整命令格式:
1 | install(<FILES|PROGRAMS> files... |
FILES
和PROGRAMS
的不同之处在于文件的默认权限,前者是一般文件(头文件,配置文件等),而后者为可执行文件,默认有可执行权限,包括:OWNER_EXECUTE
,GROUP_EXECUTE
和WORLD_EXECUTE
.
例如安装头文件:
1 | file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h) |
安装的时候执行自定义的脚本/命令
1 | install([[SCRIPT <file>] [CODE <code>]] |
执行安装
1 | cmake --build . --target install |
删除安装make install
会生成一个 install_manifest.txt
的 log 文件记录安装的文件及其目标目录,可以按图索蕀地删除,如下:
1 | sudo xargs rm < install_manifest.txt |
打包
include(CPack)
启用打包功能include(CPack)
会在构建路径(Build tree)下生成两个cpack
的配置文件,CPackConfig.cmake
和CPackSourceConfig.cmake
,其实也就对应了两个构建目标:package
和package_source
;在执行构建编译之后使用
cpack
命令行工具进行打包安装;对于make工具,也可以使用命令make package
1 | include(CPack) |
变量 | 用途 |
---|---|
CPACK_GENERATOR |
打包使用的压缩工具,比如”ZIP” |
CPACK_OUTPUT_FILE_PREFIX |
打包安装的路径前缀 |
CPACK_INSTALL_PREFIX |
打包压缩包的内部目录前缀 |
CPACK_PACKAGE_FILE_NAME |
打包压缩包的名称,由CPACK_PACKAGE_NAME ,CPACK_PACKAGE_VERSION ,CPACK_SYSTEM_NAME 三部分构成 |
例子
1 | # cmake -B cmake-build -DCPACK_OUTPUT_FILE_PREFIX=`pwd`/output |
打包执行命令
cpack命令
1
2cpack -G TGZ --config CPackConfig.cmake
cpack -G TGZ --config CPackSourceConfig.cmake-G
参数指定生成器,常用的有ZIP
,TGZ
,7Z
等,可以同时指定多个--config
参数可以指定打包配置文件
cmake
命令1
2cmake --build . --target package
cmake --build . --target package_sourcemake
命令1
2make package
make package_source
测试
CMake默认测试
option
命令设置测试开关1
2
3
4
5option(CMAKE_TEMPLATE_ENABLE_TEST "Whether to enable unit tests" ON)
if (CMAKE_TEMPLATE_ENABLE_TEST)
message(STATUS "Unit tests enabled")
enable_testing()
endif()CMakeLists.txt
中通过命令enable_testing()
或者include(CTest)
来启用测试功能;使用
add_test
命令添加测试样例,指定测试的名称和测试命令,参数;1
2add_test(NAME test_add COMMAND test_add 10 24 34)
add_test(NAME test_minus COMMAND test_minus 40 96 -56)- NAME:测试名称
- COMMAND:测试文件名与测试参数
构建编译完成后使用
ctest
命令行工具运行测试.1
2
3
4
5
6
7
8
9
10# cmake -B cmake-build
# cmake --build cmake-build
# cd cmake-build && ctest && cd -
Test project /Users/Farmer/gitee/cmake-template/cmake-build
Start 1: test_add
1/2 Test #1: test_add ......................... Passed 0.00 sec
Start 2: test_minus
2/2 Test #2: test_minus ....................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 2- ctest -VV:看到更加详细的测试流程和结果
- –test-dir:3.20往后的版本中,指定测试执行目录
集成Google Test
- 整个gtest源码拷贝到项目中,每次一起编译.
例如把gtest源码考到third_party目录中1
2
3
4
5
6
7
8
9
10
11
12
13
14├── CMakeLists.txt
├── src #待测试工程
│ ├── CMakeLists.txt
│ ├── add.cpp
│ └── add.h
├── test #测试工程
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── test.cpp
└── third_party #包括gtest的第三方库源码
└── gtest
├── googletest
├── CMakeLists.txt
└── ...
- 待测试项目编为库:src/CMakeLists.txt
1
2
3
4
5
6
7
8cmake_minimum_required(VERSION 3.10.2)
project(src)
# 定义需要参与编译的源文件
aux_source_directory(. source)
# 把源码添加进来参与编译
add_library(${PROJECT_NAME} ${source})
# 定义需要暴露的头文件
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) - 测试工程编为可执行文件test/CMakeLists.txt
1
2
3
4cmake_minimum_required(VERSION 3.10.2)
project(test)
add_executable(${PROJECT_NAME} main.cpp test.cpp)
target_link_libraries(${PROJECT_NAME} gtest src)
模块化及库依赖
定义子目录的构建系统
只要是定义目录的构建系统,都是在此目录下创建一个CMakeLists.txt
文件,并且在头部定义一个子项目 project()
. 当使用 project()
函数后,会随之产生与项目相关的一些变量.
变量 | 用处 |
---|---|
PROJECT_NAME | 当前 project() 设定的项目名字 |
CMAKE_PROJECT_NAME | 最顶端的project() 设定的项目总名字 |
PROJECT_SOURCE_DIR | 当前项目的路径,CMakeLists.txt 的路径 |
PROJECT_BINARY_DIR | 当前项目的构建路径 |
XXX_SOURCE_DIR | XXX 项目的路径 |
XXX_BINARY_DIR | XXX 项目的构建路径 |
仅头文件的库
1 | project (sublibrary2) |
包含子目录
命令add_subdirectory
1 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) |
source_dir
就是要包含的目标目录,该目录下必须存在一个CMakeLists.txt
文件,一般为相对于当前CMakeLists.txt
的目录路径,当然也可以是绝对路径binary_dir
是可选的参数,用于指定子构建系统输出文件的路径,相对于当前的Binary tree
,同样也可以是绝对路径. 一般情况下,source_dir
是当前目录的子目录,那么binary_dir
的值为不做任何相对路径展开的source_dir
;但是如果source_dir
不是当前目录的子目录,则必须指定binary_dir
,这样CMake才知道要将子构建系统的相关文件生成在哪个目录下.- 如果指定了
EXCLUDE_FROM_ALL
选项,在子路径下的目标默认不会被包含到父路径的ALL
目标里,并且也会被排除在IDE工程文件之外.但是,如果在父级项目显式声明依赖子目录的目标文件,那么对应的目标文件还是会被构建以满足父级项目的依赖需求.
子项目下包含目录
target_include_directories()
函数.这个函数尤其适用于把头文件单独放在.../include/PROJECT_NAME
目录下防止头文件命名冲突. 例如上面例子中的使用.
1 | $ tree |
#include "sublib1/sublib1.h"
.
1 | target_include_directories(${PROJECT_NAME} |
但要注意的是,安装时也需要加上父目录, 例如/usr/local/include/sublib1/sublib1.h
导入编译好的目标文件
使用
add_library
命令,通过指定IMPORTED
选项表明这是一个导入的库文件,通过设置其属性指明其路径:1
2
3add_library(math STATIC IMPORTED)
set_property(TARGET math PROPERTY
IMPORTED_LOCATION "./lib/libmath.a")也可以使用
find_library
命令来查找1
find_library(LIB_MATH_DEBUG mathd HINTS "./lib")
对于不同的编译类型,可以通过
IMPORTED_LOCATION_<CONFIG>
来指明不同编译类型对应的库文件路径:1
2
3
4
5
6add_library(math STATIC IMPORTED GLOBAL)
set_target_properties(math PROPERTIES
IMPORTED_LOCATION "${LIB_MATH_RELEASE}"
IMPORTED_LOCATION_DEBUG "${LIB_MATH_DEBUG}"
IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
)
与第三方库配合案例
配合 protobuf 自动生成源码文件参与构建
Google 的 ProtoBuf 是比 JSON 或 XML 等在效率,兼容性上更加优秀的数据序列化库. 其主要操作是把阅读性较高的 .proto
文件转换成 C++ 的 .pb.cc
.pb.h
文件实现功能(其他语言生成想对应的文件格式).
1 | $ tree |
main.cpp
依赖 AddressBook.proto
中的数据.
1 | cmake_minimum_required(VERSION 3.5) |
PROTOBUF_GENERATE_CPP()
函数会生成 PROTO_SRCS
(.pb.cc
文件), PROTO_HDRS
(.pb.h
文件).然后传递给add_executable()
函数参与构建.
CI 中的 CMake
包括如下方面(不限于),详细例子可以参考git.
代码静态检查
单元测试(例如与 Google Test 的集成)
pkg管理
部署
构建脚本
构建目录:in-place, out-of-source
分别为 cmake .
, cmake <relative_path_to_project_root_path>
前者在项目的目录(包含 CMakeLists.txt)里直接构建,后者只要指定项目目录就可以随地构建. 推荐后者否则 CMake 的中间文件与源码的混合导致混乱. 因此一般会在项目里新建一个 build
目录, 在其中构建.
示例
1 |
|
GCC编译过程和CMake命令之间的关联
gcc参数 | CMake命令 | 含义 |
---|---|---|
-D |
add_ definitions |
设置预编译宏 |
编译器选项 | add_compile_options |
设置:编译器的选项,控制编译行为 |
-I |
include_directories |
设置头文件搜索路径 |
-L |
link_directories /target_link_directories |
指定链接器搜:索库文件的路径 |
-I |
link_libraries /target_link_libraries |
指定要链接的库文件 |
链接器选项 | add_link_options /target_link_options |
指定链接器的链接选项 |
预处理
在预处理阶段,主要处理各种宏,开发的过程中往往会通过#ifdef
来判断是否定义了对应的宏,来灵活地切换不同代码,比如:
1 |
这个时候,如果需要使用大写的版本,就可以使用gcc
的-D
参数:
1 | gcc -DUPPER_CASE ... |
而在 CMake 中,可以使用命令:
1 | add_definitions(-DUPPER_CASE) |
编译
在编译的时候,需要把源文件处理成机器代码,主要有两个方面:
- 对于源文件里面的代码具体怎样进行编译
- 源文件内部调用的外部函数怎么查找
对于第一点,就是各种编译选项,有很多类型:
- 编译警告选项,比如
-Wall
,-Wextra
- 代码优化选项,比如:
-O0
,-Ofast
- 调试选项,比如:
-g
,-fvar-tracking
- 预处理选项,比如:
-M
,-MP
- 代码生成选项,比如:
-fPIC
,-fPIE
- 等等,还有针对不同语言特有的选项
所有的选项在GNU GCC官网上有详细的介绍,参见:**Option-Summary**.
对于第二点,在源文件内部,调用的外部函数是在头文件中声明的,所以通过#include
的头文件编译器必须能够找到,这个时候需要使用-I
参数指定头文件的查找路径,以确保编译器可以找到源文件所使用的头文件.
在使用gcc
命令时,选项直接作为参数传递即可,比如:
1 | gcc -c xxx.c -Os -g -Wall -Wextra -pedantic -Werror -o xxx.o -Isrc/c |
那么在CMake中,可以:
- 使用
add_compile_options
命令指定编译选项 - 使用
include_directories
命令指定头文件搜索路径
因此上面的gcc
命令的效果等同于:
1 | add_compile_options(-Os -g -Wall -Wextra -pedantic -Werror) |
需要注意的是,因为CMake的构建目标必须是库或者可执行文件,所以并没有命令仅生成.o
文件,所以这里使用add_library
代替.
编译选项
一般有三种方式传递编译选项给编译器.
使用
CMAKE_C_FLAGS
或者CMAKE_CXX_FLAGS
变量(默认为空,或者是根据编译器而定的特定值), 设置全局选项,一般放在 CMakeLists 顶部位置.作用域在当前目录以及所包含的所有子目录中. 但是对于现代 CMake 不是很推荐. 类似的还有设置链接选项的CMAKE_LINKER_FLAGS
命令.1
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)
使用 per-target flags, 针对每个构建 target 设置编译属性. 对于 PUBLIC or INTERFACE 的依赖关系,子模块/库的编译选项会被父模块继承.类似的命令还有
target_compile_options()
. 这是较推荐的方式,可以较精确地控制作用域.1
2
3target_compile_definitions(cmake_examples_compile_flags
PRIVATE EX3
)-D
命令行手动全局添加.1
cmake .. -DCMAKE_CXX_FLAGS="-DEX3"
链接
链接需要做的就是把最终目标依赖的东西都组装起来.
对于这里的可执行文件来说,先从demo.o
的main函数开始,链接整个程序执行过程中需要的所有函数的实现;不同实现可能在不同的.o
文件或者库文件内,通过头文件声明的函数名,在.o
和.a
文件里面查找需要的实现;如果找不到,就会引发一个链接错误.
对于项目内部的构建目标库文件及其他的.o
文件,在链接的时候直接使用即可,而对于外部的第三方库或者系统的库文件,则需要使用-L
和-l
参数来告知链接器.
和编译一样的,除了-L
和-l
,链接器也还有很多其他参数`比如:
1 | -pie -pthread -r -s -static -static-pie |
详细的参数介绍详见:**Link-Options**.
对应地,CMake对应可以使用的命令为:
- 对于
-L
,使用link_directories
或者target_link_directories
命令 - 对于
-l
,使用link_libraries
或者target_link_libraries
命令 - 指定链接器的选项,使用
add_link_options
或者target_link_options
命令
上述命令中,以
target_
开头的是针对特定的目标进行设置,否则是针对所有的目标.
假设目标程序使用了外部库文件/usr/lib/libmath.a
就可以使用命令:
1 | gcc demo.c -L/usr/lib -lmath -pthread |
对应地,CMake使用的命令应该是:
1 | add_link_options(-pthread) |
CMake 命令行选项
上文已经介绍了一些命令行选项,最后进行一下汇总. 我们可以通过cmake --help
获取如下选项说明:
调用方式
1 | cmake [options] <path-to-source> |
选项如下(省略 help 类)
1 | -S <path-to-source> = Explicitly specify a source directory. |
参考链接:
https://www.zhihu.com/column/c_1369781372333240320
https://stackoverflow.com/questions/42027646/default-search-paths-for-cmake-include-vs-find-package
https://blog.csdn.net/qq_38410730/article/details/103741579
https://www.jianshu.com/p/54c3418c3eed
https://stackoverflow.com/questions/31037882/whats-the-cmake-syntax-to-set-and-use-variables
https://www.cnblogs.com/rickyk/p/3872568.html
https://github.com/ttroy50/cmake-examples