注:本系列内容大部分是基于GNU make的标准,其中cc命令默认调用Linux自带的c编译器程序

  1. makefile介绍
  2. makefile编写(1):规则
  3. makefile编写(2):变量
  4. makefile编写(3):条件执行和函数

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源文件时,隐含规则使用“(CC)(CC)”来引用编译器;“(CFLAGS)”引用编译选项