Cmake总结
总结Cmake的使用
背景
之前我学习了,Linux中autotools工具链的使用,但是在学习的过程中我发现还有比autotools更加方便,更加好用的制作Makefile文件的工具——Cmake
Autotools和Cmake的对比
首先我们已经知道了Autotools工具链的使用非常繁琐:
而Cmake的使用只需要编写Cmake.txt文件,将自己的需求告诉Cmake就可以了。其实Cmake就是一种语言,我们通过Cmake的语法编写Cmake.txt文件内容。
安装Cmake
注意:由于Cmake是一个版本控制工具,所以我们使用的Cmake最好比我们使用的编译器的版本的发行时间要晚,所以我们尽量使用新版本的Cmake。
常用的函数以及操作
| 1 |  | 
在Cmake中使用变量的值的一般方法是%{},但是在if语句中使用变量只需直接使用变量名即可,无需使用%{}
语法规则小总结
最简单的语法规则是:
- 变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名
- 指令(参数 1 参数 2…)
参数使用括弧括起,参数之间使用空格或分号分开。
以上面的 ADD_EXECUTABLE 指令为例,如果存在另外一个 func.c 源文件,就要写成:
ADD_EXECUTABLE(hello main.c func.c)或者
ADD_EXECUTABLE(hello main.c;func.c)
- 指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。
需要注意的是Cmake的语法是比较灵活的,比如说:在包含文件名的时候可以使用””将文件名包裹,但是不使用也是可以的,但如果文件名中有空格的话就必须使用””。同时在函数中分隔参数可以使用空格分隔,也可以使用,分隔。注意这里不是所有的参数都是用,分隔。而是同类型的参数之间,比如说文件名之间。
构建过程
| 1 |  | 
构建动态库和静态库
动态库
我们知道可以直接使用:
| 1 |  | 
Cmake方法:
| 1 |  | 
制作静态库
我们知道可以直接使用gcc命令制作静态库
| 1 |  | 
但是当有很多源文件的时候,我们在使用这种方法就很不方便了。于是cmake支持创建静态库
| 1 |  | 
使用动态库&静态库
例子
| 1 |  | 
基础知识
本地变量
注意,Cmake本身就制定了一些变量,这里变量都是大写的。
https://modern-cmake-cn.github.io/Modern-CMake-zh_CN/chapters/basics/variables.html#fn_1
小总结:
- CMake具有作用域的概念
内部编译和外部编译
这两者的区别就是,内部编译是在源文件目录中直接make,而外部编译是在其他目录中(一般是Build)目录下 Cmake的。这两者的主要区别就是在内部编译中,编译所生成的中间文件都会留在源文件中,而外部编译不会。
安装
CMake中的安装需要使用一个函数:INSTALL。安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。这个函数一般和CMAKE_INSTALL_PREFIX变量一起使用。
GNU Make用于控制如何从程序的源代码文件编译并链接为可执行文件,通过make命令从名称为makefile的文件中获取构建信息,该文件定义了一系列规则来指定源文件的编译先后顺序、是否需要重新编译、甚至于进行更为复杂的操作。通过makefile文件可以方便的实现工程的自动化编译,只需要执行make命令即可完成编译动作,从而极大的提高了开发人员的工作效率。
CMake 3.17是一款源代码构建管理工具,最初作为各种 Makefile 方言的生成器,后来逐步发展为现代化的构建系统,广泛用于 C 和 C++ 工程源代码的构建。官方提供的《CMake Tutorial》 为开发人员提供了一个循序渐进的指南,涵盖了 CMake 构建过程中常见问题的解决方案。如果需要构建从第三方发布的源代码包,则可以参考《User Interaction Guide》。而《Using Dependencies Guide》则主要针对需要使用第三方库的开发人员。
GNU Make
make是一款用于解释makefile文件当中命令的工具,而makefile关系到整个工程的编译规则。许多 IDE 集成开发环境都整合了该命令,例如:Visual C++ 里的nmake,Linux 里的 GNU make,本章节主要讲解 GNU make 相关的内容。开始进一步讲解之前,需要先了解一下 C/C++ 源代码的编译过程,具体内容可参见笔者的《基于 Linux 的 GCC 与 GDB 应用调试》 - 编译步骤一文:
- 预处理 Preprocessing:解析各种预处理命令,包括头文件包含、宏定义的扩展、条件编译的选择等;
- 编译 Compiling:对预处理之后的源文件进行翻译转换,产生由机器语言描述的汇编文件;
- 汇编 Assembly:将汇编代码转译成为机器码;
- 链接 Link:将机器码中的各种符号引用与定义转换为可执行文件内的相应信息(例如虚拟地址);
makefile 文件
基本规则
执行make命令时,实际会解析当前目录下的makefile文件,该文件用于告知make命令如何对源代码进行编译与链接,一个 makefile 的基本编写规则如下所示:
| 1 | target ... : prerequisites ... | 
- target:即可以是 1 个目标文件,也可以是 1 个执行文件,甚至还可以是 1 个标签;
- prerequisites:生成该- target所依赖的文件或者其它- target;
- command:该- target所要执行的 Shell 命令,需要保持 1 个【Tab】的缩进;
上述的基本编写规则最终会形成一套依赖关系,其中**target依赖于prerequisites,而生成规则定义在command;如果prerequisites中的文件比target上的文件要新,则command所定义的命令就会被执行**。
观察下面的例子,其中的反斜杠\表示换行,将其保存为一个makefile或者Makefile文件,然后在当前目录执行make命令,就可以生成可执行文件app。如果需要删除可执行文件以及中间生成的目标文件,则执行make clean命令即可。
| 1 | app : main.o kbd.o command.o display.o insert.o search.o files.o utils.o | 
输入make命令之后,就会开始执行上述的makefile文件,具体执行流程如下所示:
- make会在当前目录下查找- Makefile或者- makefile文件;
- 找到后将当中定义的第 1 个target作为最终的目标文件;
- 如果app文件不存在,或者其依赖的.o文件修改时间要比app执行文件更新。那么,他就会执行command定义的命令来生成app文件;
- 如果app依赖的.o文件也不存在,那么查找.o文件对应的依赖规则生成.o文件;
- 最后,基于工程中.c和.h源文件生成.o依赖文件,然后再基于这些.o文件生成app执行文件;
定义变量
上面示例中app生成规则中的一系列.o文件反复出现,这里我们可以将其声明为一个变量:
| 1 | objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o | 
自动推导
GNU Make 可以自动识别并推导目标与依赖关系之后的command命令,只要make发现 1 个.o文件,就会自动将对应的.c文件添加至依赖关系当中,同时也会将对应的gcc -c命令推导出来。
| 1 | objects = main.o kbd.o command.o display.o \ | 
这种方法被称为make的隐含规则,上述代码中.PHONY表示clean是一个伪目标文件,关于隐晦规则和伪目标文件的内容后续将会进行更为详细的介绍。
通过隐含规则可以进一步简化上面的makefile,这样虽然可以最大幅度减少代码,但是文件的依赖关系显得较为凌乱,所以这种风格较少被采用。
| 1 | objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o | 
清理中间文件
习惯上,每个makefile文件都应该编写一个用于清理中间文件的规则,这样不仅便于重新编译,也有利于保持工程的整洁。
| 1 | clean: | 
之前代码采用了上面较为简单粗暴的方式,但是更为稳健的方法是采用下面这样的风格:
| 1 | .PHONY : clean | 
.PHONY关键字用于表标识clean是一个伪目标,rm命令前的小减号-表示忽略操作出现问题的文件,习惯上会将clean放置在makefile的最后。
Makefile 组成
Makefile 文件主要包含显式规则、隐式规则、变量定义、文件指示、注释。
- 显式规则:由Makefile编写者明确指定,用于描述如何生成target;
- 隐式规则:利用make命令的自动推导功能,简略书写Makefile;
- 变量的定义:通常为字符串,当Makefile被执行时,其中的变量会扩散到相应的引用位置;
- 文件指示:在Makefile当中引用另外的Makefile,类似于 C 语言里的#include。或者根据条件指定Makefile的有效部分,类似于 C 语言中的#if。除此之外,还可以用于定义一条拥有多行的命令;
- 注释:注释采用#字符,需要时可以采用反斜杠进行转义\#;
注意:
Makefile中的命令command必须以【Tab】键开始。
引用其它 Makefile
使用include关键字可以将其它Makefile包含进来,类似于 C 语言中的#include预处理语句,被包含的文件会自动替换至包含位置。
| 1 | include <filename> | 
filename可以是当前操作系统 Shell 命令或者文件(可以包含路径和通配符),include关键字之前可以存在空字符,但是绝不允许出现【Tab】键。
例如:存在 4 个 Makefilea.mk、b.mk、c.mk、foo.make以及 1 个变量$(bar)(包含e.mk和f.mk) ,那么下面 2 条语句就是等价的:
| 1 | include foo.make *.mk $(bar) | 
make命令开始执行时,会查找include的其它Makefile,如果没有指定绝对或者相对路径的话,make会首先在当前目录下查找,如果没有查询到则会进入如下目录:
- 如果make命令执行时,带有-I或者--include-dir参数,那么make就会在该参数指定的目录下查找;
- 此外,make还会去查找<prefix>/include目录(通常为/usr/local/bin或者/usr/include);
最后,如果文件未能找到,make将会生成警告信息,然后继续载入其它文件,一旦makefile读取完成,make会再次进行查询,如果依然未能找到,则报出一条致命错误信息。如果想让make忽略读取错误,则可以在include前添加减号-。
| 1 | -include <filename> | 
注意:其它版本
make采用的兼容命令是sinclude,其作用与-include相同。
这里,重新再来总结一下 GNU Make 的工作步骤:
- 读取所有Makefile文件;
- 查找被include的其它Makefile;
- 初始化Makefile文件当中定义的变量;
- 分析并且推导隐式规则;
- 创建target目标文件的依赖关系;
- 根据依赖关系,决定哪些target需要重新生成;
MAKEFILES 环境变量
如果当前定义了MAKEFILES环境变量,其值为采用空格分隔的其它Makefile,执行make时会将这个该环境变量的值include进来。但是与include所不同的是,该环境变量引入的Makefile的target不会生效,其定义的文件如果发现错误,make也会不理会。
日常开发环境,不建议使用MAKEFILES环境变量,因为定义后会影响到所有make命令的执行。反而是在makefile文件出现一些莫名其妙错误的时候,需要检查当前是否定义了这个环境变量。
规则
规则描述了Makefile文件的依赖关系以及如何生成目标文件。定义在 Makefile 中的target可以有很多,但是第 1 条规则中的target会被确立为最终的目标。
| 1 | 
 | 
- targets:目标文件名称,以空格分隔,可以使用通配符;
- prerequisites:目标文件的依赖,如果某个依赖文件比目标文件要新,那么就会重新进行生成;
- command:Shell 命令行,如果不与- target : prerequisites在一行,那么必须以【Tab】开头;如果保持在一行,则可以采用分号- ;进行分隔;
注意:如果
prerequisites和command过长,可以使用反斜杠\进行换行。通常make会以 Bash Shell 也就是/bin/sh来执行命令。
通配符
make支持*、?、~三个通配符。~字符在 Linux 下表示当前用户的$HOME目录,在 Windows 下则根据环境变量HOME设置而定。
通配符可以应用在command当中,下面代码会在清除所有.o文件之前,查看一下main.c文件。
| 1 | clean: | 
通配符还可以应用于prerequisites,下面代码中的print目标依赖于所有.c文件,其中的$?是后续将会讲到的自动化变量。
| 1 | print: *.c | 
通配符同样可以应用在变量中,但是并不会因此而自动展开,下面代码里变量objects的值就是*.o。
| 1 | objects = *.o | 
如果需要让通配符在变量当中展开,即让objects的值是所有.o文件名的集合。
| 1 | objects := $(wildcard *.o) | 
Autoconf
Automake
CMake
CMake 教程提供了一个循序渐进的指南,涵盖了常见的构建系统问题。本文涉及的示例代码可以在 CMake 源码树的Help/guide/tutorial目录下找到,每个步骤都拥有其相应的子目录,循序渐进直至提供完整的解决方案。
基本出发点
最为基础的项目是从源代码构建可执行文件,这样只需要一个 3 行的CMakeLists.txt文件,这将是整个教程的起点。在【Step1】目录当中创建如下CMakeLists.txt文件:
| 1 | cmake_minimum_required(VERSION 3.10) | 
CMake 支持大写、小写、混合大小写的命令,上面的CMakeLists.txt文件使用了小写命令。教程源代码Step1目录中提供了用于执行数字平方根计算的cxx文件。
| 1 | 
 | 
添加版本号和配置头文件
我们要添加的第一个特性是为项目提供 1 个版本号。虽然源代码中也可以完成这件事,但是使用CMakeLists.txt可以提供更好的灵活性。首先,修改CMakeLists.txt文件,使用project()命令设置项目名称和版本号。
| 1 | cmake_minimum_required(VERSION 3.10) | 
然后,继续编写配置,把一个头文件上保存的版本号传递到源代码:
| 1 | configure_file(TutorialConfig.h.in TutorialConfig.h) | 
由于配置文件将会被写入到二叉树,所以必须将该目录添加至搜索包含文件的路径列表当中,在CMakeLists.txt文件的末尾添加以下行:
| 1 | target_include_directories(Tutorial PUBLIC | 
在当前目录下创建TutorialConfig.h文件,并且包含如下内容:
| 1 | /* 配置主、副版本号 */ | 
当 CMake 配置该头文件以后,上述的@Tutorial_VERSION_MAJOR@和@Tutorial_VERSION_MINOR@的值将会被替换。
接下来修改tutorial.cxx来包含上面的TutorialConfig.h头文件,并最终通过修改后的tutorial.cxx打印版本号。
| 1 | #include "TutorialConfig.h" | 
指定 C++ 标准
接下来,将tutorial.cxx文件中的atof替换为std::stod,从而为项目添加一些 C++11 特性。同时,删除#include <cstdlib>。
| 1 | const double inputValue = std::stod(argv[1]); | 
CMake 中启用特定 C++ 标准支持的最简单方法是使用CMAKE_CXX_STANDARD变量,这里将CMakeLists.txt文件里的CMAKE_CXX_STANDARD变量设置为11,并将CMAKE_CXX_STANDARD_REQUIRED设置为True:
| 1 | cmake_minimum_required(VERSION 3.10) | 
编译与测试
从命令行导航到 CMake 源代码树的 Help/guide/tutorial 目录,并运行以下命令:

