代码构建系统之CMake

  1. 1. CMake语言基础
    1. 0x1 CMake脚本基础
    2. 0x2 CMake语法
      1. 文件编码
      2. 脚本文件
      3. 命令调用
      4. 命令参数
      5. 注释
      6. 变量
      7. 列表
      8. 函数
      9. 日志输出
      10. option选项
      11. 条件语句
      12. 循环语句
    3. 0x3 CMake系统变量
      1. 输入变量
      2. 输出变量
  2. 2. CMake命令
    1. 0x1 脚本命令
      1. configure_file
      2. add_test
      3. cmake_policy
      4. set_property/get_property
      5. file
    2. 0x2 项目命令
      1. add_custom_command
      2. add_custom_target
      3. add_dependencies
      4. add_executable
      5. add_library
      6. add_subdirectory
      7. target_include_directories
      8. target_link_directories
  3. 3. Generator expressions
    1. 0x1 布尔生成器表达式
      1. 逻辑表达式
      2. 字符串比较
      3. 变量查询
    2. 0x2 字符串生成表达式
      1. 条件表达式
      2. 字符串转换
  4. 4. CMake packages
  5. 5. CMake 基本教程
    1. 0x1 构建起始
    2. 0x2 添加依赖库
    3. 0x3 添加库的使用要求
    4. 0x4 部署目标和测试用例
  6. 6. 参考

基于CMake的构建系统被组织为一组高级逻辑目标。逻辑目标可以分为三类:可执行文件包含自定义命令的自定义目标;目标之间的依赖关系在构建系统中表示,以确定构建顺序和更改时的重新生成规则。

1. CMake语言基础

CMake脚本语言基础的结构可以分为几个部分:

0x1 CMake脚本基础

CMake构建命令的输入必须是以CMake语言编写的文件,且文件名字有两种:CMakeLists.txt和以.cmake结尾的文件。CMake的脚本文件在项目中的组织形式有三种:

  • 目录(CMakeLists.txt)
    CMake在处理项目源码树进行构建的时候,以顶层目录中的CMakeLists.txt文件为入口点,这个文件包含了全部构建描述或者使用add_subdirectory()命令引入子目录进行构建;add_subdirectory()引入的子目录必须也包含CMakeLists.txt文件;每个CMakeLists.txt处理的源码目录,CMake会在构建过程中创建对应的同名目录树作为默认的工作目录和输出目录。

  • 脚本(<script>.cmake)
    独立的<script>.cmake脚本文件可以脚本模式运行,通过cmake -P命令执行;脚本模式只是简单的执行CMake脚本文件,并不会进行项目的构建,它不允许在CMake脚本中定义目标构建的行为。

  • 模块(<module>.cmake)
    CMake脚本文件中允许使用include()命令引入另一个<module>.cmake脚本文件,项目源码树中还可以提供它们自己的模块,并在CMAKE_MODULE_PATH变量中指定它们的位置。

0x2 CMake语法

文件编码

CMake源码文件使用ASCII编码能够获得最大程度的跨平台的调用,换行符可以使用\n或者\r\n\r\n在文件读入的时候会被转换成\n。3.0开始脚本支持UTF-8 BOM编码,CMake 3.2开始Windows平台支持脚本文件的UTF-8编码。

脚本文件

CMake的脚本文件由0或者多个命令调用通过换行符/空格/注释组成。如下正则描述的脚本文件的组件:

1
2
3
4
5
6
7
8
file         ::=  file_element* # 脚本文件有>=0个file_element组成
# 每个文件元素的组成如下:
file_element ::= command_invocation line_ending |(bracket_comment|space)* line_ending
# 行结束格式:以newline结尾,0或1个line_comment
line_ending ::= line_comment? newline
# 空格:>=1个空格符或者tab符
space ::= <match '[ \t]+'>
newline ::= <match '\n'>

命令调用

CMake命令调用格式是以标识符,圆括号,参数通过空格隔开组成的,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 命令调用格式:标识符(参数)
command_invocation ::= space* identifier space* '(' arguments ')'
# CMake的标识符(指CMake命令),必须以字母或者下划线开头,非开头字符可以是数字,长度>=1
identifier ::= <match '[A-Za-z_][A-Za-z0-9_]*'>
# 参数部分可以用()进行参数嵌套
arguments ::= argument? separated_arguments*
separated_arguments ::= separation+ argument? |
separation* '(' arguments ')'
separation ::= space | line_ending

#e.g.
add_executable(hello world.c)

CMake的命令名字大小写不敏感

命令参数

CMake命令调用的参数有以下几种:

  • 括号参数

    1
    2
    3
    4
    5
    6
    # CMake括号参数开始以: [+大于等于0个=+[,例如:'[[', '[=['...'[=====['
    bracket_argument ::= bracket_open bracket_content bracket_close
    bracket_open ::= '[' '='* '['
    bracket_content ::= <any text not containing a bracket_close with
    the same number of '=' as the bracket_open>
    bracket_close ::= ']' '='* ']'

    CMake 3.0以前不支持括号参数CMake的内容被视为纯文本,即会忽略所有转义和变量的eval, 这里内容部分不能出现和括号参数相同的结束符

  • 引号参数

    1
    2
    3
    4
    5
    6
    # 以双引号参数开始和结束的参数
    quoted_argument ::= '"' quoted_element* '"'
    # 参数部分,其中特殊字符会被翻译
    quoted_element ::= <any character except '\' or '"'> |
    escape_sequence | quoted_continuation
    quoted_continuation ::= '\' newline

    引号参数中的文本和Shell语法是一致的,特殊字符会进行生效,例如\t会显示成制表符,${var}会打印对应的变量值。CMake3.0以前的版本不支持\进行换行的连接转义,会报错。

  • 非引号参数
    未被引号包括的参数属于非引号参数,参数的内容组成包含一系列文本块和转义符;

    1
    2
    3
    4
    5
    6
    7
    8
    foreach(arg
    NoSpace
    Escaped\ Space
    This;Divides;Into;Five;Arguments
    Escaped\;Semicolon
    )
    message("${arg}")
    endforeach()

    输出如下:

    1
    2
    3
    4
    5
    6
    7
    8
    NoSpace
    Escaped Space
    This
    Divides
    Into
    Five
    Arguments
    Escaped;Semicolon

注释

CMake脚本注释以#开始的文本,但不能出现在上述的命令参数中,注释有两种:

  • 括号注释
    括号注释以#开始,紧跟着括号参数格式'[[', '[=['...'[=====[',注释文本放在括号参数中,如下:

    1
    2
    3
    #[[This is a bracket comment.
    It runs until the close bracket.]]
    message("First Argument\n" #[[Bracket Comment]] "Second Argument")

    同括号参数一样,CMake 3.0以前不支持括号注释

  • 行注释
    常用的注释

    1
    2
    3
    # This is a line comment.
    message("First Argument\n" # This is a line comment :)
    "Second Argument") # This is a line comment.

变量

作为任何语言都有的基本存储单元,变量也是CMake脚本中含有的。CMake的变量的值都是string类型,和shell脚本一样,只不过有些命令会将变量的值处理成其他类型的值。CMake变量名基本可以是任何字符组成的,甚至可以是CMake的命令,但还是建议使用字母数字组合,CMake变量名是大小写敏感的。

变量的修改可以通过set()unset()命令,当然其他命令也有可以修改变量值的语义。

CMake的变量同样是有作用域的:

  • 函数作用域
    function()中定义的变量,有效期只在function()命令调用期间,函数返回后就会释放;

  • 目录作用域
    项目构建目录树中,每个层级的目录都有自己绑定的变量,在处理当前目录的CMakeLists.txt时,CMake会拷贝当前目录的父目录的所有变量来初始化当前目录作用域的变量

    不是在function()set()的变量都属于目录作用域的变量,在该目录和子目录树的CMake中都是可见的,例如:

    1
    2
    3
    4
    5
    if(1)    
    set (var "test_value")
    endif()
    # 下面会输出var=test_value的结果
    message("var=${var}")
  • 全局缓存作用域
    CMake将Cache变量或者Cache Entries单独存放在一个区域;Cache变量可以在目录树构建的过程中一直有效,可以通过set()unset()命令加上**CACHE**选项来进行Cache变量的生效和失效;可以通过 $CACHE{VAR}方法直接去全局缓存作用域种查找变量值。

变量的查找顺序是:首先从函数作用域中查找变量,其次是目录作用域,最后是全局缓存作用域。

CMake中部分变量标识符是预留的,不能使用的,如下:

  • 以**CMAKE_**开始的变量;
  • 以**_CMAKE_**开始的变量;
  • 以**_**开头,后面紧跟CMake Command名字的变量;

CMake还有一种变量是环境变量,它和普通变量的差别:

  • 作用域:环境变量是只在当前目录及其子目录树作用域生效;且不会被CACHE;
  • 访问方式:$ENV{<variable>};
  • 初始化:环境变量的生效只在调用过程中,对环境变量的修改不会在返回到上层目录树中生效。即也不会在后面的非子目录构建和测试进程中生效。

上面介绍了几种变量后,set()unset()命令的语法格式就好理解了

1
2
3
4
5
6
7
8
9
# 设置普通变量,如果设置了PARENT_SCOPE,那么变量可以在上层目录和函数生效
set(<variable> <value>... [PARENT_SCOPE])

# 设置Cache变量,Cache变量被设置后默认不会再次被修改,可以通过FORCE来进行对已有的Cache变量进行修改
# 当当前作用域没有普通变量,或者使用了FORCE选项,那么本地变量也会被用最新的CACHE变量值覆盖
set(<variable> <value>... CACHE <type> <docstring> [FORCE])

# 设置环境变量
set(ENV{<variable>} [<value>])

列表

上面说了,CMake所有的值都存储为字符串,但是字符串在某些上下文中可以被视为列表;例如在非引号参数在被处理期间,字符串会被;组成一个列表。列表的元素在构造过程中会通过;连接在一起;如下:

1
2
set (var this is a list elements test)
message("${var}") # this;is;a;list;elements;test

针对列表有专门的列表操作命令list()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Reading
list(LENGTH <list> <out-var>)
list(GET <list> <element index> [<index> ...] <out-var>)
list(JOIN <list> <glue> <out-var>)
list(SUBLIST <list> <begin> <length> <out-var>)

Search
list(FIND <list> <value> <out-var>)

Modification
list(APPEND <list> [<element>...])
list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>)
list(INSERT <list> <index> [<element>...])
list(POP_BACK <list> [<out-var>...])
list(POP_FRONT <list> [<out-var>...])
list(PREPEND <list> [<element>...])
list(REMOVE_ITEM <list> <value>...)
list(REMOVE_AT <list> <index>...)
list(REMOVE_DUPLICATES <list>)
list(TRANSFORM <list> <ACTION> [...])

Ordering
list(REVERSE <list>)
list(SORT <list> [...])

函数

CMake的函数用来定义一串命令的集合,函数名字大小写不敏感的,这里和变量名大小写敏感是不一样的。格式如下:

1
2
3
function(<name> [<arg1> ...])
<commands>
endfunction()

函数在被调用的时候,首先会用参数值替换掉函数体的所有命令中的参数,最后执行函数体中的命令;CMake为函数体定义了变量ARGC来表示传入函数参数的个数,ARGV表示函数的参数列表,可以通过ARGV0, ARGV1, ARGV2...来访问各个参数,如果ARGV#下标超过了ARGC,其结果是未定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function(FunName var1 var2)
message("${var1}")
message("${var2}")
message("${ARGV}")
message("${ARGC}")
endfunction()

set (var this is a test)
FunName(${var})
# 执行结果如下:
# this
# is
# this;is;a;test
# 4

CMake脚本支持宏定义,可以传入参数,功能类似C语言的宏,宏名字大小写不敏感的,这里和变量名大小写敏感是不一样的。官方建议宏命令的名字采用全小写。宏定义的格式如下:

1
2
3
macro(<name> [<arg1> ...])
<commands>
endmacro()

CMake同样为宏定义了变量ARGC来表示传入参数的个数,ARGV表示参数列表。

宏和函数很相似,都是用来定义一组命令集合,然后在调用的时候进行执行,它们之间的区别有:

  • 函数中的ARGV0, ARGV1, ARGV2...都是真正的变量,而宏定义中的的变量都是字符串替换,类似C语言中的对宏的预处理。
  • 同样和C一样,函数调用会转移控制权,而宏调用是直接在调用处插入对应的宏定义进行执行;所有对于宏定义中,需要注意含有控制流的语句,例如return(),可能会产生异常的退出

日志输出

CMake提供了项目在构建的时候用于消息输出的命令message(),如下:

1
2
3
4
5
# 普通消息
message([<mode>] "message text" ...)

# 上报检查消息
message(<checkState> "message text" ...)

同个message输入多条消息时,message会将多条消息连接在一起进行输出。

  • 普通消息
    普通消息的<mode>决定了消息的类型,CMake会根据消息的类型进行相关的流程控制:
1
2
3
4
5
6
7
8
9
10
11
FATAL_ERROR		# 致命ERROR, CMake会停止处理和中止构建
SEND_ERROR # CMake会继续处理,但中止构建
WARNING # 发出告警
AUTHOR_WARNING # dev状态下告警
DEPRECATION #
(none) or NOTICE # 重要消息,输出到stderr,默认消息都是此类消息

STATUS # 项目输出简明的用户感兴趣的消息,输出到stdout
VERBOSE # 相对冗长和详细的信息
DEBUG
TRACE

其中FATAL_ERRORNOTICE之间的消息类型都属于系统级别的错误消息,不能进行忽略,他们都是输出到stderr
STATUSTRACE之间的消息类型都是用于调试和展示的消息类型,他们的日志级别由高到低,都是输出到stdout;且消息打印时前面都会有-- 前缀;低于STATUS优先级的日志默认时不会输出的,可以通过以下两种方式来设置日志级别进行控制输出:

  1. --log-level
  2. CMAKE_MESSAGE_LOG_LEVEL

- 上报检查消息 在项目进行构建前,通常会检查依赖的库是否存在,基本的做法如下:

1
2
3
4
5
6
7
8
9
message(STATUS "Looking for someheader.h")
#... 这里做检查,将检查结果set到checkSuccess

# 然后根据checkSuccess变量的值输出相关日志
if(checkSuccess)
message(STATUS "Looking for someheader.h - found")
else()
message(STATUS "Looking for someheader.h - not found")
endif()

CMake在3.17.4版本中为这类需求提供了更健壮和方便的命令:

1
2
3
4
5
message(<checkState> "message" ...)
# checkState 取值:
# CHECK_START # 记录一个要检查的消息
# CHECK_PASS # 检查通过,在CHECK_START消息的基础上输出的消息
# CHECK_FAIL # 检查失败,在CHECK_START消息的基础上输出的消息

状态CHECK_START必须和CHECK_PASS或者CHECK_FAIL配对使用,且CHECK_PASS或者CHECK_FAIL在进行消息输出时,会查找最近一条的CHECK_START消息。如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
message(CHECK_START "Finding my things")
list(APPEND CMAKE_MESSAGE_INDENT " ")

message(CHECK_START "Finding partA")
# ... do check, assume we find A
message(CHECK_PASS "found")

message(CHECK_START "Finding partB")
# ... do check, assume we don't find B
message(CHECK_FAIL "not found")

list(POP_BACK CMAKE_MESSAGE_INDENT)

输出结果为:

1
2
3
4
5
-- Finding my things
-- Finding partA
-- Finding partA - found
-- Finding partB
-- Finding partB - not found

option选项

CMake提供了option命令,定义一个开关,用户可以构建的时候选择开启或者关闭;option命令格式如下:

1
option(<variable> "<help_text>" [value])

option中value的值只能是ONOFF,如果不设置,默认为OFF;如果variable的名字已经存在,option的设置不会生效;option定义的同样是一个变量,只不过它只有ONOFF两个值,可以通过CMake命令的参数选项-Dvar=value来进行设置,-Dvar=value是设置Cache变量;

条件语句

同其他语言一样,CMake脚本语言的控制语句包括:条件语句,循环语句;

1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>) # optional block, can be repeated
<commands>
else() # optional block
<commands>
endif()

Condition条件参数的语法适用于if, else if, while语句。复合条件表达式的优先级顺序如下:

  1. 最内部的圆括号优先级最高;
  2. 一元测试命令,例如:EXISTSCOMMANDDEFINED
  3. 二元测试命令,例如:EQUALLESSLESS_EQUALGREATERGREATER_EQUALSTREQUALSTRLESSSTRLESS_EQUALSTRGREATERSTRGREATER_EQUALVERSION_EQUALVERSION_LESSVERSION_LESS_EQUALVERSION_GREATERVERSION_GREATER_EQUALMATCHES
  4. 逻辑操作符,其中逻辑操作符内部的优先级依次是:NOTANDOR

布尔值为TRUE的常量值包含:1ONYESTRUEY非0数字

布尔值为FALSE的常量值包含:0OFFNOFALSENIGNORENOTFOUND空串以-NOTFOUND结尾的串

布尔常量大小写不敏感;除了上面列出的常量值,其他的参数都会被认为是一个变量或者字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(<constant>)                  # 常量值为True,表达式成立
if(<variable|string>) # 变量的值不是False常量时,表达式成立
if(NOT <condition>) # 条件为False成立
if(<cond1> AND <cond2>) # 两个条件都为True
if(<cond1> OR <cond2>) # 两个条件只要有一个为True
if(COMMAND command-name) # 当名字为:command, macro,function这些可调用对象时为True
if(POLICY policy-id) # 是否为policy
if(TARGET target-name) # 名字是否是已存在的逻辑目标,add_executable(), add_library(), or add_custom_target()
if(EXISTS path-to-file-or-directory) # 判断给定名字的文件/目录是否存在
if(file1 IS_NEWER_THAN file2) # file1是否比file2要新,一个file不存在,两个文件一样
if(<variable|string> LESS <variable|string>) # 参数都是有效数字且左<右,则为True
if(<variable|string> STRLESS <variable|string>) # 字典顺序左<右,则为True
if(<variable|string> VERSION_LESS <variable|string>) # 版本号比较
if(<variable|string> IN_LIST <variable>) # 元素是否在列表里

循环语句

命令foreach来遍历list<items>的每个元素,读入loop_var变量中。

1
2
3
4
#items列表元素用空格or分号分割
foreach(<loop_var> <items>)
<commands>
endforeach()

语句foreach支持的参数格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
foreach(<loop_var> <items>)
#遍历items中的每个元素
#e.g. foreach(ele 1 2 3 4 5)
foreach(<loop_var> RANGE <stop>)
#生成一个[0,stop]的整数序列,然后依次遍历
#e.g. foreach(ele RANGE 5)
#输出:0 1 2 3 4 5
foreach(<loop_var> RANGE <start> <stop> [<step>])
#生成一个[start,stop],步长为step的整数序列,然后依次遍历
#e.g. foreach(ele RANGE 1 5 2)
#输出:1 3 5
foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])
#遍历lists中的每个元素
#e.g
# set(A 1 2 3)
# foreach(ele IN LISTS A)
foreach(<loop_var>... IN ZIP_LISTS <lists>)
#同时遍历多个lists,依次获取多个lists的同下标的元素
#e.g
# set(A 1 2 3)
# set(B 4 5 6)
# foreach(ele IN ZIP_LISTS A B)
#输出:1 4
# 2 5
# 3 6

循环语句while

1
2
3
while(<condition>)
<commands>
endwhile()

0x3 CMake系统变量

CMake文档中将特殊的变量分为两类:环境变量系统变量。为了理解我统一以系统变量来称之,然后以输入还是输出类型来进行区分:

  • CMake输入变量
    CMake有一些系统构建时预留使用的变量,可以在脚本中通过修改这些变量的值,来改变CMake构建的默认行为。
  • CMake输出变量
    CMake会提供一些变量用来边表示系统构建过程中的运行数值,便于用户进行构建脚本的编写和使用。

输入变量

  • 改变行为

    1
    2
    3
    4
    5
    # 可以设置一系列目录路径,指定依赖库的安装路径
    # 供命令find_package(), find_program(), find_library()等使用,这些命令会查到对应路径下的子目录(bin, lib, or include)
    CMAKE_PREFIX_PATH
    CMAKE_COLOR_MAKEFILE
    CMAKE_INSTALL_PREFIX
  • 控制构建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 构建工具模式下(cmake --build),设置用来编译的并发进程个数
    CMAKE_BUILD_PARALLEL_LEVEL
    #
    CMAKE_EXPORT_COMPILE_COMMANDS
    CMAKE_GENERATOR
    CMAKE_GENERATOR_INSTANCE
    CMAKE_GENERATOR_PLATFORM
    CMAKE_GENERATOR_TOOLSET
    CMAKE_<LANG>_COMPILER_LAUNCHER
    CMAKE_MSVCIDE_RUN_PATH
    CMAKE_NO_VERBOSE
    CMAKE_OSX_ARCHITECTURES
    DESTDIR
    LDFLAGS
    MACOSX_DEPLOYMENT_TARGET
    <PackageName>_ROOT
    VERBOSE
  • 语言相关
    如下语言相关的环境变量在Set后会被写入Cache Entry,不能在进行修改,或者说子模块的修改不再生效;

    1
    2
    3
    4
    5
    6
    CMAKE_C_COMPILER	# 设置C的编译器
    CMAKE_C_FLAGS # 设置C的编译参数
    CMAKE_CXX_COMPILER # 设置C++的编译器
    CMAKE_CXX_FLAGS # 设置C++的编译参数

    # 当然CMAKE还支持ASM,C#,Fortan,Objc,Swift,CUDA等语言的选项设置

输出变量

一些常见的CMake输出变量如下:

1
2
3
4
5
6
CMAKE_BINARY_DIR
CMAKE_BUILD_TOOL
CMAKE_CURRENT_BINARY_DIR
CMAKE_CURRENT_FUNCTION
CMAKE_MAJOR_VERSION
CMAKE_PROJECT_NAME

2. CMake命令

CMake构建脚本是基于命令组成的,前面介绍的CMake语言基础是CMake脚本命令的部分,CMake命令主要分为四类:脚本命令,项目命令,CTest命令,废弃命令。上面说的基本语法命令都属于脚本命令的范畴。下面是一些比较常见的脚本命令。

0x1 脚本命令

configure_file

configure_file命令的设计目的是为了将CMake构建参数透传给项目源代码

1
2
3
4
5
configure_file(<input> <output>
[COPYONLY] [ESCAPE_QUOTES] [@ONLY]
[NO_SOURCE_PERMISSIONS]
[NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

configure_file命令格式如下:将input文件拷贝成output文件,并将input文件中内容为@VAR${VAR}的标识符用对应的变量值替换,如果该变量有定义的话;configure_file命令参数含义:

1
2
3
4
5
6
7
8
9
10
COPYONLY:
只简单的将input文件拷贝成output文件,不做任何修改
ESCAPE_QUOTES:
内容替换时,忽略转义字符
@ONLY:
只对'@VAR@'标记的变量进行替换,这样可以忽略input中'${VAR}'的脚本
NO_SOURCE_PERMISSIONS:
拷贝input文件时,不拷贝文件的控制权限,拷贝为默认的644权限
NEWLINE_STYLE <style>:
替换output文件的换行符为特定换行,该参数和COPYONLY参数互斥

此外,对于input中输入行为#cmakedefine VAR ...的,会根据CMake构建变量,被替换为:

1
#define VAR ...

或者

1
/* #undef VAR */

以下是一个简单的使用示:

input文件名为config.h.in

1
2
3
#cmakedefine VAR1 ${VAR1}
#define VAR2 @VAR2@
static const std::string VAR3="@VAR3@"

CMakeLists.txt内容如下:

1
2
3
4
set(VAR1 1)
set(VAR2 2)
set(VAR3 3)
configure_file(config.h.in config.h @ONLY)

构建完后,会生成config.h文件,内容如下:

1
2
3
#define VAR1 ${VAR1}
#define VAR2 2
static const std::string VAR3="3"

add_test

add_test命令用来通过ctest工具为项目构建过程添加测试命令。

1
2
3
4
5
6
7
8
9
add_test(NAME <name> COMMAND <command> [<arg>...]
[CONFIGURATIONS <config>...]
[WORKING_DIRECTORY <dir>]
[COMMAND_EXPAND_LISTS])

NAME:
测试用例的名字
COMMAND:
测试用例执行的命令,如果<command>是构建目标,则cmake会自动将其替换为生成构建目录的路径

CMakeLists.txt示例如下:

1
2
3
4
add_executable(a.out test.cpp)

enable_testing()
add_test(NAME mytest COMMAND a.out)

构建完后执行测试用例

1
2
3
4
5
6
7
8
9
10
$cmake .
$make test
Running tests...
Test project /home/walkerdu/cmake_test
Start 1: mytest
1/1 Test #1: mytest ........................... Passed 0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) = 0.00 sec

cmake_policy

cmake policy的设计目的是为了在多版本中保持向后兼容的功能,因为cmake有时候需要对之前版本的某个特性进行bug修复或者修改优化其行为。当新的policy引入时,新版本的cmake会对向后兼容的特性进行在构建的时候进行告警/错误提示(根据cmake_minimum_required(VERSION))。对此可以通过cmake_policy命令显示的设置使用NEW or OLD版本的特性,如下:

1
2
cmake_policy(SET CMP<NNNN> NEW)
cmake_policy(SET CMP<NNNN> OLD)

cmake policy栈可以将显示的policy设置保存在栈结构中,当构建进入子目录时,policy的设置会压栈,离开子目录后改设置会弹出,这样子目录的设置不会影响父目录和同层目录。这样对多个子目录的维护可以相互独立。

1
2
3
cmake_policy(PUSH)
#cmake command
cmake_policy(POP)

这里需要指明不是所有情况的子目录的policy的设置都会影响到父目录,例如通过include()命令或者find_package()命令引入目录的policy设置不会对父目录有任何影响。

set_property/get_property

用于设置属性值,其实本质上就是一个变量值,但是该名字有特定的作用范围,例如针对特定目标;CMake系统使用的属性名可在手册中查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
set_property(<GLOBAL                      |
DIRECTORY [<dir>] |
TARGET [<target1> ...] |
SOURCE [<src1> ...]
[DIRECTORY <dirs> ...] |
[TARGET_DIRECTORY <targets> ...]
INSTALL [<file1> ...] |
TEST [<test1> ...] |
CACHE [<entry1> ...] >
[APPEND] [APPEND_STRING]
PROPERTY <name> [<value1> ...])

get_property(<variable>
<GLOBAL |
DIRECTORY [<dir>] |
TARGET <target> |
SOURCE <source> |
[DIRECTORY <dir> | TARGET_DIRECTORY <target>] |
INSTALL <file> |
TEST <test> |
CACHE <entry> |
VARIABLE >
PROPERTY <name>
[SET | DEFINED | BRIEF_DOCS | FULL_DOCS])

可以简单使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set_property(GLOBAL
PROPERTY XXX "this"
)
set_property(GLOBAL
APPEND
PROPERTY XXX "is"
)
get_property(test_p
GLOBAL
PROPERTY XXX
)

message(${test_p})

#输出结果为:thisis

file

file命令是针对文件系统的操作,可以对文件/目录的内容进行读取,修改操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 文件读取操作
file(READ <filename> <out-var> [...])
file(STRINGS <filename> <out-var> [...])
file(<HASH> <filename> <out-var>)
file(TIMESTAMP <filename> <out-var> [...])
file(GET_RUNTIME_DEPENDENCIES [...])

# 文件修改操作
file({WRITE | APPEND} <filename> <content>...)
file({TOUCH | TOUCH_NOCREATE} [<file>...])
file(GENERATE OUTPUT <output-file> [...])
file(CONFIGURE OUTPUT <output-file> CONTENT <content> [...])

# 文件系统操作
file({GLOB | GLOB_RECURSE} <out-var> [...] [<globbing-expr>...]) # 匹配文件
file(RENAME <oldname> <newname>)
file({REMOVE | REMOVE_RECURSE } [<files>...])
file(MAKE_DIRECTORY [<dir>...])
file({COPY | INSTALL} <file>... DESTINATION <dir> [...])
file(SIZE <filename> <out-var>)
file(READ_SYMLINK <linkname> <out-var>)
file(CREATE_LINK <original> <linkname> [...])

# 文件路径转换
file(RELATIVE_PATH <out-var> <directory> <file>)
file({TO_CMAKE_PATH | TO_NATIVE_PATH} <path> <out-var>)

# 文件网络操作
file(DOWNLOAD <url> <file> [...])
file(UPLOAD <file> <url> [...])

# 加锁
file(LOCK <path> [...])

# 归档
file(ARCHIVE_CREATE OUTPUT <archive> PATHS <paths>... [...])
file(ARCHIVE_EXTRACT INPUT <archive> [...])

0x2 项目命令

add_custom_command

自定义构建规则,用于生成构建目标。add_custom_command有两种使用方法:

  • 用于生成文件

add_custom_command第一种用法是新增一个定制命令用来生成OUTPUT的目标文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add_custom_command(OUTPUT output1 [output2 ...]
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]]
[BYPRODUCTS [files...]]
[IMPLICIT_DEPENDS <lang1> depend1
[<lang2> depend2] ...]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[DEPFILE depfile]
[JOB_POOL job_pool]
[VERBATIM] [APPEND] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])

声明一个命令用于生成特定目标文件OUTPUT

  • 构建事件

第二种用法,是向目标添加一个命令,可以在目标构建之前,链接之前,构建之后执行相关命令。该命令将成为目标的一部分。如果已经构建了目标,则不会执行该命令

1
2
3
4
5
6
7
8
9
add_custom_command(TARGET <target>
PRE_BUILD | PRE_LINK | POST_BUILD
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])

add_custom_target

自定义一个目标,执行定义的目录。该命令在构建的时候没有任何输出,也不会执行,需要构建完后,手动执行对应的目标,其实就是定一个Makefile中的target

1
2
3
4
5
6
7
8
9
10
add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]])

add_dependencies

在顶层目标之间,设置依赖关系。顶层目标即通过add_executable()add_library()add_custom_target()命令定义的目标。

1
add_dependencies(<target> [<target-dependency>]...)

add_executable

添加项目的可执行目标,通过给定的源文件进行构建生成。

1
2
3
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])

目标name必须是项目中全局唯一的。命令中的源文件列表可以为空,后面通过target_sources()命令来添加用来构建目标的源文件。

构建的目标的默认生成在和源码对应的构建树目录中,可以通过修改RUNTIME_OUTPUT_DIRECTORY 属性或是 CMAKE_RUNTIME_OUTPUT_DIRECTORY变量来进行修改默认行为。可执行目标的名字根据平台而定,例如WINS下会生成name.exe的目标。

add_library

添加一个库目标,如下:

1
2
3
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])

基本的限制和使用方式和add_executable命令一样,目标name必须是项目中全局唯一的。命令中的源文件列表可以为空,后面通过target_sources()命令来添加用来构建目标的源文件等。

其中STATICSHAREDMODULE选项分别用来生成不同的库:静态库,动态库,运行时dlopen使用的库。

add_subdirectory

添加项目构建子目录,子目录中除了源文件外,必须在子目录根下含有CMakeLists.txt命令文件。

1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

target_include_directories

该命令用来为构建的目标添加引用目录,

1
2
3
target_include_directories(<target> [SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

为需要编译的target添加包含目录,该target必须已经通过add_executable或者add_library定义。

  • BEFORE选项,本条引用的目录的内容会添加到属性的最前面,而不是追加在最后面。

关于PRIVATEPUBLICINTEFACE的功能如下:

  • INTEFACE选项:只对依赖此target的目标生效;
  • PRIVATE选项:只本target生效,不会对依赖此target的目标的属性有任何影响;
  • PUBLIC选项,不仅对本target生效,同样对依赖此target的目标的属性也会生效;

实现原理就是:PRIVATEPUBLIC选项将内容写入目标的INCLUDE_DIRECTORIES属性中,INTEFACEPUBLIC选项将内容写入目标的INTERFACE_INCLUDE_DIRECTORIES属性中。

该命令是在目标构建时,用来为链接器提供依赖库的搜索路径。具体的用法和target_include_diretories基本类似

1
2
3
target_link_directories(<target> [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

CMake建议避免使用该命令,可以通过使用依赖库的绝对路径来进行构建。

3. Generator expressions

Generator expressions是CMake提供的生成器表达式,类似高级语言的表达式语句,例如Python的列表生成器表达式。Generator expressions用来在构建时生成对应配置。生成器表达式的语法格式如下:

1
$<...>

0x1 布尔生成器表达式

布尔表达式的值只有01,这通常用来构建条件表达式。布尔表达式有以下几类:

逻辑表达式

  • $<BOOL:string>

    string转换成01string为:0OFFNOFALSENIGNORENOTFOUND空串以-NOTFOUND结尾的串的时候得到的布尔值是0,其他情况全为1。判断布尔值的时候,string大小写不敏感

  • $<AND:conditions>

    逗号分割的conditions的所有元素的布尔值都是1时,表达式的值为1,否则为0。

  • $<OR:conditions>

  • $<NOT:condition>

字符串比较

  • $<STREQUAL:string1,string2>

    比较两个字符串是否相等,字符串比较是大小写敏感的,如果想忽略大小写,可以使用如下字符串装换表达式:$<STREQUAL:$<UPPER_CASE:${foo}>,"BAR"> # "1" if ${foo} is any of "BAR", "Bar", "bar", ...

  • $<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>

变量查询

  • $<TARGET_EXISTS:target>:目标target是否存在;
  • $<CXX_COMPILER_VERSION:version>:c++编译器版本号是否匹配

0x2 字符串生成表达式

条件表达式

  • $<condition:true_string>condition为1,则使用true_string的值
  • $<IF:condition,true_string,false_string>

字符串转换

  • $<JOIN:list,string>:将字符串的内容添加到list中;
  • $<REMOVE_DUPLICATES:list>:去除列表中重复的项;
  • $<FILTER:list,INCLUDE|EXCLUDE,regex>
  • $<LOWER_CASE:string>
  • $<UPPER_CASE:string>
  • $<GENEX_EVAL:expr>
  • $<TARGET_GENEX_EVAL:tgt,expr>

4. CMake packages

CMake在构建系统中引入了package的概念,为构建目标提供依赖信息。命令find_package()用来进行packages的查找。find_package()的执行结果有两种:导入目标或者是构建相关变量的引入

1
2
3
4
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])

CMake的包提供两种引用模式:

  • Module模式

    此模式为默认模式。Module模式会去查找Find<PackageName>.cmake文件,执行该命令文件,进行版本检查,从而找到对应的库,Find<PackageName>.cmake需要将库的信息通过变量将依赖的信息例如PackageName_INCLUDE_DIRSPackageName_LIBS传递出来。

  • Config模式

    Config模式会查找<PackageName>Config.cmake文件,查找依赖库,然后和Module模式一样设置对应的变量。

Module模式下,会首先在CMAKE_MODULE_PATH路径下进行库cmake文件的搜索,如果没找到,则会采取Config模式,此模式下的查找路径为:

1
2
3
4
<PackageName>_DIR
CMAKE_PREFIX_PATH
CMAKE_FRAMEWORK_PATH
CMAKE_APPBUNDLE_PATH

对于Module模式,一般都是在自己项目工程内使用,Find<PackageName>.cmake放在项目工程的cmake目录中,对于Config模式,一般都是提供的外部项目引入使用。

5. CMake 基本教程

CMake官方教程给出了如何构建一个项目的示例;最基本的项目构建都是对项目源码进行构建生成可执行目标;

0x1 构建起始

最简单的项目,CMakeLists.txt需要如下三行命令:

1
2
3
4
5
6
7
8
#添加项目构建需要的最低CMake版本号
cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial)

# 设置构建目标
add_executable(Tutorial tutorial.cxx)

设置C++编译标准:

1
2
3
# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

其实设置C++编译标准,更多的时候是通过CMAKE_CXX_FLAGS选项来设置;

0x2 添加依赖库

现在Tutorial项目里需要依赖一个数学库用来进行根号运算,该数学库的源码放在MathFunctions子目录中,该目录结构如下:

1
2
3
4
5
6
7
Tutorial
|----tutorial.cxx
|----CMakeLists.txt
|----MathFunctions
|----MathFunctions.h
|----mysqrt.cxx
|----CMakeLists.txt

MathFunctions目录的CMakeLists.txt将该函数库构建成了一个静态库,如下:

1
add_library(MathFunctions mysqrt.cxx)

为了使用该数学库TutorialCMakeLists.txt需要将该函数库纳入构建,并生成链接依赖,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 添加MathFunctions库的目录
add_subdirectory(MathFunctions)

# 设置构建目标
add_executable(Tutorial tutorial.cxx)

# 设置构建目标依赖的库
target_link_libraries(Tutorial PUBLIC MathFunctions)

# 设置构建目录的源码需要依赖的头文件的搜索目录列表
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)

上面是很基本的依赖库引入的示例,现在使用开关选项来决定是否使用自定义的函数库,这个在大型的项目构建中是很常见的一个设置,如下:在顶层的CMakeLists.txt中引入下面代码:

1
2
3
4
5
option(USE_MYMATH "Use tutorial provided math implementation" ON)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

接下来,函数库依赖的构建脚本在引入开关USE_MYMATH后,变更如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 通过USE_MYMATH开关来决定添加MathFunctions库的目录
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

# 设置构建目标
add_executable(Tutorial tutorial.cxx)
# 设置构建目标依赖的库
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# 设置构建目录的源码需要依赖的头文件的搜索目录列表
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES}
)

引入开关USE_MYMATH后,通过将是否要引入的外部依赖函数库的路径存放在变量EXTRA_LIBSEXTRA_INCLUDES中,来动态的进行构建。同样在源码中也需要引入USE_MYMATH开关来决定是否需要include对应的头文件,这首先就是需要上面的configure_file来配置TutorialConfig.h.in文件,来生成对应的开关的宏在TutorialConfig.h中,如下:TutorialConfig.h.in的内容如下:

1
#cmakedefine USE_MYMATH

最后,项目的源码文件tutorial.cxx,对应的修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include "TutorialConfig.h"
...
#ifdef USE_MYMATH
# include "MathFunctions.h"
#endif
...

#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif

通过上面的配置和修改,就可以在构建的时候通过控制开关USE_MYMATH,来决定构建的目标是依赖自定义的函数库,还是系统默认的函数库了。

0x3 添加库的使用要求

使用要求可以更好的控制库和可执行文件的连接和include,同时可以很好的控制CMake内目标的属性的传递。使用要求的命令最常见的有以下几个:

1
2
3
4
target_compile_definitions()
target_compile_options()
target_include_directories()
target_link_libraries()

这里通过CMake的使用要求来重构上面添加依赖函数库的方式。首先我们定义任何需要使用MathFunctions 函数库的构建都要include当前MathFunctions 源码目录的头文件。但是MathFunctions 本地不需要,所以这里使用了INTERFACE属性来表示依赖者需要,而提供者不需要的功能,对Tutorial/MathFunctions/CMakeLists.txt的改造如下:

1
2
3
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)

这个表示所有需要使用MathFunctions 库的目标构建的时候都会自动添加该路径为头文件查找路径。这样顶层的Tutorial/CMakeLists.txt的构建脚本就可以去掉EXTRA_INCLUDES相关配置了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 通过USE_MYMATH开关来决定添加MathFunctions库的目录
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
endif()

# 设置构建目标
add_executable(Tutorial tutorial.cxx)
# 设置构建目标依赖的库
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# 设置构建目录的源码需要依赖的头文件的搜索目录列表
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)

0x4 部署目标和测试用例

部署目标相对很简单,对于MathFunctions 函数库的安装和头文件的部署,在MathFunctions/CMakeLists.txt添加命令如下:

1
2
install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

同时对于Tutorial目标的部署,修改Tutorial/CMakeLists.txt如下:

1
2
3
4
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include
)

6. 参考

https://www.jianshu.com/p/7e4aa4be239a

https://phenix3443.github.io/notebook/cmake/cmake-build-system.html

https://cmake.org/cmake/help/v3.18/manual/cmake-buildsystem.7.html

https://zh.wikipedia.org/wiki/CMake

https://www.coder.work/article/6715298