CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有 Makefile(通过 make 命令进行项目的构建),大多是 IDE 软件都集成了 make,比如:VS 的 nmake、linux 下的 GNU make、Qt 的 qmake 等,如果自己动手写 makefile,会发现,makefile 通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。
而 CMake 恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需 make 编译即可,所以可以把 CMake 看成一款自动生成 Makefile 的工具,其编译流程如下图:
makefile 构建项目的过程cmake 构建项目的过程介绍完 CMake 的作用之后,再来总结一下它的优点:
CMake 支持大写、小写、混合大小写的命令。如果在编写 CMakeLists.txt 文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。
CMake 使用 # 进行行注释,可以放在任何位置。
1 | # 这是一个 CMakeLists.txt 文件 |
CMake 使用 #[[ ]] 形式进行块注释。
1 | #[[ 这是一个 CMakeLists.txt 文件。 |
准备工作,为了方便测试,在我本地电脑准备了这么几个测试文件
add.c
1 |
|
sub.c
1 |
|
mult.c
1 |
|
div.c
1 | c++ |
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 | # PROJECT 指令的语法是: |
add_executable:定义工程会生成一个可执行程序
1 | add_executable(可执行程序名 源文件名称) |
这里的可执行程序名和 project 中的项目名没有任何关系
源文件名可以是一个也可以是多个,如有多个可用空格或 ; 间隔
1 | # 样式1 |
执行 CMake 命令
万事俱备只欠东风,将 CMakeLists.txt 文件编辑好之后,就可以执行 cmake 命令了。
1 | cmake 命令原型 |
1 | tree |
当执行 cmake 命令之后,CMakeLists.txt 中的命令就会被执行,所以一定要注意给 cmake 命令指定路径的时候一定不能出错。
执行命令之后,看一下源文件所在目录中是否多了一些文件:
1 | tree -L 1 |
我们可以看到在对应的目录下生成了一个 makefile 文件,此时再执行 make 命令,就可以对项目进行构建得到所需的可执行程序了。
1 | make |
最终可执行程序 app 就被编译出来了(这个名字是在 CMakeLists.txt 中指定的)。
通过上面的例子可以看出,如果在 CMakeLists.txt 文件所在目录执行了 cmake 命令之后就会生成一些目录和文件(包括 makefile 文件),如果再基于 makefile文件执行 make 命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为 build:
1 | mkdir build |
现在 cmake 命令是在 build 目录中执行的,但是 CMakeLists.txt 文件是 build 目录的上一级目录中,所以 cmake 命令后指定的路径为..,即当前目录的上一级目录。
当命令执行完毕之后,在 build 目录中会生成一个 makefile 文件
1 | tree build -L 1 |
这样就可以在 build 目录中执行 make 命令编译项目,生成的相关文件自然也就被存储到 build 目录中了。这样通过 cmake 和 make 生成的所有文件就全部和项目源文件隔离开了,各回各家,各找各妈。
在上面的例子中一共提供了 5 个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在 cmake 里定义变量需要使用 set。
1 | # SET 指令的语法是: |
VAR:变量名VALUE:变量值1 | # 方式1: 各个源文件之间使用空格间隔 |
在编写 C++ 程序的时候,可能会用到 C++11、C++14、C++17、C++20 等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准:
1 | g++ *.cpp -std=c++11 -o app |
上面的例子中通过参数 -std=c++11 指定出要使用 c++11 标准编译程序,C++ 标准对应有一宏叫做 DCMAKE_CXX_STANDARD。在 CMake 中想要指定 C++ 标准有两种方式:
在 CMakeLists.txt 中通过 set 命令指定
1 | #增加-std=c++11 |
在执行 cmake 命令的时候指定出这个宏的值
1 | 增加-std=c++11 |
在 CMake 中指定可执行程序输出的路径,也对应一个宏,叫做 EXECUTABLE_OUTPUT_PATH,它的值还是通过 set 命令进行设置:
1 | set(HOME /home/robin/Linux/Sort) |
第一行:定义一个变量用于存储一个绝对路径
第二行:将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录。
如果一个项目里边的源文件很多,在编写 CMakeLists.txt 文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在 CMake 中为我们提供了搜索文件的命令,可以使用 aux_source_directory 命令或者 file 命令。
在 CMake 中使用 aux_source_directory 命令可以查找某个路径下的所有源文件,命令格式为:
1 | aux_source_directory(< dir > < variable >) |
dir:要搜索的目录variable:将从 dir 目录下搜索到的源文件列表存储到该变量中1 | cmake_minimum_required(VERSION 3.0) |
如果一个项目里边的源文件很多,在编写 CMakeLists.txt 文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在 CMake 中为我们提供了搜索文件的命令,他就是 file(当然,除了搜索以外通过 file 还可以做其他事情)。
1 | file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型) |
GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。搜索当前目录的 src 目录下所有的源文件,并存储到变量中
1 | file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) |
CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径。
关于要搜索的文件路径和类型可加双引号,也可不加:
1 | file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h") |
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在 CMake 中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是 include_directories:
1 | include_directories(headpath) |
举例说明,有源文件若干,其目录结构如下:
1 | tree |
CMakeLists.txt 文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
其中,第六行指定就是头文件的路径,PROJECT_SOURCE_DIR 宏对应的值就是我们在使用 cmake 命令时,后面紧跟的目录,一般是工程的根目录。
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,下面来讲解在 cmake 中生成这两类库文件的方法。
在 cmake 中,如果要制作静态库,需要使用的命令如下:
1 | add_library(库名称 STATIC 源文件1 [源文件2] ...) |
在 Linux 中,静态库名字分为三部分:lib+ 库名字 +.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在 Windows 中虽然库名和 Linux 格式不同,但也只需指定出名字即可。
下面有一个目录,需要将 src 目录中的源文件编译成静态库,然后再使用:
1 | . |
根据上面的目录结构,可以这样编写 CMakeLists.txt 文件:
1 | cmake_minimum_required(VERSION 3.0) |
这样最终就会生成对应的静态库文件 libcalc.a。
在 cmake 中,如果要制作动态库,需要使用的命令如下:
1 | add_library(库名称 SHARED 源文件1 [源文件2] ...) |
在 Linux 中,动态库名字分为三部分:lib+ 库名字 +.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在 Windows 中虽然库名和 Linux 格式不同,但也只需指定出名字即可。
根据上面的目录结构,可以这样编写 CMakeLists.txt 文件:
1 | cmake_minimum_required(VERSION 3.0) |
这样最终就会生成对应的动态库文件 libcalc.so。
对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:
1 | cmake_minimum_required(VERSION 3.0) |
对于这种方式来说,其实就是通过 set 命令给 EXECUTABLE_OUTPUT_PATH 宏设置了一个路径,这个路径就是可执行文件生成的路径。
由于在 Linux 下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用 EXECUTABLE_OUTPUT_PATH 宏了,而应该使用 LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用。
1 | cmake_minimum_required(VERSION 3.0) |
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,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.alib)去尾(.a)之后的名字 xxx参数 2-N:要链接的其它静态库的名字
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:
1 | link_directories(<lib path>) |
这样,修改之后的 CMakeLists.txt 文件内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
添加了第 8 行的代码,就可以根据参数指定的路径找到这个静态库了。
在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的,在此不再过多赘述
在 cmake 中链接动态库的命令如下:
1 | target_link_libraries( |
target:指定要加载动态库的文件的名字
PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为 PUBLIC
如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
动态库的链接具有传递性,如果动态库 A 链接了动态库 B、C,动态库 D 链接了动态库 A,此时动态库 D 相当于也链接了动态库 B、C,并可以使用动态库 B、C 中定义的方法。
1 | target_link_libraries(A B C) |
PUBLIC:在 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 | link_directories(path) |
所以修改之后的 CMakeLists.txt 文件应该是这样的:
1 | cmake_minimum_required(VERSION 3.0) |
通过 link_directories 指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。
在 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 | # 输出一般日志信息 |
有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过 file 命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用 set 命令也可以使用 list 命令。
如果使用 set 进行字符串拼接,对应的命令格式如下:
1 | set(变量名1 ${变量名1} ${变量名2} ...) |
关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
1 | cmake_minimum_required(VERSION 3.0) |
如果使用 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 |
我们在通过 file 搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:
1 | tree |
在当前这么目录有五个源文件,其中 main.cpp 是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要 add.cpp、div.cp、mult.cpp、sub.cpp 这四个源文件就可以了。此时,就需要将 main.cpp 从搜索到的数据中剔除出去,想要实现这个功能,也可以使用 list
1 | list(REMOVE_ITEM <list> <value> [<value> ...]) |
通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了 REMOVE_ITEM。
1 | cmake_minimum_required(VERSION 3.0) |
可以看到,在第8行把将要移除的文件的名字指定给 list 就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在 list 中每个文件对应的路径都是一个 item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。
关于 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
:列表元素的索引
- 从 0 开始编号,索引 0 的元素为列表中的第一个元素;
- 索引也可以是负数,`-1` 表示列表的最后一个元素,`-2` 表示列表倒数第二个元素,以此类推
- 当索引(不管是正还是负)超过列表的长度,运行会报错
- `<output variable>`:新创建的变量,存储指定索引元素的返回结果,也是一个列表。
3. 将列表中的元素用连接符(字符串)连接起来组成一个字符串
```cmake
list (JOIN <list> <glue> <output variable>)
<list>:当前操作的列表
<glue>:指定的连接符(字符串)
<output variable>:新创建的变量,存储返回的字符串
查找列表是否存在指定的元素,若果未找到,返回 - 1
1 | list(FIND <list> <value> <output variable>) |
<list>:当前操作的列表
<value>:需要再列表中搜索的元素
<output variable>
1
2
3
4
5
6
7
8
9
10
:新创建的变量
- 如果列表 `<list>` 中存在 `<value>`,那么返回 `<value>` 在列表中的索引
- 如果未找到则返回 - 1。
5. 将元素追加到列表中
```cmake
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宏名称) |
针对于上面的源文件编写一个 CMakeLists.txt,内容如下:
1 | cmake_minimum_required(VERSION 3.0) |
通过这种方式,上述代码中的第八行日志就能够被输出出来了。
下面的列表中为大家整理了一些 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 目录进行的构建,那么得到的就是这个目录的路径 |