原文链接:http://www.orlion.ga/816/
一、基本规则
对于一个拥有多个文件的c项目,编译时可能是这样的指令:
gcc main.c stack.c -o main
如果编译之后又对stack.c进行了修改,则又要重新把所有的源文件编译一遍,即使main.c和那些头文件都没有修改也要跟着重新编译,一个大型项目往往上千个源文件组成,全部编译要几个小时,只改一个源文件就要重新编译显然不合理。如果是一下的编译方式会好一些:
gcc -c main.c gcc -c stack.c gcc main.o stack.o -o main
如果编译后修改了stack.c要重新编译只需要两步:
gcc -c stack.c gcc main.o stack.o -o main
这样又有一个问题,如果修改了三个源文件,但是有一个忘记编译了,结果编译完了修改没生效。更复杂的是修改了头文件所有包含该头文件的源文件都需要重新编译。
要解决以上的问题就需要写一个Makefile文件和源码放到同一个目录下:
然后在这个目录下运行make编译:
make命令会自动读取当前目录下的Makefile文件,完成相应的编译步骤。Makefile由一组规则组成,每条规则的格式是:
target ... : prerequisites ... command1 command2 ...
例如:
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o
main是这条规则的目标(target),main.o、stack.o、is_empty、pop.o、push.o是这条规则的条件(prerequisite)。目标和条件之间的关系是:欲更新目标,必须更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须跟着被更新。所谓"更新"就是执行一遍规则中的命令列表,命令列表中的每条命令必须以一个Tab开头,注意不能是空格,对于Makefile中的每个一Tab开头的命令,make会创建一个Shell进程去执行它。
对于上边的例子,make执行如下步骤:
-
尝试更新Makefile中的第一条规则的目标main,第一条规则的目标称之为缺省目标,只要缺省目标更新了就算完成任务了,其他工作都是为这个目的而做的。由于main文件还没有生成,显然需要更新,但规则说必须先更新了main.o stack.o is_empty.o pop.o push.o这五个条件才能更新main。
-
所以make会进一步查找以这五个条件为目标的规则,这些目标文件也没有生成,页需要更新,所以执行相应的命令(gcc -c main.c gcc -c stack.c …)来更新它们。
-
最后执行gcc main.o stack.o is_empty.o pop.o push.o -o main更新main。
如果再执行make则不会重新编译,如果源文件修改了则会重新编译更新对应的目标文件然后重新执行gcc main.o stack.o is_empty.o pop.o push.o -o main来更新main
通常Makefile都会有一个clean规则,用于清除编译过程中产生的二进制文件,保留源文件:
clean: @echo "cleanning project" -rm main *.o @echo "clean completed"
把这条规则加到Makefile文件尾,然后执行这条规则:
如果在make的命令行中指定了一个目标(例如clean),则更新这个目标,如果不指定目标则更新Makefile中第一条规则的目标(缺省目标)。
如果make执行的命令前面加了@字符则不显示命令本身而只显示它的结果;如果命令前面加了“-”即使这条命令出错make也会继续执行,(如果不加则会立刻终止,不再执行后续的命令)通常rm命令和mkdir命令前面要加“-”,因为rm要删除的文件可能不存在,mkdir要创建的目录可能已经存在了,而这些错误是可以忽略的,例如上边已经执行过一遍make clean了,再执行一遍就没有文件可以删了这时rm会报错,但是make忽略了这一错误继续执行后边的echo。
如果当前目录下存在一个文件clean会怎么样呢?如果存在一个clean文件,clean目标又不依赖于任何条件,make就认为它不需要更新了,而我们希望把clean当做一个特殊的名字使用,不管它存在不存在都要更新,可以添加一条特殊规则,把clean声明为一个伪目标:
.PHONY: clean
这条规则没有命令列表。(这条规则写在clean:规则之后也行)类似于clean这样的命令常见的就是install了(make install)
二、隐含规则和模式规则
Makefile有很多灵活的写法,可以写得更简洁。
一个目标所依赖的所有条件不一定非要写在一条规则中,可以分开来写:
main.o: stack.h main.o: main.c gcc - c main.c
如果一个目标分来来写则其中只有一条规则允许有命令列表,其他规则应该没有规则列表,否则make会报警并采用最后一条规则的命令列表。
现在我们的Makefile可以改写为:
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o -o main main.o: stack.h main.o: main.c gcc -c main.c stack.o: stack.c gcc -c stack.c ... clean: -rm main *.o .PHONY: clean
可以看到我们把main.o分开来写,但是这样会显得繁琐了些,这时可以简化:
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o -o main main.o: stack.h clean: -rm main *.o .PHONY: clean
但是现在main.ostack.ois_empty.o…这五个目标连编译命令没了怎么编译的呢?
解释一下前五条命令怎么来的:如果一个目标在Makefile中所有规则都没有命令列表,make会尝试在内建的隐含规则数据库中查找适用的规则。make的隐含规则数据库可以用"make -p"命令打印,打印出来的格式也是Makefile的格式,包含很多变量和规则,其中和这个例子有关的隐含规则有:
# default OUTPUT_OPTION = -o $@ # default CC = cc # default COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c %.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<
#号在Makefile中表示单行注释,cc是一个Makefile变量,用CC = cc定义和赋值,用$(CC)取它的值,其值应该是cc。Makefile变量像C的宏定义一样,代表一串字符,在取值的地方展开。cc是一个符号链接,通常指向gcc。
CFLAGS这个变量没有定义,$(CFLAGS)展开是空,CPPFLAGS和TARGET_ARCH也是如此。这样$(COMPLIE.c)展开应该是"cc 空 空 空 -c"去掉“空”得到"cc -c",注意中间留下4个空格,所以%.o:%.c规则的命令$(COMPLIE.c) $(OUTPUT_OPTION) $<展开之后是cc -c -o $@ $<,和上面的编译命令已经很接近了。
$@和$<是两个特殊的变量,$@的取值为规则中的目标,$<的取值为规则中的第一个条件。$.o:%.c是一种特殊的规则,称为模式规则。现在回顾一下整个过程,在我们的Makefile中以main.o为目标的规则都没有命令列表,所以make会查找隐含规则,发现隐含规则中有这样一条模式规则适用,main.o符合%.c的模式,现在%就代表main(称为main.o这个名字的Stem),再替换到%.c中就是main.c。所以这条规则就相当于:
main.o: main.c cc -c -o main.o main.c
随后在处理stack.o目标时又用到这条模式规则,这时又相当于:
stack.o: stack.c cc -c -o stack.o stack.c
其他三个也同样处理。这三条规则可以由make的隐含规则推导出来,所以不必写在Makefile中。
之前我们写的Makefile都是以目标为中心,现在可以以条件为中心:
target1 target2: prerequisite1 prerequisite2 command $< -o $@
这条规则相当于:
target1: prerequisite1 prerequisite2 command prerequisite1 -o target1 target2: prerequisite1 prerequisite2 command prerequisite1 -o target2
三、变量
foo = $(bar) bar = Huh? all: @echo $(foo)
我们执行make将会打印出Huh?。当make读到foo = $(bar)是,确定foo的值是$(bar),但并不立即展开$(bar),然后读到bar = Huh?,确定bar的值是Huh?,然后在执行规则all:的命令列表时才需要展开$(foo),得到$(bar),再展开$(bar),得到Huh?。因此,虽然bar的定义写在foo之后,$(foo)展开还是能够取到$(bar)的值。、
这种特性有好处就是可以把变量的定义推迟到后边。当然可以使用":="运算符make在遇到变量定义时立即展开:
x := foo y := $(x) bar all: @echo "-$(y)-"
还有一个比较有用的赋值运算符是?=,例如foo ?= $(bar)的意思是:如果foo没有定义过,那么?=相当于=,定义foo的值是$(bar),但不立即展开;如果先前已经定义了foo,则什么也不做,不会给foo重新赋值。
+=运算符可以给变量追加值,例如:
objects = main.o objects += $(foo) foo = foo.o bar.o
常用的特殊变量:
$@,表示规则中的目标。
$<,表示规则中的第一个条件。
$?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
$^,表示规则中的所有条件,组成一个列表,以空格分隔。
四、自动处理头文件的依赖关系
在写main.o、stack.o、is_empty.o、pop.o、push.o这五个目标的规则时要查看源代码,找出它们依赖于哪些头文件,这很容易出错,一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。为了解决这个问题,可以用gcc的-M选项自动生成目标文件和源文件的依赖关系:
如果不需要输出系统头文件的依赖可以使用"-MM"。
接下来是怎么把这些规则包含到Makefile中,GNU make官方手册建议这样写:
all: main main: main.o stack.o is_empty.o pop.o push.o gcc $^ -o $@ clean: -rm main *.o .PHONY: clean sources = main.c stack.c is_empty.c pop.c push.c include $(sources:.c=.d) %.d: %.c set -e; rm -f $@; $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; sed 's,($*).o[ :]*,1.o $@ : ,g' < $@.$$$$ > $@; rm -f $@.$$$$
source变量包含我们要编译的所有.c文件,$(sources: .c=.d)是一个变量替换语法,把sources变量中每一项的.c替换成.d所以include这一句相当于:
include main.d stack.d is_empty.d pop.d push.d
这里include表示包含五个文件。这五个文件也应该符合Makefile的语法。这时候执行make一开始找不到.d文件会报警告,但是make会把include的文件名也当做目标来尝试更新,而这些目标适用模式规则%.d:%.c,所以执行它的命令列表。
不管是Makefile本事还是被包含的文件,只要有一个文件在make过程中给更新了,make就会重新读取整个Makefile以及被它包含的所有文件。
五、常用的make命令行选项
-n选项只打印要执行的命令,而不会真的执行命令,这个选项有助于我们检查Makefile写得是否正确,由于Makefile不是顺序执行的,用这个选项可以先看看命令的执行顺序,确认无误了再真正执行命令。
-C选项可以切换到另一个目录执行那个目录下的Makefile,比如先退到上一级目录再执行我们的Makefile(假设我们的源代码都放在testmake目录下):
$ cd .. $ make -C testmake
一些规模较大的项目会把不同的模块或子系统的源代码放在不同的子目录中,然后在每个子目录下都写一个该目录的Makefile,然后在一个总的Makefile中用make -C命令执行每个子目录下的Makefile。
在make命令行也可以用=或:=定义变量,如果这次编译我想加调试选项-g,但我不想每次编译都加-g选项,可以在命令行定义CFLAGS变量,而不必修改Makefile编译完了再改回来:
make CFLAGS=-g