CMake学习笔记2

CMake学习笔记2

2.7 日志

在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上显示其他所有消息。CMakeGUI会在它的log区域显示所有消息。

CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。

1
2
3
4
5
6
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")
2.8 变量操作
2.8.1 追加

有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set命令也可以使用list命令。

使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:

1
set(变量名1 ${变量名1} ${变量名2} ...)

关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
set(SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")

使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:

1
list(APPEND <list> [<element> ...])

list命令的功能比set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND表示进行数据追加,后边的参数和set就一样了。

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
list(APPEND SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")

CMake中,使用set命令可以创建一个list。一个在list内部是一个由分号;分割的一组字符串。例如,set(var a b c d e)命令将会创建一个list:a;b;c;d;e,但是最终打印变量值的时候得到的是abcde

1
2
3
4
set(tmp1 a;b;c;d;e)
set(tmp2 a b c d e)
message(${tmp1})
message(${tmp2})

输出的结果:

1
2
abcde
abcde
2.8.2 字符串移除

我们在通过file搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:

1
2
3
4
5
6
7
8
9
$ tree
.
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

0 directories, 5 files

在当前这么目录有五个源文件,其中main.cpp是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要add.cppdiv.cppmult.cppsub.cpp这四个源文件就可以了。此时,就需要将main.cpp从搜索到的数据中剔除出去,想要实现这个功能,也可以使用list

1
list(REMOVE_ITEM <list> <value> [<value> ...])

通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/*.cpp)
# 移除前日志
message(STATUS "message: ${SRC_1}")
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/main.cpp)
# 移除后日志
message(STATUS "message: ${SRC_1}")

可以看到,在第8行把将要移除的文件的名字指定给list就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。

关于list命令还有其它功能,但是并不常用,在此就不一一进行举例介绍了。

  1. 获取 list 的长度。

    1
    list(LENGTH <list> <output variable>)
  2. 读取列表中指定索引的的元素,可以指定多个索引

    1
    list(GET <list> <element index> [<element index> ...] <output variable>)
  3. 将列表中的元素用连接符(字符串)连接起来组成一个字符串

    1
    list (JOIN <list> <glue> <output variable>)
  4. 查找列表是否存在指定的元素,若果未找到,返回-1

    1
    list(FIND <list> <value> <output variable>)
  5. 将元素追加到列表中

    1
    list (APPEND <list> [<element> ...])
  6. 在list中指定的位置插入若干元素

    1
    list(INSERT <list> <element_index> <element> [<element> ...])
  7. 将元素插入到列表的0索引位置

    1
    list (PREPEND <list> [<element> ...])
  8. 将列表中最后元素移除

    1
    list (POP_BACK <list> [<out-var>...])
  9. 将列表中第一个元素移除

    1
    list (POP_FRONT <list> [<out-var>...])
  10. 将指定的元素从列表中移除

    1
    list (REMOVE_ITEM <list> <value> [<value> ...])
  11. 将指定索引的元素从列表中移除

    1
    list (REMOVE_AT <list> <index> [<index> ...])
  12. 移除列表中的重复元素

    1
    list (REMOVE_DUPLICATES <list>)
  13. 列表翻转

    1
    list(REVERSE <list>)
  14. 列表排序

    1
    list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
2.9 宏定义

在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#define NUMBER 3

int main()
{
int a = 10;
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif
for(int i=0; i<NUMBER; ++i)
{
printf("hello, GCC!!!\n");
}
return 0;
}

在程序的第七行对DEBUG宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,第八行就相当于被注释掉了,因此最终无法看到日志输入出(上述代码中并没有定义这个宏)。

为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++命令中去指定,如下:

1
$ gcc test.c -DDEBUG -o app

gcc/g++命令中通过参数 -D指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏,其名字为DEBUG

CMake中我们也可以做类似的事情,对应的命令叫做add_definitions:

1
add_definitions(-D宏名称)

针对于上面的源文件编写一个CMakeLists.txt,内容如下:

1
2
3
4
5
cmake_minimum_required(VERSION 3.0)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)

通过这种方式,上述代码中的第八行日志就能够被输出出来了。

3.预定义宏

下面的列表中为大家整理了一些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 保姆级教程(下)

1. 嵌套的CMake

如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。

先来看一下下面的这个的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ 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命令:

1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])