相信大家在入门编程世界的时候都是使用的 IDE ( Integrated Development Environment,集成开发环境),IDE 通常集成了代码编辑器、编译器或解释器、调试器等工具,同时还拥有友好的图形界面。通常我们遇到的 Visual Studio、Eclipse、IntelliJ IDEA、Xcode 以及嵌入式开发的 Keil MDK 等都是 IDE,有了这些 IDE 软件的帮助,大家在进行开项目发过程中往往只需要点点鼠标,将重点放在代码的编写和逻辑上。而当我们接触到 Linux 系统时,往往都是用命令行的形式来进行各种操作(虽然现在 Linux 的图形界面也很完善了,但是谁又能拒绝得了“黑框指令”这种装逼的方式呢),这时候我们就需要学习使用编译器命令来编译程序。这往往会带来一些“重复性劳动”(指出了代码编写和调试外的行为—反复执行编译器指令),那么我们如何简化我们的开发过程呢?这就不得不提到本文主角—Makefile了,通过使用 Makefile 我们就可以极大减少哪些“重复性劳动”。下面来一起学习下吧。
目录
一、make 与 Makefile
1.1 make
make是电脑上的一个工具(应用程序),像QQ、微信、wegame、steam、Linux中的ls、touch等等一样。它可以被我们执行。通常都在操作系统中被默认安装了不信的小伙伴可以输入已下命令查看一下:
make -v
1.2 makefile
正如字面意思所示,file是文件的意思,那么makefile也应该是个文件,它用来给我们电脑中的make工具服务,通过在makefile中编辑命令来告诉make工具如何执行,makefile文件有将命令“打包”的作用,它不仅可以写编译相关的命令,还可以写所有可以在命令行中输入的命令。所以通过编写makefile文件我同样可以实现脚本类似的功能。同时make工具还有检查时间戳的功能,它可以通过对比时间戳来判断目标文件需不需要重新构建,这极大提高了工程开发的效率。
二、 Makefile引入及规则
在对make和makefile是什么有了初步了解之后,我们已经知道了makefile文件是帮助我们能简化项目开发,提高效率的文件,那我们开始实际体验一下makefile吧
2.1 C语言的编译过程
首先,我们写一个程序它包含三个源文件a.c , b.c , c.c:
//a.c源文件
#include <stdio.h>
int main()
{
func_b();
func_c();
return 0;
}
//b.c源文件
#include <stdio.h>
void func_b()
{
printf("This is B\n");
}
//c.c源文件
#include <stdio.h>
void func_c()
{
printf("This is C\n");
}
编译:
gcc -o test.exe a.c b.c c.c
执行:
./test.exe
输出:
gcc -o test.exe a.c b.c c.c 这条命令虽然简单,但是它完成的功能不简单。
我们来看看它做了哪些事情,
我们知道.c程序 ==》 得到可执行程序它们之间要经过四个步骤:
1.预处理
2.编译
3.汇编
4.链接
我们经常把前三个步骤统称为编译了。我们具体分析:gcc -o test.exe a.c b.c c.c 这条命令 它们要经过下面几个步骤:
- 1)对于a.c:执行:预处理 编译 汇编 的过程,a.c ==>xxx.s ==>xxx.o文件。
- 2)对于b.c:执行:预处理 编译 汇编 的过程,b.c ==>yyy.s ==>yyy.o 文件。
- 3)对于c.c:执行:预处理 编译 汇编 的过程,c.c ==>xxx.s ==>zzz.o文件。
- 4)最后:xxx.o、yyy.o和zzz.o链接在一起得到一个test.exe应用程序。
第一次编译 a.c 得到 xxx.o 文件,这是很合乎情理的, 执行完第一次之后,如果修改 a.c 又再次执行:gcc -o test a.c b.c c.c,对于 a.c 应该重新生成 xxx.o,但是对于 b.c 和 c.c又会重新编译一次,这完全没有必要,b.c和c.c 根本没有修改,直接使用第一次生成的 yyy.o和zzz.o文件就可以了。
缺点:对所有的文件都会再处理一次,即使 b.c 和c.c没有经过修改,b.c 也会重新编译一次,当文件比较少时,这没有没有什么问题,当文件非常多的时候,就会带来非常多的效率问题如果文件非常多的时候,我们,只是修改了一个文件,所用的文件就会重新处理一次,编译的时候就会等待很长时间。
对于这些源文件,我们应该分别处理,执行:预处理 编译 汇编,先分别编译它们,最后再把它们链接在一次,比如:
编译:
gcc -c -o a.o a.c
gcc -c -o b.o b.c
gcc -c -o c.o c.c
链接:
gcc -o test a.o b.o c.o
比如:上面的例子,当我们修改a.c之后,a.c会重现编译然后再把它们链接在一起就可以了。b.c 和c.c就不需要重新编译。
那么问题又来了,怎么知道哪些文件被更新了/被修改了?
2.2 编译过程的效率优化策略
很简单,我们可以比较时间:比较 a.o 和 a.c 的时间,如果a.c的时间比 a.o 的时间更加新的话,就表明 a.c 被修改了,同理b.o和b.c、c.c和c.o也会进行同样的比较。比较test.exe和 a.o,b.o,c.o的时间,如果a.o或b.o或c.o的时间比test.exe更加新的话,就表明应该重新生成test.exe。
我们可以通过stat命令获取文件的时间
stat [文件名]
例如我们查看a.c与a.o的时间:
stat a.c
stat a.o
我们可以看到此处有三个时间:
- Acess:记录最新一次文件内容被读取的时间;
- Modify:记录最新一次对文件修改的时间;
- Change: 记录最新一次文件权限等被修改的时间。
由上图很容易得出a.c的Modify时间是早于a.o的时间的,这说明我们现在的a.o文件就是最新的,不需要再去生成。我们对a.c的内容进行修改:
vim a.c
在a.c中添加一个新行再次运行stat a.c命令:
我们可以看到,我们修改a.c后a.c的Modify时间已经要比a.o的新了,这说明我们需要去更新a.o文件。
那么,我们每次都要去对比时间,反而把时间浪费在这上边效率更低了呀!这时,我们就该清楚我们今天的主角了——make工具了。make工具正好可以帮助我们去对比这些修改时间来决定文件是否需要重新构建。
2.3 makefile基本用法
在前面我们已经了解了make工具与makefile的区别与联系、make工具的主要用途等。现在我们来了解下makefile的基本使用规则吧。
2.3.1 makefile的语法规则
makefile的基本语法规则为:
目标文件:[依赖文件1][依赖文件2][依赖文件3]......
[Tab键] 命令
注意:
- 目标文件:必填
- 依赖文件:可以有零或任意数量个,不同依赖文件之间要用空格分开;
- 命令:命令前面可以有多条,每条命令要单独占一行,其前面必须用一个Tab键开头。
- 每个目标依赖文件对和命令列表为一组,在makefile中这样的小组可以有多个组。
规则:
- 可以用make [目标文件]来构建对应目标文件
- make指令后如果没有指定哪个目标文件,则会默认只执行最开始的第一组文件依赖关系并且递归构建所有依赖文件。
- 当目标文件不存在时,make总会试图去创建它而执行其对应的命令列表
- 当目标文件存在时,make会递归的依次对比目标文件与每个依赖文件的修改时间,当依赖文件的修改时间较新的时候,make会执行对应命令列表试图更新目标文件。
2.3.2 实验验证
创建一个makefile文件:
touch makefile
编辑makefile文件:
vim makefile
在makefile文件中输入以下代码:
test.exe :a.o b.o c.o
# test是目标,它依赖于a.o b.o c.o文件,一旦a.o或者b.o或c.o比test.exe新的时候,就需要执行下面的命令,重新生成test.exe可执行程序。
gcc -o test.exe a.o b.o c.o
a.o : a.c #a.o依赖于a.c,当a.c更加新的话,执行下面的命令来生成a.o
gcc -c -o a.o a.c
b.o : b.c #b.o依赖于b.c,当b.c更加新的话,执行下面的命令,来生成b.o
gcc -c -o b.o b.c
c.o : c.c #b.o依赖于c.c,当c.c更加新的话,执行下面的命令,来生成c.o
gcc -c -o c.o c.c
clean:
rm -rf *.o *.exe #用于清除编译生成的所有文件
上面是makefile中的五条规则。makefile,就是名字为“makefile”的文件。当我们想编译程序时,直接执行make命令就可以了,一执行make命令它想生成第一个目标test.exe可执行程序,如果发现a.o 或者b.o没有,就要先生成a.o或者b.o,发现a.o依赖a.c,有a.c但是没有a.o,他就会认为a.c比a.o新,就会执行它们下面的命令来生成a.o,同理b.o和b.c的处理关系也是这样的。
如果修改a.c ,我们再次执行make,它的本意是想生成第一个目标test.exe应用程序,它需要先生成a.o,发现a.o依赖a.c(执行我们修改了a.c)发现a.c比a.o更加新,就会执行gcc -c -o a.o a.c命令来生成a.o文件。b.o依赖b.c,发现b.c并没有修改,就不会执行gcc -c -o b.o b.c来重新生成b.o文件。现在a.o b.o c.o都有了,其中的a.o比test.exe更加新,就会执行 gcc -o test a.ob.o来重新链接得到test可执行程序。所以当执行make命令时候就会执行下面两条命令:
gcc -c -o a.o a.c
gcc -o test a.o b.o
我们第一次执行make的时候,会执行下面四条命令(四条命令都执行):
再次执行make 就会显示下面的提示:
make: `test' is up to date.
我们再次执行make就会判断Makefile文件中的依赖,发现依赖没有更新,所以目标文件就不会重现生成,就会有上面的提示。当我们修改a.c后,重新执行make,
就会执行下面两条指令:
gcc -c -o a.o a.c
gcc -o test a.o b.o c.o
我们同时修改a.c b.c c.c,执行make就会执行下面四条指令:
gcc -c -o a.o a.c
gcc -c -o b.o b.c
gcc -c -o c.o c.c
gcc -o test.exe a.o b.o c.o
a.c文件修改了,重新编译生成a.o, b.c修改了重新编译生成b.o,c.c文件修改了,重新编译生成c.o,a.o,b.o,c.o都更新了重新链接生成test.exe可执行程序,makefile的规则其实还是比较简单的。规则是Makefie的核心,
执行make命令的时候,就会在当前目录下面找到名字为:Makefile的文件,根据里面的内容来执行里面的判断/命令。
三、Makefile语法
本文章只介绍一些常用的基础语法,如果遇到一些生僻的语法我们可以去使用我们的AI大法或者官方文档。
3.1 通配符
假如一个目标文件所依赖的依赖文件很多,那样岂不是我们要写很多规则,这显然是不合乎常理的
我们可以使用通配符,来解决这些问题。
我们对上节程序进行修改代码如下:
test.exe: a.o b.o c.o
gcc -o test.exe $^
%.o : %.c
gcc -c -o $@ $<
- %.o:表示所用的.o文件
- %.c:表示所有的.c文件
- $@:其中@表示目标
- $<:其中<表示第1个依赖文件
- $^:其中 ^ 表示所有依赖文件
注:在makefile的语法中我们可以吧$理解为取值符号
修改makefile文件
执行make指令:
可以看到,在使用通配符的情况下命令也被正确执行了
3.2 假想目标(.PHONY)
3.2.1 引出问题
我们想清除文件,我们在Makefile的结尾添加如下代码就可以了:
clean:
rm -rf *.o *.exe #用于清除编译生成的所有文件
- 1)执行 make :生成第一个可执行文件。
- 2)执行 make clean : 试图创建clean文件而执行对应命令,清除所有文件,即执行: rm *.o test。
- make后面可以带上目标名,也可以不带,如果不带目标名的话它就想生成第一个规则里面的第一个目标。
执行:make [目标] 也可以不跟目标名,若无目标默认第一个目标。我们直接执行make的时候,会在makefile里面找到第一个目标然后执行下面的指令生成第一个目标。当我们执行 make clean 的时候,就会在 Makefile 里面找到 clean 这个目标,然后执行里面的命令,这个写法有些问题,原因是我们的目录里面没有 clean 这个文件,这个规则执行的条件成立,他就会执行下面的命令来删除文件。
如果:该目录下面有名为clean文件怎么办呢?
我们在该目录下创建一个名为 “clean” 的文件:
touch clean
执行make:
make
执行make clean:
make clean
有如下提示:
这不坏啦,我们明明想让make clean指令去清除我们执行make时生成的中间文件。但是它却没有正常被执行。
3.2.2 分析原因
前面我们提到了make的执行规则是:make指令在目标文件不存在时总会试图去创建它,但是在目标文件存在时,make指令会递归对比目标文件与依赖文件的时间戳,当依赖文件的时间戳比目标文件的时间戳更新的时候,就会重新构建目标文件,在这个例子中,clean为目标,他的依赖文件列表为空,当clean文件存在的时候,make工具就只会去对比时间戳,因为其没有依赖文件可供对比,那么clean文件的时间戳永远是最新的,所以就不会执行clean下面的命令列表。
3.2.3 解决办法
如何去解决以上问题呢,我们可以通过.PHONY 来生命某个目标为假想目标,他告诉告诉 make 工具,某些目标并不是真实存在的文件名。其用法如下:
.PHONY:<目标文件名>
在这里我们可以将clean来声明成一个假想目标:
.PHONY:clean
clean:
rm -rf *.o *.exe
再次执行make clean指令:
可以发现,声明为假想目标后,clean命令即使在工作目录中存在clean文件时也可以正常使用啦。
3.3 变量
通常我们常用再makefile中的变量定义方式为:
- 简单定义:使用冒号等号”:=“来定义变量,这种方式定义的变量值是立即确定的,不会受后边定义的值的影响;
- 递归定义:使用等号”=“定义变量,这种方式定义的变量可以在整个makefile中后续被修改;
- 条件定义:使用问号等号”?=“定义变量,如果这个变量之前按没有被定义过,那么就赋予等号后面的值;
- 追加定义:使用加号等号”+=“来给变量追加值,如果变量值之前有值,那么新值会被追加到现有值的后面,中间用空格隔开。
3.3.1 简单定义
使用冒号等号”:=“来定义变量,这种方式定义的变量值是立即确定的,不会受后边定义的其他变量的值的影响。
-
举例
编写代码:
A := $(B)
B := 123
.PHONY:all
all:
@echo "A=$(A)"
@echo "B=$(B)"
-
分析
A和B都是用冒号等号”:=“定义的变量,所以对于A和B都会再其定义时将变量进行展开,也就是定义的时候就已经把该变量的值确定了。所以对于A:=$(B)这条语句,因为这里变量B还没有被定义,但是冒号等号”:=“定义的变量性质决定了A必须在此刻就确定自己的值,所以A的值只能时空。对于B:=123,同样由于冒号等号”:=“赋值符的性质,其必须在此刻被定义好值,所以这时变量B的值为字符串123。对于这个makefile文件我们如果要执行make指令或者make all指令,其结果应该是
A=
B=123
-
验证
我们来验证下,将上述代码写入makefile文件:
运行
make
完美,和我们预想的输出一样!!
3.3.2 递归定义
使用等号”=“定义变量,这种方式定义的变量可以在整个makefile中后续被修改;
-
举例
编写代码:
A = 123
B = $(A)
C := $(A)
D := $(B)
A = abc
.PHONY:all
all:
@echo "A = $(A)"
@echo "B = $(B)"
@echo "C = $(C)"
@echo "D = $(D)"
-
分析
显然A和B都是等号直接定义,根据等号定义的变量可以到使用时再展开。我们从上往下看,第一条指令A = 123结束后A被暂时赋值为123,继续执行下一条B = $(A)指令,此时B也是用”=“定义的变量,此时B只是定义还没有被使用,所以暂时不展开,对于C := $(A),这里变量C是由冒号等号”:=“定义的变量,所以需要被立刻展开,所以C现在被赋值为A目前的值123。对于D := $(B)这里变量D也是冒号等号”:=“定义的变量,所以也应该直接在此处展开,可以看出D的值要依赖变量B,这里需要将B展开来给D赋值,所以变量B也被暂时展开,B又依赖A的值此时A的值还是123所以B被赋值为123,D被赋值为B的值即也为123.对于A = abc,此处是A是被等号赋值的变量,变量A暂时被更新为abc
所以再下面执行make或make all指令的时侯,我们预期输出结果为:
A = abc
B = abc
C = 123
D = 123
-
验证
将上述代码输入进makefile文件,执行make指令观测输出结果:
完美,跟我们分析的结果一摸一样!!!
3.3.3 条件定义
使用问号等号”?=“定义变量,如果这个变量之前按没有被定义过,那么就赋予等号后面的值;
-
举例
编写代码:
A = 123
A ?= abc
.PHONY:all
all:
@echo "A = $(A)"
-
分析
根据语法规则,该段代码中A再问号等号赋值的前面已经被赋值为123,所以第二条赋值语句不会被执行,此时执行make或make all指令的话输出结果应该是:
A = 123
当我们把第一个赋值语句注释掉的时候,再次执行make指令其结果将会是:
A = abc
-
验证
在makefile中输入以上代码:
输出:
接着将第一条语句注释掉重新执行:
执行:
完美,和我们预测的结果一模一样!!!
3.3.4 追加定义
使用加号等号”+=“来给变量追加值,如果变量值之前有值,那么新值会被追加到现有值的后面,中间用空格隔开。
-
举例
编写代码:
A =
A += 123
A += abc
.PHONY:all
all:
@echo "A = $(A)"
-
分析
根据追加定义的概念,第一句中A被赋值为空,执行到第二句时A被施以追加定义,赋值为了123,第三句指令中A仍被追加定义一个abc 所以A目前的值为123 abc
-
验证
将上述代码编写进makefile中:
运行:
运行正确!!!
四、Makefile函数
Makefile
函数是在 Makefile
文件中使用的一系列内置函数,它们可以用来处理变量、文件名、条件判断等。这些函数可以简化 Makefile
的编写,使得 Makefile
更加灵活和强大。同样,在本节中我们同样去了解一些常见函数的使用方法。
首先,必须知道的是,在Makefile中想要引用一个函数,要用$
$(函数名 参数列表...) #参数之间一般都用逗号“,”隔开
foreach函数
定义
$(foreach var,list,stmt)
对list中的每一个变量执行后面的stmt操作。
我们拆分一下名字==>for--each,这里for就像C语言的for循环,each表示“每个”,这样很容易就知道这个函数应该就是遍历的意思,它遍历的是谁呢,就是我们的list,他从list中遍历出每个值先传给变量var然后在通过var传给stmt“加工”并返回“加工”后的结果。这就是该函数的大致意思啦。
示例
编写代码:
A = a b c
B = $(foreach i,$(A),$(i).c)
.PHONY:all
all:
@echo "B = $(B)"
在上面的代码中,先将变量A展开,foreach函数遍历变量A的每个值,通过变量i被“加工”为“*.c”形式。
执行
filter/filter-out函数
定义
$(filter pattern...,text)
$(filter-out pattern...,text)
- 对于filter函数,filter有过滤的意思,所以该函数的作用就是从后面的text中筛选出符合pattern的东西
- filter-out函数,作为filter函数的对立面出现,他的作用和filter相反,用来筛选出不符合pattern的东西
示例
编写代码:
A = a.c b.c c.o
B = $(filter %.c,$(A))
C = $(filter-out %.c,$(A))
.PHONY:all
all:
@echo "A = $(A)"
@echo "B = $(B)"
@echo "C = $(C)"
运行结果为:
A = a.c b.c c.o
B = a.c b.c
C = c.o
执行
wildcard函数
定义
$(wildcard pattern)
wildcard的意思是“通配符”,当然该函数的目的也是从当前文件中找出符合pattern的文件,其中pattern中可以带有通配符。
比如我工作目录有5个文件:aaa.c bbb.c ccc.c abc.c acc.c
当我想筛选出所有以a开头的.c文件的时候就可以使用该函数
示例
创建5个文件:
touch aaa.c bbb.c ccc.c abc.c acc.c
在makefile文件中输入一下代码:
A = $(wildcard a*.c)
B = $(wildcard *.c)
C = $(wildcard a*c.c)
.PHONY:all
all:
@echo "A = $(A)"
@echo "B = $(B)"
@echo "C = $(C)"