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

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

makefile条件判断

makefile的条件语句可以根据一个变量的值来控制make执行或者忽略makefile的特定部分,使用条件控制可以增加处理的灵活性和高效性。条件语句可以是两个不同变量、或者变量和常量值的比较。要注意的是:条件语句只能用于控制make实际执行的makefile文件部分,无法控制规则的shell命令执行流程。

下面还是以一个简单的例子开始:判断$(CC)变量是否为gcc,如果是的话则使用GNU函数编译目标

libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
else
    $(CC) -o foo $(objects) $(normal_libs)
endif

在上例的这个规则中目标foo可以根据变量$(CC)值来选取不同的函数库来编译程序。可以从上面的示例中看到三个关键字:ifeq、else和endif,其中:

  • ifeq表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起
  • else表示条件表达式为假的情况
  • endif表示一个条件语句的结束,任何一个条件语句块都应该以endif结束

makefile中条件表达式工作于文本级别(条件判断处理为文本级别的处理过程),条件的解析是由make来完成的。make是在读取并解析makefile时根据条件表达式忽略条件表达式中的某一个文本行(这句话是GNU make中文手册中的,所以说makefile的解析可能不是跳转指令,而是直接将文本行忽略?),解析完成后保留的只有表达式满足条件所需要执行的文本行。上例还有一种更简洁实现方式:(虽然我也没看出哪里简洁)

libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
    $(CC) -o foo $(objects) $(libs)

makefile条件判断的基本语法

一个简单的不包含else分支的条件判断语句的语法格式为:

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
endif

首先看条件判断后对应的执行语句部分,表达式中TEXT-IF-TRUE可以是若干任何文本行,当条件为真时它将被make作为需要执行的一部分,当条件为假时不作为需要执行的一部分。

包含else的语法格式为:

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif

和TEXT-IF-TRUE一样,TEXT-IF-FALSE可以是若干任何文本行。注意两个分支的情况只是基本情况,实际上条件判断的执行文本中也可包含条件判断语句(也就是说条件语句是可以嵌套的),从而达成多分支的效果,但是也要注意makefile是不支持elif类型的关键字的,所以需要在else后面再加上CONDITIONAL-DIRECTIVE。下面是一个简单的例子:

TARGET_ARCH=x86
ifeq ($(TARGET_ARCH), arm)
LOCAL_SRC_FILES := a
else ifeq ($(TARGET_ARCH), x86)
LOCAL_SRC_FILES := b
ifdef LOCAL_SRC_FILES
TARGET_ARCH=mips
endif                        #注意每个独立的条件判读语句块必须以endif结束
else ifeq ($(TARGET_ARCH), mips)   
LOCAL_SRC_FILES := c
else
LOCAL_SRC_FILES := d
endif
test:
    @echo $(TARGET_ARCH)     #输出mips
    @echo $(LOCAL_SRC_FILES) #输出b

注意else CONDITIONAL-DIRECTIVE实际上也算是makefile中条件判断的一个结构(只不过不是所有的版本都支持),因此可以和整个条件判断语句块共享一个endif,如果碰到不支持else CONDITIONAL-DIRECTIVE类型的make时,只要在后面单独加上一个endif表示一个单独的条件语句块就可以了。

条件判断关键字

条件判断表达式CONDITIONAL-DIRECTIVE对于两种基本格式都是同样的,其本身可以是下面四种用于测试不同条件的关键字。
ifeq关键字用来判断参数是否相等,格式如下:(ARG可以是任何变量值、常量、函数返回值结合起来的表达式文本)

ifeq (ARG1, ARG2)
ifeq ‘ARG1’ ‘ARG2’
ifeq “ARG1” “ARG2”
ifeq “ARG1” ‘ARG2’
ifeq ‘ARG1’ “ARG2”

ifeq条件判断表达式替换展开ARG1和ARG2后,对它们的值进行比较。如果相同则(条件为真)将TEXT-IF-TRUE作为make要执行的一部分,否则将TEXT-IF-FALSE(如果有的话)作为make要执行的一部分.

关键字ifneq实现的条件判断语句和ifeq的形式完全相同但意义相反,此关键字用来判断参数是否不相同,如果不同则为真。同样的条件为真时将执行TEXT-IF-TRUE控制流,反之执行TEXT-IF-FALSE控制流。

关键字ifdef用来判断一个变量是否已经定义,格式为:

ifdef VARIABLE-NAME

如果变量VAEIABLE_NAME的值非空(在makefile中只有未定义的变量以及使用未定义变量赋值的变量值为空),那么表达式为真,将TEXT-IF-TRUE作为make要执行的一部分,否则表达式为假执行TEXT-IF-FALSE。对于ifdef关键字需要说明的是:ifdef只是测试一个变量当前是否已经定义,不是对变量进行替换展开来判断变量的值是否为空

  • 条件判断的执行可能在变量展开(比如递归展开)之前,一定在规则执行之前,因此模式字符和自动化变量在条件判断中都是使用不了的
  • 不能将完整的条件判断语句分写在两个不同的makefile文件中(虽然都这么说但我也不知道为什么不能,理论上说include文件展开是发生在变量初始化、函数展开和条件分支判断以前的)

对于变量VARIABLE-NAME,除了未定义的VARIABLE-NAME或者没有给出任何初始化值的定义,使用其它方式对它的定义都会使ifdef条件判断表达式返回真。也就是说即使通过其它方式给它赋了一个空值,ifdef也会返回真,比如:

foo =
bar = $(foo)
ifdef foo
frobozz1 = yes
else
frobozz1 = no
endif
ifdef bar
frobozz2 = yes
else
frobozz2 = no
endif
test:
    @echo $(frobozz1) $(frobozz2) #输出no yes

从这里也可以看出,当需要判断一个变量的值是否为空时,需要使用ifeq而不是ifdef。关键字ifndef和ifdef格式完全相同,实现的功能相反。
在CONDITIONAL-DIRECTIVE这一行上,多余的空格是被允许的,但是不能以[Tab]键做为开始(不然就被认为是命令),而注释符“#”同样也是安全的,包括else和endif也一样,只要不是以[Tab]键开始就行。

此外,makefile虽然没有提供逻辑或和逻辑与运算符,但可以借助于其内嵌函数和字符串拼接来实现相同的功能,比如如果让ifeq(ARG1, ARG2)和ifeq(ARG3, ARG4)达成逻辑与的关系,可以这样:

ifeq(ARG1_ARG3,ARG2_ARG4)

这样只有ARG1和ARG2、ARG3和ARG4同时相等时才为真,_字符起到分隔两个字串的作用,可以是任何一定不存在于两个字串中的字符。如果是ifdef条件判断的话,由于是指定变量名的方式进行判断,因此采用变量引用和函数的方式比较难以实现逻辑运算,不过可以通过条件语句嵌套的方式实现:

ifdef VARIABLE-NAME1
ifdef VARIABLE-NAME2
TEXT-IF-TRUE
endif
else
TEXT-IF-FALSE
endif

这样只有VARIABLE-NAME1和VARIABLE-NAME2同时为真才能执行true控制流,否则执行false控制流。如果想实现逻辑或关系的话可以像这样:

ifeq($(filter-out(ARG1 ARG3,ARG2 ARG4)),)
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif

函数filter-out会过滤掉字串ARG2 ARG4中满足模式ARG1和ARG3的字串,如果返回空串说明ARG2和ARG4一定符合模式ARG1和ARG3的其中一个。但是这种方法没有考虑的特殊情况是ARG1等于ARG4,ARG3等于ARG2的情况,所以只有在确定两组比较条件不等的情况才能使用这种方法。如果两组条件中的有相同的字串,此时条件判断语句可以这样:

ifeq($(findstring(ARG1 ARG2_ARG3)),)

最后,ifdef条件判断可以这样实现逻辑或(采用类似于elif的结构):

ifdef VARIABLE-NAME1
TEXT-IF-TRUE
else ifdef VARIABLE-NAME2
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif
endif

ifneq和ifndef实现逻辑运算的方法与ifeq和ifdef原理相同,只不过实现的功能相反。当然实际上除此外还有很多手段实现makefile的逻辑运算,比如可以通过shell函数内嵌Shell脚本来处理条件,部分make工具还支持and和or函数来支持逻辑运算。

makefile函数

makefile函数提供了处理文件名、变量、文本和命令等等的方法。使用函数的makefile可以编写的更加灵活和健壮。可以在需要的地方地调用函数来处理指定文本(需要处理的文本作为函数的参数),函数调用在调用的地方被替换为它的处理结果。函数调用(引用)的展开和变量引用展开的行为基本是相同的,而且是同步进行的。

makefile函数的基本语法

函数调用的方法很像变量值引用的方法,也是以$来标识,其语法如下:

$(FUNCTION ARGUMENTS)或${FUNCTION ARGUMENTS}

这里FUNCTION就是函数名,ARGUMENTS是函数的参数,参数间以逗号,分隔,而函数名和参数之间以空格分隔。函数调用以$开头,以圆括号或花括号把函数名和参数括起,感觉上很像一个变量。函数中的参数可以使用变量引用,为了风格的统一,函数和变量的括号最好一样,如使用:

$(subst a,b,$(x))

这样的形式,而不是:

$(subst a,b,${x})”

虽然实际功能都一样,但良好的编写风格能够增加可读性。此外推荐在变量引用和函数引用中统一使用圆括号,这样在使用vim编写makefile时可以高亮显示make的内嵌函数。函数处理参数时如果参数中存在对其它变量或者函数的引用,首先对这些引用展开得到参数的实际内容后再进行处理,参数的展开顺序是按照参数的先后顺序进行的。参数中尽量不要出现逗号,和空格,这是因为逗号会被作为多个参数的分隔符,并且此时,的前导空格(试了下好像Tab也忽略)很多时候会被忽略,因此当有逗号或空格作为函数参数时,需要把它们赋值给一个变量,通过在函数的参数中引用这个变量来实现,例:

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar1:= $(subst $(space),$(comma),$(foo)i)
bar2:= $(subst  ,$(comma),$(foo)i)
bar3:= $(subst a,$(comma),$(foo)i)
bar4:= $(subst $(space),,,$(foo)i)
bar5:= $(subst  $(space),$(comma) ,$(foo)i)
bar6:= $(subst $(space), ,$(foo)i)
test:
	@cat makefile
	@echo $(bar1) #将$(foo)中,换成空格的标准写法,输出结果为a,b,ci
	@echo $(bar2) #把$(space)直接写成空格则接收第一个参数时会被忽略,此时第一个参数为空字符(默认在结尾),输出结果为a b ci,
	@echo $(bar3) #如果是正常字符的可以直接替换,输出结果为,b ci
	@echo $(bar4) #同理把$(comma)写成,则被解析为分隔符,因为subst只有三个接收参数,因此第三个,被当作第三个参数的一部分,输出结果为,abci
	@echo $(bar5) #首先在没有读入$(space)前会删去空白符,因此$(space)前面的空白符被删去;其次在第二个参数已经读入$(comma)以后,
                  #后面的空格符被当作是参数的一部分,加在$(comma)后面,因此最终输出为a, b, ci
    @echo $(bar6) #实际上此例中将空格直接作为第二个参数也是可以接收到的,subst函数做的事首先是把第三个要处理的TEXT文本中首尾空字符去掉
                  #然后将作为函数名和参数间分隔符的空格去掉,而第二个参数夹在中间,因此它是可以接收到空格的

makefile常用函数

makefile文本处理函数

常用的内嵌文本处理函数有以下11个函数。

字符串替换函数subst

  • $(subst FROM,TO,TEXT)
  • 功能:把字串TEXT中的FROM字符串替换成TO
  • 返回:替换过后的字符串
  • 示例:
    • $(subst ee,EE,feet on the street)
    • 把feet on the street中的ee替换成EE,返回结果是fEEt on the strEEt

模式字符串替换函数patsubst

  • $(patsubst PATTERN,REPLACEMENT,TEXT)
  • 功能:搜索TEXT中以空格分开的单词,将否符合模式TATTERN的单词替换为REPLACEMENT。参数PATTERN中可以使用模式通配符%表示任意长度的字串,如果REPLACEMENT中也包含%,那么REPLACEMENT中的这个%将是PATTERN中的那个%所代表的字串
  • 返回:替换过后的字符串
  • 说明:参数TEXT的单词之间的多个空格在处理时被合并为一个空格,并忽略前导和结尾空格
  • 示例:
    • $(patsubst %.c,%.o,x.c.c bar.c)
    • 把字串x.c.c bar.c符合模式[%.c]的单词替换成[%.o],返回结果是x.c.o bar.o
    • 实际上变量的替换引用就是靠patsubst函数实现的,$(VAR:PATTERN=REPLACEMENT)实际就等价于$(patsubst PATTERN,REPLACEMENT,$(VAR))
  • 如果PATTERN中没有使用%,则在实现时自动加上,也就是$(VAR::SUFFIX=REPLACEMENT)等价于$(patsubst %SUFFIX,%REPLACEMENT,$(VAR))
  • 比如:$(objects:.o=.c)等价于$(patsubst %.o,%.c,$(objects))

去空格函数strip

  • $(strip STRINT)
  • 功能:去掉STRINT字串中开头和结尾的空字符
  • 返回:去掉空格的字符串值
  • 说明:空字符包括空格、[Tab]等不可显示字符
  • 示例:
    • $(strip a b c )
    • 把字串a b c 去开头和结尾的空格,结果是a b c(这么说的话难道说lexer调用的就是这个函数?)
    • strip函数经常用在条件判断语句的表达式中,确保表达式比较的可靠和健壮

查找字符串函数findstring

  • $(findstring FIND,IN)
  • 功能:在字串IN中查找FIND字串
  • 返回:如果找到则返回FIND,否则返回空字符串
  • 说明:IN中可以包含空格、[Tab],搜索需要是严格的文本匹配
  • 示例:
    • $(findstring a,a b c)返回a字符
    • $(findstring a,b c)返回“”字符串(空字符串)

过滤函数filter

  • $(filter PATTERN…,TEXT)
  • 功能:以PATTERN模式过滤TEXT中的单词,保留符合模式PATTERN的单词,可以有多个PATTERN,PATTERN之间使用空格分隔
  • 返回:符合模式PATTERN的字串
  • 示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
    cc $(filter %.c %.s,$(sources)) -o foo
# $(filter %.c %.s,$(sources))返回值为foo.c bar.c baz.s

反过滤函数filter-out

  • $(filter-out PATTERN…,TEXT)
  • 功能:过滤掉TEXT中所有符合模式PATTERN的单词,保留不符合此模式的单词,可以有多个PATTERN,PATTERN之间使用空格分隔
  • 返回:不符合模式PATTERN的字串。
  • 示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
# $(filter-out $(mains),$(objects))返回值是foo.o bar.o

排序函数sort

  • $(sort LIST)
  • 功能:给字串LIST中的单词以首字母为准按升序进行排序,去掉重复的单词
  • 返回:排序后的空格分割的没有重复单词的字串
  • 示例:
    • $(sort foo bar lose)返回bar foo lose

取单词函数word

  • $(word N,TEXT)
  • 功能:取字符串TEXT中第N个单词,N的值从1开始
  • 返回:字串TEXT中第N个单词
  • 说明:如果N值大于字串TEXT中单词的数目,返回空字符串,如果N为0则出错
  • 示例:
    • $(word 2, foo bar baz)返回值是bar

取字串函数wordlist。

  • $(wordlist S,E,TEXT)
  • 功能:从字串TEXT中取出从S开始到E的单词串,S和E表示单词在字串中的下标+1,都是从1开始的数字
  • 返回:字串TEXT中从第S到E(包括E)的单词字串
  • 说明:当S比TEXT中的单词数大时返回空串,如果E大于TEXT单词数,返回从S开始到TEXT结束的单词串,如果S大于E,返回空串,如果S或E为0错误
  • 示例:
    • $(wordlist 2, 3, foo bar baz)返回值是bar baz

统计单词数目函数words

  • $(words TEXT)
  • 功能:统计TEXT中的单词个数
  • 返回:TEXT中的单词数
  • 示例:
    • $(words, foo bar baz)返回值是3
    • 如果要取TEXT中最后的一个单词可以这样:$(word $(words TEXT),TEXT)

取首单词函数firstword

  • $(firstword TEXT)
  • 功能:取字串TEXT中的第一个单词
  • 返回:字串TEXT的第一个单词
  • 示例:
    • $(firstword foo bar)返回值是foo
    • 这个函数的实现应该是$(word 1,TEXT)

一个简单的应用:用VPATH中的值生成CFLAGS的查找路径

VPATH=src:../includes
override CFLAGS+=$(patsubst %,-I%,$(subst :, ,$(VPATH)))

其中$(subst :, ,$(VPATH))返回src… /includes,$(patsubst %,-I%,src… /includes)返回-Isrc -I…/inludes,也就是gcc指定头文件路径的编译参数的形式。

makefile文件名操作函数

makefile支持一些针对于文件名的处理函数,这些函数主要用来对一系列空格分割的文件名进行转换,参数被作为若干个文件名进行处理并返回空格分隔的多个文件名序列。由于这类函数操作的对象是文件名,因此在参数首尾的空白符都要删掉。

取目录函数dir

  • $(dir NAMES…)
  • 功能:从文件名序列NAMES…中取出目录部分,目录部分是指最后一个反斜杠/之前的部分,如果没有反斜杠,那么返回./
  • 返回:文件名序列NAMES…的目录部分(包括最后一个/)
  • 示例:
    • $(dir src/foo.c hacks)返回值是src/ ./

取文件名函数notdir

  • $(notdir NAMES…)
  • 功能:从文件名序列NAMES…中取出非目录部分,非目录部分是指最后一个反斜杠/之后的部分
  • 返回:返回文件名序列NAMES…的非目录部分(不包括最后一个/)
  • 示例:
    • $(notdir src/foo.c hacks)返回值是foo.c hacks

取后缀函数suffix

  • $(suffix NAMES…)
  • 功能:从文件名序列NAMES…中取出各个文件名的后缀,后缀是文件名中最后一个以点.开始的部分(包含点号)
  • 返回:文件名序列NAMES…的后缀序列,如果文件没有后缀则返回空字串
  • 示例:
    • $(suffix src/foo.c src-1.0/bar.c hacks)返回值是.c .c

取前缀函数basename

  • $(basename NAMES…)
  • 功能:从文件名序列NAMES…中取出各个文件名的前缀部分,前缀部分指的是文件名中最后一个点号之前的部分
  • 返回:文件名序列NAMES…的前缀序列,如果文件没有前缀则返回空字串
  • 示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是src/foo src-1.0/bar hacks

加后缀函数addsuffix

  • $(addsuffix SUFFIX,NAMES…)
  • 功能:把后缀SUFFIX加到NAMES…中的每个单词后面
  • 返回:加过后缀的文件名序列
  • 示例:
    • $(addsuffix .c,foo bar)返回值是foo.c bar.c

加前缀函数addprefix

  • $(addprefix PREFIX,NAMES…)
  • 功能:把前缀PREFIX加到NAMES…中的每个单词前面
  • 返回:返回加过前缀的文件名序列
  • 示例:
    • $(addprefix src/,foo bar)返回值是src/foo src/bar

连接函数join

  • $(join LIST1,LIST2)
  • 功能:把LIST2中的单词对应地加到LIST1的单词后面,比如将LIST2中的第一个单词追加LIST1第一个单词字后合并为一个单词,LIST2第二个单词追加到LIST第二个单词后……以此类推
  • 返回:单空格分隔的合并后的文件名序列
  • 说明:如果LIST1和LIST2中的文件名数目不一致时,两者中多余部分将被作为返回序列的一部分(类似于归并)
  • 示例:
    • $(join aaa bbb , 111 222 333)返回值是aaa111 bbb222 333

获取匹配模式文件名函数wildcard

  • $(wildcard PATTERN)
  • 功能:列出当前目录下所有符合模式PATTERN(注意wildcard的模式一般指使用通配符)格式的文件名
  • 返回:空格分割的、存在于当前目录下的所有符合模式PATTERN的文件名
  • 说明:在makefile变量的定义和函数引用时,通配符将失效,这种情况下如果需要通配符有效就需要使用函数wildcard
  • 示例:
    • $(wildcard *.c)返回值为当前目录下所有.c源文件列表

makefile控制函数

makefile提供了两个控制make运行方式的函数,使用它们时当make执行过程中检测到某些错误的话可以为用户提供消息,并且可以控制执行过程是否继续。

错误函数error

  • $(error TEXT…)
  • 功能:error函数展开时程序产生错误(可能类似于捕获错误?),提示TEXT…信息给用户并退出执行
  • 返回:空
  • 说明:error函数是在被调用时才提示信息并结束make进程,因此如果error出现在命令中或者一个递归的变量定义中时,则在读取makefile时不会出现错误(最好是不要在读取时就报错,因此建议其中的变量不要使用直接展开定义),此时只有包含error函数引用的命令被执行或者定义中引用此函数的递归变量被展开时,才会提示错误信息TEXT…同时退出make进程。
  • 示例:
ifdef ERROR_001    
$(error error is $(ERROR_001)) #如果变量ERROR_001定义则执行时产生error调用
endif

警告函数warning

  • $(warning TEXT…)
  • 功能:类似于函数error,区别在于它不会导致make退出,只是提示TEXT…
  • 返回:空

makefile的特殊函数

下面是一些特殊的makefile内嵌函数,它们在用法、功能、参数格式等方面与其他函数有较大的区别。

遍历函数foreach

foreach函数和别的函数非常的不一样,因为这个函数是用来做循环控制流用的。makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的,它的语法是:

$(foreach VAR,LIST,TEXT)

这个函数的工作过程是这样的:

  • 如果存在变量或者函数的引用,首先展开变量VAR和LIST的引用,而TEXT中的变量引用不展开
  • 执行时把LIST中使用空格分隔的单词依次取出赋值给变量VAR
  • 执行TEXT表达式,返回一个字符串
  • 重复执行直到LIST的最后一个单词为空时结束
  • TEXT所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值

需要注意的是,TEXT中的变量或者函数引用在执行时才被展开,因此如果在TEXT中存在对VAR的引用,那么VAR的值在每一次展开时将会到得到不同的值,同时VAR参数是一个临时的局部变量,当foreach函数执行完后,参数VAR的变量将不在作用,其作用域只在foreach函数当中。一般在foreach函数时,VAR最好是一个变量名(虽然VAR在foreach函数中扮演的角色更像是一个中间变量或者“容器”,但还是建议命名尽量见名知义),LIST可以是一个表达式,而TEXT中一般会使用VAR这个参数来依次枚举LIST中的单词,下面举例说明:

names := a b c d
files := $(foreach n,$(names),$(n).o)

上例中,$(name)中的单词会被挨个取出并依次赋值给变量n,$(n).o每次通过$(n)展开后得出一个单词,将所有单词以空格分隔,最后作为foreach函数的返回,所以$(files)的值是a.o b.o c.o d.o。当函数的TEXT表达式过于复杂时,可以通过定义一个中间变量代表表达式的一部分,并在TEXT中引用这个变量,比如:

find_files=$(wildcard $(dir)/*)
dirs:=a b c d
files:=$(foreach,dir,$(dirs),$(find_files))

需要注意,上例定义的是递归展开的变量find_files,保证了定义时变量值中的引用不展开,而是在表达式被函数处理时才展开(如果这里使用直接展开式的定义将是无效的表达式,因为find_files中需要dir的引用,而后者是foreach函数中的局部变量)。

条件判断函数if

if函数提供了一个在函数上下文中实现条件判断的功能,很像makefile所支持的条件语句——ifeq或ifdef,它的语法是:

$(if CONDITION,THEN-PART[,ELSE-PART])

if函数可以包含else部分或是不含,即if函数的参数可以是两个也可以是三个。CONDITION参数是if的表达式,如果为非空字符串则返回真。注意跟ifdef不一样,if函数无所谓变量是否定义,只要是完全由空字符组成就返回真。条件表达式CONDITION决定了函数的返回值只能是THEN-PART或ELSE-PART两个之一的计算结果,并且两者只会有一个被计算,当CONDITION返回真时则计算THEN-PART,否则计算ELSE-PART,最后的结果将被作为if函数的返回值,如果CONDITION为假(空字符串),并且此时ELSE-PART没有被定义那么函数返回空字串,比如:

OBJ:=foo.c
OBJ:=$(if $(OBJ),$(OBJ)) 此时OBJ返回值为foo.c,如果变量OBJ值为空的话则函数返回空字串

变量定义查询函数origin

origin函数不像其它的函数,它并不操作变量的值,只是告诉你你的这个变量是哪里来的,也就是变量的出处(定义方式),其语法是:

$(origin VARIABLE)

VARIBLE是一个变量名而不是变量引用,因此通常origin函数的VARIABLE中不包含$,除非此变量名是一个由变量引用拼接的变量名。origin函数会以其返回值来告诉这个变量的定义方式,下面是origin函数的返回值:

  • undefined:VARIABLE从来没有定义过,返回undefined
  • default:VARIABLE是一个默认的定义(内嵌变量),比如CC这个变量,如果在makefile中重新定义这些变量,其定义方式将相应发生变化
  • environment:VARIABLE是一个环境变量,并且当makefile被执行时,-e参数没有被指定
  • environment override:VARIABLE是一个系统环境变量,并且makefile中存在一个同名的变量定义,make执行时使用-e使环境变量值替代了文件中的变量定义
  • file:VARIABLE变量被定义在makefile中
  • command line:VARIABLE变量是在命令行定义的
  • override:VARIABLE是被override指示符重新定义的
  • automatic:VARIABLE是一个命令运行中的自动化变量

这些信息对于编写Makefile是非常有用的,例如,假设有一个makefile其包了一个定义文件Make.def,在Make.def中定义了一个变量bletch,而系统中也有一个环境变量bletch,假设此时想判断一下如果变量值来源于环境变量,那么就把其重定义了,如果来源于Make.def或是命令行等非默认环境的,那么就不重新定义它,于是在makefile中可以这样写:

ifdef bletch
ifeq “$(origin bletch)” “environment”
bletch = barf, gag, etc.
endif
endif

当然,直接使用override关键字同样可以重写变量,不过用override同时会把从命令行等其他地方定义的变量都覆盖掉,而采用origin函数可以结合条件判断进行更灵活的重定义。
(个人测试下觉得这个案例有问题:如果不指定-e那么环境变量本来就会被覆盖,如果指定-e不用override的话bletch那里是重写不了的)

自己定义函数引用函数call

call函数是唯一一个可以创建定制化参数函数引用的函数,使用这个函数可以实现对用户自己定义函数的引用。可以将一个变量定义为一个复杂的表达式,在表达式中可以定义许多参数,然后用call函数来向表达式传递参数,根据不同参数进行展开来获得不同的结果,其语法是:

$(call VARIABLE,PARAM1,PARAM2,…)

当make执行这个函数时,VARIABLE参数中的临时变量如$(1),$(2),$(3)…等(如果只有一位数字的话不加括号也可以,另外序号只表示参数的次序,并不一定与VARIABLE中的变量引用同名),会被参数PARAM1,PARAM2依次取代,而VARIABLE的返回值就是call函数返回值。同origin函数一样,call函数中VARIBLE也是变量名字而不是引用(call函数做的实际上就类似于使用参数对VARIABLE进行嵌套引用)。call函数对参数的数目没有限制,也可以没有参数值(当然没有参数值的call没有任何实际存在的意义),执行时变量VARIABLE中有效的临时变量在call函数上下文中被展开(同样的,VARIABLE必须用递归赋值,因为其一定会用到call函数内定义的临时局部变量),变量定义中的$(1)作为第一个参数,将call函数参数值中的第一个参数赋值给它,变量中的$(2)一样被赋值为call函数的第二个参数值,依此类推(变量$(0)代表变量VARIABLE本身),之后对变量VARIABLE表达式的计算值,得到函数返回结果,例如:

reverse = $(1) $(2)
foo = $(call reverse,a,b)
#此时foo的值就是a b

当然参数的次序是可以自定义的,不一定是数字序号的顺序,如:

reverse = $(2) $(1)
foo = $(call reverse,a,b)
#此时的foo的值就是b a

下面以一个例子来简要说明一下自定义函数的方法,假设需要定义一个函数pathsearch可以在PATH路径中搜索第一个指定的程序:
首先参考下PATH变量的格式:

发现路径之间使用:分隔,目录名后面不带/,因此首先可以对变量名进行一步处理:

dir_name=$(addsuffix /,$(subst :, ,$(PATH)))

这样使用PATH中的路径构造出了一个文件名序列。同时还需要让pathsearch能够接收call参数以指定路径下的程序名,因此可以在此基础上做下修改,在后面添加一个参数:

dir_name=$(addsuffix /$(1),$(subst :, ,$(PATH)))

接下来就可以完成pathsearch的定义,在工作目录匹配符合PATH路径的文件,取其中第一个:

pathsearch=$(firstword $(wildcard $(dir_name)))

通过call函数可以实现搜索制定程序路径的功能:

LS=$(call pathsearch,ls)

变量VARIBLE不仅可以是自定义函数引用,还可以是makefile的内嵌函数,此时call函数的PARAM参数会按顺序传递给内嵌函数的参数),不过这种情况对PARAM参数的使用需要注意,因为不合适或不正确的参数将会导致函数的返回值难以预料。函数call还可以套嵌使用,此时每一层call函数的调用都为它自己的局部变量$(1)等赋值,并且覆盖上一层函数为它所赋的值,比如:

map = $(foreach a,$(2),$(call $(1),$(a)))
o = $(call map,origin,o map MAKE)

此例中,$(call map,origin,o map MAKE)这个函数调用使用了变量map所定义的表达式,两个参数分别为origin函数调用和字串o map MAKE,首先进行第一次展开后外层call函数调用变为$(foreach a,o map MAKE,$(origin $(a))),此就变成了一个foreach函数调用,返回结果为file file default。

取变量文本值函数value

value函数提供了一种在不对变量进行展开的情况下获取变量值的方法,语法格式为:

$(value VARIABLE)

VARIABLE是变量名字使用value函数取这个变量进行取值时得到的是不包含任何引用的字面。注意这并不是说value函数会取消之前已经执行过的替换扩展,此时与变量定义方式是相关的,如果定义一个直接展开的变量,那么在定义过程中直接对其它变量的引用进行替换而得到自身的值,value函数取到的值是替换过后的变量值,但是如果定义的是一个递归展开,那么value取值时就不会展开,而是将定义的字面值返回,比如:

FO=$PATH
FOO:=P$(FO)
FOOO=$(echo 123)
all:
    @echo $(value FOO)   #输出PATH
    @echo $(value FOOO)  #输出123
# 首先FO=$PATH因为没加括号,所以FO相当于$(P)ATH等于ATH
# FOO:=P$(FO),定义时就已经展开为PATH,value返回字面值PATH
# FOOO=$(echo 123)在定义时不会展开,因此value返回未展开的字面值$(echo 123),相当于在Shell中执行echo 123

构造依赖关系链函数eval

eval其实在函数式语言里面很常见。LISP系语言的解释器,最终执行的是一个apply-eval递归,所以eval就是求值的意思。实际上不只是LISP,可以说任意解释器最终都是apply-eval递归,bash里面也有eval,只不过在LISP里面这种apply-eval通过其(语法…)形式,更加显式地表达出来了,所以LISP里的apply-eval也更著名。makefile中的eval函数是一个非常特殊的函数,使用它可以在makefile中构造一个可变的规则结构关系(依赖关系链),其中可以使用其它变量和函数。eval函数对它的参数进行展开,展开的结果作为makefile的一部分,make可以对展开内容进行语法解析,展开的结果可以包含一个新变量、目标、隐含规则或者是显式规则等。eval函数执行时会对它的参数进行两次展开,第一次展开过程是由函数本身完成的,第二次是函数展开后的结果被作为makefile内容时由make解析时展开的,所以基于这一点在eval函数的参数中存在的引用应该使用$$来代替$,并且经常使用函数value来取一个变量的文本值。语法格式如下:

$(eval TEXT)

eval函数非常常见的应用是对于自定义宏包的二次展开,下面是一个简单的例子用以说明:

###############################################
pointer := pointed_value
define foo
var := 123
arg := $1
$$($1) := ooooo
endef
#$(info $(call foo,pointer))
#$(eval $(call foo,pointer))
target:
        @echo -----------------------------
        @echo var: $(var), arg: $(arg)
        @echo pointer: $(pointer), pointed_value: $(pointed_value)
        @echo done.
        @echo -----------------------------
###############################################
#执行$(info $(call foo,pointer))的输出结果:   
#-----------------------------
#var: , arg:      
#从中可以看直接执行$(call foo,pointer)的话返回结果时对宏包foo进行第一次求值
#而foo求值的过程只是将其中的参数展开就结束了,返回的相当于是一个makefile的代码块,并没有执行
#pointer: pointed_value, pointed_value:
#done.
#-----------------------------
#执行$(eval $(call foo,pointer))的输出结果:
#-----------------------------
#var: 123, arg: pointer
#eval函数的作用就是进行二次展开,相当于将foo的结果在进行一次展开(求值),这时foo中的变量就被成功赋值了
#pointer: pointed_value, pointed_value: ooooo
#done.
#-----------------------------
###############################################

下面通过一个逻辑较完整的案例来说明eval函数的用法(来自GNU make手册):

# sample Makefile 
PROGRAMS = server client 
server_OBJS = server.o server_priv.o server_access.o 
server_LIBS = priv protocol  
client_OBJS = client.o client_api.o client_mem.o 
client_LIBS = protocol 
# Everything after this is generic 
.PHONY: all 
all: $(PROGRAMS) 
define PROGRAM_template 
$(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%) 
ALL_OBJS += $$($(1)_OBJS) 
endef 
$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog)))) 
$(PROGRAMS): 
    $(LINK.o) $^ $(LDLIBS) -o $@ 
clean: 
    rm -f $(ALL_OBJS) $(PROGRAMS)

其中$(LINK.o)为$(CC) $(LDFLAGS),对所有的.o文件和指定的库文件进行链接,这个makefile的关键点在于

$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))

可以二次展开(展开并执行)为:

server:$(server_OBJS) -l$(server_LIBS)
client:$(client_OBJS) -l$(client_LIBS)
ALL_OBJS=server_OBJS client_OBJS

shell函数

shell函数也不像其它的函数,顾名思义它的参数应该就是操作系统Shell的命令,它和Shell中的 反引号`是相同的功能(Shell先执行``中的命令,将结果暂时保存,在适当的地方输出)。shell函数把系统执行Shell命令后的输出作为函数返回,make仅对shell函数的返回结果进行处理,将函数返回结果中的所有换行符\n或者一对\n\r替换为单空格,去掉末尾的回车符号\n或者\n\r,进行函数展开时,它所调用的命令(参数)得到执行,比如:

contents := $(shell cat foo)
files := $(shell echo *.c)

注意,shell函数会新生成一个Shell进程来执行命令,所以要注意其运行性能,如果makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于系统性能是非常有害的,特别是makefile的隐晦的规则可能会让shell函数执行的次数完全不可控。当引用shell函数的中的变量定义使用直接展开(:=)定义时可以保证函数的展开是在make读入makefile时完成,后续对此变量的引用就不会有展开过程,这样就可以避免规则命令行中的变量引用在命令行执行时展开的情况发生,反之使用递归展开(=)定义其中变量的话会有变量引用在命令行执行时展开的情况发生,而展开shell函数需要另外的Shell进程完成,一定会影响命令的执行效率。