转载
作者: 苏丙榅链接: https://subingwen.cn/cmake/CMake-advanced/
来源: 爱编程的大丙 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
彩色MarkDown
Gitee:CMake 保姆级教程 - 爱编程的大丙
作者: 苏丙榅
链接: https://subingwen.cn/cmake/CMake-advanced/
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.嵌套的CMake
如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt
,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt
文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
先来看一下下面的这个的目录结构:
$ tree
.
├── build
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── CMakeLists.txt
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert.cpp
│ └── select.cpp
├── test1
│ ├── calc.cpp
│ └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp
6 directories, 15 files
include 目录
:头文件目录calc 目录
:目录中的四个源文件对应的加、减、乘、除算法- 对应的头文件是
include
中的calc.h
- 对应的头文件是
sort 目录
:目录中的两个源文件对应的是插入排序和选择排序算法- 对应的头文件是
include
中的sort.h
- 对应的头文件是
test1 目录
:测试目录,对加、减、乘、除算法进行测试test2 目录
:测试目录,对排序算法进行测试
可以看到各个源文件目录所需要的CMakeLists.txt
文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。
1.1 准备工作
1.1.1 节点关系
众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。
因此,我们需要了解一些关于 CMakeLists.txt
文件变量作用域的一些信息:
- 根节点
CMakeLists.txt
中的变量全局有效 - 父节点
CMakeLists.txt
中的变量可以在子节点中使用 - 子节点
CMakeLists.txt
中的变量只能在当前节点中使用
1.1.2 添加子目录
接下来我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir
:指定了CMakeLists.txt
源文件和代码文件的位置,其实就是指定子目录binary_dir
:指定了输出文件的路径,一般不需要指定,忽略即可。EXCLUDE_FROM_ALL
:在子路径下的目标默认不会被包含到父路径的ALL
目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式CMakeLists.txt
文件之间的父子关系就被构建出来了。
1.2 解决问题
在上面的目录中我们要做如下事情:
- 通过
test1 目录
中的测试文件进行计算器相关的测试 - 通过
test2 目录
中的测试文件进行排序相关的测试
现在相当于是要进行模块化测试,对于calc
和sort
目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。
1.2.1 根目录
根目录中的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)
在根节点对应的文件中主要做了两件事情:定义全局变量
和添加子目录
。
- 定义的全局变量主要是给子节点使用,目的是为了提高子节点中的
CMakeLists.txt
文件的可读性和可维护性,避免冗余并降低出差的概率。 - 一共添加了四个子目录,每个子目录中都有一个
CMakeLists.txt
文件,这样它们的父子关系就被确定下来了。
1.2.2 calc 目录
calc 目录中的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALCLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${CALC_LIB} STATIC ${SRC})
- 第3行
aux_source_directory
:搜索当前目录(calc目录)下的所有源文件 - 第4行
include_directories
:包含头文件路径,HEAD_PATH
是在根节点文件中定义的 - 第5行
set
:设置库的生成的路径,LIB_PATH
是在根节点文件中定义的 - 第6行
add_library
:生成静态库,静态库名字CALC_LIB
是在根节点文件中定义的
1.2.3 sort 目录
sort 目录中的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(SORTLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${SORT_LIB} SHARED ${SRC})
- 第6行
add_library
:生成动态库,动态库名字SORT_LIB
是在根节点文件中定义的
这个文件中的内容和calc
节点文件中的内容类似,只不过这次生成的是动态库。
在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库。
1.2.4 test1 目录
test1 目录中的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
# include_directories(${HEAD_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})
- 第4行
include_directories
:指定头文件路径,HEAD_PATH
变量是在根节点文件中定义的 - 第6行
link_libraries
:指定可执行程序要链接的静态库
,CALC_LIB
变量是在根节点文件中定义的 - 第7行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的 - 第8行
add_executable
:生成可执行程序,APP_NAME_1
变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。
1.2.5 test2 目录
test2 目录中的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
# include_directories(${HEAD_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})
- 第四行
include_directories
:包含头文件路径,HEAD_PATH
变量是在根节点文件中定义的 - 第五行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的 - 第六行
link_directories
:指定可执行程序要链接的动态库的路径,LIB_PATH
变量是在根节点文件中定义的 - 第七行
add_executable
:生成可执行程序,APP_NAME_2
变量是在根节点文件中定义的 - 第八行
target_link_libraries
:指定可执行程序要链接的动态库的名字
在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库。
1.2.6 构建项目
一切准备就绪之后,开始构建项目,进入到根节点目录的build 目录
中,执行cmake 命令
,如下:
$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/cmake/calc/build
可以看到在build
目录中生成了一些文件和目录,如下所示:
$ tree build -L 1
build
├── calc # 目录
├── CMakeCache.txt # 文件
├── CMakeFiles # 目录
├── cmake_install.cmake # 文件
├── Makefile # 文件
├── sort # 目录
├── test1 # 目录
└── test2 # 目录
然后在build 目录
下执行make 命令
:
通过上图可以得到如下信息:
- 在项目根目录的
lib
目录中生成了静态库libcalc.a
- 在项目根目录的
lib
目录中生成了动态库libsort.so
- 在项目根目录的
bin
目录中生成了可执行程序test1
- 在项目根目录的
bin
目录中生成了可执行程序test2
最后再来看一下上面提到的这些文件是否真的被生成到对应的目录中了:
$ tree bin/ lib/
bin/
├── test1
└── test2
lib/
├── libcalc.a
└── libsort.so
由此可见,真实不虚,至此,项目构建完毕。
写在最后:在项目中,如果将程序中的某个模块制作成了动态库或者静态库
并且在CMakeLists.txt 中指定了库的输出目录
,而后其它模块又需要加载这个生成的库文件,此时直接使用就可以了,如果没有指定库的输出路径或者需要直接加载外部提供的库文件,此时就需要使用 link_directories 将库文件路径指定出来。
2.流程控制
在 CMake 的 CMakeLists.txt 中也可以进行流程控制,也就是说可以像写 shell 脚本那样进行条件判断
和循环
。
2.1 条件判断
关于条件判断其语法格式如下:
if(<condition>)
<commands>
elseif(<condition>) # 可选快, 可以重复
<commands>
else() # 可选快
<commands>
endif()
在进行条件判断的时候,如果有多个条件,那么可以写多个elseif
,最后一个条件可以使用else
,但是开始和结束是必须要成对出现的,分别为:if
和endif
。
2.1.1 基本表达式
if(<expression>)
如果是基本表达式,expression
有以下三种情况:常量
、变量
、字符串
。
- 如果是
1
,ON
,YES
,TRUE
,Y
,非零值
,非空字符串
时,条件判断返回True
- 如果是
0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,空字符串
时,条件判断返回False
2.1.2 逻辑判断
NOT
if(NOT <condition>)
其实这就是一个取反操作,如果条件
condition
为True
将返回False
,如果条件condition
为False
将返回True
。AND
if(<cond1> AND <cond2>)
如果
cond1
和cond2
同时为True
,返回True
否则返回False
。OR
if(<cond1> OR <cond2>)
如果
cond1
和cond2
两个条件中至少有一个为True
,返回True
,如果两个条件都为False
则返回False
。
2.1.3 比较
基于数值的比较
if(<variable|string> LESS <variable|string>) if(<variable|string> GREATER <variable|string>) if(<variable|string> EQUAL <variable|string>) if(<variable|string> LESS_EQUAL <variable|string>) if(<variable|string> GREATER_EQUAL <variable|string>)
variable
:变量、string
:字符串LESS
:如果左侧数值小于
右侧,返回True
GREATER
:如果左侧数值大于
右侧,返回True
EQUAL
:如果左侧数值等于
右侧,返回True
LESS_EQUAL
:如果左侧数值小于等于
右侧,返回True
GREATER_EQUAL
:如果左侧数值大于等于
右侧,返回True
基于字符串的比较
if(<variable|string> STRLESS <variable|string>) if(<variable|string> STRGREATER <variable|string>) if(<variable|string> STREQUAL <variable|string>) if(<variable|string> STRLESS_EQUAL <variable|string>) if(<variable|string> STRGREATER_EQUAL <variable|string>)
variable
:变量、string
:字符串STRLESS
:如果左侧字符串小于
右侧,返回True
STRGREATER
:如果左侧字符串大于
右侧,返回True
STREQUAL
:如果左侧字符串等于
右侧,返回True
STRLESS_EQUAL
:如果左侧字符串小于等于
右侧,返回True
STRGREATER_EQUAL
:如果左侧字符串大于等于
右侧,返回True
2.1.4 文件操作
判断文件或者目录是否存在[
EXISTS
]if(EXISTS path-to-file-or-directory)
如果文件或者目录存在返回
True
,否则返回False
。判断是不是目录[
IS_DIRECTORY
]if(IS_DIRECTORY path)
- 此处目录的 path 必须是绝对路径[ 如
/home/user/
] - 如果目录存在返回
True
,目录不存在返回False
。
- 此处目录的 path 必须是绝对路径[ 如
判断是不是软连接[
IS_SYMLINK
]if(IS_SYMLINK file-name)
- 此处的 file-name 对应的路径必须是绝对路径[ 如
/home/user/file-name
] - 如果软链接存在返回
True
,软链接不存在返回False
。 - 软链接相当于 Windows 里的快捷方式
- 此处的 file-name 对应的路径必须是绝对路径[ 如
判断是不是绝对路径[
IS_ABSOLUTE
]if(IS_ABSOLUTE path)
关于绝对路径:
- 如果是
Linux
,该路径需要从根目录开始描述[ 如/home/user/file-name
] - 如果是
Windows
,该路径需要从盘符开始描述[ 如C:\Users\user\
]
- 如果是
- 如果是绝对路径返回
True
,如果不是绝对路径返回False
。
2.1.5 其它
判断某个元素是否在列表中[
IN_LIST
]if(<variable|string> IN_LIST <variable>)
- CMake 版本要求:大于等于3.3
- 如果这个元素在列表中返回
True
,否则返回False
。
比较两个路径是否相等[
PATH_EQUAL
]if(<variable|string> PATH_EQUAL <variable|string>)
- CMake 版本要求:大于等于3.24
- 如果这个元素在列表中返回
True
,否则返回False
。
关于路径的比较其实就是另个字符串的比较,如果路径格式书写没有问题也可以通过下面这种方式进行比较[
STREQUAL
]:if(<variable|string> STREQUAL <variable|string>)
我们在书写某个路径的时候,可能由于误操作会多写几个分隔符,比如把
/a/b/c
写成/a//b///c
,此时通过STREQUAL
对这两个字符串进行比较肯定是不相等的,但是通过PATH_EQUAL
去比较两个路径,得到的结果确实相等的,可以看下面的例子:cmake_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()
输出的日志信息如下:
路径相等 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 路径不相等
通过得到的结果我们可以得到一个结论:在进行路径比较的时候,如果使用 PATH\_EQUAL 可以自动剔除路径中多余的分割线然后再进行路径的对比,使用 STREQUAL 则只能进行字符串比较。
2.2 循环
在 CMake 中循环有两种方式,分别是:foreach
和while
。
2.2.1 foreach
使用 foreach 进行循环,语法格式如下:
foreach(<loop_var> <items>)
<commands>
endforeach()
通过foreach
我们就可以对items
中的数据进行遍历,然后通过loop_var
将遍历到的当前的值取出,在取值的时候有以下几种用法:
方法1
foreach(<loop_var> RANGE <stop>)
RANGE
:关键字,表示要遍历范围stop
:这是一个正整数,表示范围的结束值
,在遍历的时候从 0 开始,最大值为 stop
。loop_var
:存储每次循环取出的值
举例说明:
cmake_minimum_required(VERSION 3.2)
project(test)
# 循环
foreach(item RANGE 10)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
输出的日志信息是这样的:
$ cmake ..
-- 当前遍历的值为: 0
-- 当前遍历的值为: 1
-- 当前遍历的值为: 2
-- 当前遍历的值为: 3
-- 当前遍历的值为: 4
-- 当前遍历的值为: 5
-- 当前遍历的值为: 6
-- 当前遍历的值为: 7
-- 当前遍历的值为: 8
-- 当前遍历的值为: 9
-- 当前遍历的值为: 10
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
再次强调:在对一个整数区间进行遍历的时候,得到的范围是这样的 【0,stop】,右侧是闭区间包含 stop 这个值。
方法2
foreach(<loop_var> RANGE <start> <stop> [<step>])
这是上面方法1
的加强版,我们在遍历一个整数区间的时候,除了可以指定起始范围,还可以指定步长。
RANGE
:关键字,表示要遍历范围start
:这是一个正整数,表示范围的起始值,也就是说最小值为 start
stop
:这是一个正整数,表示范围的结束值,也就是说最大值为 stop
step
:控制每次遍历的时候以怎样的步长增长,默认为1,可以不设置
loop_var
:存储每次循环取出的值
举例说明:
cmake_minimum_required(VERSION 3.2)
project(test)
foreach(item RANGE 10 30 2)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
输出的结果如下:
$ cmake ..
-- 当前遍历的值为: 10
-- 当前遍历的值为: 12
-- 当前遍历的值为: 14
-- 当前遍历的值为: 16
-- 当前遍历的值为: 18
-- 当前遍历的值为: 20
-- 当前遍历的值为: 22
-- 当前遍历的值为: 24
-- 当前遍历的值为: 26
-- 当前遍历的值为: 28
-- 当前遍历的值为: 30
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
再次强调:在使用上面的方式对一个整数区间进行遍历的时候,得到的范围是这样的 【start,stop】,左右两侧都是闭区间,包含 start 和 stop 这两个值,步长 step 默认为1,可以不设置。
方法3
foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])
这是foreach
的另一个变体,通过这种方式我们可以对更加复杂的数据进行遍历,前两种方式只适用于对某个正整数范围内的遍历。
IN
:关键字,表示在 xxx 里边LISTS
:关键字,对应的是列表list
,通过set、list
可以获得ITEMS
:关键字,对应的也是列表loop_var
:存储每次循环取出的值
cmake_minimum_required(VERSION 3.2)
project(test)
# 创建 list
set(WORD a b c d)
set(NAME ace sabo luffy)
# 遍历 list
foreach(item IN LISTS WORD NAME)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
在上面的例子中,创建了两个 list
列表,在遍历的时候对它们两个都进行了遍历(可以根据实际需求选择同时遍历多个或者只遍历一个
)。输出的日志信息如下:
$ cd build/
$ cmake ..
-- 当前遍历的值为: a
-- 当前遍历的值为: b
-- 当前遍历的值为: c
-- 当前遍历的值为: d
-- 当前遍历的值为: ace
-- 当前遍历的值为: sabo
-- 当前遍历的值为: luffy
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
一共输出了7个字符串,说明遍历是没有问题的。接下来看另外一种方式:
cmake_minimum_required(VERSION 3.2)
project(test)
set(WORD a b c "d e f")
set(NAME ace sabo luffy)
foreach(item IN ITEMS ${WORD} ${NAME})
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
在上面的例子中,遍历过程中将关键字LISTS
改成了ITEMS
,后边跟的还是一个或者多个列表,只不过此时需要通过${}
将列表中的值取出。其输出的信息和上一个例子是一样的:
$ cd build/
$ cmake ..
-- 当前遍历的值为: a
-- 当前遍历的值为: b
-- 当前遍历的值为: c
-- 当前遍历的值为: d e f
-- 当前遍历的值为: ace
-- 当前遍历的值为: sabo
-- 当前遍历的值为: luffy
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
小细节:在通过 set 组织列表的时候,如果某个字符串中有空格,可以通过双引号将其包裹起来,具体的操作方法可以参考上面的例子。
方法4
注意事项:这种循环方式要求CMake的版本大于等于 3.17。
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
可以获得
cmake_minimum_required(VERSION 3.17)
project(test)
# 通过list给列表添加数据
list(APPEND WORD hello world "hello world")
list(APPEND NAME ace sabo luffy zoro sanji)
# 遍历列表
foreach(item1 item2 IN ZIP_LISTS WORD NAME)
message(STATUS "当前遍历的值为: item1 = ${item1}, item2=${item2}" )
endforeach()
message("=============================")
# 遍历列表
foreach(item IN ZIP_LISTS WORD NAME)
message(STATUS "当前遍历的值为: item1 = ${item_0}, item2=${item_1}" )
endforeach()
在这个例子中关于列表数据的添加是通过list
来实现的。在遍历列表的时候一共使用了两种方式,一种提供了多个变量来存储当前列表中的值,另一种只有一个变量,但是实际取值的时候需要通过变量名_0、变量名_1、变量名_N
的方式来操作,注意事项:第一个列表对应的编号是0,第一个列表对应的编号是0,第一个列表对应的编号是0。
上面的例子输出的结果如下:
$ cd build/
$ cmake ..
-- 当前遍历的值为: item1 = hello, item2=ace
-- 当前遍历的值为: item1 = world, item2=sabo
-- 当前遍历的值为: item1 = hello world, item2=luffy
-- 当前遍历的值为: item1 = , item2=zoro
-- 当前遍历的值为: item1 = , item2=sanji
=============================
-- 当前遍历的值为: item1 = hello, item2=ace
-- 当前遍历的值为: item1 = world, item2=sabo
-- 当前遍历的值为: item1 = hello world, item2=luffy
-- 当前遍历的值为: item1 = , item2=zoro
-- 当前遍历的值为: item1 = , item2=sanji
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/robin/abc/a/build
2.2.2 while
除了使用foreach
也可以使用 while
进行循环,关于循环结束对应的条件判断的书写格式和if/elseif
是一样的。while
的语法格式如下:
while(<condition>)
<commands>
endwhile()
while
循环比较简单,只需要指定出循环结束的条件即可:
cmake_minimum_required(VERSION 3.5)
project(test)
# 创建一个列表 NAME
set(NAME luffy sanji zoro nami robin)
# 得到列表长度
list(LENGTH NAME LEN)
# 循环
while(${LEN} GREATER 0)
message(STATUS "names = ${NAME}")
# 弹出列表头部元素
list(POP_FRONT NAME)
# 更新列表长度
list(LENGTH NAME LEN)
endwhile()
输出的结果如下:
$ cd build/
$ cmake ..
-- names = luffy;sanji;zoro;nami;robin
-- names = sanji;zoro;nami;robin
-- names = zoro;nami;robin
-- names = nami;robin
-- names = robin
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/robin/abc/a/build
可以看到当列表中的元素全部被弹出之后,列表的长度变成了0,此时while
循环也就退出了。
作者: 苏丙榅
链接: https://subingwen.cn/cmake/CMake-advanced/
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3. 使用函数
使用cmake定义函数的方法:
function(FunctionName ARG1 ARG2)
...
endfunction()
function(FunctionName ARGS)
...
endfunction()
FunctionName
:函数名写在第一个参数位置,往后就是函数的参数。我们可以用多个参数来分别承接外部的输入,也可以用特殊内部变量ARGV
来承接所有输入(实现变参数长度)。
下面是使用例子:
function(print_variables_1 ARG1 ARG2)
message("${ARG1}")
message("${ARG2}")
endfunction()
print_variables_1("Hello" "World")
Hello
World
function(print_variables_2)
foreach(variable ${ARGV})
message("${variable}")
endforeach()
endfunction()
print_variables_2("Hello" "World" "CMake" "Function")
Hello
World
CMake
Function
注意:函数的作用域是全局的,无论是被定义在CMakeLists.txt的父源码还是子源码里面,都是可以被访问到的。
4. 选择构建过程
为了提供项目的灵活性,并可以在编译时给予用户一定选择的权力,cmake提供了option函数,让用户能够在cmake指令中传入一些变量,进而可以影响到编译过程。
cmake_minimum_required (VERSION 3.20)
project (Tutorial)
option(ENABLE_FEATURE "This is a build switch" OFF)
if (ENABLE_FEATURE)
message("The feature is on")
else()
message("The feature is off")
endif()
option(ENABLE_FEATURE "This is a build switch" OFF)
的意思是定义了一个名为ENABLE_FEATURE
的选项。它的描述是 “This is a build switch”。初始值被设置为OFF
,意味着默认情况下该功能是禁用的。
如果我们在调用cmake命令时传入-D参数,那么就可以在构建时修改此选项,以达到切换构建效果的作用。
cmake --build . -DENABLE_FEATURE=ON
5.包
包(Package)可以理解为比库更大的软件集合,是由一系列包含库和头文件的文件夹组合。
5.1 查找包
使用find_package
用于在系统上查找并加载指定的外部库或模块。它用于在 CMake 构建过程中自动查找和配置项目所需的依赖项。
该命令的语法如下:
find_package(<package_name> [version] [EXACT] [QUIET] [REQUIRED] [COMPONENTS <component1> <component2> ...])
<package_name>
: 要查找的外部库或模块的名称。version
:库或模块的版本号,可以是一个具体的版本号或一个版本号的范围。EXACT
: 指示查找的版本必须与指定的版本完全匹配。QUIET
: 控制是否输出查找过程的详细信息。REQUIRED
: :找不到指定的库或模块时构建过程将会失败。COMPONENTS
: 指定需要找到的特定组件或附加模块。
一旦成功找到目标,可以使用类似于 include_directories
、target_include_directories
和 target_link_libraries
等命令来包含头文件和链接库。
以下是一个示例:
find_package(OpenCV 4.2.0 REQUIRED COMPONENTS core highgui)
- 在这个示例中,CMake 将会在系统上搜索名为 "OpenCV" 的库,并强制要求找到版本号为 4.2.0 的库。同时,需要找到 “core” 和 “highgui” 两个组件。
cmake官方为我们预定义了许多寻找包的依赖描述,存储在<CMakePath>/share/cmake-<version>/Modules
目录下。每个以Find<LibaryName>.cmake
命名的文件都可以帮我们找到一个包。如果这些路径都不能满足我们的需求,那么我们还可以为自己的包添加搜索路径,方法是追加到变量CMAKE_MODULE_PATH
中,如下所示。
list(APPEND CMAKE_MODULE_PATH "/path/to/package1")
list(APPEND CMAKE_MODULE_PATH "/path/to/package2")
list(APPEND CMAKE_MODULE_PATH "/path/to/package3")
更多有关find_package的知识点可以参考:https://blog.csdn.net/zhanghm1995/article/details/105466372。
5.2 创建包
如果我们希望自己写的包也能够被cmake使用find_package
找到的话,就需要按照cmake的要求组织我们的包。关键操作是创建一个名为Find<LibraryName>.cmake
的文件,并在此文件中描述此包关联的库信息。
下面是一个例子。在MyPackage目录下存放所有包相关的内容,并编写一个FindMyPackage.cmake
文件来描述包的依赖信息。
.
├── CMakeLists.txt
├── MyPackage
│ ├── FindMyPackage.cmake
│ ├── module1
│ │ ├── include
│ │ │ └── func1.h
│ │ └── lib
│ │ └── libfunc1.a
│ └── module2
│ ├── include
│ │ └── func2.h
│ └── lib
│ └── libfunc1.a
└── tutorial.cxx
- 在
FindMyPackage.cmake
文件中我们编写以下内容。其中,find_path
用来指定包所用到的头文件,以及指定头文件时应该搜索的路径,并把结果保存在MyPackage_INCLUDE_DIR
变量中;find_library
同理。
find_path(MyPackage_INCLUDE_DIR
NAMES func1.h func2.h
PATHS ${CMAKE_CURRENT_LIST_DIR}/module1/include ${CMAKE_CURRENT_LIST_DIR}/module2/include
)
find_library(MyPackage_LIBRARY
NAMES func1 func2
PATHS ${CMAKE_CURRENT_LIST_DIR}/module1/lib ${CMAKE_CURRENT_LIST_DIR}/module2/lib
)
set(MyPackage_FOUND FALSE)
if(MyPackage_INCLUDE_DIR AND MyPackage_LIBRARY)
set(MyPackage_FOUND TRUE)
endif()
然后我们在项目中使用这个库。
注意:我们把包的路径通过
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/MyPackage)
添加到了cmake搜索路径中,才能被cmake使用find_package
找到。
cmake_minimum_required (VERSION 3.20)
project (Tutorial)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/MyPackage)
find_package(MyPackage REQUIRED)
add_executable(Tutorial tutorial.cxx)
target_include_directories(Tutorial PRIVATE ${MyPackage_INCLUDE_DIR})
target_link_libraries(Tutorial PRIVATE ${MyPackage_LIBRARY})
6.安装项目
在完成项目的构建之后,我们可能需要把编译后产物放在某一个目录下进行运行,这个时候就涉及到安装我们的项目。安装项目的内容可能包括:可执行文件、库文件、配置文件、头文件、帮助文档等。
install函数
如果需要安装项目,那么除了使用cmake执行构建命令之外,还需要执行安装命令,如下所示。
#cmake --build .
cmake --install .
方式一
install(PROGRAMS <data_files> DESTINATION <destination>)
- 第一个参数用来指定拷贝的文件类型。如下面用到的
TARGETS
、FILES
。
注意:如果需要拷贝可执行文件,那么一定要使用PROGRAMS
而不是FILES
。否则的话cmake会在安装的过程中修改可执行文件的执行权限为普通文件,剥夺了可执行的文件权限。
cmake_minimum_required(VERSION 3.10)
project(INSTALL)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
add_executable(main main.cpp)
message(STATUS "installing exectuables into " ${PROJECT_SOURCE_DIR}/install)
file(GLOB MY_INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/*.h)
install(TARGETS main DESTINATION ${PROJECT_SOURCE_DIR}/install)
install(FILES ${MY_INCLUDE_FILES} DESTINATION ${PROJECT_SOURCE_DIR}/install/include)
DESTINATION
: 目的地,目标- 第7行
file(GLOB MY_INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/*.h)
使用file
命令和GLOB
模式匹配,将${PROJECT_SOURCE_DIR}/include
文件夹中的所有.h
文件存储在 自定义MY_INCLUDE_FILES
变量中。 - 第8行
install(TARGETS main DESTINATION ${PROJECT_SOURCE_DIR}/install)
安装名为“main”
的可执行文件到${PROJECT_SOURCE_DIR}/install
文件夹。 - 第9行
install(FILES ${MY_INCLUDE_FILES} DESTINATION ${PROJECT_SOURCE_DIR}/install/include)
安装MY_INCLUDE_FILES
变量中的所有头文件到${PROJECT_SOURCE_DIR}/install/include
文件夹。
如果在拷贝过程中需要提前确认文件是否存在,以避免找不到文件而报错,则可以使用下面的方法。
if(EXISTS "path/to/file.txt")
# do something
else()
# do something
endif()
方式二
如果在安装的过程中希望执行一些程序来辅助安装过程,那么可以通过以下代码来实现。可以用这个方法来新建一些文件夹或者执行python脚本。
install(CODE "EXECUTE_PROCESS(COMMAND echo \"Hello, World!\")"
# install(...)