Linux操作系统(十五)——Makefile与Cmake

一、 Makefile
1、什么是Makefile
什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂。特别在linux下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
关于程序的编译和链接,在此,我想多说关于程序编译的一些规范和方法,一般来说,无论是C、C++,首先要把源文件编译成*中间代码文件,在Windows下也就是 .obj 文件,linux下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是dll或者 .lib 文件,在linux下,是Archive File,也就是 .a 文件。
2、Makefile 介绍
make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。首先,我们用一个示例来说明Makefile的书写规则。以便给大家一个感性认识。这个示例来源于GNU的make使用手册,在这个示例中,我们的工程有5个C文件,和4个头文件,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:
- 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
- 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。
3、Makefile的规则
在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。

- target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。
- prerequisites就是,要生成那个target所需要的文件或是目标。
- command也就是make需要执行的命令。(任意的Shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
4、一个例子
正如前面所说的,如果一个工程有4个头文件,和5个C文件,我们为了完成前面所述的那三个规则,我们的Makefile应该是下面的这个样子的。源文件如下

#include <stdio.h>
#include "display.h"
#include "command.h"
#include "insert.h"
#include "kbd.h"
int main(int argc, const char *argv[])
{
display();
display_kbd();
display_command();
display_insert();
printf("hello world!!\n");
return 0;
}
display.h 与 display.c
#ifndef __DISPLAY__
#define __DISPLAY__
int display(void);
#endif
#include <stdio.h>
int display()
{
printf("display\n");
return 0;
}
kbd.c 与 kbd.h
#include <stdio.h>
int display_kbd()
{
printf("display_kdb\n");
return 0;
}
#ifndef __KDB__
#define __KDB__
int display_kbd(void);
#endif
command.h 与 command.c
#ifndef __COMMAND__
#define __COMMAND__
int display_command(void);
#endif
#include <stdio.h>
int display_command()
{
printf("display_command\n");
return 0;
}
insert.c 与 insert.h
#include <stdio.h>
int display_insert()
{
printf("display_insert\n");
return 0;
}
#ifndef __INSERT__
#define __INSERT__
int display_insert(void);
#endif
makefile



在这个makefile中,
- 目标文件(target)包含:执行文件main和中间目标文件(*.o)
- 依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h文件。
- 每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 main 的依赖文件。
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的系统命令,一定要以一个Tab键作为开头。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行定义的命令。这里要说明一点的是,clean不是一个文件,它只不过是一个动作名字,其冒号后什么也没有。
5、make是如何工作的
在默认的方式下,也就是我们只输入make命令。那么,
1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2. 它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“main”这个文件,把这个文件作为最终的目标文件。
3. 如果main文件不存在,或是main所依赖的后面的 .o 文件的文件修改时间要比main这个文件新,那么,他就会执行后面所定义的命令来生成main这个文件。
4. 如果main所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。
5. 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件声明make的终极任务,也就是执行文件main了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——make clean,以此来清除所有的目标文件,以便重编译。于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如display.c,那么根据我们的依赖性,我们的目标display.o会被重编译(也就是在这个依性关系后面所定义的命令),于是display.o的文件也是最新的啦,于是display.o的文件修改时间要比main要新,所以main也会被重新链接了(重新生成)。
6、makefile中使用变量
在上面的例子中,先让我们看看main的规则:

我们可以看到[.o]文件的字符串被重复了两次,如果我们的工程需要加入一个新的[.o]文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。比如,我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:

于是,我们就可以很方便地在我们的makefile中以$(objects)的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子:

于是如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。
7、编程使用变量
在makefile中使用变量



8、让make自动推导
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。


这种方法,也就是make的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean是个伪目标文件。
9、编程自动推导
让makefile自动推导



10、另类风格的makefile
即然我们的make可以自动推导命令,那么我看到那堆[.o]和[.h]的依赖就有点不爽,那么多的重复的[.h],能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。


这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的.o文件,那就理不清楚了。
11、编程另类风格
另类风格makefile



12、清空目标文件的规则
每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”。一般的风格都是:

更为稳健的做法是:

前面说过,.PHONY意思表示clean是一个“伪目标”。而在rm -rf 命令,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。
二、CMake
1、CMake的介绍

CMake是开源、跨平台的构建工具,可以让我们通过编写简单的配置文件去生成本地的Makefile,这个配置文件是独立于运行平台和编译器的,这样就不用亲自去编写Makefile了,而且配置文件可以直接拿到其它平台上使用,无需修改,非常方便。本文主要讲述在Linux下如何使用CMake来编译我们的程序。
2、安装CMake
本文使用ubuntu22.04,安装cmake使用如下命令,

安装完成后,在终端下输入cmake -version查看cmake版本,这样cmake就安装好了。


3、编程编写CMakeLists.txt
首先让我们从最简单的代码入手,先来体验下cmake是如何操作的。使用Makefile中的例子,在main.c相同目录下编写CMakeLists.txt



第一行意思是表示cmake的最低版本要求是2.8,我们安装的是3.10.2;第二行是表示本工程信息,也就是工程名叫demo;第三行比较关键,表示最终要生成的elf文件的名字叫main,使用的源文件是main.c
在终端下切到main.c所在的目录下,然后输入以下命令运行cmake

再来看看目录下的文件,可以看到成功生成了Makefile,还有一些cmake运行时自动生成的文件。然后在终端下输入make并回车,可以看到执行cmake生成的Makefile可以显示进度,并带颜色。再看下目录下的文件,可以看到我们需要的elf文件main也成功生成了,然后运行main,运行成功!如果想重新生成main,输入make clean就可以删除main这个elf文件。
4、MakeCache.txt引发的错误
在使用cmake . 的时候, 出现了错误 , 提示如下:

cmake并没有提供类似于cmake clean,这样的方式来让我们清除产生的缓存,但是它编译的缓存(*.cmake, Makefile,CmakeCache.txt,CMakeFiles目录)会遍布各个目录。解决方法:在上一级目录下建立一个build目录,然后在build目录中编译即可。
5、MakeCache.txt错误解决
MakeCache.txt错误解决


6、同一目录下多个源文件
接下来进入稍微复杂的例子:在同一个目录下有多个源文件。在之前的目录下添加2个文件,display.c和display.h。添加完后整体文件结构如下

修改CMakeLists.txt,在add_executable的参数里把display.c加进来,然后重新执行cmake生成Makefile并运行make,然后运行重新生成的elf文件main,可以类推,如果在同一目录下有多个源文件,那么只要在add_executable里把所有源文件都添加进去就可以了。如果有一百个源文件,再这样做就有点坑了,无法体现cmake的优越性,cmake提供了一个命令可以把指定目录下所有的源文件存储在一个变量中,这个命令就是 aux_source_directory(dir var)。第一个参数dir是指定目录,第二个参数var是用于存放源文件列表的变量。
7、编程练习多文件
aux_source_directory实现自动获取源文件,我们在main.c所在目录下再添加4个文件。添加完后整体文件结构如下:

CMakeLists.txt


使用aux_source_directory把当前目录下的源文件存列表存放到变量SRC_LIST里,然后在add_executable里调用SRC_LIST(注意调用变量时的写法)。再次执行cmake和make,并运行main,可以看到运行成功了。
8、不同目录下多个源文件
一般来说,当程序文件比较多时,我们会进行分类管理,把代码根据功能放在不同的目录下,这样方便查找。那么这种情况下如何编写CMakeLists.txt呢?

CMakeLists.txt



这里出现了一个新的命令:include_directories。该命令是用来向工程添加多个指定头文件的搜索路径,路径之间用空格分隔。如果没有这个命令来指定头文件所在位置,就会无法编译。
10、正规一点的组织结构
正规一点来说,一般会把源文件放到src目录下 ,生成的对象文件放入到build目录下,最终输出的elf文件会放到bin目录下,这样整个结构更加清晰。让我们把前面的文件再次重新组织下:

我们在最外层目录下新建一个CMakeLists.txt,内容如下:

这里出现一个新的命令add_subdirectory(),这个命令可以向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制的存放位置。这里指定src目录下存放了源文件,当执行cmake时,就会进入src目录下去找src目录下的CMakeLists.txt,所以在src目录下也建立一个CMakeLists.txt,内容如下:

这里又出现一个新的命令set,是用于定义变量的,EXECUTABLE_OUT_PATH和PROJECT_SOURCE_DIR是CMake自带的预定义变量,其意义如下,
- EXECUTABLE_OUTPUT_PATH :可执行文件的存放位置
- PROJECT_SOURCE_DIR:工程的根目录
所以,这里set的意思是把存放elf文件的位置设置为工程根目录下的bin目录。
11、编程练习正规的写法
使用正规的写法规划工程。

CMakeLists.txt

src/CMakeLists.txt


前面的工程使用了2个CMakeLists.txt,最外层的CMakeLists.txt用于掌控全局,使用add_subdirectory来控制其它目录下的CMakeLists.txt的运行。上面的例子也可以只使用一个CMakeLists.txt,把最外层的CMakeLists.txt内容改成如下,

同时,还要把src目录下的CMakeLists.txt删除。
12、另外一种写法

CMakeLists.txt



openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)