我在这里记录一下对GNU make处理makefile这个过程的思考:
1. 执行make命令后,GNU make扫描整个makefile,将其中出现的变量赋值和目标依赖关系记录到数据库(就是make解析makefile得到的所有数据集合,用make -p查看)中,其中每个变量记录变量名及其字面值(用$(value)可以查看,即如myVar = $(CFLAG)的字面值就是$(CFLAG),暂不进行展开)。这个过程中对$所标记的变量或函数不进行任何展开,除非遇到几种特殊情况(后文提到)。
2. 扫描整个数据库,展开所有$引导的变量和函数(这里至少有两趟扫描,首先会扫描$(value)调用并展开,第二次扫描才展开其他$),然后按照拓扑顺序,从尾端依赖到主目标,逐个检查时间戳或文件存在性,执行相应的命令。
上面描述的重点是,在make完成对整个文件的扫描,执行步骤2前,是不会进行$展开的(后文提到的几个例外会实时展开$)。
基于上述描述,再来看一下惰性变量赋值:
Var1 = abc
Var2 = $(Var1)
Var1 = def
逐行看,第1行,在数据库中记录Var1="abc";第2行,记录Var2="$(Var1)";第3行,修改数据库中的记录Var1为"def"。最后扫描makefile完毕,对数据库中变量进行$展开求值:Var1的值"def"中不包含$,不用展开,Var2的值"$(Var1)"展开为"def"。这就完成了惰性赋值,保证了Var1和Var2同步。
再来陈述我已经知道的几个在扫描makefile中就会进行$展开的例外:
1. 实时赋值:=。分析如下代码:
Var1 = abc
Var2 := $(Var1)
第1行记录Var1="abc"到数据库;第2行,检测到:=,故先将"$(Var1)"展开为"abc",再记录Var2="abc"。对比上面的惰性赋值,make工具扫描完makefile后执行最终$展开时,Var2的字面值中是已经没有$的了。
2. $(eval)调用。当make扫描makefile时,如果发现包含$(eval)的语句,会实时展开eval的参数,然后将展开的结果字符串插入到脚本当前位置,作为脚本还没扫描的一部分继续扫描。下面是一段测试脚本:
Name = a1
Step1 := $(Name)
$(eval Name=a2)
Step2 := $(Name)
Name = a3
Step3 := $(Name)
all:
echo $(Name)
echo $(Step1)
echo $(Step2)
echo $(Step3)
然后来分析(注意这里的措辞):第1行在数据库中添加Name="a1";第2行添加Step1="a1"(进行过一次实时展开);第3行等价于脚本Name=a2,因此是修改数据库,将Name的字面值从"a1"改为"a2";第4行实时展开后记录Step2="a2";第5行修改Name为"a3";第6行记录Step3="a3"。最后运行make打印"a3 a1 a2 a3"(这里的空格实际是换行)。
3.目标或变量名中出现的$会被实时展开。毕竟,扫描脚本的时候实时展开名称中的$,才能判断该变量值或者目标依赖和命令,应该被记录到数据库中的哪个位置。虽说在实现make时采用延迟展开名称中的$的做法并非办不到,但一则这种实现更复杂,二则,延迟展开名称中的$这种做法会和前两种实时展开相冲突,如:
Name = abc
$(Name) = 123
Name3 := $(abc)
Name = def
all:
echo $(Name3)
由于第3行有一个实时展开(这个实时展开规则是make保证的),因此第3行之前应该有一个叫做abc的变量存在,这样最后才会打印一个更符合只觉的123,如果名称中的$是延迟展开的话,最后打印空行,恐怕会让很多人惊诧了。
上面这个例子只是为了说明为什么有必要实时展开名称中的$,下面再加一个测试例外3的简单例子:
Name = abc
$(Name) = 123
tar_$(Name):
echo $@
echo $(abc)
echo $(Name)
Name = def
分析,第1行记录Name="abc";第2行实时展开后,记录abc="123";3、4、5行进行实时展开后,添加目标记录tar_abc,命令为echo $@ 、echo $(abc)和echo $(Name)。最后输出"tar_abc 123 def"(空格是换行)。输出里有意思的是,由于一个是实时展开,一个是延迟展开,两个$(Name)分别是"abc"和"def"。
继续在学习make中,根据使用情况不定期纠正本文中错误和增加新条款。