一、GNU Make基础

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阶段再展开一次的能力,这种能力一般和自动变量搭配使用才能发挥其真正的作用。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值