MAKEFILE教程
之后要尝试做一些项目,在加上前一段时间接触PA,发现自己对自动构建项目一类的操作实在是一知半解,而且之后也需要要尝试自动化编译一些大型的源码,所以学习一下makefile的基本使用,完善一下自己对工具链的认知。这里我看的教程是廖雪峰的makefile入门
Makefile基础
在Linux中我们使用make命令时,他就会在当前目录下找一个名为Makefile的文件,并根据里面的内容进行自动化的执行。我们以下面这个需求为例子:
| 1 | a.txt + b.txt -> m.txt | 
上述逻辑我们编写makefile
规则
Makefile由各种规则构成,每一条规则需要指出一个目标文件和若干个依赖文件,以及用于生成目标文件的命令,例如我们想要生成m.txt则规则如下:
| 1 | # 目标文件: 依赖文件1 依赖文件2 依赖文件... | 
其中#用来注释,一条规则的格式如上,Tab后使用命令来实现目标文件
现在我们就可以完整的实现上述的规则:
| 1 | x.txt: m.txt c.txt | 
我们可以尝试执行一下:
| 1 | ylin@Ylin:~/Program/test$ make | 
make默认执行第一条规则,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txt和x.txt。
所以我们可以知道,实际上Makefile就是一堆规则(即你些的目标文件和依赖),当满足规则时,就调用规则后的命令,创建出一个新的目标文件
把默认的规则放在第一条,其他规则的顺序makefile会自动判断依赖。make会把每次执行的命令输出出来,便于我们观察调试。
如果我们在不对任何目标文件进行修改的情况下,我们在此使用make就会得到:
| 1 | ylin@Ylin:~/Program/test$ make | 
make会根据文件的创建和修改时间来判断是否应该更新一个目标文件,例如我们这里只修改c.txt,并不会触发对m.txt的更新,因为他的依赖文件没有发生改变:
| 1 | ylin@Ylin:~/Program/test$ make | 
make只会重新编译那些依赖被修改,或者是尚未完成的部分,重新编译的过程并不是每一条命令都会执行,make只会选择必要的部分执行,我们称这种编译为增量编译。能否正确的实现增量编译,取决于我们编写的规则。
伪目标
为了进一步的便于自动化的构建,有时候我们会需要定义一些常用的规则。例如在我们使用make之后,我们自动生成了m.txt和x.txt,现在我们可以定义一个规则用于清理这些生成的文件:
| 1 | clean: | 
然后我们可以通过make clean来调用这个规则:
| 1 | ylin@Ylin:~/Program/test$ ls | 
但是make这里实际上是把clean当作一个目标文件,我们使用make clean规则时,make检查到没有目标文件clean,于是调用命令尝试构建目标文件,但是clean文件不会被生成,所以我们总可以使用它。可是如果目录中有一个clean文件怎么办呢?make认为clean已经被构建了,就不会再使用命令。为了解决这个问题,我们希望make不要将clean视作文件,我们可以添加一个标识:
| 1 | 
 | 
此时,clean就不再被视作一个文件,而是伪目标。一般大型项目会有clean,install一类的常用的伪目标规则,方便用户快速的构建一些任务
执行多条命令
一个规则可以有多条命令:
| 1 | cd: | 
运行结果如下: 1
2
3
4
5
6ylin@Ylin:~/Program/test$ make cd
pwd
/home/ylin/Program/test
cd ..
pwd
/home/ylin/Program/test
我们发现命令cd ..并没有修改当前目录,导致每次输出的pwd都是一样的,这是因为make针对每条指令都会创建一个独立的shell环境,所以命令之间无法互相影响。但是我们可以用以下方法实现
| 1 | cd_ok: | 
我们查看新的执行结果:
| 1 | ylin@Ylin:~/Program/test$ make cd_ok | 
当然也可以再;后加\便于分行阅读:
| 1 | cd_ok: | 
我们需要注意,在shell中;代表无论当前命令是否生效,都会执行下一个命令。与其相反的一个执行多条命令的语法是&&,当前面的命令执行失败时,后续的命令就不会再继续执行了
控制打印
默认情况下,make会打印出执行的每一条命令,如果我们不想打印某一条命令,我们只需要在命令前面加上@,告诉make不打印该命令:
| 1 | no_output: | 
执行结果如下:
| 1 | ylin@Ylin:~/Program/test$ make no_output | 
控制错误
make在执行命令时,会检查每一条命令的返回值,如果返回值错误,就会中断执行。
例如我们手动构建一个错误(用rm删除一个不存在的文件):
| 1 | has_error: | 
会发生:
| 1 | ylin@Ylin:~/Program/test$ make has_error | 
但是有时候我们希望忽略错误,我们可以在特定的指令前面加上-用来忽略错误的命令:
| 1 | ignore_error: | 
输出结果如下:
| 1 | ylin@Ylin:~/Program/test$ make ignore_error | 
对于执行可能出错,但是不影响逻辑的命令,可以使用-忽略
编译C程序
现在我们尝试一下编译一个简单的C语言程序,其依赖文件如下:
| 1 | main.c + hello.h -> main.o | 
文件内容如下:
| 1 | // main.c | 
我们可以编写以下规则,用于自动构建可执行程序:
| 1 | a.out: main.o hello.o | 
同时也可以通过make clean来删除中间文件:
| 1 | ylin@Ylin:~/Program/test$ ls | 
我们可以看到完整的命令流程如下:
| 1 | ylin@Ylin:~/Program/test$ make clean && make | 
隐式规则
为了编译这个项目,我们一共编写了三条规则,现在我们尝试删除两个.o文件的规则,然后再编译试试:
| 1 | a.out: main.o hello.o | 
然后我们执行make,输出如下:
| 1 | ylin@Ylin:~/Program/test$ make | 
然后可以发现
我们并没有制定相关的规则,可是程序还是正常的进行了编译,这是make中的隐式规则,因为make本来就是为了编译C程序设计的,所以为了避免重复的编译.o文件,在一开始没有找到对应的规则时,会自动的调用隐式规则。对于C C++ ASM ...等程序,都有内置的隐式规则,这里不展开叙述。
使用变量
我们在编译时难免会遇到许多重复的文件名,为了方便使用,我们引入变量用来解决重复的问题。我们以上一节的Makefile为例:
| 1 | a.out: main.o hello.o | 
我们可以定义一个变量来替换它:
| 1 | TARGET = a.out | 
对于变量定义,我们使用变量名 = 值,变量名通常使用大写。在引用变量时通常使用$(变量名)
当然,对于我们的依赖文件列表,也可以使用变量进行替换:
| 1 | TARGET := a.out | 
但是对于依赖文件很多的情况下,我们可能需要一个自动化的方式,来将我们的源文件批量编译成目标文件。我们注意到每个.o文件都是由对应的.c文件编译产生的,我们可以让make先获取.c文件列表再替换生成得到.o文件列表:
| 1 | TARGET := a.out | 
内置变量
为了方便我们使用,make也内置了很多的内置变量,例如我们可以用$(CC)替换命令cc:
| 1 | $(TARGET): $(OBJS) | 
这样方便我们在交叉编译时,指定编译器。诸如此类的内置变量还有很多,遇到了再学吧。
自动变量
在makefile中,经常会看到$@、$<之类的变量,这种变量称为自动变量,它们在一个规则中自动指向某个值。例如$@标识目标文件,$^表示所以依赖文件,所以我们也可以这样写:
| 1 | a.out: hello.o main.o | 
模式规则
我们前面提到隐式转换可以在必要时自动创建.o文件,但实际上隐式规则的命令是固定的:
| 1 | $(CC) $(CFLAGS) -c -o $@ $< | 
我们只能修改编译器变量和编译选项变量,但没办法运行多条命令。
此时我们就要引入自定义模式规则,它使用make的匹配模式规则,如果匹配上了,就自动创建一条模式规则,我们可以把我们的makefile写成以下内容:
| 1 | TARGET := a.out | 
当程序执行a.out: hello.o main.o时,发现没有hello.o,于是查找以hello.o为目标文件的规则,结果匹配到模式规则*.o : *.c,于是模式规则会动态的创建以下规则:
| 1 | hello.o: hello.c | 
我们可以尝试执行一下make:
| 1 | ylin@Ylin:~/Program/test$ make | 
这样他可以比隐式的规则更灵活。但是我们现在也遇到一个问题,就是当我们修改hello.h头文件时,不会触发main.c重新编译的问题,我们之后在解决。
自动完成依赖
我们刚刚提到,在我们可以解决自动把.c编译成.o文件,但是难以解决.c文件对.h文件的依赖规则。因为要想知道.h的依赖关系,我们需要分析文件内容才能做到,并没有一个简单的文件名映射规则。
好在我们可以使用gcc提供的-MM参数,自动分析我们所需要的.h文件参数:
| 1 | ylin@Ylin:~/Program/test$ cc -MM main.c | 
因此,我们可以利用这个功能,为每个.c文件都生成一个依赖项,并把它保存到.d(中间文件中),再使用include将其导入makefile中,这样我们就精准的实现了.c文件的依赖分析。
我们可以更新我们的makefile:
| 1 | TARGET := a.out | 
当我们运行时,通过include会引入.d文件,但是一开始.d文件并不存在,这时会通过模式规则匹配到%.d: %.c。这里用了一个复杂的sed将.d文件创建了出来。我们运行make结果如下:
| 1 | ylin@Ylin:~/Program/test$ make | 
我们可以看到.d文件中类似:
| 1 | main.o main.d : main.c hello.h | 
现在,我们文件的依赖就加入了头文件,当我们对头文件进行修改时,就会触发重新编译
| 1 | ylin@Ylin:~/Program/test$ make | 
完善Makefile
我们现在对项目目录进行整理,我们将源码放入src目录,将编译生成的文件放入build目录:
| 1 | ylin@Ylin:~/Program/test$ tree . | 
现在我们可以使用我们上述的操作,更新我们的makefile:
| 1 | SRC_DIR := ./src | 
我们通过设置目录路径变量实现了对一个简单项目的自动化编译。至此我们的对makefile的基本使用就了解了
对于makefile,它还有很多的复杂的用法,但是之后我会更好的利用它去做更多的项目。