GNU Makefile函数式编程

最近两天在写一个包含多项目,多目标编译的makefile系统,经过研究android的编译系统,发现一个非常有价值函数式用法

立即展开与延后展开

实例

我们先来看个例子,如下文件组织

./
├── 1.mk
├── 2.mk
├── test1.c
├── test2.c
└── test3.c

怀着美好的愿望,写了1.mk

PHONY := all clean modules
CC := gcc
MODULES :=
all: modules

%.o : %.c
    $(CC) -c $< -o $@

LOCAL_MODULE:= test1
LOCAL_SRC_FILES:= test1.c
LOCAL_DPS := test2
MODULES += $(LOCAL_MODULE)
$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
    $(CC) $(LOCAL_SRC_FILES:%.c=%.o) -o $@

LOCAL_MODULE:= test2
LOCAL_SRC_FILES:= test2.c
LOCAL_DPS := test3
MODULES += $(LOCAL_MODULE)
$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
    $(CC)  $(LOCAL_SRC_FILES:%.c=%.o) -o $@

LOCAL_MODULE:= test3
LOCAL_SRC_FILES:= test3.c
LOCAL_DPS :=
MODULES += $(LOCAL_MODULE)
$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
    $(CC) $(LOCAL_SRC_FILES:%.c=%.o) -o $@

modules : $(MODULES)

PHONY += $(MODULES)
.PHONY: $(PHONY)

clean:
    -rm -rf *.o *.exe

但运行结果并不美好

$ make -f 1.mk
gcc -c test3.c -o test3.o
gcc test3.o -o test3
gcc -c test2.c -o test2.o
gcc  test3.o -o test2
gcc -c test1.c -o test1.o
gcc test3.o -o test1

原因何在?原来构造目标$(LOCAL_MODULE)下面的命令行里的$(LOCAL_SRC_FILES)是延后展开的.也就是说,这个变量变成了test3.c

以下两节内容全部摘自makefile手册

立即展开项

变量取值

变量定义解析的规则如下:

IMMEDIATE = DEFERRED
IMMEDIATE ?= DEFERRED
IMMEDIATE := IMMEDIATE
IMMEDIATE += DEFERRED or IMMEDIATE
define IMMEDIATE
DEFERRED
Endef

当变量使用追加符( +=)时,如果此前这个变量是一个简单变量(使用 :=定义的)则认为它是立即展开的,其它情况时都被认为是“延后”展开的变量。

条件语句

所有使用到条件语句在产生分支的地方, make 程序会根据预设条件将正确地分支展开。就是说条件分支的展开是“立即”的。其中包括: “ifdef”、“ifeq”、“ifndef”和“ifneq”所确定的所有分支命令。

规则的定义

所有的规则在 make 执行时,都按照如下的模式展开:

IMMEDIATE : IMMEDIATE ; DEFERRED
    DEFERRED

其中,规则中目标和依赖如果引用其他的变量,则被立即展开。而规则的命令行中的变量引用会被延后展开。此模板适合所有的规则,包括明确规则、模式规则、后缀规则、静态模式规则。

Makefile的执行过程

  1. 依次读取变量“MAKEFILES”定义的 makefile 文件列表
  2. 读取工作目录下的 makefile 文件(根据命名的查找顺序“ GNUmakefile”,“makefile”,“Makefile”,首先找到那个就读取那个)
  3. 依次读取工作目录 makefile 文件中使用指示符“include”包含的文件
  4. 查找重建所有已读取的 makefile 文件的规则(如果存在一个目标是当前读取的某一个 makefile 文件,则执行此规则重建此 makefile 文件,完成以后从第一步开始重新执行)
  5. 初始化变量值并展开那些需要立即展开的变量和函数并根据预设条件确定执行分支
  6. 根据“终极目标”以及其他目标的依赖关系建立依赖关系链表
  7. 执行除“终极目标”以外的所有的目标的规则(规则中如果依赖文件中任一个文件的时间戳比目标文件新,则使用规则所定义的命令重建目标文件)
  8. 执行“终极目标”所在的规则

问题解决

那要怎样才能解决本问题呢?通过读android NDK的makefile,发现用eval,define,call能解决本问题。
我们先睹为快

解决实例

PHONY := all clean modules
CC := gcc
MODULES :=
all: modules

%.o : %.c
    $(CC) -c $< -o $@

#------------------------------------------
#param: 1: TGT
#       2: DPS
#       3: OBJS
#------------------------------------------
define _tgt_rule
$(1): $(2) $(3)
    $$(CC) $(3) -o $$@
endef
tgt_rule = \
    $(eval $(call _tgt_rule,$(1),$(2),$(3)))

LOCAL_MODULE:= test1
LOCAL_SRC_FILES:= test1.c
LOCAL_DPS := test2
MODULES += $(LOCAL_MODULE)
#$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
#   $(CC) $(LOCAL_SRC_FILES:%.c=%.o) -o $@
$(call tgt_rule,$(LOCAL_MODULE),$(LOCAL_DPS),$(LOCAL_SRC_FILES:%.c=%.o))

LOCAL_MODULE:= test2
LOCAL_SRC_FILES:= test2.c
LOCAL_DPS := test3
MODULES += $(LOCAL_MODULE)
#$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
#   $(CC) $(LOCAL_SRC_FILES:%.c=%.o) -o $@
$(call tgt_rule,$(LOCAL_MODULE),$(LOCAL_DPS),$(LOCAL_SRC_FILES:%.c=%.o))

LOCAL_MODULE:= test3
LOCAL_SRC_FILES:= test3.c
LOCAL_DPS :=
MODULES += $(LOCAL_MODULE)
#$(LOCAL_MODULE) : $(LOCAL_DPS) $(LOCAL_SRC_FILES:%.c=%.o)
#   $(CC) $(LOCAL_SRC_FILES:%.c=%.o) -o $@
$(call tgt_rule,$(LOCAL_MODULE),$(LOCAL_DPS),$(LOCAL_SRC_FILES:%.c=%.o))

modules : $(MODULES)

.PHONY: $(PHONY)

clean:
    -rm -rf *.o *.exe

eval: 将右边的内容直接展开到makefile,即相当于变成makefile本身的内容,与C语言的“宏”行为一至
define: 定义命令包, (1), (2),…分别表示第一个参数,第二个参数,…;直接替换,相当于C语言的宏定义
call: 类似于C语言的宏引用

运行结果如下:

$ make -f 2.mk
gcc -c test3.c -o test3.o
gcc test3.o -o test3
gcc -c test2.c -o test2.o
gcc test2.o -o test2
gcc -c test1.c -o test1.o
gcc test1.o -o test1

以下三节内容全部摘自makefile手册

eval函数

函数功能

函数“eval”是一个比较特殊的函数。使用它可以在Makefile中构造一个可变的规则结构关系(依赖关系链),其中可以使用其它变量和函数。函数“eval”对它的参数进行展开,展开的结果作为 Makefile的一部分, make
可以对展开内容进行语法解析。展开的结果可以包含一个新变量、目标、隐含规则或者是明确规则等。也就是说此函数的功能主要是:根据其参数的关系、结构,对它们进行替换展开。

返回值

函数“eval”的返回值时空,也可以说没有返回值。

函数说明

“eval”函数执行时会对它的参数进行两次展开。第一次展开过程发是由函数本身完成的,第二次是函数展开后的结果被作为Makefile内容时由make解析时展开的。明确这一过程对于使用“eval”函数非常重要。理解了函数“eval”二次展开的过程后。实际使用时,如果在函数的展开结果中存在引用(格式为: $(x)),那么在函数的参数中应该使用“$$”来代替“$”(参考 6.1变量的引用 一节)。因为这一点,所以通常它的参数中会使用 函数“value” 来取一个变量的文本值。

我们看一个例子。例子看起来似乎非常复杂,因为它综合了其它的一些概念和函数。不过我们可以考虑两点:其一,通常实际一个模板的定义可能比例子中的更为复杂;其二,我们可以实现一个复杂通用的模板,在所有Makefile中包含它,亦可作到一劳永逸。相信这一点可能是大多数程序员所推崇的。
示例:

# 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)_OBJ) $$($(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)

来看一下这个例子:它实现的功能是完成“ PROGRAMS ”的编译链接。例子中“$(LINK.o)”为“$(CC) $(LDFLAGS)”,意思是对所有的.o文件和指定的库文件进行链接。可参考 10.2 make隐含规则一览 一节

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

展开为:

server : $(server_OBJS) –l$(server_LIBS)
client : $(client_OBJS) –l$(client_LIBS)

define 定义命令包

书写Makefile时,可能有多个规则会使用相同的一组命令。就像c语言程序中需要经常使用到函数“printf”。这时我们就会想能不能将这样一组命令进行类似c语言函数一样的封装,以后在我们需要用到的地方可以通过它的名字( c语言中的函数名)来对这一组命令进行引用。这样就可减少重复工作,提高了效率。在GNU make中,可以使用指示符“define”来完成这个功能(关于指示符“define”可参考 6.8 多行定义 一节)。通过“define”来定义这样一组命令,同时用一个变量(作为一个变量,不能和Makefile中其它常规的变量命名出现冲突)来代表这一组命令。通常我们把使用“define”定义的一组命令称为一个命令包。定义一个命令包的语法以“define”开始,以“endef”结束,例如:

define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

这里,“run-yacc”是这个命令包的名字。在“define”和“endef”之间的命令就是命令包的主体。需要说明的是:使用“define”定义的命令包中,命令体中变量和函数的引用不会展开。命令体中所有的内容包括“$”、 “(”、“)”等都是变量“run-yacc”的定义。它和c语言中宏的使用方式一样。关于变量可参考 第六章 Makefile中的变量
例子中,命令包中第一个命令是对引用它 所在规则中的第一个依赖文件(函数“firstword”,可参考 8.2 文本处理函数 一节)运行yacc程序。 yacc程序总是生成一个命名为“y.tab.c”的文件。第二行的命令就是把这个文件名改为规则目标的名字。
定义了这样一个命令包后,后续应该如何使用它?前面已经提到,命令包是使用一个变量来表示的。因此我们就可以按使用变量的方式来使用它。当在规则的命令行中使用这个变量时,命令包所定义的命令体就会对它进行替代。由于使用“define”定义的变量属于递归展开式变量(参考 6.2 两种变量定义(赋值) 一节),因此,命令包中所有命令中对其它变量的引用,在规则被执行时会被完全展开。例如这样一个规则:

foo.c : foo.y
    $(run-yacc)

此规则在执行时,我们来看一下命令包中的变量的替换过程: 1. 命令包中的“$^”会被“foo.y”替换; 2. “$@”被“foo.c”替换。大家应该对“$<”和“$@”不陌生吧,如果陌生可以参考 10.5.1 自动化变量 一小节。
当在一个规则中引用一个已定义的命令包时,命令包中的命令体会被原封不动的展开在引用它的地方(和 c 语言中的宏一样)。这些命令就成为规则的命令。因此我们也可在定义命令包时使用前缀来控制单独的一个命令行(例如“@”,“-”和“+”)。例如:

define frobnicate
    @echo "frobnicating target $@"
    frob-step-1 $< -o $@-step-1
    frob-step-2 $@-step-1 -o $@
endef

此命令包的第一行命令执行前不会被回显,其它的命令在执行前都被回显。
另一方面,如果一个规则在引用此命令包之前使用了控制命令的前缀字符。那么这个前缀字符将会被添加到命令包定义的每一个命令行之中。例如:

frob.out: frob.in
    @$(frobnicate)

这个规则执行时不会回显任何要执行的命令。关于命令行回显可参考 5.1 命令回显一节

Call函数

“call”函数是唯一一个可以创建定制化参数函数的引用函数。使用这个函数可以实现对用户自己定义函数引用。我们可以将一个变量定义为一个复杂的表达式,用“call”函数根据不同的参数对它进行展开来获得不同的结果。

函数语法

$(call VARIABLE,PARAM,PARAM,...)

函数功能

在执行时,将它的参数“PARAM”依次赋值给临时变量“$(1)”、“$(2)”(这些临时变量定义在“VARIABLE”的值中,参考下边的例子)…… call 函数对参数的数目没有限制,也可以没有参数值,没有参数值的“call”没有任何实际存在的意义。执行时变量“VARIABLE”被展开为在函数上下文有效的临时变量,变量定义中的“$(1)”作为第一个参数,并将函数参数值中的第一个参数赋值给它;变量中的“$(2)”一样被赋值为函数的第二个参数值;依此类推(变
量$(0)代表变量“VARIABLE”本身)。之后对变量“VARIABLE” 表达式的计算值。

返回值

参数值“PARAM”依次替换“$(1)”、“$(2)” …… 之后变量“VARIABLE”定义的表达式的计算值。

函数说明

  1. 函数中“VARIBLE”是一个变量名,而不是变量引用。因此,通常“call”函数中的“VARIABLE”中不包含“$”(当然,除非此变量名是一个计算的变量名)。 2. 当变量“VARIBLE”是一个 make 内嵌的函数名时(如“if”、
    “foreach”、“strip”等),对“PARAM”参数的使用需要注意,因为不合适或者不正确的参数将会导致函数的返回值难以预料。 3. 函数中多个“PARAM”之间使用逗号分割。 4. 变量“VARIABLE”在定义时不能定义为直接展开式!只能定义为递归展开式。

函数示例

首先,来看一个简单的例子。

示例 1 :

reverse = $(2) $(1)
foo = $(call reverse,a,b)

变量“foo”的值为“ba”。这里变量“reverse”中的参数定义顺序可以根据需要来调整,并不是需要按照“$(1)”、“$(2)”、“$(3)”…… 这样的顺序来定义。看一个稍微复杂一些的例子。我们定义了一个宏“pathsearch”来在“PATH”路径中搜索第一个指定的程序。

示例 2:

pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH)))))
LS := $(call pathsearch,ls)

变量“LS”的结果为“/bin/sh”。执行过程:函数“subst”将环境变量“PATH”转换为空格分割的搜索路径列表;“addsuffix”构造出可能的可执行程序“$(1)”(这里是“ls”)带路径的完整文件名(如:“/bin/$(1)”),之后使用函数“wildcard”
匹配,最后“firstword”函数取第一个文件名。
函数“call”以可以套嵌使用。每一层“call”函数的调用都为它自己的局部变量“$(1)”等赋值,覆盖上一层函数为它所赋的值。

示例 3:

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

那么变量“o”的值就为“file file default”。我们这里使用了 “origin”函数。我们分析函数的执行过程:首先,“o=$(call map,origin, o map MAKE)”这个函数调用使用了变量“map”所定义的表达式;使用内嵌函数名“origin”作为它的第一个参数值,使用 Makefile中的变量“o map MAKE”作为他的第二个参数值。当使用“call”函数展开后等价于“$(foreach a,o map MAKE,$(origin $(a)))”。

注意

和其它函数一样, “call”函数会保留出现在其参数值列表中的空字符。因此在使用参数值时对空格处理要格外小心。如果参数中存在多余的空格,函数可能会返回一个莫名奇妙的值。为了安全,在变量作为“call”函数参数值之前,应去掉其值中的多余空格(可以使用 “strip”函数 )。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值