makefile编写(2):变量
注:本系列内容大部分是基于GNU make的标准,其中cc命令默认调用Linux自带的c编译器程序
makefile变量
在makefile中的定义的变量从效果和实现来看比较像c/c++语言中的宏,它代表一个文本字串(没有数据类型),可以使用在目标文件、依赖文件、命令或是makefile的其它部分中,在makefile中执行时其会自动地展开在引用的地方。但是与c/c++的宏所不同的是,makefile的变量值是可以在文中改变的,具有“变量”的性质(其实只要把makefile的执行过程单纯类比于c程序的预处理过程,会发现两者并没什么不同)。下面给出一个简单的例子:
# 变量会在使用它的地方精确地展开,就像c/c\++中的宏一样
f = c
prog.o : prog.$(f)
$(f)$(f) -$(f) prog.$(f)
# 展开后得到:
prog.o : prog.c
cc -c prog.c
上例仅仅是为了说明变量展开的过程,实际编程肯定不会这样做。
makefile变量的基本语法
在makefile中,自定义变量的命名可以包含字符、数字,下划线(可以是数字开头),但不应该有:、#、=或空字符(空格、换行、Tab等)。变量是大小写敏感的,foo、Foo和FOO是三个不同的变量名(与UNIX的风格一致)。传统的makefile的变量名是全大写的命名方式,但还是推荐使用驼峰命名,如:MakeFlags,这样可以避免与预定义环境变量冲突而覆盖掉后者。
定义变量的基本语法如下:
变量的名称=值列表
值列表中既可以是零项,又可以是一项或者是多项。等号左右的空白符没有明确的要求,因为make解析时多余的空白符会被自动删除,变成Shell的赋值形式。注意makefile变量值都是字符串,所以任何情况下都不要额外加""和’’(除非是在Shell命令中),否则会被单纯的看作是字面值本身加上引号。
引用变量时的格式为:
$(值列表)或${值列表T}
存在一个特殊情况,就是在之前定义f=c的例子中,变量f不加括号实际是一样的:
- 变量名为单字符的情况下,直接使用$x的格式就可以实现变量引用
- 自动化变量其实也是使用这种格式
- 多字符变量的引用必须使用括号,否则make将把变量名的首字母作为作为变量而不是整个字符串
- 比如$PATH会被解释为$§后面再加一个ATH符号
- 这一点和Shell中变量引用方式不同,Shell中$xx可以直接引用变量xx
不过为了更加安全地使用,一般还是推荐一律给变量加括号。
另外还存在的一种特殊情况是在命令中使用变量,是完全可以按照Shell命令的形式写的。只不过由于makefile中符号$有特殊的含义(元字符),不能直接按照Shell脚本的格式写,否则默认是在按照makefile的形式引用变量。此时需要使用两个连续的$$“转义”符号$,这样变量就会被解析为Shell脚本的$xx形式。(同理,在规则中需要使用符号$的地方,都需要使用两个连续的$$)
makefile变量赋值
makefile中支持四种变量基本赋值方式。第一种方式是简单的使用=号,可以用一个或多个字串为变量赋值,也可以用其他变量引用赋值。注意,在这种情况下如果用变量值来定义变量的话,右侧变量可以定义在文件的任何一处,也就是说=右侧不一定非要是已定义的值,也可以用后面定义的值,如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
执行make all将会打印出的变量$(foo)的值是Huh? ($(foo)的值是$(bar),$(bar)的值是$(ugh),$(ugh)的值是Huh?),可见变量是可以使用后面的变量来定义的。这主要是因为脚本语言都是不需要提前声明变量的,而Shell的特点是只要变量第一次出现就默认创建,如果没有给出初始化值则变量会被赋予一个空字符串。之后的过程与宏展开类似,make在读入所有变量以后,将变量最终的值展开在所有引用处(作用域内)。不过这也带来了一个严重的问题,那就是可能形成递归定义,如:
CFLAGS = $(CFLAGS) -O
A = $(B)
B = $(A)
这相当于形成了循环依赖,make将会报错。另外如果在变量中使用shell、wildcard等函数时,使用=赋值这种方式效率很低,并且可能发生不可预知的错误,因为这些函数调用将会因为递归调用而不可控。因此,=赋值在makefile中又被称为递归展开式赋值,赋值语句可能影响多个变量,所有定值变量相关的其他定值变量都受影响。
为了避免上面的这些情况,可以使用make中的:=操作符来替代递归展开定义变量,使用方法如:
x := foo
y := $(x) bar
x := later
# 其等价于:
# y := foo bar
# x := later
反之,如果是这样的话:
y := $(x) bar
x := foo
此时y的值是bar,而不是foo bar,也就是说并不是与最终的赋值相同(变量的值在程序中不同位置是不同的)。:=与c/c++等语言中常规理解的赋值方式是一致的,只对赋值语句之后的变量有效,这种赋值方式又称为直接展开式或覆盖式赋值。
一个特别的用法是借助:=定义一个空字符变量:
nullstring :=
space := $(nullstring) # end of the line
此例中nullstring是一个空变量,其中什么也没有,而space值是一个空格。因为在操作符的右边是很难描述一个空格的(make会默认删除),因此先用一个空变量来标明变量的值,而后面采用#注释符来表示变量定义的终止,这样就可以定义出值是一个空格的变量(这是我自己测试的结果,推测可能是跟c语言注释一样被转换为一个空格了)。注意这里关于#的使用,如果这样定义一个变量:
dir := /foo/bar # directory to put the frobs in
dir这个变量的值是/foo/bar后面还跟个空格,如果使用这样的变量$(dir)/file来指定目录那么是错误的,因此在赋值语句时一定不要在同行注释前加上空格(除非想在变量值中增加空字符)。
此外还有两种常用的赋值操作符是?=和+=。?=表示条件赋值,如果变量未定义则使用符号中的值定义变量,如果该变量已经赋值(只要是显式的赋值就算,哪怕是空串)则该赋值语句无效,如:
FOO ?= bar
# 实际上这条语句等价于:
# ifeq ($(origin FOO), undefined)
# FOO = bar
# endif
+=表示追加赋值,被赋值的变量用空格隔开的方式在原本值后面追加一个新值,因此:
objects = main.o foo.o bar.o utils.o
objects += another.o
# 等价于
# objects = main.o foo.o bar.o utils.o another.o
需要注意的是:
- 如果变量之前没有定义过,那么+=会自动变成=
- 如果前面变量上次被=或?=赋值过,那么+=会以=的方式(可以由之后的值在此展开)在后面追加内容,并且+=的结果可以作为递归赋值的在所有引用处展开
- 如果前面变量上次被:=赋值过,那么+=会以:=的方式(直接用变量当前值展开)在后面追加内容
下面是一个例子:
variable1 := $(variable3)value1
variable1 += $(variable3)
variable2 = $(variable3)
variable2 ?= another
variable2 += $(variable3)
variable3 += value3
variable3 = more
variable3 += value3
test:
@echo $(variable1)
@echo $(variable2)
@echo $(variable3)
# 输出结果为:
#value1
more value3 more value3
more value3
# 因此这段makefile等价于:
# variable1 := value1 此时$(variable3)为空串
# variable1 := value1 +=在此处追加一个空串$(variable3)
# variable2 = $(variable3) $(variable3) variable2上次是?=赋值,所以variable2此时值为$(variable3),+=按递归的方式再追加一个$(variable3)
# variable3 = more variable3先被+=赋值为value3,=将之前+=的值覆盖为more
# variable3 = mare value3 variable3在上一步的基础上追加一个value3
变量替换引用
在makefile中可以替换变量中共有的部分,其格式为:
$(var:a=b) 或是 ${var:a=b}
上面的意思是把变量var中所有以a字符为后缀的字串替换为b结尾。结尾的含义是空格(一般Tab也算)之前(变量值多个字之间使用空格分开)。而对于变量其它部分的a字符不进行替
换,比如:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
此例中先定义了一个$(foo)变量,第二行的意思是把$(foo)中所有以.o字串结尾全部替换成.c,所以$(bar)的值就是a.c b.c c.c。注意括号中的变量使用的是变量名而不是变量名的引用,变量名的后面要使用冒号和参数选项分开,表达式中间不能使用空格。另外一种常用的变量替换是与模式字符结合起来使用(形式与规则目标依赖关系中的静态模式相同),如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
这个例子中加不加%都是一样的,不过加上%在这种替换中能起到的作用并不仅于此。实际中对变量值的操作往往不只是修改后缀字符,可能改变字串中的不同位置的字符,比如:(加上%还可以匹配前缀)
foo:=a123c a1234c a12345c
obj1=$(foo:a%c=x%y)
obj2=$(foo:a%c=x5%y)
obj3=$(foo:a1%c=x%5y)
obj4=$(foo:a%5c=x5%y)
obj5=$(foo:a%2%c=x5%3%y)
All:
@cat makefile
@echo $(obj1) # 输出x123y x1234y x12345y
@echo $(obj2) # 输出x123y x1234y x12345y
@echo $(obj3) # 输出x5123y x51234y x512345y
@echo $(obj4) # 输出a123c a1234c x51234y
@echo $(obj5) # 输出a123c a1234c a12345c(我自己的测试结果,看来一个模式中不能有多个\% \?)
可以看到
实际上,变量的替换引用其实是函数patsubst的一个简化实现,在make中两者的行为是一致的。
变量嵌套引用
变量嵌套引用可以理解为“把变量的值再当成变量”,先看一个例子:
x = y
y = z
a := $($(x))
此例中,$(x)的值是y,所以$($(x))就是$(y),于是$(a)的值就是z,注意这里是x=y而不是x=$(y)。使用更多层次进行嵌套也是一样的,比如:
x = $(y)
y = z
z = u
u = Hello
a := $($($(x)))#输出为Hello
这里的$($($(x)))被替换成了$($(z)),因为$(z)值是u,所以最终结果是:a:=$(u),也就是Hello。使用变量时并不是只能引用一个变量,可以有多个变量的引用以及任意的文本字符,最后变量的引用是其中所有字符展开后的结果,比如:
first_second = Hello
a = first
b = second
all = $($a_$b)
上例中$a_$b组成了first_second,于是$(all)的值就是Hello。因为展开后的字串是变量引用的形式,所以这种嵌套引用同样可以用在表达式左值:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
上例中定义了三个变量:dir、foo_sources和foo_print。
要注意区分变量的嵌套引用和变量的递归赋值:
- 嵌套引用是用一个变量表示另外一个变量,然后进行多层的引用
- 递归展开的变量表示当一个变量存在对其它变量的引用时,对此变量的替换方式
- 两种方式是完全可以混用的,比如可以在定义个一个递归展开式变量时使用套嵌引用的方式
实际使用应该尽量避免过多使用变量嵌套引用,在必须要使用的时候应该做到嵌套层数越少越好,因为使用这种方法表达会比较复杂,可读性不好。
环境变量
make运行时的系统环境变量可以在make开始运行时被载入到makefile文件中,但是如果makefile中已定义了这个变量,或这个变量由make在命令行中带入,那么系统的环境变量的值将被覆盖(除非make指定了-e参数(在实现上类似于在命令中设置环境变量的值),那么系统环境变量值将覆盖makefile中定义的变量)。比如当在不同的makefile中指定一部分统一的编译选项(或者说命令行参数),那么只要设置一个环境变量就可以在读取的所有makefile中默认使用这个变量,在make中已经分别用CFLAGS和LDFLAGS定义了编译选项(-c、-o、-I、-S等)和链接选项(-L、-static等),如:
CFLAGS = -g -I./include
LDFLAGS = -L./lib
ALL:
$(CC) $(CFLAGS) $(LDFLAGS) main.c
如果makefile中定义了CFLAGS和LDFLAGS,那么则会使用makefile中的这些变量,如果没有定义则使用此时系统环境变量CFLAGS的值。当make嵌套调用时,上层makefile中定义的环境变量会传递到下层的makefile中,默认情况下只有通过这种方式或者在make命令中设置的变量会被传递,而定义在文件中的变量,如果要向下层makefile传递,则需要使用export关键字来声明。但是不推荐使用环境变量的方式(注意make做的只是引用并在局部覆盖环境变量,如果要修改或添加环境变量的话是Shell才能做的事)来完成普通变量的工作,特别是在make的嵌套调用中,任何一个环境变量的错误定义都对系统上的所有make产生影响,甚至是毁坏性的。因为环境变量具有全局的特征,所以尽量不要污染环境变量,大多数的合格系统管理员都应该明白环境变量对系统是多么的重要。
override指示符
如果变量是在make命令参数中设置的,那么makefile对这个变量的赋值会被忽略。实际上从这里就可以看出,虽然环境变量和make命令中的变量都会从外部向makefile中引入变量,但两者的行为是有差异的:
- 环境变量是把已经定义好的值引入到makefile中,如果makefile中存在同名变量则会在局部将其覆盖
- make命令中设置的变量是将makefile中的同名变量用其值直接替换
如果想在makefile中设置这类参数的值,或者说不希望makefile的变量定义被替代,那么可以在makefile中使用指示符override来对这个变量进行声明,其语法是:
override <variable> = <value>
override <variable> := <value>
override <variable> += <more text>
从另外一个角度来说,override是实现了在makefile中增加或者修改命令行参数的一种机制。通常可能会有这样的需求:通过命令行来指定一些附加的编译参数,对一些通用参数或者必需的编译参数在makefile中指定,而在命令中指定一些特殊的参数,对于这种需求就可以使用指示符override来实现(通用的直接写在命令中,特殊的在makefile中追加),如:
EXEF = foo
override CFLAGS += -Wall –g
...
$(EXEF) : foo.c
$(CC) $(CFLAGS) $(addsuffix .c,$@) –o $@
假设一般情况下默认以-O2优化级别进行编译,那么执行makefile时只需用:
make CFLAGS=-O2
此时既可以-O2优化级别进行编译,同时又加入了该makefile目标文件编译时的特殊要求-Wall -g。
目标指定变量
正常在makefile中定义一个变量,那么这个变量对此makefile的所有规则都是有效的,在整个文件都可以访问这些变量,它就像是一个“全局变量”(仅限于定义那个makefile中的所有规则,如果需要对其它的makefile有效,需要使用export声明),类似于c语言中外部静态变量,使用static声明的全局变量)。不过在makefile中同样可以为某个目标设置局部变量,这种变量被称为目标指定变量,可以和“全局变量”同名,因为它的作用范围只在指定它的规则的上下文中有效,不会影响规则链以外的全局变量的值。其语法是:
<target …> : <variable-assignment>
<target …> : override <variable-assignment>
<variable-assignment>可以是任何有效的赋值表达式。这个特性非常有用,比如设置了这样一个变量,这个变量可以作用到由这个目标所引发的所有的规则中去:
prog : CFLAGS = -g
bar : CFLAGS = -Wall -g
prog : prog.o foo.o
$(CC) $(CFLAGS) prog.o foo.o
prog.o : prog.c
$(CC) $(CFLAGS) prog.c
foo.o : foo.c
$(CC) $(CFLAGS) foo.c
bar : bar.o
$(CC) $(CFLAGS) bar.o
bar.o : bar.c
$(CC) $(CFLAGS) bar.c
此例中不管全局的$(CFLAGS)值是什么,在prog目标文件以及其所引发的所有规则中(prog.o foo.o的规则),$(CFLAGS)值都是-g,而bar目标文件所引发的规则中$(CFLAGS)值为-Wall -g。
模式指定变量
makefile中的规则可以使用模式字符以匹配任何非空字符串,模式字符%也可以用在目标指定变量中,此时称为模式指定变量。模式指定变量的好处在于可以给定一种模式,把变量定义在符合这种模式的所有目标上。模式指定变量的语法与目标指定变量一致,唯一区别是这里的目标是一个或者多个模式目标,比如可以如下方式给所有以[.o]结尾的目标文件定义目标变量:
%.o : CFLAGS = -O
对于同一个目标指定变量,如果使用追加方式赋值,它的局部变量值是:为所有规则定义的全局值+引发它所在规则被执行的目标所指定的值+它所符合的模式指定值+此目标所指定的值。
多行变量
还有一种设置变量值的方法是使用define关键字,使用define关键字设置变量值可以包含换行符,这样打包起来的变量可以称为宏包,命令包的实现其实就是一个这样的变量。define指示符后面跟的是变量的名字,然后重起一行定义变量的值,定义是以endef关键字结束,其工作方式和=操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。因为命令需要以[Tab]键开头,所以用define定义的多行变量中没有以[Tab]键开头,那么make就不会把其认为是命令,下面的这个示例展示了define的用法:
define two-lines
echo foo
echo $(bar)
endef
对于多行变量定义,同样可以使用override指示符,如:
override define foo
bar
endef
隐含变量
在makefile内嵌隐含规则的命令中所使用的变量都是预定义的变量,将这些变量称为隐含变量。允许对这些变量进行修改,无论是用那种定义方式,只要make在运行时它的定义有效,make的隐含规则都会使用这些变量。当然,也可以使用-R或–nobuiltin-variables选项来取消定义所有的隐含变量(同时将取消使用所有的隐含规则)。例如编译.c源文件的隐含规则为
$(CC) -c $(CFLAGS) $(CPPFLAGS)
.c文件默认的编译命令是cc –c,此时假如采用任何一种赋值方式将变量CC定义为ncc,那么上例中编译.c源文件所执行的命令将是ncc -c,如果对这些变量重定义后需要在整个项目的各个子目录都有效,同样需要使用关键字export将其导出,否则目录间编译命令可能出现不一致。
编译.c源文件时,隐含规则使用“(CFLAGS)”引用编译选项