CMake总结

CMake总结

[TOC]
对于稍微有规模的工程, Linux 下的 C++ 开发下, 构建编译工具 CMake 很受欢迎. 基于自己的实战总结以及学习所记录的笔记,现整理于下. CMake 内容非常丰富,这里只是冰山一角,但是对于入门而言,内容应该够用了.我会持续总结整理.

简介

CMake(Cross platform Make)是个一个开源的跨平台自动化建构(编译,测试和打包)系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录,多个应用程序与多个库.

CMake 是 meta-building, 也就是说并不直接建构出最终的软件,而是产生标准的建构文档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依一般的建构方式使用.CMake 的配置文件取名为CMakeLists.txt. 官网地址,logo如下:

logo.png

C++ 还有一套 Google 开发的构建工具 Bazel, 两者的区别与特点可以参考文章.

GCC,make 和 CMake的关系

首先 GCC 与 make 都只是CMake 支持的编译器平台之一. 对于 Linux 下的 C++ 开发, GCC/make 比较常用. 这里简单总结一下 GCC,make 和 CMake的关系.
GCC 离编译最近,为了节省 GCC 一个个输入参数以及多文件系统的效率,使用 make 工具自动构建.make的语法还是较为繁琐,CMake 则用更加贴近用户的方式自动生成 make 文件,属于更上一级的构建工具(meta building).它们的关系如下:

gcc_make_cmake.jpg

功能概况

共 cmake,ctest 和 cpack 三个命令行工具分别负责构建,测试和打包

使用 cmake 一般流程为:
process.jpg.

  1. configure: 配置阶段
  2. generate: 生成构建系统(buildsystem,比如 make 工具对应的 Makefile);
  3. build: 执行构建(比如 make ),生成目标文件;
  4. 执行测试,安装或打包,甚至参与到 CI 的过程中与其他工具集成,例如代码检查等(非必须).

CMake-GUI

CMake 还提供了 GUI 以便调试. 我更倾向于命令行交互,因此只把安装过程写出来,具体使用教程请参考官网教程.

Ubuntu 下:

1
2
#依赖于qt
sudo apt install cmake-qt-gui

主界面如下:
cmake-gui.png

核心语法

CMake 的命令有不同类型,包括脚本命令,项目配置命令和测试命令,细节可以查看官网cmake-commands.

CMake语言在项目配置中组织为三种源文件类型:

  1. 目录:CMakeLists.txt,针对的是一个目录,描述如何针对目录树(Source tree)生成构建系统,会用到项目配置命令;
  2. 脚本:<script>.cmake,是一个 CMake 语言的脚本文件,可使用cmake -P直接执行,只能包含脚本命令,无法执行构建过程,更无法 define build targets or actions;
  3. 模块:<module>.cmake,实现一些模块化的功能,可以被前面两者包含,比如include(CTest)启用测试功能.

CMakeLists.txt 的主要构成元素(也就是变量指代的可操作的概念):

  • 目录
  • 源码文件
  • 构建目标(可执行文件,库)
  • 属性
  • 作用域
  • 缓存条目
  • 模块(非必须)
  • 测试项目(非必须)
  • 安装文件(非必须)

注释与转义

#:注释

\:转义,例如空格\ ,\n换行

$<COMMA>,

$<SEMICOLON>;

命令对大小写不敏感(case-insensitive).

打印命令

message命令

1
message([<mode>] "message text" ...)

mode选项

  1. 空或者NOTICE:比较重要的信息,如前面演示中的格式.
  2. DEBUG:调试信息,主要针对开发者.
  3. STATUS:项目使用者可能比较关心的信息,比如提示当前使用的编译器.
  4. WARNING:CMake 警告,不会打断进程.
  5. SEND_ERROR:CMake 错误,会继续执行,但是会跳过生成构建系统.
  6. FATAL_ERROR:CMake 致命错误,会终止进程.

变量

变量的 type

  • STRING 字符串(数字是特殊的字符串),也可以被视为只有一个元素的列表.

  • 字符串列表(直接设置多个值,或者使用分号;隔开)

  • 布尔值

    • 0,OFF,NO,FALSE,N,IGNORE,空字符串,NOTFOUND,及以”-NOTFOUND”结尾的字符串均视为False.
    • ON,YES,TRUE,Y,非0值以及所有非Fasle变量均被视为True

如果从作用域等实用性角度来分,又分为两种变量类型. Normal VariablesCache 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(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
2
3
set(MySourcesList "File.name" "File with Space.name")
list(APPEND MySourcesList "File.name" "File with Space.name")
add_excutable(MyExeTarget ${MySourcesList})

变量名通常全部用大写.
当变量的值中存在空格时,注意转义符,或者加上双引号.

Normal Variable

对于 Normal Variable, 又可以按照场景细分几个类型,例如:

环境变量

设置方法:
set(ENV{<variable>} [<value>])

1
2
3
# 注意引用格式 
set(ENV{ENV_VAR} "$ENV{PATH}")
message("Value of ENV_VAR: $ENV{ENV_VAR}")

环境变量的变化不会 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
2
3
4
5
6
7
8
set(SOURCES
src/Hello.cpp
src/main.cpp
)
# or
file(GLOB SOURCES "src/*.cpp")

add_executable(${PROJECT_NAME} ${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
2
3
4
5
6
7
8
9
# parent CMakeLists.txt
cmake_minimum_required(VERSION 3.2)
project(CMakeVariableScopeTutorial)
set(A "Parent")
add_subdirectory(child)
message(STATUS ${A}) # Prints "Parent"

# child CMakeLists.txt
set(A "Child")

结果为:”Parent”

function()

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.2)
project(CMakeVariableScopeTutorial)

function(func param)
set(${param} "Child")
endfunction()

set(A "Parent")

func(A)

message(STATUS ${A})# Prints "Parent"

结果为:”Parent”

include()

1
2
3
4
5
6
7
8
9
# parent CMakeLists.txt
cmake_minimum_required(VERSION 3.2)
project(CMakeVariableScopeTutorial)
set(A "Parent")
include(child/CMakeLists.txt)
message(STATUS ${A}) # Prints "Child"

# child CMakeLists.txt
set(A "Child")

结果为:”Child”

macro()

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.2)
project(CMakeVariableScopeTutorial)

macro(mac)
set(A "Child")
endmacro()

set(A "Parent")

mac(A)

message(STATUS ${A}) # Prints "Child"

结果为:”Child”

上述概念的图解如下:
add_subdirectory.png
function.png
include.png
marco.png

图片以及代码实例请参考链接.

考虑到数据安全性,我们应该优先选择 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# parent CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(test)
set(MY_GLOBAL_VAR "111" CACHE STRING INTERNAL FORCE)
message("第一次在父目录缓存变量 MY_GLOBAL_VAR=$CACHE{MY_GLOBAL_VAR}")
message("第一次在父目录普通变量1次 MY_GLOBAL_VAR=$CACHE{MY_GLOBAL_VAR}")
set(MY_GLOBAL_VAR "666")
message("第一次在父目录普通变量2次 MY_GLOBAL_VAR=${MY_GLOBAL_VAR}")

add_subdirectory(src)
message("第二次在父目录局部 MY_GLOBAL_VAR=${MY_GLOBAL_VAR}")
message("第二次在父目录全局 MY_GLOBAL_VAR=$CACHE{MY_GLOBAL_VAR}")

# child CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
message("子目录,MY_GLOBAL_VAR=${MY_GLOBAL_VAR}")
set(MY_GLOBAL_VAR "777" CACHE STRING INTERNAL FORCE )

运行结果:

1
2
3
4
5
6
第一次在父目录缓存变量 MY_GLOBAL_VAR=111
第一次在父目录普通变量1次 MY_GLOBAL_VAR=111
第一次在父目录普通变量2次 MY_GLOBAL_VAR=666
子目录,MY_GLOBAL_VAR=666
第二次在父目录局部 MY_GLOBAL_VAR=666
第二次在父目录全局 MY_GLOBAL_VAR=777

可以看到第三行与第四行的输出区别.

由于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
2
$<LOWER_CASE:string>
$<UPPER_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
2
3
4
5
6
7
8
9
10
11
12
13
14
if(address)
message("defined address!")
else()
message("NOT defined address")
endif()

option(address "hello world" ON)
message("option is ${address}")

if(address)
message("defined address!")
else()
message("NOT defined address")
endif()

输出为

1
2
3
NOT defined address!
option is a
defined address!
  • 注意大括号后变量值会真的成为字符串 ON/OFF 而不是布尔值的 ON/OFF.
    1
    2
    3
    if(${address} STREQUAL "ON")
    else()
    endif()

变量的 debug

  • message() 函数打印
  • 检查 CMakeCache.txt, 即便构建失败也会生成 CMakeCache.txt.
  • 使用 variable_watch() 函数观察.
  • cmake --trace ..... 选项打印更多信息.

Properties

Cmake 中还有一种存储信息的方式,就是用 property.它就像一个变量,但需要依附于其他 item,比如 directory 或 target.

设置 property

1
2
3
4
5
6
7
# Sets one property on zero or more objects of a scope.
set_property(TARGET TargetName
PROPERTY CXX_STANDARD 11)

# Sets properties on targets. Targets can have properties that affect how they are built.
set_target_properties(TargetName PROPERTIES
CXX_STANDARD 11)

第一种方式通用性更好,且可以指定多个 target/file/test 等.
第二种方式就是专门用来给 target 设置 property 的,它可以为单个 target 设置多个 property.

获取 property

1
2
# Gets one property from one object in a scope.
get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(expression)
# then section.
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
#...
elseif(expression2)
# elseif section.
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
#...
else(expression)
# else section.
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
#...
endif(expression)

else和endif中的表达式是可以省略的.

使用小括号可以组合多个条件语句,比如:(cond1) AND (cond2 OR (cond3)).

1
2
3
4
5
6
7
8
set(EMPTY_STR "")
if (NOT EMPTY_STR AND FLAG AND NUM LESS 50 AND NOT NOT_DEFINE_VAR)
message("The first if branch...")
elseif (EMPTY_STR)
message("EMPTY_STR is not empty")
else ()
message("All other case")
endif()

判断条件

  • 字符串(包括数值)比较,比如: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
2
3
> EXISTS, COMMAND, DEFINED 
> EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL, STREQUAL, STRLESS, STRLESS_EQUAL, STRGREATER, STRGREATER_EQUAL, VERSION_EQUAL, VERSION_LESS, VERSION_LESS_EQUAL, VERSION_GREATER, VERSION_GREATER_EQUAL, MATCHES
> NOT,AND,OR

循环语句

foreach 循环

  1. 第一种形式

    1
    2
    3
    4
    5
    foreach(loop_var arg1 arg2 ...)
    COMMAND1(ARGS ...)
    COMMAND2(ARGS ...)
    ...
    endforeach(loop_var)

    注意endforeach(loop_var)的变量最好不要省略,因为foreach循环是依靠变量来跳出循环的.

  2. 第二种形式

    1
    foreach(loop_var RANGE total)
  3. 第三种形式

    1
    foreach(loop_var RANGE start stop [step])
  4. 第四种形式

    1
    2
    foreach(loop_var IN [LISTS [list1 [...]]]
    [ITEMS [item1 [...]]])

while循环

1
2
3
4
5
while(condition)
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
endwhile(condition)

注意endwhile中的条件最好不要省略.

在while和foreach循环中,取变量的值请用${var}.breakcontinue 的用法基本与c一样,放心使用.

文件操作

读写,创建或复制文件和目录,计算文件hash,下载文件,压缩文件等等.

1
2
3
4
file(GLOB_RECURSE ALL_SRC
src/module1/*.c
src/module2/*.c
)

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)下查找:

  1. Module mode:CMAKE_MODULE_PATH 定义的目录下(如果不设置默认 /usr/share/cmake_VERSION_NUMBER/Modules, 如果设置了的话,搜索目录为上述目录+自定义的目录 )查找 FindXXX.cmake 的脚本文件.
    例如 Boost 库就有官方的支持接口,FindBoost.cmake.对于更多模块可以查看,官网的链接:**cmake-modules**

  2. 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
2
3
4
/usr/lib/cmake/eigen3 $ tree
.
├── Eigen3Config.cmake
└── UseEigen3.cmake

Eigen3Config.cmake 文件内容如下,可以看到是一些目录/版本号的定义以及对 UseEigen3.cmake 的调用.

1
2
3
4
5
6
7
8
9
10
11
12
set ( EIGEN3_FOUND 1 )
set ( EIGEN3_USE_FILE "${CMAKE_CURRENT_LIST_DIR}/UseEigen3.cmake" )

set ( EIGEN3_DEFINITIONS "" )
set ( EIGEN3_INCLUDE_DIR "/usr/include/eigen3" )
set ( EIGEN3_INCLUDE_DIRS "/usr/include/eigen3" )
set ( EIGEN3_ROOT_DIR "/usr" )

set ( EIGEN3_VERSION_STRING "3.2.92" )
set ( EIGEN3_VERSION_MAJOR "3" )
set ( EIGEN3_VERSION_MINOR "2" )
set ( EIGEN3_VERSION_PATCH "92" )

UseEigen3.cmake的内容如下.

1
2
add_definitions     ( ${EIGEN3_DEFINITIONS} )
include_directories ( ${EIGEN3_INCLUDE_DIRS} )

上面只是特定例子模块的构成,不同模块可能不同.

具体语法

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(Boost_FOUND)
message ("boost found")
include_directories(${Boost_INCLUDE_DIRS})
else()
message (FATAL_ERROR "Cannot find Boost")
endif()

target_include_directories( third_party_include
PRIVATE ${Boost_INCLUDE_DIRS}
)

target_link_libraries( third_party_include
PRIVATE
${Boost_SYSTEM_LIBRARY}
${Boost_FILESYSTEM_LIBRARY}
)

模块别名

从3.5 版本开始支持第三库的别名功能.例如, Boost 库.

Boost::boost -> header only libraries

Boost::system -> the boost system library.

Boost::filesystem -> filesystem library.

这样就可以与C++代码里的模块用法无缝对接:

1
2
3
4
target_link_libraries( third_party_include
PRIVATE
Boost::filesystem
)

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()命令生成的)相关的一些信息,包括:

  1. $<TARGET_FILE:tgt>:获取编译目标的文件路径
  2. $<TARGET_FILE_NAME:tgt>:获取编译目标的文件名
  3. $<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类, 根据生成的值是布尔值还是字符串划分.一般支持嵌套.官网生成器表达式大全.

布尔值生成表达式

  1. 逻辑符号
    原型:$<condition:value-if-true>
1
2
3
4
5
6
7
8
#如果为 debug 模式就把 debug.c 文件包含进 mylib 里参与构建.嵌套了下面的$<CONFIG:cfgs>.
add_library(mylib common.c $<$<CONFIG:DEBUG>:debug.c>)
#如果为 debug 模式,添加 --coverage 选项.
add_library(Foo ...)
target_link_options(Foo
PRIVATE
$<$<CONFIG:Debug>:--coverage>
)
  1. 字符串比较

    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>
  2. Variable Queries 参数为变量

1
2
3
4
5
6
7
$<TARGET_EXISTS:target>
$<CONFIG:cfgs>
$<CXX_COMPILER_ID:compiler_ids>
$<CUDA_COMPILER_ID:compiler_ids>
$<CXX_COMPILER_VERSION:version>
$<COMPILE_LANGUAGE:languages>
$<LINK_LANGUAGE:languages>

$<CONFIG> 表达式在 single-configuration generator 的情况下 $<CONFIG> = ${CMAKE_BUILD_TYPE}.但是在 Multi-configuration generator 时不相同, 因为可以在 build 阶段改变.详细介绍在后文.

字符串生成表达式

  1. 符号生成

    1
    2
    3
    4
    $<ANGLE-R> # ">"
    $<COMMA> # ","
    $<SEMICOLON> # ";"

  2. 条件表达式

    1
    2
    $<condition:true_string>
    $<IF:condition,true_string,false_string>
  3. 字符串操作

    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版本以上
  4. 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版本以上
  5. 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>
  6. 输出相关的Output-Related Expressions

1
2
3
4
5
6
7
8
9
$<TARGET_NAME:...> # This is required if exporting targets to multiple dependent export sets. 不支持嵌套
$<LINK_ONLY:...> # 3.1版本以上
$<INSTALL_INTERFACE:...>
$<BUILD_INTERFACE:...>
$<MAKE_C_IDENTIFIER:...>
$<TARGET_OBJECTS:objLib> # 3.1版本以上
$<SHELL_PATH:...> # 3.4版本以上
$<OUTPUT_CONFIG:...>
$<COMMAND_CONFIG:...> # 3.20版本以上

一个例子:

1
2
3
4
5
6
7
8
add_library(Foo ...)
target_include_directories(Foo
PUBLIC
# 构建时,提供依赖的目录
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
# 安装时提供安装目录
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

这里不会把所有项目一一列出,更多参考官网.

生成器表达式的调试

由于生成器表达式不会在 CMakeLists.txt 处理的过程中被计算,只在构建时计算.因此message()函数无法打印出其值.

有2种方式打印出来.

  1. add_custom_target 使用”虚拟” target
    1
    add_custom_target(genexdebug COMMAND ${CMAKE_COMMAND} -E echo "$<...>")
  2. 使用文件名
    1
    file(GENERATE OUTPUT filename CONTENT "$<...>")

指定构建平台

除了 GCC 与 make, CMake 还支持其他很多平台,通过cmake --help 命令可以得到如下输出(v3.15):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Generators

The following generators are available on this platform (* marks default):
* Unix Makefiles = Generates standard UNIX makefiles.
Green Hills MULTI = Generates Green Hills MULTI files
(experimental, work-in-progress).
Ninja = Generates build.ninja files.
Watcom WMake = Generates Watcom WMake makefiles.
CodeBlocks - Ninja = Generates CodeBlocks project files.
CodeBlocks - Unix Makefiles = Generates CodeBlocks project files.
CodeLite - Ninja = Generates CodeLite project files.
CodeLite - Unix Makefiles = Generates CodeLite project files.
Sublime Text 2 - Ninja = Generates Sublime Text 2 project files.
Sublime Text 2 - Unix Makefiles
= Generates Sublime Text 2 project files.
Kate - Ninja = Generates Kate project files.
Kate - Unix Makefiles = Generates Kate project files.
Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files.
Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.

可以大致分为3类:

  1. 纯命令行构建的 MakeFiles 类.
    1
    2
    3
    4
    5
    6
    7
    8
    Borland Makefiles
    MSYS Makefiles
    MinGW Makefiles
    NMake Makefiles
    NMake Makefiles JOM
    Ninja
    Unix Makefiles
    Watcom WMake
  2. IDE构建的配置文件类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Visual 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
  3. 调用其他IDE构建的类(依赖于配置文件或者 MakeFiles)
    1
    2
    3
    4
    5
    6
    CodeBlocks
    CodeLite
    Eclipse CDT4
    KDevelop3
    Kate
    Sublime Text 2

指定构建平台

1
2
3
4
$ cmake .. -G Ninja

$ ls
build.ninja CMakeCache.txt CMakeFiles cmake_install.cmake rules.ninja

Single-configuration generator 与 Multi-configuration generator

根据平台支持在 build 阶段修改 CMAKE_BUILD_TYPE(构建类型)与否,将平台分为2种类型: Single-configuration generatorMulti-configuration generator.
Single-configuration generator 无法在 build 阶段修改构建类型, 代表的平台为 Unix Makefiles generator.

1
2
cmake -H. -B_builds -DCMAKE_BUILD_TYPE=Debug
cmake --build _builds

Multi-configuration generator 可以在 build 阶段修改构建类型, 代表的平台为 Xcode ,Visual Studio.

1
2
cmake -H. -B_builds -DCMAKE_CONFIGURATION_TYPES=Release;Debug -GXcode
cmake --build _builds --config Debug

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
    5
    PROJECT_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
2
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)

也可以通过配置编译选项实现,见下面.

配置编译选项

add_compile_options命令可以为所有编译器配置编译选项(同时对多个编译器生效); 通过设置变量CMAKE_C_FLAGS可以配置c编译器的编译选项; 而设置变量CMAKE_CXX_FLAGS可配置针对c++编译器的编译选项. 比如:

1
2
3
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")

指定编译器/链接器

CMAKE_C_COMPILER, CMAKE_CXX_COMPILER,CMAKE_LINKER.

-D选项命令行传入

1
cmake .. -DCMAKE_C_COMPILER=clang-3.6 -DCMAKE_CXX_COMPILER=clang++-3.6

命令行输入脚本化的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
clang_bin=`which clang`
clang_xx_bin=`which clang++`

if [ -z $clang_bin ]; then
clang_ver=`dpkg --get-selections | grep clang | grep -v -m1 libclang | cut -f1 | cut -d '-' -f2`
clang_bin="clang-$clang_ver"
clang_xx_bin="clang++-$clang_ver"
fi

echo "Will use clang [$clang_bin] and clang++ [$clang_xx_bin]"


mkdir -p build.clang && cd build.clang && \
cmake .. -DCMAKE_C_COMPILER=$clang_bin -DCMAKE_CXX_COMPILER=$clang_xx_bin && make

配置编译类型

设置内置变量

CMAKE_BUILD_TYPE:Debug,Release,RelWithDebInfo,MinSizeRel等,比如:

1
set(CMAKE_BUILD_TYPE Debug)

针对不同的编译类型设置不同的编译选项,比如对于Debug版本,开启调试信息,不进行代码优化:

1
2
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")

对于Release版本,不包含调试信息,优化等级设置为2:

1
2
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
执行cmake命令的时候通过参数-D指定
1
cmake -B build -DCMAKE_BUILD_TYPE=Debug

添加全局宏定义

add_definitions命令.

CMakeLists.txt内容如下:

1
2
3
4
option(TEST_DEBUG "option for debug" OFF)
if (TEST_DEBUG)
add_definitions(-DTEST_DEBUG)
endif(TEST_DEBUG)

构建时使用命令行参数控制参数宏的开启与关闭.

1
2
cmake -DTEST_DEBUG=1 .. #打开
cmake -DTEST_DEBUG=0 .. #关闭

源码中就可以使用 flag 对代码进行开关控制.

1
2
3
4
5
6
#ifdef TEST_DEBUG
...
...
#else
...
#endif

注意,在后期的版本中此命令被如下几个命令代替

include目录

包含目录有如下几种命令:

通过命令include_directories来设置头文件的搜索目录

  • target_include_directories()
    目的:针对构建 target 指定所需的文件(含目录)
    原型:
1
2
3
target_include_directories(<target> [SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

参数讲解

  • <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.cpp
    CMakeLists.txt
    1
    2
    3
    4
    target_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. 编译:确定编译目标所需要的源文件
  2. 链接:确定链接的时候需要依赖的额外的库

编译库

1
2
3
4
5
6
7
8
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
target_include_directories(math
PUBLIC
src/c/math/*.h
)
  1. 使用file命令获取待编译的源码文件.
  2. add_library命令编译
  3. STATIC:静态库,SHARED:动态库
  4. target_include_directories:编译目标所依赖的文件/目录
  5. 路径传递属性:如下

开放度的属性解读如下图:
一个 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构建目标.
openess_propotey.png

这些路径被添加到构建目标里后,当更高一层次的构建目标使用当前构建目标时,可以再次使用PRIVARE等关键字,组合过滤目录的暴露性.

1
2
3
4
5
6
7
8
add_executable(main
src/main.cpp
)

target_link_libraries( main
PRIVATE
math
)

如上main对math使用了PRIVATE,也就意味着其他链接到main的目标无法访问math的目录了,即便math对目录的添加是public性质的.

编译可执行文件

1
2
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
  1. add_executable命令构建可执行文件(参数分别为可执行文件名, 源码文件名 list). 可以让 project 的名字与构建目标一致:add_executable(${PROJECT_NAME} main.cpp)
  2. 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
2
3
4
5
6
7
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message("Setting build type to 'RelWithDebInfo' as none was specified.")
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
"MinSizeRel" "RelWithDebInfo")
endif()

当然也可以通过指定编译器选项例如CMAKE_CXX_FLAGS_DEBUG变量直接手动指定.

通过 CMake 向工程源码传递参数

我们可以通过 configure_file() 函数把 CMake 的配置文件 NAME.h.in 转换成 NAME.h 头文件供指定目录下的源码进行访问, 由此可以实现编译时期进行调参的需求. 前文已经提及了生成版本号的用处.

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

实现的功能可以概括为:将 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
#define BUILD_Version 1

一个具体的使用案例是有些时候,我们需要在项目中标明版本号,Git 的 hash 号,编译时间等信息,但是显然,对于 Git 的 hash 号,编译时间我们不想自己手动填写.现在提供一种途径,将这些信息写入到头文件中,再编译到so库文件或者可执行程序中.这样,就可以通过提供库文件的接口或者可执行程序的打印中得到这些值了.

安装和打包

安装库与可执行文件

  • 通过install命令来说明需要安装的内容及目标路径
  • 通过设置CMAKE_INSTALL_PREFIX变量说明安装的路径
    有2种方式:
    set命令指定,如下面是将默认的/usr/local/改成当前构建的目录下面(注意此种方法要求设置在构建目标前).
    1
    2
    3
    4
    if( 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
2
3
4
install(TARGETS math demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
  • TARGETS:指定需要安装的目标列表
  • RUNTIME DESTINATION,LIBRARY DESTINATION,ARCHIVE DESTINATION:分别指定应该安装到安装目录下个哪个子目录.ARCHIVE一般是指静态库,LIBRARY则是指共享库,在不同平台上,略有差异.如果是 DLL 库的话,需要如下指定安装目录
    1
    2
    3
    install (TARGETS library_name
    LIBRARY DESTINATION lib
    RUNTIME DESTINATION bin)

例子

1
2
3
4
5
6
7
8
9
# cmake -B cmake-build -DCMAKE_INSTALL_PREFIX=./output
# cmake --build cmake-build
# cd cmake-build && make install && cd -
Install the project...
-- Install configuration: ""
-- Installing: .../cmake-template/output/lib/libmath.a
-- Installing: .../gitee/cmake-template/output/bin/demo
-- Installing: .../gitee/cmake-template/output/include/math/add.h
-- Installing: .../gitee/cmake-template/output/include/math/minus.h

install命令详解

install命令可以安装的目标类型:

  • 构建目标
  • 文件
  • 程序
  • 目录
  • 执行自定义的脚本
  • 执行自定义的命令
  • EXPORT
1
2
3
4
5
6
install(TARGETS <target>... [...])
install({FILES | PROGRAMS} <file>... [...])
install(DIRECTORY <dir>... [...])
install(SCRIPT <file> [...])
install(CODE <code> [...])
install(EXPORT <export-name> [...])

常用共用关键字

  • 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
    2
    install(TARGETS target
    RUNTIME PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)

安装目录

完整命令格式:

1
2
3
4
5
install(DIRECTORY dirs...
TYPE <type> | DESTINATION <dir>
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]]
参数 作用
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
2
install(<FILES|PROGRAMS> files...
TYPE <type> | DESTINATION <dir>

FILESPROGRAMS的不同之处在于文件的默认权限,前者是一般文件(头文件,配置文件等),而后者为可执行文件,默认有可执行权限,包括:OWNER_EXECUTE,GROUP_EXECUTEWORLD_EXECUTE.

例如安装头文件:

1
2
file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h)
install(FILES ${MATH_LIB_HEADERS} DESTINATION include/math)

安装的时候执行自定义的脚本/命令

1
2
install([[SCRIPT <file>] [CODE <code>]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL] [...])

执行安装

1
2
3
4
5
cmake --build . --target install
# 或者针对make构建工具
make install
# cmake3.15版本往后,--install指定构建目录;--prefix指定安装路径,覆盖安装路径变量CMAKE_INSTALL_PREFIX.
cmake --install . --prefix "../output"

删除安装
make install 会生成一个 install_manifest.txt 的 log 文件记录安装的文件及其目标目录,可以按图索蕀地删除,如下:

1
sudo xargs rm < install_manifest.txt

打包

  • include(CPack)启用打包功能

    include(CPack)会在构建路径(Build tree)下生成两个cpack的配置文件,CPackConfig.cmakeCPackSourceConfig.cmake,其实也就对应了两个构建目标:packagepackage_source

  • 在执行构建编译之后使用cpack命令行工具进行打包安装;对于make工具,也可以使用命令make package

1
2
3
4
5
6
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
变量 用途
CPACK_GENERATOR 打包使用的压缩工具,比如”ZIP”
CPACK_OUTPUT_FILE_PREFIX 打包安装的路径前缀
CPACK_INSTALL_PREFIX 打包压缩包的内部目录前缀
CPACK_PACKAGE_FILE_NAME 打包压缩包的名称,由CPACK_PACKAGE_NAME,CPACK_PACKAGE_VERSION,CPACK_SYSTEM_NAME三部分构成

例子

1
2
3
4
5
6
7
8
9
# cmake -B cmake-build -DCPACK_OUTPUT_FILE_PREFIX=`pwd`/output
# cmake --build cmake-build
# cd cmake-build && cpack && cd -
CPack: Create package using ZIP
CPack: Install projects
CPack: - Run preinstall target for: CMakeTemplate
CPack: - Install project: CMakeTemplate
CPack: Create package
CPack: - package: /Users/Farmer/gitee/cmake-template/output/CMakeTemplate-1.0.0-Darwin.zip generated.

打包执行命令

  • cpack命令

    1
    2
    cpack -G TGZ --config CPackConfig.cmake
    cpack -G TGZ --config CPackSourceConfig.cmake
    • -G参数指定生成器,常用的有ZIP,TGZ,7Z等,可以同时指定多个
    • --config参数可以指定打包配置文件
  • cmake命令

    1
    2
    cmake --build . --target package
    cmake --build . --target package_source
  • make命令

    1
    2
    make package
    make package_source

测试

CMake默认测试

  1. option命令设置测试开关

    1
    2
    3
    4
    5
    option(CMAKE_TEMPLATE_ENABLE_TEST "Whether to enable unit tests" ON)
    if (CMAKE_TEMPLATE_ENABLE_TEST)
    message(STATUS "Unit tests enabled")
    enable_testing()
    endif()
  2. CMakeLists.txt中通过命令enable_testing()或者include(CTest)来启用测试功能;

  3. 使用add_test命令添加测试样例,指定测试的名称和测试命令,参数;

    1
    2
    add_test(NAME test_add COMMAND test_add 10 24 34)
    add_test(NAME test_minus COMMAND test_minus 40 96 -56)
    • NAME:测试名称
    • COMMAND:测试文件名与测试参数
  4. 构建编译完成后使用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

  1. 整个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
    8
    cmake_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
    4
    cmake_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
2
3
4
5
6
7
8
9
project (sublibrary2)

add_library(${PROJECT_NAME} INTERFACE)
add_library(sub::lib2 ALIAS ${PROJECT_NAME})

target_include_directories(${PROJECT_NAME}
INTERFACE
${PROJECT_SOURCE_DIR}/include
)

包含子目录

命令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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ tree
.
├── CMakeLists.txt
├── subbinary
│ ├── CMakeLists.txt
│ └── main.cpp
├── sublibrary1
│ ├── CMakeLists.txt
│ ├── include
│ │ └── sublib1
│ │ └── sublib1.h
│ └── src
│ └── sublib1.cpp
└── sublibrary2
├── CMakeLists.txt
└── include
└── sublib2
└── sublib2.h

#include "sublib1/sublib1.h".

1
2
3
target_include_directories(${PROJECT_NAME}
INTERFACE
${PROJECT_SOURCE_DIR}/include

但要注意的是,安装时也需要加上父目录, 例如/usr/local/include/sublib1/sublib1.h

导入编译好的目标文件

  • 使用add_library命令,通过指定IMPORTED选项表明这是一个导入的库文件,通过设置其属性指明其路径:

    1
    2
    3
    add_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
    6
    add_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
2
3
4
5
$ tree
.
├── AddressBook.proto
├── CMakeLists.txt
├── main.cpp

main.cpp 依赖 AddressBook.proto 中的数据.

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
38
cmake_minimum_required(VERSION 3.5)

project (protobuf_example)

# find the protobuf compiler and libraries
find_package(Protobuf REQUIRED)

# check if protobuf was found
if(PROTOBUF_FOUND)
message ("protobuf found")
else()
message (FATAL_ERROR "Cannot find Protobuf")
endif()

# Generate the .h and .cxx files
PROTOBUF_GENERATE_CPP(PROTO_SRCS PROTO_HDRS AddressBook.proto)

# Print path to generated files
message ("PROTO_SRCS = ${PROTO_SRCS}")
message ("PROTO_HDRS = ${PROTO_HDRS}")

# Add an executable
add_executable(protobuf_example
main.cpp
${PROTO_SRCS}
${PROTO_HDRS})

target_include_directories(protobuf_example
PUBLIC
${PROTOBUF_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}
)

# link the exe against the libraries
target_link_libraries(protobuf_example
PUBLIC
${PROTOBUF_LIBRARIES}
)

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
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
#!/bin/bash

set -euf -o pipefail

BUILD_DIR="cmake-build"
INSTALL_DIR=$(pwd)/output
rm -rf "${BUILD_DIR}"

# Configure
BUILD_TYPE=Debug
cmake -B "${BUILD_DIR}" \
-DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
-DCPACK_OUTPUT_FILE_PREFIX="${INSTALL_DIR}"

# Build
cmake --build "${BUILD_DIR}"

cd "${BUILD_DIR}"
# Test
make test CTEST_OUTPUT_ON_FAILURE=TRUE GTEST_COLOR=TRUE
# GTEST_COLOR=TRUE ctest --output-on-failure

# Install
# cmake --build . --target install
# cmake --install . --prefix "../output" # After cmake 3.15
make install

# Package
# cmake --build . --target package
make package

cd -

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
2
3
4
#ifdef UPPER_CASE
#define name "REAL_COOL_ENGINEER"
#else
#define name "real_cool_engineer"

这个时候,如果需要使用大写的版本,就可以使用gcc-D参数:

1
gcc -DUPPER_CASE ...

而在 CMake 中,可以使用命令:

1
add_definitions(-DUPPER_CASE)

编译

在编译的时候,需要把源文件处理成机器代码,主要有两个方面:

  1. 对于源文件里面的代码具体怎样进行编译
  2. 源文件内部调用的外部函数怎么查找

对于第一点,就是各种编译选项,有很多类型:

  1. 编译警告选项,比如-Wall,-Wextra
  2. 代码优化选项,比如:-O0,-Ofast
  3. 调试选项,比如:-g,-fvar-tracking
  4. 预处理选项,比如:-M,-MP
  5. 代码生成选项,比如:-fPIC,-fPIE
  6. 等等,还有针对不同语言特有的选项

所有的选项在GNU GCC官网上有详细的介绍,参见:**Option-Summary**.

对于第二点,在源文件内部,调用的外部函数是在头文件中声明的,所以通过#include的头文件编译器必须能够找到,这个时候需要使用-I参数指定头文件的查找路径,以确保编译器可以找到源文件所使用的头文件.

在使用gcc命令时,选项直接作为参数传递即可,比如:

1
gcc -c xxx.c -Os -g -Wall -Wextra -pedantic -Werror -o xxx.o -Isrc/c

那么在CMake中,可以:

  1. 使用add_compile_options命令指定编译选项
  2. 使用include_directories命令指定头文件搜索路径

因此上面的gcc命令的效果等同于:

1
2
3
add_compile_options(-Os -g -Wall -Wextra -pedantic -Werror)
include_directories(src/c)
add_library(xxx STATIC xxx.c)

需要注意的是,因为CMake的构建目标必须是库或者可执行文件,所以并没有命令仅生成.o文件,所以这里使用add_library代替.

编译选项

一般有三种方式传递编译选项给编译器.

  1. 使用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)
  2. 使用 per-target flags, 针对每个构建 target 设置编译属性. 对于 PUBLIC or INTERFACE 的依赖关系,子模块/库的编译选项会被父模块继承.类似的命令还有 target_compile_options() . 这是较推荐的方式,可以较精确地控制作用域.

    1
    2
    3
    target_compile_definitions(cmake_examples_compile_flags
    PRIVATE EX3
    )
  3. -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对应可以使用的命令为:

  1. 对于-L,使用link_directories或者target_link_directories命令
  2. 对于-l,使用link_libraries或者target_link_libraries命令
  3. 指定链接器的选项,使用add_link_options或者target_link_options命令

上述命令中,以target_开头的是针对特定的目标进行设置,否则是针对所有的目标.

假设目标程序使用了外部库文件/usr/lib/libmath.a就可以使用命令:

1
gcc demo.c -L/usr/lib -lmath -pthread

对应地,CMake使用的命令应该是:

1
2
3
4
add_link_options(-pthread)
add_executable(demo demo.c)
link_directories(/usr/lib)
target_link_libraries(demo math)

CMake 命令行选项

上文已经介绍了一些命令行选项,最后进行一下汇总. 我们可以通过cmake --help获取如下选项说明:

调用方式

1
2
3
cmake [options] <path-to-source>
cmake [options] <path-to-existing-build>
cmake [options] -S <path-to-source> -B <path-to-build>

选项如下(省略 help 类)

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
38
39
40
41
42
43
44
45
46
47
48
49
-S <path-to-source>          = Explicitly specify a source directory.
-B <path-to-build> = Explicitly specify a build directory.
-C <initial-cache> = Pre-load a script to populate the cache.
-D <var>[:<type>]=<value> = Create or update a cmake cache entry.
-U <globbing_expr> = Remove matching entries from CMake cache.
-G <generator-name> = Specify a build system generator.
-T <toolset-name> = Specify toolset name if supported by
generator.
-A <platform-name> = Specify platform name if supported by
generator.
-Wdev = Enable developer warnings.
-Wno-dev = Suppress developer warnings.
-Werror=dev = Make developer warnings errors.
-Wno-error=dev = Make developer warnings not errors.
-Wdeprecated = Enable deprecation warnings.
-Wno-deprecated = Suppress deprecation warnings.
-Werror=deprecated = Make deprecated macro and function warnings
errors.
-Wno-error=deprecated = Make deprecated macro and function warnings
not errors.
-E = CMake command mode.
-L[A][H] = List non-advanced cached variables.
--build <dir> = Build a CMake-generated project binary tree.
--install <dir> = Install a CMake-generated project binary
tree.
--open <dir> = Open generated project in the associated
application.
-N = View mode only.
-P <file> = Process script mode.
--find-package = Run in pkg-config like mode.
--graphviz=[file] = Generate graphviz of dependencies, see
CMakeGraphVizOptions.cmake for more.
--system-information [file] = Dump information about this system.
--loglevel=<error|warn|notice|status|verbose|debug|trace>
= Set the verbosity of messages from CMake
files.
--debug-trycompile = Do not delete the try_compile build tree.
Only useful on one try_compile at a time.
--debug-output = Put cmake in a debug mode.
--trace = Put cmake in trace mode.
--trace-expand = Put cmake in trace mode with variable
expansion.
--trace-source=<file> = Trace only this CMake file/module. Multiple
options allowed.
--warn-uninitialized = Warn about uninitialized values.
--warn-unused-vars = Warn about unused variables.
--no-warn-unused-cli = Don't warn about command line options.
--check-system-vars = Find problems with variable usage in system
files.

参考链接:

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

作者

cx

发布于

2022-01-12

更新于

2022-07-16

许可协议