今天我们来玩玩Makefile。
这边是借鉴的陈皓老师的《跟我一起写 Makefile》
pdf下载链接如下。
链接:https://pan.baidu.com/s/1woRq2nEkgzLv1o5uE0FZHg?pwd=mhrh
提取码:mhrh
我们之前已经算是入门了gcc,那我们的下一站就是Makefile,上一篇我们也发现了,当我们的main.c包含了自己写的头文件,那么编译命令会变得很长,如果包含的头文件很多,那么光是敲编译命令都够花我们半条命了。
所以也有下面这个说法。
会不会写 makefile ,从一个侧面说明了一个人是否具备完成大型工程的能力。
那么Makefile到底是做什么的呢?简单来说,Makefile通过定义一系列编译规则,指导make工具如何根据源文件的修改情况来执行增量编译,从而极大地提高了开发效率,Makefile写出来也就是一个脚本文件。
使用Makefile不仅使得编译过程自动化,还带来了诸多好处:
- 自动化处理复杂的依赖关系:Makefile能够自动处理和解析项目中各文件之间的复杂依赖关系,确保只有必要的部分被重新编译。
- 支持跨平台构建:通过编写与平台无关的Makefile,可以在不同操作系统间实现一致的构建过程。
- 提高开发效率:开发者只需关注源代码的编写,编译过程的大部分操作都由Makefile自动完成,减少了手动操作的错误和时间消耗。
那么废话不多说,我们直接开始先写一个Makefile吧。
先别管这里的内容是什么意思,这内容我们写到Makefile里,Makefile就是这个脚本文件的名字,没有后缀,当然了也可以叫makefile和GNUmakefile(最好不要叫这个,因为只有GNU make才能识别)。
我们写好之后要执行Makefile,就直接执行一个make命令。
然后就可以看到执行了一堆命令,这些命令是我们写在Makefile里面的。
并且生成了下面一堆中间文件。
那其实大部分情况我们是不需要中间文件的,那么我们是不是也可以把删除中间文件的命令写进Makefile里呢?那当然是可以了,因为Makefile本质上就是脚本文件,自然是可以写的。
然后我们执行
make clean
可以看到执行了我们写在Makefile里面的最后的那个语句。
接下来我们开始揭秘Makefile。
我们先看第一行test:test.o,这实际上是一个规则。这段的含义是我们需要生成test(也就是目标文件),为了生成test首先需要拥有test.o(也就是依赖文件),通过执行下面首行缩进一个tab的那个命令来生成。
如果当前目录下没有依赖文件,那么就会在Makefile里找有没有目标文件是依赖文件的规则。
那么前四个规则大家应该就能看懂了。
但是第五个clean它不是简单的规则,它是一个特殊的目标,一般用来删除编译过程中产生的中间文件,并且我们看到它是没有依赖的。
执行的时候就是输入命令 make clean。
因为clean涉及到删除,而在Linux中删除就是真删除了,不像Window里还能从回收站里找到,因此编写clean的时候我们需要格外小心,但是保不齐我们会失误,那怎么办呢?
我们可以在make clean后面加个-n选项来模拟删除,然后再次检查是否出错,此时还不是真的删除,因此还有改正的机会。
如上可以看得出来当我们执行make clean命令加上-n选项之后,它会展示出执行的删除命令,但是并没有真正执行,这时我们就可以对make clean 进行检查了。
除了直接在Makefile里写clean,更稳健的写法是下面这样的。
其中.PHONY表示后面的clean是一个伪目标,并不是正常的目标。并且在 rm 前面加上了一个 - ,这表示当我们删除某些不存在的文件的时候忽略报错,接着往后执行。
当然还有个潜规则就是clean放在Makefile的最后面。
接着再回到我们的第一版最简单的Makefile,它是由多个规则构成的,每个规则里有目标文件,依赖文件和执行命令。在Makefile里,它默认会把第一个规则中的目标文件当成是最终目标,也就是说它不一点会执行每个规则,而是只要能够生成最终目标就行。
要指定最终目标也是可以的,在Makefile开头加上ALL:最终目标即可。像下面这样,实际上不一定非得叫ALL,原理就是Makefile会以第一个规则为最终目标,那么我放在第一行的那就是最终目标。
上面就是Makefile最基础最基础的内容了。
那接下来我们就可以更进一步了。
既然Makefile是脚本文件,那么它有变量也是正常的对叭。
一般写在文件开头,格式就是 变量名 = 变量值 ,当我们使用时是这样的 $( 变量名 ),这边需要提的是Makefile中的变量比较接近于宏定义,也就是直接替换的,没有类型。
光讲可能比较抽象,我们直接看下面的例子。
上面例子把目标文件直接赋值给了变量targetFile,我们在规则中可以使用$(targetFile)的方式取出test的这个值去使用,当然了上面只是简单地做个示范,我们还可以拿变量去装一些更复杂的东西,比如说一堆的文件(没错,一个变量能装的可不止一个文件,用空格隔开可以装多个)
那我们要给变量赋值赋上一堆文件,除了一个个手敲上去,还有别的更省力的方式那就是通配符。
比如说我们需要给一个变量赋值当前目录下所有的 .c 文件,那么我们用通配符就可以非常方便的赋值。
大家想的是下面这样的对叭。
cfile = *.c
但是实际上这样是不行的,这样在Makefile中,cfile这个变量的值就是 *.c ,而不是我们想要的所有 .c 文件,这时候就需要用上函数了,既然Makefile是脚本文件,那么有个函数也是很正常的对叭。
正确的写法是下面这样的。
cfile = $(wildcard *.c)
这个函数就是 $(wildcard ),在括号里,wildcard后写的通配符就会被展开,就会是我们想要的效果了。
除了上面我们自己去写变量,Makefile里有一些自动化变量我们可以直接去使用的,我们挑几个常用的来说。
$@ : 本条规则中的目标
$< : 本条规则中第一个依赖,如果第一个依赖代表多个文件(后面会说怎么实现),那么$<就表示为将这多个文件一个个取出来。
$^ :本条规则中的所有依赖,用空格分隔。
$* : 可以简单理解成目标文件的名字但是不含后缀名。
$? :比目标更新的依赖,也就是最后修改时间比目标更晚的依赖。
用了上面的自动化变量,那么我们第一版的Makefile可以写成下面这样了。
现在我们改一改我们的test.c,像下面这样。包含了自己写的头文件,用了里面的方法。
对应的在Makefile里也需要修改。我们添加了对于mytool1的编译。
是可以正常make的。
然后我们发现,后面两个规则中的命令是一样的,那么实际上我们是可以对他们进行一个合并操作的,也就是说在一个规则里,可以有多个目标。可以像下面这样改写,但是由于gcc -o选项只能输出一个文件,因此在命令里不能使用 $@,但是还好-c选项可以不指定输出文件,默认就是输入文件的后缀改成 .o当输出文件了。
也是可以正常make的。
这个例子主要是想说在Makefile中,在一个规则里可以有多个目标。然后同一个目标也可以有多个规则(这边不演示了)。
然后实际上我们还有种更简单的写法,那就是模式规则。
这边直接贴出怎么写的。
看不懂什么意思没关系,我们make一下,make之后会显示出它执行的命令,然后我们再分析分析。
看的出来Makefile直接帮我们把我们需要的依赖都执行了。
这是因为上面的%可以看成是任意的东西(类似于通配符中的*),当我们的目标的依赖不存在的时候,Makefile就会在规则中寻找是否目标的依赖和哪个规则的目标一致,找到就会执行这个规则中的命令。就比如说我们上面的Makefile,需要寻找目标为test.o以及目标为mytool1.o的依赖,最终都找到了 %.o:%.c 这个规则,因为%代表任意东西,那么自然是可以匹配上的,这就是模式规则强大的地方,我们可以少写很多东西了。
不过上面这种写法有一个可以改进的点,那就是将$^改成$<,我们来看看二者的区别。
我们上面的例子简单,所以没什么差别,但是当我们的依赖变多之后,依赖嵌套依赖之后,我们就得用$<了。
使用模式规则也有个弊端,那就是模式规则会匹配所有符合条件的,因为%可以匹配任意东西,因此只要后缀符合,都会被我们的模式规则匹配到,如果我只有一部分的文件需要用模式规则中的命令怎么办呢?
这就要用到我们的静态模式规则了。
在模式规则的基础上,在前面加上需要匹配到的目标即可。
可能有小伙伴会说,这样不就又绕回去了嘛,还是要把目标的名字都写出来。那其实我们可以再静态模式规则的后面再写个模式规则,而只在静态模式规则中写上需要特殊处理的目标即可,剩下的可以都丢给后面的模式规则去兜底。
一般情况下我们不会将工具文件和我们的主文件放在同一级目录下,因此我新建个文件夹my_tools,然后将工具文件放进去,接着我们就毫不意外地发现Makefile找不到库文件了。
我们可以在Makefile中指定头文件目录,指定的方式也很简单,就像下面这样。
VPATH = 路径1:路径2……
可以指定多个路径,用冒号:隔开即可。
指定完之后仍然报错,但这就是链接阶段报的错误了。
我们需要在规则的命令中添加头文件路径。
最终是下面这样的。
可以正常运行,在Makefile执行规则命令的时候也帮我们把头文件路径加上去了。
VPATH是一个特殊变量,指明了这个特殊变量,当Makefile找不到依赖文件,也没有目标是依赖的规则,那么Makefile就会到VPATH指明的路径去寻找。
除了VPATH还有vpath,也就是小写的,但是vpath不是变量,而是关键字,使用可以参考下面这样。
看的出来vpath一次只能添加一条路径,但是和VPATH相比的话,vpath可以更细致地指定路径,可以给不同后缀的文件指定不同的路径。
至此我们就算是入门Makefile成功了,相信各位小伙伴已经能够给自己以前写的小项目编写(相对)高效的Makefile了。
如果想进一步了解Makefile的话,最好的方法是去看官方文档,当然了,也可以看看陈皓老师写的《跟我一起写 Makefile》,下载链接在本文的开头。