CMake 保姆级教程(下)

目录
CMake 保姆级教程(上)
CMake 保姆级教程(下)

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 添加子目录: add_subdirectory

接下来我们还需要知道在 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 解决问题

在上面的目录中我们要做如下事情:

  1. 通过 test1 目录中的测试文件进行计算器相关的测试
  2. 通过 test2 目录中的测试文件进行排序相关的测试
    现在相当于是要进行模块化测试,对于calcsort目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。

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})
link_directories(${LIB_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(SORTTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
link_directories(${LIB_PATH})
add_executable(${APP_NAME_2} ${SRC})
target_link_libraries(${APP_NAME_2} ${SORT_LIB})
  • 第四行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 命令:
在这里插入图片描述
通过上图可以得到如下信息:

  1. 在项目根目录的lib目录中生成了静态库libcalc.a
  2. 在项目根目录的lib目录中生成了动态库libsort.so
  3. 在项目根目录的bin目录中生成了可执行程序test1
  4. 在项目根目录的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,但是开始和结束是必须要成对出现的,分别为:ifendif

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>)

其实这就是一个取反操作,如果条件conditionTrue将返回False,如果条件conditionFalse将返回True

  • AND
if(<cond1> AND <cond2>)

如果cond1cond2同时为True,返回True否则返回False

  • OR
if(<cond1> OR <cond2>)

如果cond1cond2两个条件中至少有一个为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>)
  • 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>)
  • STRLESS:如果左侧字符串小于右侧,返回True
  • STRGREATER:如果左侧字符串大于右侧,返回True
  • STREQUAL:如果左侧字符串等于右侧,返回True
  • STRLESS_EQUAL:如果左侧字符串小于等于右侧,返回True
  • STRGREATER_EQUAL:如果左侧字符串大于等于右侧,返回True

2.1.4 文件操作

  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

2.1.5 其它

  • 判断某个元素是否在列表中

    if(<variable|string> IN_LIST <variable>)
    
    • CMake 版本要求:大于等于3.3
    • 如果这个元素在列表中返回True,否则返回False。
  • 比较两个路径是否相等

    if(<variable|string> PATH_EQUAL <variable|string>)
    
    • CMake 版本要求:大于等于3.24
    • 如果这个元素在列表中返回True,否则返回False。

关于路径的比较其实就是另个字符串的比较,如果路径格式书写没有问题也可以通过下面这种方式进行比较:

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 则只能进行字符串比较。

关于 if 的更多条件判断,请参考官方文档

2.2 循环

在 CMake 中循环有两种方式,分别是:foreachwhile

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循环也就退出了。

2.3 可选参数: option

在 CMake 中,option 用于定义一个可选的参数,它可以影响到构建过程中的一些决策。这个参数可以被用户设置为 ON 或 OFF,默认值是 OFF。

Option 的语法如下:

option(<option_variable> "help string" [initial value])

其中,

  • option_variable:定义一个变量名作为 Option 的名称。
  • help string:描述 Option 的帮助信息。
  • initial value:Option 的初始值,默认为 OFF。
    例如,我们可以使用以下代码来定义一个 Option:
option(BUILD_EXAMPLES "Build examples" ON)

这个 Option 定义了一个 BUILD_EXAMPLES 变量,它控制是否构建示例程序,并且默认值为 ON。

在 CMakeLists.txt 文件中可以使用 ${BUILD_EXAMPLES} 来获取 BUILD_EXAMPLES 变量的值,然后根据这个值来控制构建过程中相关的行为。

例如:

if(${BUILD_EXAMPLES})
    add_subdirectory(examples)
endif()

这段代码检查 BUILD_EXAMPLES 是否为真(即 ON),如果是,则添加 examples 目录进行编译。如果 BUILD_EXAMPLES 是假(即 OFF),则不会编译 examples 目录下的内容。

通过设置 Option 来影响构建过程,可以使得用户能够自由选择需要构建哪些组件或者功能,增加了 CMake 构建系统的灵活性和可配置性。

3. 安装 install

在cmake的时候,最常见的几个步骤就是:

mkdir build && cd build
cmake ..
make
make install 

那么,make install主要用于安装已经编译好的程序。具体来说,这条命令将复制编译生成的文件树中的文件到指定的位置。make install的时候,是需要我们定义一个install的目标么?

显然并不需要,作为一个经常需要被运行的指令,官方提供了一个命令install,只需要经过该命令的安装内容,不需要显示地定义install目标。此时,make install就是运行该命令的内容。

install命令
install用于指定在安装时运行的规则。它可以用来安装很多内容,可以包括目标二进制、动态库、静态库以及文件、目录、脚本等:

install(TARGETS <target>... [...])
install({FILES | PROGRAMS} <file>... [...])
install(DIRECTORY <dir>... [...])
install(SCRIPT <file> [...])
install(CODE <code> [...])
install(EXPORT <export-name> [...])

有时候,也会用到一个非常有用的变量CMAKE_INSTALL_PREFIX用于指定cmake install时的相对地址前缀。 用法如:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..

目标文件的安装

install(TARGETS targets... [EXPORT <export-name>]
        [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
          PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
         [DESTINATION <dir>]
         [PERMISSIONS permissions...]
         [CONFIGURATIONS [Debug|Release|...]]
         [COMPONENT <component>]
         [NAMELINK_COMPONENT <component>]
         [OPTIONAL] [EXCLUDE_FROM_ALL]
         [NAMELINK_ONLY|NAMELINK_SKIP]
        ] [...]
        [INCLUDES DESTINATION [<dir> ...]]
        )

参数中的TARGET可以是很多种目标文件,最常见的是通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,即可执行二进制、动态库、静态库:

目标文件内容安装目录变量默认安装文件夹
ARCHIVE静态库${CMAKE_INSTALL_LIBDIR}lib
LIBRARY动态库${CMAKE_INSTALL_LIBDIR}lib
RUNTIME可执行二进制文件${CMAKE_INSTALL_BINDIR}bin
PUBLIC_HEADER与库关联的PUBLIC头文件${CMAKE_INSTALL_INCLUDEDIR}include
PRIVATE_HEADER与库关联的PRIVATE头文件${CMAKE_INSTALL_INCLUDEDIR}include

为了符合一般的默认安装路径,如果设置了DESTINATION参数,推荐配置在安装目录变量下的文件夹。

例如:

INSTALL(TARGETS myrun mylib mystaticlib
       RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
       LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
       ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

上面的例子会将:可执行二进制myrun安装到${CMAKE_INSTALL_BINDIR}目录,动态库libmylib.so安装到${CMAKE_INSTALL_LIBDIR}目录,静态库libmystaticlib.a安装到${CMAKE_INSTALL_LIBDIR}目录。

该命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;
    PERMISSIONS:指定安装文件的权限。有效权限是OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID和SETGID;
  • CONFIGURATIONS:指定安装规则适用的构建配置列表(DEBUG或RELEASE等);
  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;
  • OPTIONAL:如果要安装的文件不存在,则指定不是错误。

注意一下CONFIGURATIONS参数,此选项指定的值仅适用于此选项之后列出的选项:例如,要为调试和发布配置设置单独的安装路径,请执行以下操作:

install(TARGETS target
        CONFIGURATIONS Debug
        RUNTIME DESTINATION Debug/bin)
install(TARGETS target
        CONFIGURATIONS Release
        RUNTIME DESTINATION Release/bin)

也就是说,DEBUG和RELEASE版本的DESTINATION安装路径不同,那么DESTINATION必须在CONFIGUATIONS后面。

普通文件的安装

install(<FILES|PROGRAMS> files...
        TYPE <type> | DESTINATION <dir>
        [PERMISSIONS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>]
        [RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])

FILES|PROGRAMS若为相对路径给出的文件名,将相对于当前源目录进行解释。其中,FILES为普通的文本文件,PROGRAMS指的是非目标文件的可执行程序(如脚本文件)

如果未提供PERMISSIONS参数,默认情况下,普通的文本文件将具有OWNER_WRITE,OWNER_READ,GROUP_READ和WORLD_READ权限,即644权限;而非目标文件的可执行程序将具有OWNER_EXECUTE, GROUP_EXECUTE,和WORLD_EXECUTE,即755权限。

其中,不同的TYPE,cmake也提供了默认的安装路径,如下表:

TYPE类型安装目录变量默认安装文件夹
BIN${CMAKE_INSTALL_BINDIR}bin
SBIN${CMAKE_INSTALL_SBINDIR}sbin
LIB${CMAKE_INSTALL_LIBDIR}lib
INCLUDE${CMAKE_INSTALL_INCLUDEDIR}include
SYSCONF${CMAKE_INSTALL_SYSCONFDIR}etc
SHAREDSTATE${CMAKE_INSTALL_SHARESTATEDIR}com
LOCALSTATE${CMAKE_INSTALL_LOCALSTATEDIR}var
RUNSTATE${CMAKE_INSTALL_RUNSTATEDIR}/run
DATA${CMAKE_INSTALL_DATADIR}
INFO${CMAKE_INSTALL_INFODIR}/info
LOCALE${CMAKE_INSTALL_LOCALEDIR}/locale
MAN${CMAKE_INSTALL_MANDIR}/man
DOC${CMAKE_INSTALL_DOCDIR}/doc

请注意,某些类型的内置默认值使用DATAROOT目录作为前缀,以CMAKE_INSTALL_DATAROOTDIR变量值为内容。

该命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;
  • PERMISSIONS:指定安装文件的权限。有效权限是OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID和SETGID;
  • CONFIGURATIONS:指定安装规则适用的构建配置列表(DEBUG或RELEASE等);
  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;
  • OPTIONAL:如果要安装的文件不存在,则指定不是错误;
    RENAME:指定已安装文件的名称,该名称可能与原始文件不同。仅当命令安装了单个文件时,才允许重命名。
    目录的安装
install(DIRECTORY dirs...
        TYPE <type> | DESTINATION <dir>
        [FILE_PERMISSIONS permissions...]
        [DIRECTORY_PERMISSIONS permissions...]
        [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>] [EXCLUDE_FROM_ALL]
        [FILES_MATCHING]
        [[PATTERN <pattern> | REGEX <regex>]
         [EXCLUDE] [PERMISSIONS permissions...]] [...])

该命令将一个或多个目录的内容安装到给定的目的地,目录结构被逐个复制到目标位置。每个目录名称的最后一个组成部分都附加到目标目录中,但是可以使用后跟斜杠来避免这种情况,因为它将最后一个组成部分留空。这是什么意思呢?

比如,DIRECTORY后面如果是abc意味着abc这个目录会安装在目标路径下,abc/意味着abc这个目录的内容会被安装在目标路径下,而abc目录本身却不会被安装。即,如果目录名不以/结尾,那么这个目录将被安装为目标路径下的abc,如果目录名以/结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。

FILE_PERMISSIONS和DIRECTORY_PERMISSIONS选项指定对目标中文件和目录的权限。如果指定了USE_SOURCE_PERMISSIONS而未指定FILE_PERMISSIONS,则将从源目录结构中复制文件权限。如果未指定权限,则将为文件提供在命令的FILES形式中指定的默认权限(644权限),而目录将被赋予在命令的PROGRAMS形式中指定的默认权限(755权限)。

可以使用PATTERN或REGEX选项以精细的粒度控制目录的安装,可以指定一个通配模式或正则表达式以匹配输入目录中遇到的目录或文件。PATTERN仅匹配完整的文件名,而REGEX将匹配文件名的任何部分,但它可以使用/和$模拟PATTERN行为。

**某些跟随PATTERN或REGEX表达式后的参数,仅应用于满足表达式的文件或目录。**如:EXCLUDE选项将跳过匹配的文件或目录。PERMISSIONS选项将覆盖匹配文件或目录的权限设置。

例如:

install(DIRECTORY icons scripts/ DESTINATION share/myproj
        PATTERN "CVS" EXCLUDE
        PATTERN "scripts/*"
        PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
                    GROUP_EXECUTE GROUP_READ)

这条命令的执行结果是:将icons目录安装到share/myproj,将scripts/中的内容安装到share/myproj,两个目录均不包含目录名为CVS的子目录,对于scripts/*的文件指定权限为OWNER_EXECUTE,OWNER_WRITE,OWNER_READ,GROUP_EXECUTE,GROUP_READ。

安装时脚本的运行
有时候需要在install的过程中打印一些语句,或者执行一些cmake指令:

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

SCRIPT参数将在安装过程中调用给定的CMake脚本文件(即.cmake脚本文件),如果脚本文件名是相对路径,则将相对于当前源目录进行解释。CODE参数将在安装过程中调用给定的CMake代码。将代码指定为双引号字符串内的单个参数。

例如:

install(CODE "MESSAGE(\"Sample install message.\")")

这条命令将会在install的过程中执行cmake代码,打印语句。

参考资料:
https://subingwen.cn/cmake/CMake-advanced/
https://blog.csdn.net/qq_38410730/article/details/102837401

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值