CMake使用
1. CMake使用
1.1注释
1.1.1 注释行
Cmake使用#进行行注释,可以放在任何位置
1 | #这是一个CMake行注释 |
1.1.2注释块
Cmake使用#[[]]进行块注释
1 | #[[注释第一行 |
1.2 CMake两种基本用法
1.2.1 所有文件共处一个目录下
- 添加
CMakeLists.txt文件:
1 | cmake_minimum_required(VERSION 3.5.0) |
cmake_minimum_required(可选):指定使用的最低版本,如果不写可能会有警告project:定义工程名称,并且可以指定工程的版本、工程描述、web主页地址、支持的语言,其中工程名称是必填项,其余的为选填
1 | #project命令格式 |
add_executable:定义工程生成一个可执行程序
1 | add_executable(可执行程序名称 源文件名称...) |
这里的可执行程序名称只是最后生成的exe文件名称,与项目名称是两码事,源文件名可以有多个,多个使用空格或者分号来处理
- 执行
CMake命令:
在执行cmake命令时要确保执行命令所在的路径在项目根目录中即CMakeLists.txt所在目录中
1 | #执行以下指令进行生成对应文件 |
1.3基本配置
1.3.1变量
在CMake中很多时候我们也可以使用变脸来管理一些参数,比如上文中的add_executable中如果有多个文件那么就需要使用变量来管理
- 设置变量
set
具体语法如下:
1 | #set语法 |
var:变量名VALUE:变量值,一个变量可以存储多个值且所有的值都属于字符串形式存储,当有多个时打印出来的会显示为一整行,在存储时会用分号隔开
1 | #set设置变量 |
1.3.2确立语言标准
在编写c++时,可能会用到c++11、c++14、c++17、c++20等版本指定,那么就需要在编译时指定要使用那个版本去编译cpp程序
1 | g++ *.cpp -std=c++11 -o app |
在上述例子中通过参数-std=c++11我们指定了需要使用c++11来编译文件,c++标准对应有个宏叫DCMAKE_CXX_STANDARD。在CMake中规定cpp有两种方式:
- 使用CMakeLists直接规定:
1 | #使用c++11标准 |
- 在执行cmake时就指定了标准:
1 | #使用c++11标准 |
1.3.3指定输出路径
在CMake中输出路径也是由一个宏来存储的叫做EXECUTABLE_OUTPUT_PATH,它的值通过set进行设置
1 | #假定设置好了set(HOME <项目目录>) |
这里的演示是绝对路径,但是在具体项目中建议使用相对路径。
以及如果EXECUTABLE_OUTPUT_PATH对应目录不存在CMake会自动帮我们创建
1.4搜索文件
在上文中我们使用了变量来管理文件,但是当我们项目文件数很多时,使用变量去记录罗列同样也很麻烦,所以我们需要使用cmake中给我们提供的文件搜索功能来统一管理
方式1:使用aux_source_directory
1 | aux_source_directory(<dir> <virable>) |
dir:要搜索的目录virable:将从dir目录下搜索到的文件存储到这个变量中
使用示例:
1 | cmake_minimum_required(VERSION 3.3.0) |
PROJECT_SOURCE_DIR这个宏是用户在输入时的路径,如果用户不输入就是默认执行CMakeLists所在的路径,大多是根目录下的顶层CMakeLists.txt所在目录
方式2:使用file
在一个项目里源文件很多可以使用file来搜索(但是其功能不局限于搜索)
1 | file(GLOB/GLOB_RECURSE 变量名 要搜索的文件类型) |
GLOB:将指定目录下的搜到的满足条件的文件名生成一个列表来存储GLOB_RECURSE:递归搜索指定目录,将搜索道德满足条件的文件名生成一个列表来存储
使用示例:
1 | file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) |
CMAKE_CURRENT_SOURCE_DIR:这个宏表示当前访问的CMakeLists文件所在路径- 其中路径参数可以加双引号也可以不加
1.5包含头文件
在项目编译源文件时,很多时候需要将源文件对应头文件路径指定出来,这样才能保证编译过程中编译器能够找到这些头文件,顺利通过编译。在CMake中引入头文件使用include_directories:
1 | include_directories(<headpath>) |
使用示例:
1 | ├── CMakeLists.txt |
1.6库制作
有些时候我们并不需要将他们编译为可执行程序,而是生成一些动态/静态库分享给第三方使用。
如果要制作静态/动态库,需要使用的命令如下:
1 | add_library(库名称 STATIC 源文件1 [源文件2]...)#静态库 |
在linux中,静态库分为三个部分:lib+库名字+.a,在生成之后会自动填充剩余部分,我们只需要指定名字即可,在windows下同样只需要给出名字即可,如果是动态库则是.os结尾windows下则是.dll
同样使用以下目录结构:
1 | ├── CMakeLists.txt |
这次我们只需要库泽CMakeLists内容如下:
1 | cmake_minimum_required(VERSION 3.10.0) |
- 如果要指定生成库的路径则需要改变
LIBRARY_OUTPUT_PATH(对于静态库和动态库都适用) - 指定生成可执行的文件路径使用
EXECUTABLE_OUTPUT_PATH,(动态库也属于可执行文件但是静态库不属于,所以无法使用于静态库)
使用方式:
1 | set(LIBRARY_OUTPUT_PATH ..) |
- 在分享给别人使用时需要给出库文件和头文件,只需要这两者即可
1.7库链接
再使用第三方库时cmake也为我们提供了链接第三方库的方法,在cmake中链接静态/动态库使用:
1 | link_libraries(<lib/libname> [<static lib>]...)#(不推荐) |
- 参数:想要链接的静态库的名字,可以写完整名字如
libText.a也可以写掐头去尾的名字如Text
上方的link_libraries不推荐使用,因为其使用时是相当于全局载入会污染全局,在新版cmake中推荐使用target_link_libraries
1 | target_link_libraries(<target> [PUBLIC/PRIVATE/INTERFACE] <lib/lib_name>) |
- target:指定链接的对象
- 这个对象可能是源文件
- 这个对象可能是动态库文件
- 这个对象可能是一个可执行文件
- PUBLIC/PRIVATE/INTERFACE:指定动态库的访问权限,默认为
PUBLIC- 如果各个库之间没有依赖关系,无需做任何设置,三者没有区别,一般无需指定,使用默认的
PUBLIC即可 动态库的链接具有传递性,如果之前有动态库A、B、C链接,然后又有动态库D链接了动态库A则相当于D也连接了B和C(注意,只有在PUBLIC模式下才有传递性中间有任何一个使用了PRIVATE的话则对应库无法被传递出去)- 三个选择的区别:
- PUBLIC:此模式下对应链接的库会被连接到target中,并且符号也会被导出
- PRIVATE:此模式下对应的库仅仅被链接到target中,并且终止,第三方无法感知你调用了什么库
- INTERFACE:这个模式下对应的库不会被链接到target中,只会导出符号
- 如果各个库之间没有依赖关系,无需做任何设置,三者没有区别,一般无需指定,使用默认的
- lib/lib_name:指定链接的库
- 注意:参数2和3一般是一个整体的参数,可以有多个,并且使用
target_link_libraries时应该在add_executable之后
如果该库不是系统提供的(自己制作或者第三方提供的)可能出现库找不到的现象,此时可以将库的路径也指定出来。使用(也同样适用动态库):
1 | link_directories(<lib_path> [<lib_path>])#这里可以指定多个路径 |
项目结构为:
1 | . |
这样我们的CMakeLists代码为:
1 | cmake_minimum_required(VERSION 3.10.0) |
1.8日志
在CMake中提供了显示指令message:
1 | message([STATUS/WARNING/AUTHOR_WARNING/SEND_ERROR/FATAL_ERROR] "message info") |
- (无):重要消息
STATUS:非重要消息WARNING:CMake警告,但是会继续执行AUTHOR_WARNING:CMake警告(dev),会继续执行SEND_ERROR:CMake错误,继续执行,但是会跳过生成步骤FATAL_ERROR:CMake错误,终止所有处理过程
使用示例
1 | #输出一般信息 |
1.9变量操作
1.9.1字符串的追加
有时候在项目中源文件并不一定都在同一个文件夹中,但是文件最终需要一起来编译最终可执行文件或者库文件,使用file 对各个路径进行搜索后还需要一个字符串拼接操作,字符串拼接操作可以用set进行也可以使用list进行
- 使用
set
1 | set(变量名 ${变量名1} ${变量名2} ...) |
上述方式是从变量名1开始依次向后拼接,最后放到一个新的变量中去如果这个新的变量名有数据那么就会被覆盖掉
- 使用
list:
1 | list(APPEND <list> [<element> ...]) |
list的功能比set强大,字符串拼接只是其中一功能,他需要在第一个位置指定出我们要做的操作,APPEND表示数据追加,后面的参数就和set一样了。
注意:在cmake底层管理时这些字符串是通过;来分隔管理的,但是一般输出时看的是一个完整的字符串
1.9.2字符串的移除
在通过file搜索某个目录时,就得到了该目录下所有的文件,但是有时候我们需要排除其部分文件则需要使用字符串移除的操作还是使用list
1 | list(REMOVE_ITEM <list> [<element> ...]) |
与上面的追加一样的格式,但是是将第一个参数改为了REMOVE_ITEM
注意:由于搜索文件后实际上是存储的对应文件的完整路径,那么在剔除某个特定文件时实际上应该是剔除这个完整路径元素
1.9.3list的其余操作
- 获取
list长度
1 | list(LENGTH <list> <output_virable>) |
LENGTH:命令参数,用于获取读列表长度<list>:当前操作的列表<output_variable>:新创建的变量用来存储结果
- 读取列表中指定索引的元素,可以指定多个索引
1 | list(GET <list> <element index> [<element index> ...] <output_variable>) |
<list><element index>:列表元素的索引- 从0开始编号,索引0的元素是列表第一个元素
- 索引也可以是负数,例如-1表示最后一个数,-2表示倒数第二个数
- 当索引长度超过列表长度就会报错(不论正负)
<output variable>:创建新变量存储结果
- 将列表中元素用连接符(字符串)连接起来组成一个字符串
1 | list(JOIN <list> <value> <output variable>) |
<list>:当前操作列表<value>:指定字符串<output variable>:创建新变量存储结果
- 查找列表是否存在指定元素,若未找到,返回-1
1 | list(FIND <list> <value> <output variable>) |
<list>:当前操作列表<value>:指定需要查询的内容<output variable>:创建新变量存储结果- 如果
<list>中存在<value>就返回下标,否则就返回-1
- 如果
- 将元素追加到列表中
1 | list(APPEND <list> [<element> ...] <output>) |
- 在指定位置插入若干个元素
1 | list(INSERT <list> [<element> ...] <output>) |
- 将元素插入到列表0索引位置
1 | list(PREPEND <list> [<element> ...]) |
- 将列表中最后一个元素移除
1 | list(POP_BACK <list> [<out_var> ...]) |
- 将列表第一个元素移除
1 | list(POP_FRONT <list> [<out_var> ...])#如果指定了变量就会存入变量,否则不存储 |
- 将指定元素从列表中移除
1 | list(REMOVE_ITEM <list> <value> [<value> ...])#如果指定了变量就会存入变量,否则不存储 |
- 将指定索引位置元素从列表中移除
1 | list(REMOVE_AT <list> <index> [<index> ...])#如果指定了变量就会存入变量,否则不存储 |
- 移除列表中重复元素
1 | list(REMOVE_DUPLICATES <list>) |
- 列表翻转
1 | list(REVERSE <list>) |
- 列表排序
1 | list(SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>]) |
COMPARE:指定排序方法STRING:按照字母顺序进行排序(为默认排序方法)FILE_BASENAME:如果是一系列路径名,会使用basename进行排名NATURAL:使用自然数排序
CASE:指明是否大小写敏感SENSITIVE:按照大小写敏感的方法进行排序(为默认方法)INSENSITIVE:按照大小写不敏感的方法进行排序
ORDER:指明排序顺序ASCENDING:按照升序排列(为默认方法)DESCENDING:按照降序排列
1.10宏定义
在进行代码编写时我们有时候需要一个宏变量来控制程序,如:
test.cpp:
1 |
|
在程序第五行中,如果这个宏被定义了则就会输出这个消息,如果没定义就不会输出,在代码中我们明显看到没有定义,实际运行时就不会出现输出
为了让测试更灵活我们可以不在代码中明确定义,在测试时去定义,一种方式就是在gcc/g++运行时去定义 如下:
1 | gcc test.cpp -DDEBUG -o app |
在gcc/g++命令中我们通过使用-D参数来定义宏名字.
在cmake中我们也可以做类似的事情,对应的命令叫:add_definitions:
1 | add_definitions(-D宏名称) |
具体:
1 | add_definitions(-DDEBUG) |
2. CMake嵌套
如果项目很大,或者项目中有很多源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每一层目录都加一个CMakeLists.txt文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
如果想使用子节点的CMakeLists.txt则需要将子节点加入到父节点的CMakeLists.txt中,CMake为我们提供了add_subdirectory
注意:父节点中的CMakeLists中的变量可以为子节点所用,但是子节点的不可为父节点所用
1 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) |
source_dir:指定了CMakeLists.txt和源代码的位置,就是指定子目录binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可EXCLUDE_FROM_ALL:在子目录下的目标默认不会被包含到父路径的ALL目标里,并且也会被安排在IDE工程文件之外。用户必须显式构建在子路径下的目标,基本不用
使用演示
项目目录如下:
1 | . |
根目录下CMakeLists.txt:
1 | cmake_minimum_required(VERSION 3.10.0) |
src/CMakeLists.txt
1 | # 搜索源文件 |
test/CmakeLists.txt
1 | # 搜索源文件 |
3. CMake语法
3.1 流程控制if、loop
3.1.1判断语句
- if语法:
if(\
\
elseif(\
\
else()
\
endif()
用法演示:
1 | set(varbool TRUE) |
3.1.2 循环语句
- for语法:
foreach(\
\
enfforeach()
foreach(\
foreach(\
foreach(\
使用ZIPLISTS时取用时${num\
- while语法:
while(\
\
endwhile()
使用演示:
1 | cmake_minimum_required(VERSION 4.00.0) |
运行结果如下:
1 | 2 |
3.2 函数
语法:
function(\
\
endfunction()
使用示例:
1 | cmake_minimum_required(VERSION 4.00.0) |
运行后得到:
1 | my func name: MyFunc |
实际上虽然我们确定了是一个参数,但是依旧可以传递多个参数,在这里函数依旧可以捕捉到多余的参数。
3.3 cmake作用域
cmake有两个作用域;
- function scope函数作用域
- Directory scope 当从add_subdirectory()命令执行嵌套目录中的CMakeLists.txt列表文件时,注意父CMakeLists.txt其中的变量可以被子CMakeLists.txt使用
示例:
1 | cmake_minimum_required(VERSION 4.00.0) |
运行:
1 | cmake -B build |
3.4 宏
语法
macro(\
\
endmacro()
注意:尽量不要写宏,会读就行
1 | cmake_minimum_required(VERSION 4.00.0) |
运行结果:
1 | var:first value |
注意,当使用了宏以后内部赋值会改动外部同名变量的内容,起传入的参数在内部使用,宏生效后同名外部变量会被改变
