一、Makefile
简介
在编写C/C++程序的时候,我们经常需要编译并运行代码。在程序规模较小的情况下,可以简单地直接调用编译器来完成这项工作。然而,在很多情况下程序往往包括大量的代码文件,手动调用编译器变得麻烦无比。尤其要命的是,编译的时候需要考虑源文件之间的依赖情况。例如对于下面的三个文件:
文件main.c:
2 blabla
文件xxx.h:
文件xxx.cpp:
2 implemented some functions or classes
上面三个文件的依赖情况分别为:
2 xxx.cpp => xxx.h
因此,在xxx.h文件被改变时,将导致main.c和xxx.cpp都需要重新编译,而main.c和xxx.cpp被改变时则只需要编译相应的文件,并执行链接操作即可(这个例子也说明了为什么.h文件不能轻易变动,可以把.h文件视为模块提供的接口,如同契约一样已经制定就不能轻易变动)。
这个问题如何解决呢?可以不考虑依赖关系,把所有源文件重新编译——这种方式在我们编写一些玩具代码的时候的确可以,然而在一个中等规模的项目就会成为一个噩梦。。C/C++代码的编译速度本来就慢(这一点自己编译过开源代码同学应该深有体会),如果每次都彻底重新编译的话会浪费大量的时间。Make工具的产生一开始就是用来解决这一问题的(当然不限于此)。
先上一个最简单的Make用例:
hello.c文件
2 int main()
3 {
4 printf(“hello world! ”);
5 return 0;
6 }
2 cc hello.c -o hello
3 hello world
规则和命令
Makefile允许我们定义一系列的依赖规则,并根据该依赖规则来执行相应的命令。下面是一个最简单的Makefile:
Makefile文件
2 hello: hello.c
3 gcc -o hello hello.c
其中第零行是一行注释,同shell脚本一样,Makefile使用#来定义注释。第一行定义了一条依赖规则,冒号左边为需要生成的目标,冒号右边则为生成该目标所需要的文件。值得注意的是,规则的目标和依赖项都可以是多个文件,多个文件之间使用空格分开,形如a b: c d的规则表示目标a和b均依赖于c和d。
文件的第二行为一个命令。Make工具根据目标和依赖项的修改时间戳来判断是否需要执行命令来生成新的目标,即:如果目标的修改时间较所有的依赖项更新,则表明无需执行命令来重新生成目标;而如果任一依赖项的修改时间较目标的更新,则说明有必要执行命令来重新生成目标。make根据命令的程序退出状态来判断是否执行成功(0为成功,否则失败),如果执行失败则停止生成过程并报错。可以通过设定命令行前缀来改变这一过程,命令行前缀包括以下两种:
-:make忽略该命令的执行情况,即使失败仍然继续生成
@:make不打印该命令,通常在执行echo命令时使用该前缀
下面是一个使用命令行前缀的示例:
2 -rm /some/file/not/exist
3 at:
4 @echo “hello”
当执行make dash时,rm命令执行失败,但是make的生成目标过程不会停止,而执行make at时,将只在屏幕上打印“hello“,不会打印出echo命令的执行情况。
在makefile中,规则后面不一定要跟一条命令,即我们可以定义一条啥事也不干的规则。而判断一条语句是规则还是命令是根据行开头是否包含TAB字符,注意,一定要是TAB字符,不能是四个空格或者其它看起来和TAB一样的东东。。这个设计被认为是Unix有史以来最糟糕的设计修补(TAOUP P358)。(幸运的是,VIM能很好地处理这一问题,在我设置了expandtab的情况下还能正确地在命令的第一列插入TAB字符。)
变量的定义和使用
Makefile中变量的定义和使用方式如下所示:
2 -rm /some/file/not/exist
3 at:
4 @echo “hello”
变量的定义非常简单,变量名和值用符号“=”分开即可,使用变量则用$(valname)的方式。在make程序解析Makefile时,会对变量进行替换。这里有一个需要注意的地方:make实际上会对Makefile扫描两次——第一次替换所有的变量,第二次才是根据规则执行命令。这意味着对于变量在多处定义时,最终替换的变量值是最后一次对变量指定的值。下面举一个例子:
Makefile文件
2 test:
3 echo $(val)
4 val = b
执行make test时,显示的结果将是b而不是a。
除了我们自定义的变量之外,make还包含了一些预定义的变量。例如,变量CC表示系统默认的C编译器(默认值为cc),因此定义编译文件的命令时通常使用的方式是$(CC) -o main $(src)而不是直接使用gcc。这一特点在后面引入Autoconf时非常有用——Autoconf检查系统的默认C编译器并设置CC变量的值,从而使得代码可以在包含不同C编译器的系统上顺利编译。完整的预定义变量可以参见make用户手册。
在上面提到的例子中,src变量被使用了两次:作为规则的依赖项和命令的参数。为了贯彻执行DRY原则,make还定义了一种称为“自动变量”的变量,用来表示规则中的特定部分。使用自动变量时,上面的例子可以中写为如下方式:
2 main: $(src)
3 $(CC) -o $@ $+
其中$@是$(@)的简写形式,表示规则的目标部分,而$+则表示规则的所有依赖项。相对于前面的方式,现在的Makefile是不是干净了一些呢?完整的自动变量列表同样包括在make用户手册中。
SUFFIXES和PHONY
假设我们的项目中只有一个文件main.c,那么Makefile很简单:
2 gcc -o main main.o
3 main.o: main.c
4 gcc -c main.c
可是在有100个甚至更多.c文件的情况下,需要对每一个.c文件进行编译,就不得不写很多次如下的代码:
2 gcc -c xxx.c
幸运的是,make提供了后缀匹配功能,从而使用如下代码就可一完成上述功能:
2 gcc -c $<
类似的规则成为后缀规则,make在解析Makefile时将扫描Makefile文件所在目录下的匹配文件,并执行相应的命令。然而这里存在一个问题,即可能存在一个名为“.c.o“的文件,make如何判断这条规则是一条普通的规则还是后缀规则呢?这里就要用到特殊的目标SUFFIXES了:
这条规则告诉make将目标为.c.o的规则视为后缀规则,而非名为.c.o的文件。
与此类似的另一个特殊目标是PHONY,用来定义一些“伪目标”。在Makefile中,我们除了定义一些需要生成的文件外,还可能会执行一些操作,而这些操作是不会生成目标文件的。例如,目前广泛使用make clean清理临时文件,make install用来安装程序。我们需要定义一些没有依赖项的规则:
2 rm *.o
当执行make clean时,make将查找当前目录,发现没有名为clean的文件,同时该文件也不需要任何依赖项,这种情况下make将总是执行规则对应的命令。然而,如果不幸地在当前目录下刚好存在一个名为clean的文件呢?这时make发现该文件没有任何依赖项,从而认为该目标已是最新的,因此将跳过命令执行阶段。
特殊目标PHONY就是用来解决这一问题的:
以上规则将告知make程序,clean目标为一个伪目标,从而make程序无需再去检查目录下clean文件的存在与否,而总是执行clean目标所对应的命令。
(未完待续。。。)