基于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 | file ::= file_element* # 脚本文件有>=0个file_element组成 |
命令调用
CMake命令调用格式是以标识符,圆括号,参数通过空格隔开组成的,格式如下:
1 | # 命令调用格式:标识符(参数) |
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
8foreach(arg
NoSpace
Escaped\ Space
This;Divides;Into;Five;Arguments
Escaped\;Semicolon
)
message("${arg}")
endforeach()输出如下:
1
2
3
4
5
6
7
8NoSpace
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
5if(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 | # 设置普通变量,如果设置了PARENT_SCOPE,那么变量可以在上层目录和函数生效 |
列表
上面说了,CMake所有的值都存储为字符串,但是字符串在某些上下文中可以被视为列表;例如在非引号参数在被处理期间,字符串会被;
组成一个列表。列表的元素在构造过程中会通过;
连接在一起;如下:
1 | set (var this is a list elements test) |
针对列表有专门的列表操作命令list()
1 | Reading |
函数
CMake的函数用来定义一串命令的集合,函数名字大小写不敏感的,这里和变量名大小写敏感是不一样的。格式如下:
1 | function(<name> [<arg1> ...]) |
函数在被调用的时候,首先会用参数值替换掉函数体的所有命令中的参数,最后执行函数体中的命令;CMake为函数体定义了变量ARGC
来表示传入函数参数的个数,ARGV
表示函数的参数列表,可以通过ARGV0, ARGV1, ARGV2...
来访问各个参数,如果ARGV#
下标超过了ARGC
,其结果是未定义的。
1 | function(FunName var1 var2) |
宏
CMake脚本支持宏定义,可以传入参数,功能类似C语言的宏,宏名字大小写不敏感的,这里和变量名大小写敏感是不一样的。官方建议宏命令的名字采用全小写。宏定义的格式如下:
1 | macro(<name> [<arg1> ...]) |
CMake同样为宏定义了变量ARGC
来表示传入参数的个数,ARGV
表示参数列表。
宏和函数很相似,都是用来定义一组命令集合,然后在调用的时候进行执行,它们之间的区别有:
- 函数中的
ARGV0, ARGV1, ARGV2...
都是真正的变量,而宏定义中的的变量都是字符串替换,类似C语言中的对宏的预处理。 - 同样和C一样,函数调用会转移控制权,而宏调用是直接在调用处插入对应的宏定义进行执行;所有对于宏定义中,需要注意含有控制流的语句,例如
return()
,可能会产生异常的退出。
日志输出
CMake提供了项目在构建的时候用于消息输出的命令message()
,如下:
1 | # 普通消息 |
同个message
输入多条消息时,message
会将多条消息连接在一起进行输出。
- 普通消息
普通消息的<mode>
决定了消息的类型,CMake会根据消息的类型进行相关的流程控制:
1 | FATAL_ERROR # 致命ERROR, CMake会停止处理和中止构建 |
其中FATAL_ERROR
到NOTICE
之间的消息类型都属于系统级别的错误消息,不能进行忽略,他们都是输出到stderr;
而STATUS
到TRACE
之间的消息类型都是用于调试和展示的消息类型,他们的日志级别由高到低,都是输出到stdout;且消息打印时前面都会有--
前缀;低于STATUS
优先级的日志默认时不会输出的,可以通过以下两种方式来设置日志级别进行控制输出:
--log-level
CMAKE_MESSAGE_LOG_LEVEL
- 上报检查消息
在项目进行构建前,通常会检查依赖的库是否存在,基本的做法如下:
1
2
3
4
5
6
7
8
9message(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 | message(<checkState> "message" ...) |
状态CHECK_START
必须和CHECK_PASS
或者CHECK_FAIL
配对使用,且CHECK_PASS
或者CHECK_FAIL
在进行消息输出时,会查找最近一条的CHECK_START
消息。如下示例:
1 | message(CHECK_START "Finding my things") |
输出结果为:
1 | -- Finding my things |
option选项
CMake提供了option
命令,定义一个开关,用户可以构建的时候选择开启或者关闭;option
命令格式如下:
1 | option(<variable> "<help_text>" [value]) |
option中value
的值只能是ON
或OFF
,如果不设置,默认为OFF
;如果variable
的名字已经存在,option
的设置不会生效;option
定义的同样是一个变量,只不过它只有ON
或OFF
两个值,可以通过CMake命令的参数选项-Dvar=value
来进行设置,-Dvar=value
是设置Cache变量;
条件语句
同其他语言一样,CMake脚本语言的控制语句包括:条件语句,循环语句;
1 | if(<condition>) |
Condition条件参数的语法适用于if, else if, while
语句。复合条件表达式的优先级顺序如下:
- 最内部的圆括号优先级最高;
- 一元测试命令,例如:
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
;
布尔值为TRUE的常量值包含:1
,ON
,YES
,TRUE
,Y
,非0数字
;
布尔值为FALSE的常量值包含:0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,空串
,以-NOTFOUND结尾的串
;
布尔常量大小写不敏感;除了上面列出的常量值,其他的参数都会被认为是一个变量或者字符串;
1 | if(<constant>) # 常量值为True,表达式成立 |
循环语句
命令foreach
来遍历list<items>
的每个元素,读入loop_var
变量中。
1 | #items列表元素用空格or分号分割 |
语句foreach
支持的参数格式如下:
1 | foreach(<loop_var> <items>) |
循环语句while
:
1 | while(<condition>) |
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
6CMAKE_C_COMPILER # 设置C的编译器
CMAKE_C_FLAGS # 设置C的编译参数
CMAKE_CXX_COMPILER # 设置C++的编译器
CMAKE_CXX_FLAGS # 设置C++的编译参数
# 当然CMAKE还支持ASM,C#,Fortan,Objc,Swift,CUDA等语言的选项设置
输出变量
一些常见的CMake输出变量如下:
1 | CMAKE_BINARY_DIR |
2. CMake命令
CMake构建脚本是基于命令组成的,前面介绍的CMake语言基础是CMake脚本命令的部分,CMake命令主要分为四类:脚本命令,项目命令,CTest命令,废弃命令。上面说的基本语法命令都属于脚本命令的范畴。下面是一些比较常见的脚本命令。
0x1 脚本命令
configure_file
configure_file
命令的设计目的是为了将CMake构建参数透传给项目源代码。
1 | configure_file(<input> <output> |
configure_file
命令格式如下:将input
文件拷贝成output
文件,并将input
文件中内容为@VAR
或${VAR}
的标识符用对应的变量值替换,如果该变量有定义的话;configure_file
命令参数含义:
1 | COPYONLY: |
此外,对于input
中输入行为#cmakedefine VAR ...
的,会根据CMake构建变量,被替换为:
1 |
或者
1 | /* #undef VAR */ |
以下是一个简单的使用示:
input
文件名为config.h.in
1 |
|
CMakeLists.txt
内容如下:
1 | set(VAR1 1) |
构建完后,会生成config.h
文件,内容如下:
1 |
|
add_test
add_test
命令用来通过ctest工具为项目构建过程添加测试命令。
1 | add_test(NAME <name> COMMAND <command> [<arg>...] |
CMakeLists.txt示例如下:
1 | add_executable(a.out test.cpp) |
构建完后执行测试用例
1 | cmake . |
cmake_policy
cmake policy的设计目的是为了在多版本中保持向后兼容的功能,因为cmake有时候需要对之前版本的某个特性进行bug修复或者修改优化其行为。当新的policy引入时,新版本的cmake会对向后兼容的特性进行在构建的时候进行告警/错误提示(根据cmake_minimum_required(VERSION)
)。对此可以通过cmake_policy命令显示的设置使用NEW or OLD版本的特性,如下:
1 | cmake_policy(SET CMP<NNNN> NEW) |
cmake policy栈可以将显示的policy设置保存在栈结构中,当构建进入子目录时,policy的设置会压栈,离开子目录后改设置会弹出,这样子目录的设置不会影响父目录和同层目录。这样对多个子目录的维护可以相互独立。
1 | cmake_policy(PUSH) |
这里需要指明不是所有情况的子目录的policy的设置都会影响到父目录,例如通过include()
命令或者find_package()
命令引入目录的policy设置不会对父目录有任何影响。
set_property/get_property
用于设置属性值,其实本质上就是一个变量值,但是该名字有特定的作用范围,例如针对特定目标;CMake系统使用的属性名可在手册中查看。
1 | set_property(<GLOBAL | |
可以简单使用如下:
1 | set_property(GLOBAL |
file
file
命令是针对文件系统的操作,可以对文件/目录的内容进行读取,修改操作;
1 | # 文件读取操作 |
0x2 项目命令
add_custom_command
自定义构建规则,用于生成构建目标。add_custom_command
有两种使用方法:
- 用于生成文件
add_custom_command
第一种用法是新增一个定制命令用来生成OUTPUT
的目标文件。
1 | add_custom_command(OUTPUT output1 [output2 ...] |
声明一个命令用于生成特定目标文件OUTPUT
。
- 构建事件
第二种用法,是向目标添加一个命令,可以在目标构建之前,链接之前,构建之后执行相关命令。该命令将成为目标的一部分。如果已经构建了目标,则不会执行该命令。
1 | add_custom_command(TARGET <target> |
add_custom_target
自定义一个目标,执行定义的目录。该命令在构建的时候没有任何输出,也不会执行,需要构建完后,手动执行对应的目标,其实就是定一个Makefile中的target。
1 | add_custom_target(Name [ALL] [command1 [args1...]] |
add_dependencies
在顶层目标之间,设置依赖关系。顶层目标即通过add_executable()
,add_library()
,add_custom_target()
命令定义的目标。
1 | add_dependencies(<target> [<target-dependency>]...) |
add_executable
添加项目的可执行目标,通过给定的源文件进行构建生成。
1 | add_executable(<name> [WIN32] [MACOSX_BUNDLE] |
目标name
必须是项目中全局唯一的。命令中的源文件列表可以为空,后面通过target_sources()
命令来添加用来构建目标的源文件。
构建的目标的默认生成在和源码对应的构建树目录中,可以通过修改RUNTIME_OUTPUT_DIRECTORY
属性或是 CMAKE_RUNTIME_OUTPUT_DIRECTORY
变量来进行修改默认行为。可执行目标的名字根据平台而定,例如WINS下会生成name.exe
的目标。
add_library
添加一个库目标,如下:
1 | add_library(<name> [STATIC | SHARED | MODULE] |
基本的限制和使用方式和add_executable
命令一样,目标name
必须是项目中全局唯一的。命令中的源文件列表可以为空,后面通过target_sources()
命令来添加用来构建目标的源文件等。
其中STATIC
,SHARED
,MODULE
选项分别用来生成不同的库:静态库,动态库,运行时dlopen使用的库。
add_subdirectory
添加项目构建子目录,子目录中除了源文件外,必须在子目录根下含有CMakeLists.txt
命令文件。
1 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) |
target_include_directories
该命令用来为构建的目标添加引用目录,
1 | target_include_directories(<target> [SYSTEM] [BEFORE] |
为需要编译的target
添加包含目录,该target
必须已经通过add_executable
或者add_library
定义。
BEFORE
选项,本条引用的目录的内容会添加到属性的最前面,而不是追加在最后面。
关于PRIVATE
,PUBLIC
,INTEFACE
的功能如下:
INTEFACE
选项:只对依赖此target
的目标生效;PRIVATE
选项:只本target
生效,不会对依赖此target
的目标的属性有任何影响;PUBLIC
选项,不仅对本target
生效,同样对依赖此target
的目标的属性也会生效;
实现原理就是:PRIVATE
和PUBLIC
选项将内容写入目标的INCLUDE_DIRECTORIES
属性中,INTEFACE
和PUBLIC
选项将内容写入目标的INTERFACE_INCLUDE_DIRECTORIES
属性中。
target_link_directories
该命令是在目标构建时,用来为链接器提供依赖库的搜索路径。具体的用法和target_include_diretories
基本类似
1 | target_link_directories(<target> [BEFORE] |
CMake建议避免使用该命令,可以通过使用依赖库的绝对路径来进行构建。
3. Generator expressions
Generator expressions是CMake提供的生成器表达式,类似高级语言的表达式语句,例如Python的列表生成器表达式。Generator expressions用来在构建时生成对应配置。生成器表达式的语法格式如下:
1 | $<...> |
0x1 布尔生成器表达式
布尔表达式的值只有0
或1
,这通常用来构建条件表达式。布尔表达式有以下几类:
逻辑表达式
$<BOOL:string>
将
string
转换成0
或1
,string
为:0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,空串
,以-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 | find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE] |
CMake的包提供两种引用模式:
Module模式
此模式为默认模式。Module模式会去查找
Find<PackageName>.cmake
文件,执行该命令文件,进行版本检查,从而找到对应的库,Find<PackageName>.cmake
需要将库的信息通过变量将依赖的信息例如PackageName_INCLUDE_DIRS
和PackageName_LIBS
传递出来。Config模式
Config模式会查找
<PackageName>Config.cmake
文件,查找依赖库,然后和Module模式一样设置对应的变量。
Module模式下,会首先在CMAKE_MODULE_PATH
路径下进行库cmake文件的搜索,如果没找到,则会采取Config模式,此模式下的查找路径为:
1 | <PackageName>_DIR |
对于Module模式,一般都是在自己项目工程内使用,Find<PackageName>.cmake
放在项目工程的cmake目录中,对于Config模式,一般都是提供的外部项目引入使用。
5. CMake 基本教程
CMake官方教程给出了如何构建一个项目的示例;最基本的项目构建都是对项目源码进行构建生成可执行目标;
0x1 构建起始
最简单的项目,CMakeLists.txt
需要如下三行命令:
1 | #添加项目构建需要的最低CMake版本号 |
设置C++编译标准:
1 | # specify the C++ standard |
其实设置C++编译标准,更多的时候是通过CMAKE_CXX_FLAGS
选项来设置;
0x2 添加依赖库
现在Tutorial
项目里需要依赖一个数学库用来进行根号运算,该数学库的源码放在MathFunctions
子目录中,该目录结构如下:
1 | Tutorial |
MathFunctions
目录的CMakeLists.txt
将该函数库构建成了一个静态库,如下:
1 | add_library(MathFunctions mysqrt.cxx) |
为了使用该数学库Tutorial
的CMakeLists.txt
需要将该函数库纳入构建,并生成链接依赖,如下:
1 | # 添加MathFunctions库的目录 |
上面是很基本的依赖库引入的示例,现在使用开关选项来决定是否使用自定义的函数库,这个在大型的项目构建中是很常见的一个设置,如下:在顶层的CMakeLists.txt
中引入下面代码:
1 | option(USE_MYMATH "Use tutorial provided math implementation" ON) |
接下来,函数库依赖的构建脚本在引入开关USE_MYMATH
后,变更如下:
1 | # 通过USE_MYMATH开关来决定添加MathFunctions库的目录 |
引入开关USE_MYMATH
后,通过将是否要引入的外部依赖函数库的路径存放在变量EXTRA_LIBS
和EXTRA_INCLUDES
中,来动态的进行构建。同样在源码中也需要引入USE_MYMATH
开关来决定是否需要include对应的头文件,这首先就是需要上面的configure_file
来配置TutorialConfig.h.in
文件,来生成对应的开关的宏在TutorialConfig.h
中,如下:TutorialConfig.h.in
的内容如下:
1 | #cmakedefine USE_MYMATH |
最后,项目的源码文件tutorial.cxx
,对应的修改如下:
1 |
|
通过上面的配置和修改,就可以在构建的时候通过控制开关USE_MYMATH
,来决定构建的目标是依赖自定义的函数库,还是系统默认的函数库了。
0x3 添加库的使用要求
使用要求可以更好的控制库和可执行文件的连接和include,同时可以很好的控制CMake内目标的属性的传递。使用要求的命令最常见的有以下几个:
1 | target_compile_definitions() |
这里通过CMake的使用要求来重构上面添加依赖函数库的方式。首先我们定义任何需要使用MathFunctions
函数库的构建都要include当前MathFunctions
源码目录的头文件。但是MathFunctions
本地不需要,所以这里使用了INTERFACE
属性来表示依赖者需要,而提供者不需要的功能,对Tutorial/MathFunctions/CMakeLists.txt
的改造如下:
1 | target_include_directories(MathFunctions |
这个表示所有需要使用MathFunctions
库的目标构建的时候都会自动添加该路径为头文件查找路径。这样顶层的Tutorial/CMakeLists.txt
的构建脚本就可以去掉EXTRA_INCLUDES
相关配置了,如下:
1 | # 通过USE_MYMATH开关来决定添加MathFunctions库的目录 |
0x4 部署目标和测试用例
部署目标相对很简单,对于MathFunctions
函数库的安装和头文件的部署,在MathFunctions/CMakeLists.txt
添加命令如下:
1 | install(TARGETS MathFunctions DESTINATION lib) |
同时对于Tutorial
目标的部署,修改Tutorial/CMakeLists.txt
如下:
1 | install(TARGETS Tutorial DESTINATION bin) |
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