通常在对单文件或者比较少量的文件进行编译的时候,只需要通过GCC命令直接编译就可以了。因为在文件数目比较少的情况下,其编译过程中的文件依赖关系还是很简单的,可以通过人工控制命令的顺序来解决文件的依赖关系。
然而在大型项目的开发过程中,编译过程面对的往往是成千上万个源码文件,而源码的相互依赖关系又非常复杂,想通过人力来维护这种编译顺序几乎是一件不可能的事情。当然,可以通过维护一个shell脚本(用于构建)文件来控制整个系统中所有文件的编译过程。但是通过shell脚本来控制项目文件的编译顺序有3个问题。
1)shell脚本语言无法原生支持依赖关系的表达,需要通过复杂的逻辑代码来表达源码的依赖关系。
2)shell脚本语言无法原生支持增量依赖编译(即如果只修改项目中的一个文件,只需要重新编译对该文件依赖的模块即可),要想实现控制逻辑也非常复杂。
3)shell脚本的维护成本相对较高。
GNU make是一个收集文件依赖关系,并根据依赖关系自动进行项目构建的工具。依赖定义在makefile文件中,make工具依据makefile的规则来按顺序执行对应的命令。现在的大型项目都会使用更加智能的构建工具cmake,cmake可以自动分析文件中的依赖关系,从而生成对应的makefile文件,使得项目的构建更加简便。不过本书在实现Linux 0.11项目代码的过程中还是采用make工具,因为这个工程结构相对清晰、简单。
当我们输入make命令时,make会到当前目录下去查找makefile文件。makefile文件由一系列的规则组成,每条规则的形式如下:
1 target…:prerequisites…
2 recipe…
第一行规定了目标文件以及文件的依赖关系。在makefile里,target和冒号是必不可少的,prerequisites在这一行里边可以没有。第一行之后是一条或多条recipe命令,即要达成这个target需要执行的命令。这里需要注意的是,这些recipe命令必须使用Tab分隔符来进行缩进,相比第一行的target需要多缩进一个制表符。
target一般是指该条规则下最终生成的文件名,如可执行文件或者.o/.so文件等。一个target往往会依赖一个或多个文件,即规则中的prerequisites。多个依赖则用空格进行分隔。例如:
1 foo.o:foo.c
2 GCC foo.c-c-o foo.o
3 bar.o:bar.c
4 GCC bar.c-c-o bar.o
5 a.out:foo.o bar.o
6 GCC bar.o foo.o-o a.out
这里第一条规则的target是foo.o文件,foo.o的生成依赖foo.c。生成foo.o的命令要通过GCC编译。第二条规则表明了bar.o文件依赖bar.c。第三条规则给出a.out同时依赖foo.o以及bar.o。所以,在这个makefile文件里,我们可以通过执行make a.out生成最终的可执行文件。在构建a.out的过程中,根据其依赖关系可知它同时依赖foo.o以及bar.o,make会先构建出它的依赖文件foo.o和bar.o,也就是先执行make foo.o以及make bar.o命令。由此可见,make是根据makefile文件定义的规则,并按照依赖的顺序进行构建的。如果构建完a.out以后又对bar.c文件进行了修改,再次执行make a.out时,make只需要重新构建bar.o以及a.out即可,不需要重新构建foo.o,这样在大型项目中可以大大加快构建的速度。
一般情况下,make命令会将第一条规则作为默认执行的规则,这样直接运行make命令就等价于执行make a.out。
除此之外,target可以用来指代一组命令的名称,常用的target是clean。例如:
1 clean:
2 rm a.out *.o
这种情况下执行make clean会删除当前目录下的.o文件以及a.out文件。此时,clean是一个伪目标。如果当前文件夹下恰好有一个clean文件,会干扰到make的执行。因为make发现这个target并没有依赖文件,所以不需要重新构建,这个target对应的recipe也就不需要执行了。为了消除这种影响,我们最好在这种伪目标下做一些声明:
1 .PHONY:clean
2 clean:
3 rm a.out *.o
这样的话,make便不会认为clean是一个需要生成的文件目标了。
在makefile中,还可以通过定义变量来避免在多处输入重复的命令。如在上面a.out的例子中,可以通过变量进行如下改写:
1 OBJS:=foo.o bar.o
2 CC:=GCC
3 CFLAGS:=-O2
4
5 a.out: $(OBJS)
6 $(CC) $(CFLAGS) $(OBJS)-o a.out
7 foo.o:foo.c
8 $(CC) $(CFLAGS)foo.c-c-o foo.o
9 bar.o:bar.c
10 $(CC) $(CFLAGS)bar.c-c-o bar.o
11
12 .PHONY:clean
13 clean:
14 rm a.out$(OBJS)
这里将目标文件的列表定义为变量OBJS,将编译命令定义为变量CC,将编译选项定义为CFLAGS。之后再修改目标文件或者编译器等只需要修改变量即可,不需要对每个规则下的命令进行修改。
除了用户定义的变量外,makefile中还有一系列自动变量,这些自动变量可以在规则执行时根据规则的target以及prerequisites进行刷新和计算。常用的自动变量主要有以下几个。
变量 $@:指代了当前规则里的target。如果一个规则的target有多个的话,则指代第一个target。例如上述例子可以改为:
1 a.out: $(OBJS)
2 $(CC) $(CFLAGS) $(OBJS)-o$@
3 foo.o:foo.c
4 $(CC) $(CFLAGS)foo.c-c-o$@
5 bar.o:bar.c
6 $(CC) $(CFLAGS)bar.c-c-o$@
变量$<:指代了当前规则里的第一个prerequisites。而$ˆ变量则表示当前规则中的所有prerequisites。由此,我们可以把上述例子继续改写为:
1 a.out: $(OBJS)
2 $(CC) $(CFLAGS)$^-o$@
3 foo.o:foo.c
4 $(CC) $(CFLAGS)$<-c-o$@
5 bar.o:bar.c
6 $(CC) $(CFLAGS)$<-c-o$@
变量$?:表示prerequisites列表中所有比target文件更新的文件。
至此,我们对makefile的规则就有了基本的了解。当然makefile还有很多高级的用法,本节只是简单介绍在开发Linux 0.11过程中用到的一些知识。如果读者对makefile的更多用法感兴趣,可以通过man make命令查看GNU make的官方手册。