CMake学习笔记

CMake学习笔记
THEDI摘自: https://subingwen.cn/cmake/CMake-primer/#2-6-3-%E6%80%BB%E7%BB%93
来源: 爱编程的大丙
视频教程:哔哩哔哩_bilibili
官方文档:cmake-commands(7) — CMake 4.0.2 Documentation
CMake概述
CMake 是一个跨平台
的项目构建工具。关于项目构建还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,比如:VS 的 nmake、linux 下的 GNU make、Qt 的 qmake等,如果自己动手写makefile,就会发现,makefile
通常依赖于当前的编译平台,而且编写makefile 的工作量比较大,解决依赖关系时也容易出错。
而 CMake
恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,再根据编译平台,自动生成本地化的Makefile和工程文件
,最后用户只需make
编译即可,所以可以把CMake看成一款自动生成 Makefile的工具,其编译流程如下图:
蓝色虚线为
makefile
构建项目的过程红色实线为
cmake
构建项目的过程
cmake
命令会读取CMakeLists.txt
,并分析你的源码依赖、编译规则等,然后会自动生成平台和工具链相关的构建文件(在 Linux 下通常是 Makefile,也可以生成 ninja 文件、VS 工程等)。
Makefile
是 make 工具使用的构建说明文件,包含了所有编译、链接目标的依赖和命令。make
命令会根据 Makefile,自动调用 gcc/g++ 等编译器,把源代码编译为目标文件(.o)、静态/动态库、可执行文件等。CMake和Makefile区别:
CMake 主要是“构建系统生成器”,不直接参与编译,只是根据你的 CMakeLists.txt 生成适合你平台的 Makefile(或其他构建系统文件)。
Makefile 里写明了实际编译与链接的步骤,make命令才会真正调用编译器执行编译动作。
所以:CMake 的作用是生成构建规则(Makefile),make 的作用是执行这些规则完成编译。
总结CMake优点:
- 跨平台
- 能够管理大型项目(流水线管理)
- 简化编译构建过程和编译过程
- 可扩展:可以为 cmake 编写特定功能的模块,扩充 cmake 功能
CMake的基本使用
注释
CMake支持大写、小写、混合大小写的命令。如果在编写CMakeLists.txt文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。
注释行
CMake 使用 # 进行行注释,可以放在任何位置。
1 | # 这是一个 CMakeLists.txt 文件 |
注释块
CMake
使用 #[[ ]]
形式进行块注释
。
1 | #[[ 这是一个 CMakeLists.txt 文件 |
只有源文件
全在一个目录下
- 准备了以下几个测试文件
- add.c
1 |
|
- div.c
1 |
|
- head.h
1 |
|
- main.c
1 |
|
- 上述文件的目录结构如下:
1 | tree |
- 添加
CMakeLists.txt
文件
在上述源文件所在目录下添加一个新文件CMakeLists.txt
,完整文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
接下来依次介绍一下在CMakeLists.txt
文件中添加的三个命令:
cmake_minimum_required
:指定使用的 cmake 的最低版本- 可选,非必须,不加可能会有警告
project
: 定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略
的,只需要指定出工程名字
即可。1
2
3
4
5
6
7
8
9
10# PROJECT 指令的语法是:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
# 一般只指定名字
project(my_project_name)add_executable
: 定义工程会生成一个可执行程序1
2
3
4add_executable(可执行文件名 源文件1 源文件2 ...)
# 可执行文件名:你想要生成的可执行程序的名字(不需要加扩展名)。
# 源文件列表:组成该程序的所有源文件(.cpp、.c、.cc等)这里的可执行程序名和
project
中的项目名没有任何关系源文件名可以是一个也可以是多个,如有多个可用空格或**;**间隔
1
2
3
4# 样式1
add_executable(app add.c div.c main.c)
# 样式2
add_executable(app add.c;div.c;main.c)
- 执行
CMake
命令
将CMakeLists.txt
文件编辑好之后,就可以执行 cmake
命令了
1 | cmake命令作用:根据 CMakeLists.txt 配置文件,自动生成适合当前平台的构建系统文件(Makefile等) |
当执行cmake
命令之后,CMakeLists.txt
中的命令就会被执行,所以一定要注意给cmake 命令指定路径的时候一定不能出错。
执行命令之后,看一下源文件所在目录中是否多了一些文件:
我们可以看到在对应的目录下生成了一个makefile
文件,此时再执行make
命令,就可以对项目进行构建得到所需的可执行程序了。
最终可执行程序app
就被编译出来了(这个名字是在CMakeLists.txt
中指定的)。
build目录
通过上面的例子可以看出,如果在CMakeLists.txt文件
所在目录执行了cmake命令
之后就会生成一些目录和文件(包括 makefile 文件
),如果再基于makefile
文件执行make
命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为build
:
现在cmake
命令是在build
目录中执行的,但是CMakeLists.txt
文件是build
目录的上一级目录中,所以cmake
命令后指定的路径为..
,即当前目录的上一级目录。
当命令执行完毕之后,在build
目录中会生成makefile
文件,和其他文件
这样就可以在build
目录中执行make
命令编译项目,生成的相关文件自然也就被存储到build目录中了。这样通过cmake
和make
生成的所有文件就全部和项目源文件隔离开了,便于管理和维护
回到build
目录,执行make
命令,生成可执行文件app
,运行app成功
私人订制
定义变量
在上面的例子中一共提供了5个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串(cmake中默认的都是字符串形式
)存储起来,在cmake里定义变量需要使用set
。
1 | # SET 指令的语法是: |
- VAR:变量名
- VALUE:变量值,取值方法:
${变量名}
1 | # 方式1: 各个源文件之间使用空格间隔 |
指定使用的C++标准
在编写C++程序的时候,可能会用到C++11、C++14、C++17、C++20等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准,不指定的话默认使用C++98的标准
1 | g++ *.cpp -std=c++11 -o app |
上面的例子中通过参数-std=c++11指定出要使用c++11标准编译程序,C++标准对应有一宏叫做DCMAKE_CXX_STANDARD。在CMake中想要指定C++标准有两种方式:
- 在 CMakeLists.txt 中通过 set 命令指定:设置CMAKE_CXX_STANDARD宏的值
1 | #增加-std=c++11 |
- 在执行 cmake 命令的时候指定出这个宏的值
1 | 增加-std=c++11 |
在上面例子中 CMake 后的路径需要根据实际情况酌情修改。
指定输出路径
在CMake中指定可执行程序输出的路径(不指定默认当前目录下输出),也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH
,它的值还是通过set
命令进行设置:
1 | # 定义变量 |
- 第一行:定义一个变量用于存储一个绝对路径
- 第二行:将拼接好的路径值设置给
EXECUTABLE_OUTPUT_PATH
宏- 如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录。
示例使用
可以看到生成的可执行文件app出现在了我们指定的/home/keqiudi/aa/bb/cc下
搜索文件
如果一个项目里边的源文件很多,在编写CMakeLists.txt
文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory
命令或者file
命令。
其中的涉及到存放源文件的变量不需要使用set定义,直接使用即可(就是自动将指定目录的源文件定义为一个变量供add_executable使用的过程,可以看作set的升级版)
方式1
在 CMake 中使用aux_source_directory
命令可以查找某个路径下的所有源文件
,命令格式为:
1 | aux_source_directory(< dir > < variable >) |
dir
:要搜索的目录variable
:将从dir
目录下搜索到的源文件列表存储到该变量中
1 | cmake_minimum_required(VERSION 3.0) |
PROJECT_SOURCE_DIR
: 自带宏定义,即CMakeLists.txt文件路径CMAKE_CURRENT_SOURCE_DIR
: 自带宏定义,也是CMakeLists.txt文件路径。与PROJECT_SOURCE_DIR完全相同,随便使用一个即可
方式2
第二种方式是使用file
(当然,除了搜索以外通过 file 还可以做其他事情)进行搜索,与方式1的区别呢,使用file,需要执行文件的类型(后缀)
1 | file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型) |
- GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
- GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
搜索当前目录的src目录下所有的源文件,并存储到变量中
1 | file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) |
- 关于要搜索的文件路径和类型可加双引号,也可不加:
1 | file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h") |
示例使用
方式1:aux_source_directory
方式2:file
包含头文件
添加方法
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories
:
1 | include_directories(headfile_path) |
举例说明,有源文件若干,其目录结构如下:
1 | tree |
CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
其中,第六行指定就是头文件的路径,PROJECT_SOURCE_DIR
宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。
示例使用
我们将原本头文件和源文件混合的情况下,创建src
和include
的文件夹,分别存放源文件和头文件。我们将原本的头文件和源文件移动到include
和src
文件夹
修改CMakeLists.txt 中的搜索文件添加以下src路径:
执行cmake和make命令:我们可以发现报错说找不到头文件
此时由两种处理方式:
修改头文件路径:将原来的head.h改为../include/head.h
修改后通过编译:
这种方法如果头文件很多的话就会很麻烦,所以推荐下面再CMakeLists中添加包含头文件
在CMakeLists.txt中添加包含头文件(推荐使用):
在10行中我们添加对头文件路径的搜索,include中是我们的所有头文件的文件夹所在目录
然后进行我们的make命令,成功找到头文件,成功运行可执行文件app
制作动态库和静态库
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,提供时需要将动态/静态库(源代码的二进制文件libxxx.a/.lib或libxxx.so/.dll)
和头文件
提供给使用者才能使用
其实就是相当于把很多相关的
.c源文件
合起来打包变成了二进制文件
。使用者看不到
.c
文件内容(隐藏实现),只能通过头文件
来看有哪些函数,如何使用,再调用函数,否则用户看不懂。
下面来讲解在cmake中生成这两类库文件的方法。
制作静态库
在cmake中,如果要制作静态库,需要使用的命令如下:
1 | add_library(库名称 STATIC 源文件1 [源文件2] ...) |
在Linux中,静态库名字分为三部分:lib
+库名字
+.a
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中静态库名字是:lib
+库名字
+.lib
,
下面有一个目录,需要将src
目录中的源文件编译成静态库,然后再使用:
1 | . |
根据上面的目录结构,可以这样编写CMakeLists.txt
文件:
1 | cmake_minimum_required(VERSION 3.0) |
这样最终就会生成对应的静态库文件libcalc.a
。
制作静态链接库示例
现在我们是创建链接库,将原来的生成可执行文件add_executable
注释掉,田间add_library()
,使用STATIC
执行cmake
和make
后,我们可以看到在当前目录下生成了libcalc.a(静态库)
可以看到图中
libcalc.a
是白色的,是因为静态库没有可执行权限的
,而我们的动态链接库是绿色的,具有执行权限
制作动态库
在cmake
中,如果要制作动态库,需要使用的命令如下:
1 | add_library(库名称 SHARED 源文件1 [源文件2] ...) |
在Linux中,动态库名字分为三部分:lib
+库名字
+.so
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中动态库的名字叫lib
+库名字
+.dll
根据上面的目录结构,可以这样编写CMakeLists.txt
文件:
1 | cmake_minimum_required(VERSION 3.0) |
这样最终就会生成对应的动态库文件libcalc.so
。
创建动态链接库示例
现在我们是创建链接库,将原来的生成可执行文件add_executable
注释掉,田间add_library()
,使用SHARED
执行cmake
和make
后,我们可以看到在当前目录下生成了libcalc.so(动态链接库)
可以看到图中
libcalc.so
是绿色的,是因为动态链接库是有可执行权限的
,而我们的静态库没有执行权限
指定库文件输出的路径
方式1- 适用于动态库
对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的
,所以可以按照生成可执行程序的方式去指定它生成的目录:
1 | cmake_minimum_required(VERSION 3.0) |
对于这种方式来说,其实就是通过set
命令给EXECUTABLE_OUTPUT_PATH
宏设置了一个路径,这个路径就是可执行文件生成的路径。
方式2-都适用(推荐使用这种)
由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH
宏了,而应该使用LIBRARY_OUTPUT_PATH
,这个宏对应静态库文件和动态库文件都适用。
1 | cmake_minimum_required(VERSION 3.0) |
示例使用
使用set()
,宏LIBRARY_OUTPUT_PATH
,指定库文件输出路径到lib
静态链接库:STATIC
动态链接库:SHARED
我们可以看到切换为动态链接库后,也在lib下生成了对应的libcalc.so
库文件
使用链接库(包含库文件)
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。
链接静态库
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。
1 | src |
现在我们把上面src
目录中的add.cpp
、div.cpp
、mult.cpp
、sub.cpp
编译成一个静态库文件libcalc.a
。
测试目录结构如下:
1 | tree |
在cmake中,链接静态库的命令如下:
1 | link_libraries(<static lib> [<static lib>...]) |
用于设置全局链接库,这些库会链接到之后定义的所有目标上
- 参数1:指定出要链接的静态库的名字
- 可以是全名
libxxx.a
- 也可以是掐头(
lib
)去尾(.a
)之后的名字 xxx
- 可以是全名
- 参数2-N:可以链接一个或多个静态库,这里依次填写要链接的其它静态库的名字即可
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时需要将静态库的路径也指定出来:
1 | #如果是系统提供的静态库不需要指定路径,只需要使用上面的link_libraries就可以找到 |
这样,修改之后的CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
这样编译之后就会将 源文件(SRC_LIST) 和 库文件(link_libraries(calc))放到一起,最终生成我们的可执行文件app
静态库使用会被打包到可执行文件中
使用静态链接库示例
目前把动态链接库和静态链接库分别创建了lib_shared
和lib_static
,在我们的main.cpp
中使用他们
src文件夹的内容是库文件的.c文件实现,这里我们制作成了动态库和静态库,所以该文件夹可以直接删去,只需要使用链接库文件(libcalc.a/so)即可
修改CMakeLists.txt:我们先不指出库文件的路径,注释掉link_directories
我们执行cmake和make,报错发现找不到calc库文件
我们将注释的静态库路径重新启用:link_directories
修改后编译就可以找到库文件的了,编译通过
链接动态库
在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的,在此不再过多赘述,如有疑惑请参考Linux静态库和动态库
在cmake
中链接动态库的命令如下:
1 | target_link_libraries( |
用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库。它支持指定库的名称、路径以及链接库的顺序。
target:指定要加载的库的文件的名字
- 该文件可能是一个源文件
- 该文件可能是一个动态库/静态库文件
- 该文件可能是一个可执行文件
PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为
PUBLIC
如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,
一般无需指定,使用默认的 PUBLIC 即可
。动态库的链接具有传递性(仅PUBLIC权限)
,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。1
2target_link_libraries(A B C) # A 链接B、C
target_link_libraries(D A)# D 链接 APUBLIC(传递多次):在public后面的库会被Link到前面的所有target中,并且里面的符号(函数)也会被导出,提供给第三方使用(具有传递性)。
PRIVATE(仅传递一次):在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库(不具有传递性)。
INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号(不具有传递性)。
链接系统动态库
动态库的链接和静态库是完全不同的:
静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
动态库在生成可执行程序的链接阶段
不会
被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存
因此,在cmake
中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后
:
1 | cmake_minimum_required(VERSION 3.0) |
在target_link_libraries(app pthread)中
:
app
: 对应的是最终生成的可执行程序的名字pthread
:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so,在指定的时候一般会掐头(lib)去尾(.so)。
链接第三方动态库
现在,自己编写并生成了一个动态库,对应的目录结构如下:
1 | tree |
假设在测试文件main.cpp中既使用了自己制作的动态库libcalc.so又使用了系统提供的线程库,此时CMakeLists.txt文件可以这样写:
1 | cmake_minimum_required(VERSION 3.0) |
在第六行中,pthread
、calc
都是可执行程序app
要链接的动态库的名字。当可执行程序app
生成之后并执行该文件,会提示有如下错误信息:
1 | ./app |
这是因为可执行程序启动之后,去加载calc
这个动态库,但是不知道这个动态库被放到了什么位置解决动态库无法加载的问题,所以就加载失败了。
在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,在动态库使用的是这个命令:
1 | target_link_directories( |
这里也可以使用之前静态库中
link_directories
,一个是全局,一个是指定
所以修改之后的CMakeLists.txt
文件应该是这样的:
1 | cmake_minimum_required(VERSION 3.0) |
通过link_directories
/target_link_directories
指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。
使用动态库链接示例
法1或法2有一点区别但都能使用,随便使用一种即可,建议使用法2
图中的PUBLIC权限可以省略,因为默认就是PUBLIC,使用其他权限时就需要明确指出
同时图中只链接了一个库cal
,如果要链接多个库之间使用空格分开即可
一定要保证target_link_libraries
在生成可执行文件(add_excutable
)之后
target_link_libraries和link_libraries区别
温馨提示:
target_link_libraries
和link_libraries
是 CMake 中用于链接库的两个命令,其实都可以用于链接动态库和静态库,但它们的使用场景和功能有所不同。下面是关于二者的总结:
target_link_libraries/target_link_directories
功能:
target_link_libraries
用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库。它支持指定库的名称、路径以及链接库的顺序。语法:
1
2target_link_libraries(target_name [item1 [item2 [...]]]
[<debug|optimized|general> <lib1> [<lib2> [...]]])优点:
- 更精确地控制目标的链接库。
- 可以指定库的不同链接条件(如调试版本、发布版本)。
- 支持多个目标和多个库之间的复杂关系。
- 更加灵活和易于维护,特别是在大型项目中。
示例:
1
2
3add_executable(my_executable main.cpp)
target_link_libraries(my_executable PRIVATE my_dynamic_library)
link_libraries/link_directories
功能:
link_libraries
用于设置全局链接库,这些库会链接到之后定义的所有目标上。它会影响所有的目标,适用于全局设置,但不如target_link_libraries
精确。语法:
1
link_libraries(lib1 lib2 [...])
缺点:
- 缺乏针对具体目标的控制,不适合复杂的项目结构。
- 容易导致意外的依赖关系,因为它对所有目标都生效。
- 一旦设置,全局影响可能导致难以追踪的链接问题。
示例:
1
2
3link_libraries(my_static_library)
add_executable(my_executable main.cpp)总结:
target_link_libraries/target_link_directories 只影响指定target,是
推荐
的方式,因为它允许更精确的控制和管理链接库的依赖,特别是在大型项目中,它能够避免全局设置可能带来的问题。link_libraries/link_directories 全局影响,虽然简单,但在复杂的项目中可能会导致意外的问题,通常适用于简单的项目或临时设置。
静态库和动态库链接方法几乎一样,可以混用,但建议用现代CMake写法。
建议在 CMake 项目中优先使用 target_link_libraries
/target_link_directories
。
日志(调试)
在编写CMakeLists的过程中,如果我们想要输出一些调试信息,就可以使用message命令
命令
在CMake中可以用用户显示一条消息,该命令的名字为message
:
1 | message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...) |
(无)
:不指定,默认是重要消息STATUS
:非重要消息WARNING
:CMake 警告, 会继续执行AUTHOR_WARNING
:CMake 警告 (dev), 会继续执行SEND_ERROR
:CMake 错误, 继续执行,但是会跳过生成的步骤FATAL_ERROR
:CMake 错误, 终止所有处理过程
CMake的命令行工具会在stdout上显示STATUS
消息,在stderr上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。
CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。
1 | # 输出一般日志信息 |
示例
CMakeLists中添加四条日志: 默认、STATUS、FATAL_ERROR、STATUS
执行到FATAL_ERROR,日志3处发生中断
,日志4不执行
注释掉FATAL_ERROR的日志部分
正常运行到日志4
变量操作
追加
有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file
命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set
命令也可以使用list
命令。
使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:
1 | set(变量名1 ${变量名1} ${变量名2} ...) |
关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
1 | cmake_minimum_required(VERSION 3.0) |
示例-使用set拼接
定义变量tmp1为helloworld,将SRC值拼接到tmp1后方
输出tmp 和tmp1的值
使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:
1 | list(APPEND <list> [<element> ...]) |
list
命令的功能比set
要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND
表示进行数据追加,后边的参数和set
就一样了。
1 | cmake_minimum_required(VERSION 3.0) |
在CMake中,使用set
命令可以创建一个list
。一个在list
内部是一个由分号;
分割的一组字符串。例如,set(var a b c d e)
命令将会创建一个list:a;b;c;d;e
,底层通过分号管理,但是最终打印变量值的时候会去掉分号将所有字符串拼接起来,得到的是abcde
。
1 | set(tmp1 a;b;c;d;e) |
输出的结果:
1 | abcde |
示例-使用list拼接
使用list append命令追加xxx1、sss2等字符串
字符串移除(list)
我们在通过file
搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:
1 | tree |
在当前这么目录有五个源文件,其中main.cpp
是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要add.cpp
、div.cpp
、mult.cpp
、sub.cpp
这四个源文件就可以了。此时,就需要将main.cpp
从搜索到的数据中剔除出去(但实际我们并不想删除main.cpp
),想要实现这个功能,也可以使用list
1 | list(REMOVE_ITEM <list> <value> [<value> ...]) |
通过上面的命令原型可以看到删除和追加数据(APPEND
)类似,只不过是第一个参数变成了REMOVE_ITEM
。
1 | cmake_minimum_required(VERSION 3.0) |
可以看到,在第8行
把将要移除的文件的名字指定给list
就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。
示例-字符串移除
我们需要的就是去掉mian.cpp的路径,让其不包含在内
使用list remove命令,指定main.cpp的绝对路径
可以看到两次输出的路径中,第二次的输出已经删除了main.cpp的路径
list其他功能
关于list
命令还有其它功能,但是并不常用,在此就不一一进行举例介绍了。
获取list的长度
1
list(LENGTH <list> <output variable>)
LENGTH
:子命令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> <glue> <output variable>)
<list>
:当前操作的列表<glue>
:指定的连接符(字符串)<output variable>
:新创建的变量,存储返回的字符串
查找列表是否存在指定的元素,若果未找到,返回-1
1
list(FIND <list> <value> <output variable>)
<list>
:当前操作的列表<value>
:需要再列表中搜索的元素<output variable>
:新创建的变量- 如果列表
<list>
中存在<value>
,那么返回<value>
在列表中的索引 - 如果未找到则返回-1。
- 如果列表
将元素追加到列表中
1
list (APPEND <list> [<element> ...])
在list中指定的位置插入若干元素
1
list(INSERT <list> <element_index> <element> [<element> ...])
将元素插入到列表的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 |
|
在程序的第七行对DEBUG
宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,第八行就相当于被注释掉了,因此最终无法看到日志输入出(上述代码中并没有定义这个宏)。
为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++
命令中去指定,如下:
1 | gcc test.c -DDEBUG -o app |
在gcc/g++
命令中通过参数 -D
指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏,其名字为DEBUG
。
在CMake
中我们也可以做类似的事情,宏定义对应的命令叫做add_definitions
:
1 | add_definitions(-D宏名称 -D宏名称 -D宏名称 ...) |
针对于上面的源文件编写一个CMakeLists.txt
,内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
通过这种方式,上述代码中的第八行日志就能够被输出出来了。
示例
两个分别是添加宏定义和未添加宏定义的输出,确实在DEBUG的控制下日志才会输出
嵌套CMake
如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt
,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt
文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
先来看一下下面的这个的目录结构:
1 | tree |
include 目录
:头文件目录calc 目录
:目录中的四个源文件对应的加、减、乘、除算法- 对应的头文件是
include
中的calc.h
- 对应的头文件是
sort 目录
:目录中的两个源文件对应的是插入排序和选择排序算法- 对应的头文件是
include
中的sort.h
- 对应的头文件是
test1 目录
:测试目录,对加、减、乘、除算法进行测试test2 目录
:测试目录,对排序算法进行测试
可以看到各个源文件目录所需要的CMakeLists.txt文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。
准备工作
节点关系
众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点
。因此,我们需要了解一些关于 CMakeLists.txt
文件变量作用域的一些信息:
- 根节点
CMakeLists.txt
中的变量全局有效
- 父节点
CMakeLists.txt
中的变量可以在子节点中使用 - 子节点
CMakeLists.txt
中的变量只能在当前节点中使用
根节点的CMakeLists中定义的变量可以在任意子节点中直接使用!
添加子目录
接下来我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:
1 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) |
source_dir
:指定了CMakeLists.txt
源文件和代码文件的位置,其实就是指定子目录binary_dir
:指定了输出文件的路径,一般不需要指定,忽略即可。EXCLUDE_FROM_ALL
:在子路径下的目标默认不会被包含到父路径的ALL目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式CMakeLists.txt
文件之间的父子关系就被构建出来了。
解决问题
在上面的目录中我们要做如下事情:
- 通过
test1 目录中
的测试文件进行计算器相关的测试 - 通过
test2 目录中
的测试文件进行排序相关的测试
现在相当于是要进行模块化测试,对于calc
和sort
目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。
根目录
根目录中的 CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
在根节点对应的文件中主要做了两件事情:定义全局变量
和添加子目录
。
- 定义的全局变量主要是给子节点使用,目的是为了提高子节点中的
CMakeLists.txt
文件的可读性和可维护性,避免冗余并降低出差的概率。 - 一共添加了四个子目录,每个子目录中都有一个
CMakeLists.txt
文件,这样它们的父子关系就被确定下来了。
calc目录
calc 目录中的 CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
- 第3行
aux_source_directory
:搜索当前目录(calc目录)下的所有源文件 - 第4行
include_directories
:包含头文件路径,HEAD_PATH
是在根节点文件中定义的 - 第5行
set
:设置库的生成的路径,LIB_PATH
是在根节点文件中定义的 - 第6行
add_library
:生成静态库,静态库名字CALC_LIB
是在根节点文件中定义的
sort目录
sort 目录中的 CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
- 第6行
add_library
:生成动态库,动态库名字SORT_LIB
是在根节点文件中定义的
这个文件中的内容和calc
节点文件中的内容类似,只不过这次生成的是动态库。
在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库。
test1目录
test1 目录中的 CMakeLists.txt
文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
第4行
include_directories
:指定头文件路径,HEAD_PATH
变量是在根节点文件中定义的第6行
link_libraries
:指定可执行程序要链接的静态库
,CALC_LIB
变量是在根节点文件中定义的第7行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的第8行
add_executable
:生成可执行程序,APP_NAME_1
变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。
test2目录
test2目录中的CMakeLists.txt文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
- 第四行
include_directories
:包含头文件路径,HEAD_PATH
变量是在根节点文件中定义的 - 第五行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的 - 第六行
link_directories
:指定可执行程序要链接的动态库的路径,LIB_PATH
变量是在根节点文件中定义的 - 第七行
add_executable
:生成可执行程序,APP_NAME_2
变量是在根节点文件中定义的 - 第八行
target_link_libraries
:指定可执行程序要链接的动态库的名字
在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库。
构建项目
一切准备就绪之后,开始构建项目,进入到根节点目录的build
目录中,执行cmake
命令,如下:
1 | cmake .. |
可以看到在build
目录中生成了一些文件(Makefile等)和目录,如下所示:
1 | tree build -L 1 |
然后在build
目录下再执行make
命令:
通过上图可以得到如下信息:
- 在项目根目录的
lib
目录中生成了静态库libcalc.a
- 在项目根目录的
lib
目录中生成了动态库libsort.so
- 在项目根目录的
bin
目录中生成了可执行程序test1
- 在项目根目录的
bin
目录中生成了可执行程序test2
最后再来看一下上面提到的这些文件是否真的被生成到对应的目录中了:
1 | tree bin/ lib/ |
确实生成了,项目构建完毕。
注意
在项目中,如果将程序中的某个模块制作成了动态库或者静态库
并且在CMakeLists.txt 中指定了库的输出目录
,而后其它模块又需要加载这个生成的库文件,此时直接使用就可以了,如果没有指定库的输出路径或者需要直接加载外部提供的库文件,此时就需要使用 link_directories 将库文件路径指定出来
。
实践练习
准备好对应的文件,calc目录、sort目录、test1目录、test2目录、include目录
思路:calc
目录和sort
目录生成对应的库文件,test1
和test2
目录调用库生成可执行程序app_calc
和app_sort
根CMakeLists.txt
calc
下的子CMakeLists.txt
sort
下的子CMakeLists.txt
test1
下的子CMakeLists.txt
test2
下的子CMakeLists.txt
回到根目录的build
目录下,执行cmake
和make
命令
可以看到,生成了对应的库文件和可执行文件
执行app_calc
和app_sort
文件,执行成功,说明没有问题。同时对应lib
和bin
中的文件也是正确的
总结
通过父CMakeLists.txt和多个子CMakeLists.txt的使用,我们更方便对项目的管理,每个子CMakeLists.txt
做的事情都单一化了
。
推荐在大型项目中使用多个CMakeLists.txt的形式管理
还有我们上面在根CMakeLists.txt
中定义变量然后再去子CMakeLists.txt
中使用的方法,看起来会比较工整、简洁。
但是在单独看子CMakeLists.txt时,对于变量值我们还需要到根CMakeLists.txt
中去寻找,这样的话可读性较低,不利于我们直观的理解,上方因为需要演示根CMakeLists.txt
变量可以在子CMakeLists.txt
中使用,所以这样写。
实际上:我们直接将对应的路径写出来即可,不需要这样传递变量使用
在静态库中链接静态库
现在去掉test1
文件夹,将原有的calc
的操作添加到sort
库中的insert.cpp
之中去
根CMakeLists
:去掉子节点test1
insert.cpp
中添加加法操作
修改sort
下的CMakeLists.txt
:链接我们自定义的calc库
执行cmake
和make
命令,生成对应的app_sort
,执行发现确实出现了加法操作,故在静态库sort中链接静态库calc成功
对应test2
下的CMakeLists.txt
:只链接了静态库sort,因为sort本来就链接了calc,这里相当于也链接了calc,就不需要再链接calc了
相当于生成的sort静态库中包含了calc静态库
在静态库中链接动态库
- 将
calc
中的CMakeLists.txt
中的生成静态库(STATIC)改成动态库(SHARED)
- 将
sort
中的CMakeLists.txt
中改为链接动态库calc到我们的静态库sort库中
- 执行
cmake
和make
命令
这里我们可以看到生成了动态库libcalc.so、静态库libsort.a、可执行文件app_sort,并且可以成功运行,说明静态库链接动态库成功
静态库被打包到了app_sort中,动态库没有被打包
CMake流程控制
在 CMake 的 CMakeLists.txt 中也可以进行流程控制,也就是说可以像写 shell 脚本那样进行条件判断
和循环
。
条件判断
关于条件判断其语法格式如下:
1 | if(<condition>) |
在进行条件判断的时候,如果有多个条件,那么可以写多个elseif
,最后一个条件可以使用else
,但是开始和结束是必须要成对出现的,分别为:if
和endif
。
基本表达式
1 | if(<expression>) |
如果是基本表达式,expression
有以下三种情况:常量
、变量
、字符串
。
- 如果是
1
,ON
,YES
,TRUE
,Y
,非零值
,非空字符串
时,条件判断返回True
- 如果是
0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,空字符串
时,条件判断返回False
逻辑判断
- NOT
1 | if(NOT <condition>) |
其实这就是一个取反操作,如果条件condition
为True
将返回False
,如果条件condition
为False
将返回True
。
- AND
1 | if(<cond1> AND <cond2>) |
如果cond1
和cond2
同时为True
,返回True
否则返回False
。
- OR
1 | if(<cond1> OR <cond2>) |
如果cond1
和cond2
两个条件中至少有一个为True
,返回True
,如果两个条件都为False
则返回False
。
比较
- 基于数值的比较
1 | if(<variable|string> LESS <variable|string>) |
LESS
:如果左侧数值小于
右侧,返回True
GREATER
:如果左侧数值大于
右侧,返回True
EQUAL
:如果左侧数值等于
右侧,返回True
LESS_EQUAL
:如果左侧数值小于等于
右侧,返回True
GREATER_EQUAL
:如果左侧数值大于等于
右侧,返回True
基于字符串的比较
1 | if(<variable|string> STRLESS <variable|string>) |
STRLESS
:如果左侧字符串小于
右侧,返回True
STRGREATER
:如果左侧字符串大于
右侧,返回True
STREQUAL
:如果左侧字符串等于
右侧,返回True
STRLESS_EQUAL
:如果左侧字符串小于等于
右侧,返回True
STRGREATER_EQUAL
:如果左侧字符串大于等于
右侧,返回True
文件操作
判断文件或者目录是否存在
1
if(EXISTS path-to-file-or-directory)
如果文件或者目录存在返回
True
,否则返回False
。判断是不是目录
1
if(IS_DIRECTORY path)
此处目录的
path
必须是绝对路径
如果目录存在返回True
,目录不存在返回False
。判断是不是软链接
1
if(IS_SYMLINK file-name)
- 此处的 file-name 对应的路径必须是绝对路径
- 如果软链接存在返回
True
,软链接不存在返回False
。 - 软链接相当于 Windows 里的快捷方式。
判断是不是绝对路径
1
if(IS_ABSOLUTE path)
- 关于绝对路径:
- 如果是
Linux
,该路径需要从根目录开始描述 - 如果是
Windows
,该路径需要从盘符开始描述
- 如果是
- 如果是绝对路径返回
True
,如果不是绝对路径返回False
。
- 关于绝对路径:
其他
判断某个元素是否在列表中
1
if(<variable|string> IN_LIST <variable>)
- CMake 版本要求:大于等于3.3
- 如果这个元素在列表中返回
True
,否则返回False
。
比较两个路径是否相等
1
if(<variable|string> PATH_EQUAL <variable|string>)
- CMake 版本要求:大于等于3.24
- 如果这个元素在列表中返回
True
,否则返回False
。
关于路径的比较其实就是另个字符串的比较,如果路径格式书写没有问题也可以通过下面这种方式进行比较:
1
if(<variable|string> STREQUAL <variable|string>)
我们在书写某个路径的时候,可能由于误操作会多写几个分隔符,比如把
/a/b/c
写成/a//b///c
,此时通过STREQUAL
对这两个字符串进行比较肯定是不相等的,但是通过PATH_EQUAL
去比较两个路径,得到的结果确实相等的,可以看下面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16cmake_minimum_required(VERSION 3.26)
project(test)
if("/home//robin///Linux" PATH_EQUAL "/home/robin/Linux")
message("路径相等")
else()
message("路径不相等")
endif()
message(STATUS "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
if("/home//robin///Linux" STREQUAL "/home/robin/Linux")
message("路径相等")
else()
message("路径不相等")
endif()输出的日志信息如下:
1
2
3路径相等
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
路径不相等通过得到的结果我们可以得到一个结论:在进行路径比较的时候,如果使用 PATH_EQUAL 可以自动剔除路径中多余的分割线然后再进行路径的对比,使用 STREQUAL 则只能进行字符串比较。
更多if条件判断,请参考官方文档:
循环
在 CMake 中循环有两种方式,分别是:foreach
和while
。
foreach
使用 foreach 进行循环,语法格式如下:
1 | foreach(<loop_var> <items>) |
通过foreach
我们就可以对items
中的数据进行遍历,然后通过loop_var
将遍历到的当前的值取出,在取值的时候有以下几种用法:
方法1
1 | foreach(<loop_var> RANGE <stop>) |
RANGE
:关键字,表示要遍历范围stop
:这是一个正整数,表示范围的结束值
,在遍历的时候从 0 开始,最大值为 stop
。loop_var
:存储每次循环取出的值
举例说明:
1 | cmake_minimum_required(VERSION 3.2) |
输出的日志信息是这样的:
1 | cmake .. |
再次强调:在对一个整数区间进行遍历的时候,得到的范围是这样的 【0,stop】,右侧是闭区间包含 stop 这个值。
方法2
1 | foreach(<loop_var> RANGE <start> <stop> [<step>]) |
这是上面方法1
的加强版,我们在遍历一个整数区间的时候,除了可以指定起始范围,还可以指定步长。
RANGE
:关键字,表示要遍历范围start
:这是一个正整数,表示范围的起始值,也就是说最小值为 startstop
:这是一个正整数,表示范围的结束值,也就是说最大值为 stopstep
:控制每次遍历的时候以怎样的步长增长,默认为1,可以不设置loop_var
:存储每次循环取出的值
举例说明:
1 | cmake_minimum_required(VERSION 3.2) |
输出的结果如下:
1 | cmake .. |
再次强调:在使用上面的方式对一个整数区间进行遍历的时候,得到的范围是这样的 【start,stop】,左右两侧都是闭区间,包含 start 和 stop 这两个值,步长 step 默认为1,可以不设置。
方法3
1 | foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]]) |
这是foreach
的另一个变体,通过这种方式我们可以对更加复杂的数据进行遍历,前两种方式只适用于对某个正整数范围内的遍历。
IN
:关键字,表示在 xxx 里边
LISTS
:关键字,对应的是列表list
,通过set
、list
可以获得
ITEMS
:关键字,对应的也是列表
loop_var
:存储每次循环取出的值
1 | cmake_minimum_required(VERSION 3.2) |
在上面的例子中,创建了两个 list
列表,在遍历的时候对它们两个都进行了遍历(可以根据实际需求选择同时遍历多个或者只遍历一个
)。输出的日志信息如下:
1 | cd build/ |
一共输出了7个字符串,说明遍历是没有问题的。接下来看另外一种方式:
1 | cmake_minimum_required(VERSION 3.2) |
在上面的例子中,遍历过程中将关键字LISTS
改成了ITEMS
,后边跟的还是一个或者多个列表,只不过此时需要通过${}
将列表中的值取出。其输出的信息和上一个例子是一样的:
1 | cd build/ |
小细节:在通过 set 组织列表的时候,如果某个字符串中有空格,可以通过双引号将其包裹起来,具体的操作方法可以参考上面的例子。
方法4
注意事项:这种循环方式要求CMake的版本大于等于 3.17。
1 | foreach(<loop_var>... IN ZIP_LISTS <lists>) |
通过这种方式,遍历的还是一个或多个列表,可以理解为是方式3
的加强版。因为通过上面的方式遍历多个列表,但是又想把指定列表中的元素取出来使用是做不到的,在这个加强版中就可以轻松实现。
loop_var
:存储每次循环取出的值,可以根据要遍历的列表的数量指定多个变量,用于存储对应的列表当前取出的那个值。- 如果指定了多个变量名,它们的数量应该和列表的数量相等
- 如果只给出了一个 loop_var,那么它将一系列的 loop_var_N 变量来存储对应列表中的当前项,也就是说 loop_var_0 对应第一个列表,loop_var_1 对应第二个列表,以此类推……
- 如果遍历的多个列表中一个列表较短,当它遍历完成之后将不会再参与后续的遍历(因为其它列表还没有遍历完)。
IN
:关键字,表示在 xxx 里边ZIP_LISTS
:关键字,对应的是列表list
,通过set
、list
可以获得
1 | cmake_minimum_required(VERSION 3.17) |
在这个例子中关于列表数据的添加是通过list
来实现的。在遍历列表的时候一共使用了两种方式,一种提供了多个变量来存储当前列表中的值,另一种只有一个变量,但是实际取值的时候需要通过变量名_0
、变量名_1
、变量名_N
的方式来操作,注意事项:第一个列表对应的编号是0,第一个列表对应的编号是0,第一个列表对应的编号是0。
上面的例子输出的结果如下:
1 | cd build/ |
while
除了使用foreach
也可以使用 while
进行循环,关于循环结束对应的条件判断的书写格式和if/elseif
是一样的。while
的语法格式如下:
1 | while(<condition>) |
while
循环比较简单,只需要指定出循环结束的条件即可:
1 | cmake_minimum_required(VERSION 3.5) |
输出的结果如下:
1 | cd build/ |
可以看到当列表中的元素全部被弹出之后,列表的长度变成了0,此时while
循环也就退出了。
预定义宏
下面的列表是一些CMake
中已经预定义的常用的宏:
宏 | 功能说明 |
---|---|
PROJECT_SOURCE_DIR | 使用 cmake 命令后紧跟的目录,一般是工程的根目录 |
PROJECT_BINARY_DIR | 执行 cmake 命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的 CMakeLists.txt 所在的路径 |
CMAKE_CURRENT_BINARY_DIR | target 编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标二进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
PROJECT_NAME | 返回通过 PROJECT 指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在 build 目录进行的构建,那么得到的就是这个目录的路径 |
基于多目录的CMake编写
看这个就可以了: