Make用来做什么?
make是一个工程管理工具,不仅限于代码工程,它可以用在需要在改变一些源文件的时候进行工程更新的任何工程,管理是通过编写makefile来实现的。对代码工程来说,我们通过编写makefile来管理代码源文件。当工程里的源文件有更新时,make可以根据源文件的修改时间,自动编译更新过的源文件。
使用变量和隐式规则来简化Rule
变量定义之后,可以使用$()
或${}
来进行引用;如果变量名只有一个字符,也可以直接使用$
进行引用。
make有对应的隐式规则从.c生成对应的.o,所以当一条Rule的目的是从.c生成对应的.o时,可以省略这条Rule的Recipe和Prerequisites部分;make会自动应用cc -c x.c -o x.o
,并且自动将x.c添加到prerequisites列表里面。
隐式规则和显式规则的区别是什么?
显式规则是指targets文件列表,prerequisites文件列表,以及recipes都显式指定的规则,不需要由make去自动推断。隐式规则可能某些部分没有具体指定,需要由make去自动推断。
使用[ \ ]来续行
当一行比较长的时候,为了便于阅读,可以使用[ \ ]来进行续行。对[ \ ]的处理,在recipe行里和非recipe行里是不同的,这里讨论的是非recipe行里的[ \ ]。在非recipe的行里,[ \ ]会被make替换为一个空格,然后,这个空格连同[ \ ]前后的空格进一步被压缩为一个空格。
如果不想 [ \ ]的地方变成空格,想让物理行在空间上连续,则可以使用类似var := one$\word
的把戏形式。在make将 [ \ ]替换为空格后,变为var := one$ word
,此时$引用的变量名为" "(一个空格),该变量不存在,则不会有实际内容,则相当于var := oneword
。
Makefile的include
Makefile可以使用include指令来包含其它的Makefile,与C语言编程里面的include不同的是,它并不是单纯的把另一个Makefile的内容替换到include的位置,而是指暂停读取当前的Makefile,转而去读取include进来的Makefile,然后才回来接着读取当前的Makefile。被include包含的文件名可以使用shell的文件名模型,如Makefile-*;也可以将变量名进行展开得到文件名;被包含的文件名可为空,则什么也不会发生,也不会打印任何错误。
如果被包含的文件以相对路径提供,但是又没在当前路径下找到,则make会去其它路径下去寻找:
1)使用-I指定的包含路径 2)/usr/local/include,/usr/gnu/include,/usr/local/include,/usr/include
.INCLUDE_DIRS变量记录了这些搜寻路径。如果不想让make去这些默认路径下去找被包含的文件,可以在make的时候为-I指定一个特殊的值[ - ],也就是-I-。
如果找不到被包含的文件,也不会立即发生错误,还是会继续读取Makefile。读取完Makefile后,会对不存在的或过期的target进行remake。如果在make的过程中发生错误,才会报缺失包含文件的致命错误。所以说,有时候因为粗心大意而导致包含文件缺失,虽然make可以过,但是结果可能不一定是对的。
关于TAB的注意事项
以TAB开头的行被认为是recipe行。所以,recipe行必须以TAB开头,非recipe行不能以TAB开头。
Make工作的两个阶段
第一阶段读取所有的makefile和使用include包含的makefile,分析所有的变量和规则,并据此构建一个关于target和prerequisites的依赖表(可以理解为一个make数据库)。第二阶段则是根据第一阶段的解析结果来决定更新哪些targets,并运行对应的recipies。
make的两个阶段均涉及到变量和函数的展开。在第一阶段解析makefile的时候发生的展开叫做立即展开(immediate),在第二阶段才发生的展开叫做延迟展开(deferred)。
先说下什么叫变量的展开,我们在makefile中定义变量,不是为了使用变量本身,而是方便在其它地方通过$()
来引用它的值,变量的值将替换对变量的引用,这就叫变量的展开,函数也是一样的道理。从这个角度看,立即展开就是读取到$()
的时候,就立即用对应的值将其替换(可能会连续展开多次,展开到不能再次展开为止),这是在make第一阶段就发生的。延迟展开就是读取到$()的时候先保持原状,直到make的第二阶段才对其进行展开)。来看一个例子,有如下一个makefile:
fruit = apple
a := $(fruit)
b = $(fruit)
fruit = orange
test :
@echo "a = $(a)"
@echo "b = $(b)"
运行make后,打印出如下结果:
a = apple
b = orange
究其原因,就是因为变量的立即展开和延迟展开的缘故。对a变量的赋值使用的是[ := ],对b变量的赋值使用的是[ = ],它们对变量的展开规则如下:
immediate = deferred
immediate := immediate
可以看到[ = ]右边的部分是延迟展开的,如果其是一个变量的引用,该变量将延迟展开。而[ := ]右边的部分是立即展开的,如果其是一个变量的引用,该变量将立即展开。所以,上例中的变量赋值 a := $(fruit)
,$(fruit)被立即展开,此时的fruit = apple
,所以a = apple
;而变量赋值b = $(fruit)
,$(fruit)
被延迟展开,所以b = $(fruit)
不变,仍然保持为对fruit变量的引用。
make的第一阶段完成后,a已经等于apple,b仍然为对fruit变量的引用,fruit则被修改为orange。然后来到make的第二阶段,此时会执行两条echo命令。规则里面recipe部分的变量也是延迟展开的,所以此时会展开$(a)
和$(b)
;$(a)
直接展开为apple,$(b)
则展开为$(fruit)
,而$(fruit)
此时等于orange,所以b变量打印出来是orange。
所以有时候我们看到CC变量被定义在Makefile的最后,却仍然可以在前面的recipe中使用$(CC)
,就是因为recipe里面的变量是在make的第二阶段里延迟展开的。至于为什么要对变量定义两种展开方式,肯定是有用处的,通常是结合自动变量一起使用,这个以后学到相应部分的时候再提。
再看一个例子:
fruit = apple
a := $(fruit)
b = $(fruit)
c := $(b)
fruit = orange
test :
@echo "a = $(a)"
@echo "b = $(b)"
@echo "c = $(c)"
这个例子在之前的那个例子里面增加了一个c变量。对c变量的赋值使用的是 [ := ],其值为对b变量的引用$(b)
,这里需要立即对$(b)
进行展开,$(b)
此时为对fruit变量的引用$(fruit)
,故展开为此时fruit变量的值apple。运行make后打印的结果如下:
a = apple
b = orange
c = apple
二次展开(Secondary Expansion)
上一部分讲了make的工作分为两个阶段,第一个阶段解析makefiles(read-in阶段),第二个阶段更新targets(target-update阶段)。然后讲到了变量和函数的两种展开方式,在read-in阶段发生的展开叫做立即展开,在target-update阶段发生的展开叫做延迟展开。这部分的讲解还是关于展开的,叫做二次展开。
二次展开只能用于规则的prerequisites部分。
什么叫二次展开?根据上一部分的学习,我们已经知道Rule部分的展开规则如下:
immediate : immediate ; deferred
deferred
可以看到,规则的prerequisites部分是立即展开的,也就是在read-in的时候就会将其展开,这是第一次。由于某些原因,我们可能希望能在target-update阶段能再将其展开一次,这就叫做二次展开。二次展开是需要我们手动开启的,方法是在第一个需要进行二次展开的prerequisites列表之前定义一个特殊的target — .SECONDEXPANSION。make会对这个target之后的所有Rule的prerequisites列表进行二次展开。接下来看一个例子,假设有如下的makefile片段:
.SECONDEXPANSION:
ONEVAR = onefile
TWOVAR = twofile
myfile: $(ONEVAR) $$(TWOVAR)
由于myfile规则之前定义了SECONDEXPANSION,所以myfile的prerequisites会进行二次展开,一次展开只能解开一层变量引用,所以read-in阶段的第一次展开后规则变为:
myfile: onefile $(TWOVAR)
到target-update阶段的时候,prerequisites进行二次展开,规则变为:
myfile: onefile twofile
这样看起来二次展开并没有什么实际的作用,因为我不用二次展开,也即将myfile的规则写为:
myfile: $(ONEVAR) $(TWOVAR)
一样可以得到相同的展开。但是我们再看一个例子,假设有如下makefile片段:
.SECONDEXPANSION:
AVAR = top
onefile: $(AVAR)
twofile: $$(AVAR)
AVAR = bottom
这里,onefile的prerequisites在read-in阶段直接展开为top;而twofile的prerequisites在read-in阶段展开为$(AVAR),在target-update阶段被二次展开为AVAR变量的值,此时AVAR = bottom,故twofile的prerequisites在经过两次展开后为bottom。
其实可以看出,我们通过使用SECONDEXPANSION,赋予prerequisites部分在target-update阶段再展开一次的能力,这种能力一般和自动变量搭配使用才能发挥其真正的作用。