声明:
本文档参考来源于 作者:爱编程的大丙 作品:CMake保姆级教程(上)
一、CMake概述
Cmake是一个项目构建工具,而且是跨平台的,以前我们都是直接写Makefile,通过make命令进行项目的构建,但是如果自己手写makefile的话,你会发现makefile通常依赖于当前的编译平台,而且编写makefile的工作量比较大,依赖关系也容易出错
CMake恰好能解决上述问题,其允许开发者指定整个工程的编译流程,再根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需要 make 编译即可,我们可以把CMake看成一款自动生成的Makefile的工具:(不用自己手写Makefile文件)

CMake的优点:
1.跨平台
2.能够管理大型项目
3.跨平台
4.能够管理大型项目
5.简化编译构建过程和编译过程
6.可扩展:可以为cmake编写特定功能的模块,扩充cmake功能
二、CMake的使用
CMake支持大写、小写、混合大小写的命令,如果在编写CMakeLists.txt 文件时使用的工具有对应的命令提示,那么大小写不重要,不用特别在意
2.1 注释
2.1.1 注释行
CMake 使用 # 进行 行注释,可以放在任何位置

2.1.2 注释块
CMake 使用 #\[ ] 形式进行 块注释

2.1 只有源文件
2.1.1 共处一室

在上述的源文件所在目录下添加一个新文件CMakeLists.txt,文件内容如下

依次介绍在CMakeLists.txt文件中添加的三个命令:
cmake_minimum_required:指定使用的cmake最低版本
这个命令是可选的,如果不加可能会有警告
project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况下支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可

add_executable:定义工程会返回一个可执行程序

这里的可执行程序名和 project 中的项目名没有任何关系
源文件名可以是一个也可以是多个,多个用空格或者 ;间隔
执行 CMake命令
将CMakeLists.txt文件编译好之后,就可以执行cmake命令了


当执行 cmake命令之后,CMakeLists.txt中的命令就会被执行,所有一定要注意给cmake命令指定路径的时候一定不能出错
执行命令之后,看一下源文件所在目录是否多了一些文件:

我们可以看到在对应的目录下生成了一个makefile文件,此时再执行make命令,就可以对项目进行构建得到所需的可执行城西


最终可执行程序 app 就被编译出来了(这个名字是在CMakeLists.txt中指定的)
2.1.2 build单独存放
通过上述例子可以看出,如果在 CMakeLists.txt 文件所在目录执行 cmake 命令之后就会生成一些目录和文件(包括 makefile 文件),如果再基于 makefile 文件执行 make 命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样就会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里面,比如这个目录名为 build(常用)

现在 cmake 命令是在 build 目录中执行的,但是 CMakeLists.txt 文件是 build 目录的上一级目录中,所以 cmake 命令后指定的路径为 .. ,即当前目录的上一级目录
当命令执行完毕之后,在 build 目录中就会生成一个 makefile 文件:

这样就可以在 build 目录中执行 make 命令编译项目,生成的相关文件自然也就被存储到 build目录中了,这样通过 cmake 和 make 生成的所有文件就全部和项目源文件隔离开了
2.2 私人订制
2.2.1 定义变量
在上面的例子中一共提供了5个源文件,假设这5个源文件需要反复使用,每次都直接将它们的名字写出来很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在 cmake 里定义变量需要使用 set

VAR : 变量名
VALUE : 变量值

2.2.2 指定使用的C++标准
在编写C++程序的时候,可能会用到C++11、C++14、C++14、C++17、C++20等新特性,那么就需要在编写的时候子啊编译命令中指定要使用哪个标准

上面的例子中通过参数 -std=c++11 指定出要使用C++11标准编译程序,C++标准对应有一宏叫做
DCMAKE_CXX_STANDARD ,在CMake中想要指定C++标准有两种方式:
①在CMakeLists.txt 中通过 set 命令指定

②在执行 cmake 命令的时候指定出这个宏的值

(注意cmake后的路径需要根据实际情况自主修改)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
如果编译器不支持指定的C++版本,CMake就会报错
2.2.3 指定输出的路径
在 cmake 中指定可执行程序输出的路径,也对应一个宏,叫做 EXECUTABLE_OUTPUT_PATH,它的值还是通过 set 命令来进行设置

第一行:定义一个变量用于存储一个绝对路径
第二行:将拼好的路径值设置给 EXECUTABLE_OUTPUT_PATH 宏
如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
由于可执行程序是基于cmake命令生成的makefile文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的./对应的就是makefile文件所在的那个目录
2.3 搜索文件
如果一个项目里边的源文件很多,在编写CMakeLists.txt 文件的时候不可能将项目目录的各个文件一 一 罗列出来,所以在CMake中为我们提供了搜索文件的命令,可以使用 aux_source_directory 命令或者 file 命令
2.3.1 方式1
在CMake里使用 aux_source_directory 命令可以查找某个路径下的 所有源文件,命令格式为:

dir :要搜索的目录
variable:将从 dir 目录下搜索到的源文件列表存储到该变量中

2.3.2 方式2
如果一个项目里边的源文件很多,在编写CMakeLists.txt 文件的时候不可能将项目目录的各个文件一一罗列出来,在CMake中为我们提供了搜索文件的命令,他就是file (也可以通过file做其他)

GLOB:将指定目录下搜索到的满足条件的所有文件名都生成一个列表,并将其存储到变量之中
GLOB_RECURSE:递归搜索目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中
搜索当前目录的src目录下的所有源文件,并存储到变量中

CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的CMakeLists.txt文件所在的路径
关于要搜索的文件路径和类型可加双引号,也可不加:

2.4 包含头文件
在编译项目源文件时,很多情况下都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译,在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定,即是 include_directories:

举例说明,有源文件若干,其目录结构如下:

CMakeLists.txt 文件内容如下:

其中,第六行就是头文件的路径,PROJECT_SOURCE_DIR 宏对应的值就是使用cmake命令时,后面紧跟的目录,一般是工程的根目录
2.5 制作动态库或静态库
有些时候我们编写的源代码并不需要将他们编译生成可支持程序,而是生成一些静态库或动态库提供给第三方使用,接下来讲解在cmake中生成这两类库文件的方法
2.5.1 制作静态库
在cmake中如果要执着静态库,需要使用的命令:

在Linux 中,静态库名字分为三部分:lib + 库名字 + .a,此处只需要指出库的名字就可以了,另外两部分在生成该文件的时候会自动填充

根据上面的目录结构,可以这样编写 CMakeLists.txt 文件

这样就会最终生成对应的动态库文件 libcalc.so
2.5.2 制作动态库
在cmake中,如果要制作动态库,需要使用的命令如下:

在Linux中,动态库名字分为三部分:lib + 库名字 + .so,此处仍只需指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充
在Windows中虽然库名和Linux风格不同,但也只需指定出名字即可
根据上面的目录,可以这样编写CMakeLists.txt文件:

这样最终就会生成对应的动态库文件 libcalc.so
2.5.3 指定输出的路径
方式1 - 适用于动态库
对于生成的库文件和可执行程序一样都可以指定输出路径,由于在Linux下生成的动态库默认是有执行权限的,所有可以按照生成可执行程序的方式去指定它生成的目录

对于这种方式就是通过 set 命令给 EXECUTABLE_OUTPUT_PATH 宏设置了一个路径,这个路径就是可执行文件生成的路径
方式2 - 都适用
由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用 EXECUTABLE_OUTPUT_PATH 宏了,而应该使用 LIBRARY_OUTPUT_PATH ,这个宏对应静态库文件和动态库文件都适用

2.6 包含库文件
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令
2.6.1 链接静态库

现在我们把上面 src 目录中的 add.cpp div.cpp mult.cpp sub.cpp编译成一个静态库文件libcalc.a
测试目录结构如下:

在cmake中,链接静态库的命令如下:

用于设置全局链接库,这些库会链接到之后定义的所有目标上
参数1:指定出要链接的静态库的名字
可以是全名 libxxx.a
也可以是掐头(lib)去尾(.a)之后的名字 xxx
参数2-N:要链接的其他静态库的名字
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来

这样,修改之后的 CMakeLists.txt 文件内容如下:

这样就可以根据参数指定的路径找到这个静态库了
2.6.2 链接动态库
在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的,在此不在过多赘述
在 cmake 中链接动态库的命令如下:

用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库,它支持特定库的名称、路径、以及链接库的顺序
target:指定要加载的库的文件的名字
该文件可能是一个源文件
可能是一个动态库/静态库文件
可能是一个可执行文件
PRIVATE | PUBLIC | INTERFACE:动态库的访问权限,默认为 PUBLIC
如果各个动态库之间没有依赖关系,无需做任何设置,三者没有区别,一般无需指定,使用默认的PUBLIC即可
动态库的链接具有传递性,如果动态库A链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法

PUBLIC:在public 后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用
PRIVATE:在private 后面的库仅被Link到前面的target中,并且终结掉,第三方库不能感知你调了什么库
INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号
链接系统动态库
动态库的链接和静态库是完全不同的:
静态库会在生成可执行程序的链接阶段会被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了
动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存
因此,在 cmake 中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后:

在 target_link_libraies(app pthread) 中:
app:对应的是最终生成的可执行程序的名字
pthread:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so,在指定的时候一般会掐头去尾
链接第三仿动态库
现在,自己生成了一个动态库,对应的目录结构如下:

假设在测试文件 main.cpp 中即使用了自己制作的动态库 libcalc.so 又使用了系统提供的线程库,此时 CMakeLists.txt 文件可以这样写:

在第六行中,pthread calc都是可执行程序 app 要链接的动态库的名字,当可执行程序 app 生成之后并执行该文件,会有提示如下错误信息:
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
这是因为可执行程序启动之后,去加载 calc 这个动态库,但是不知道这个动态库被放到了什么位置,所以就加载失败了,在 CMake 中可以生成在可执行程序之前,通过命令指定出要链接的动态库位置,指定静态库使用的也是这个命令:

所以修改之后的 CMakeLists.txt 文件应该是这样的:

通过 link_directories 指定了动态库的路径之后,在执行生成的可执行程序的时候,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题
2.6.3 总结
target_link_libraries
功能:target_link_libraries 用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库,它支持指定库的名称,路径以及链接库的顺序
语法:
target_link_libraries(target_name item1 \[item2 \[...]]
\
优点:
更精确地控制目标的链接库
可以指定库的不同链接条件
支持多个目标和多个库之间的复杂关系
更加灵活和易于维护,特别是在大型项目中
示例:
add_executable(my_executable main.cpp)
target_link_libraries(my_executable PRIVATE my_dynamic_library)
link_libraries
功能:link_libraries 用于设置全局链接库,这些库会链接到之后定义的所有目标上,它会影响所有的目标,适用于全局设置,但不如target_libraries 精确
语法:

缺点:
缺乏针对具体目标的控制,不适合复杂的项目结构
容易导致意外的依赖关系,因为它对所有的目标都生效
一旦设置,全局影响可能导致难以追踪的链接问题
2.7 日志
在CMake中可以用用户显示一条消息,该消息的名字为message:
message(STATUS\|WARNING\|AUTHOR_WARNING\|FATAL_ERROR\|SEND_ERROR "message to display" ...)
(无):重要消息
(STATUS):非重要消息
(WARNING):CMake警告,会继续执行
(AUTHOR_WAINING):CMake警告(dev),会继续执行
(SEND_ERROR):CMake错误,继续执行,但是会跳过 生成的步骤
(FATAL_ERROR):CMake错误,终止所有处理过程
CMake的命令工具会在stdout上显示STATUS消息,在stderr上显示其他所有消息,CMake的GUI会在它的log区域显示所有消息
CMake警告和错误消息的文本显示使用的是一种简单的标记语言,文本没有缩进,超长的行会回卷,段落之间以新行作为分隔符

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

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

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

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

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

输出的结果:

2.8.2 字符串移除
我们在通过 file 搜索某个目录得到该目录下的所有源文件,但是其中有些源文件并不是我们需要的,比如说:

当前五个源文件,其中 main.cpp是一个测试文件,如果我们想要把其余四个源文件生成一个动态库给别人使用,就需要将 main.cpp 从搜索到的数据中剔除出去,实现这个功能,也可以使用list:


可以看到,把 将要移除的文件的名字指定给 list 就可以了,但是一定要注意通过 file 命令搜索文件的时候得到的是文件的绝对路径(在 list 中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来,否则移除操作不会成功
2.9 宏定义
在运行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效:

这里就对 DEBUG 这个宏进行了判断,如果该宏被定义了,那么就会输出设定信息,如果没有定义这个宏,第八行就相当于被注释掉了,也就无法删除日志
为了让测试更灵活,我们可以选择不在代码中定义这个宏,而是在测试的时候将其定义出来
gcc/g++中可以使用命令去指定:

在g++/gcc 命令中通过参数 -D 指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏
在CMake中也可以类似的,对应的命令叫做 add_definitions


3. 预定义宏
PROJECT_SOURCE_DIR:跟随最近一次project()命令
PROJECT_BINARY_DIR:跟随最近一次project()命令
CMAKE_CURRENT_SOURCE_DIR:当前处理的CMakeLists.txt所在的源码路径
CMAKE_CURRENT_BINARY_DIR:当前CMake对应的 build 内文件夹
CMAKE_BINARY_DIR:全局顶层源码根目录
CMAKE_SOURCE_DIR:全局顶层 build 目录
EXECUTABLE_OUTPUTPATH:重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
PROJECT_NAME:通过返回PROJECT指令定义的项目名称