第五章:Makefile中的变量
来源: ChinaUnix博客 日期: 2006.07.21 12:56 (共有0条评论) 我要评论 |
第五章:Makefile中的变量 1 使用变量 在Makefile中,变量就是一个名字(像是C语言中的宏),代表一个文本字符串(变量的值)。在Makefile的目标、依赖、命令中引用一个变量的地方,变量会被它的值所取代(与C语言中宏引用的方式相同,因此其他版本的make也把变量称之为“宏”)。在Makefile中变量的特征有以下几点: 1. Makefile中变量和函数的展开(除规则的命令行以外),是在make读取makefile文件时进行的,这里的变量包括了使用“=”定义和使用指示符“define”定义的。 2. 变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。 3. 变量名是不包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是,尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在以后的make版本中被赋予特殊含义,并且这样命名的变量对于一些shell来说不能作为环境变量使用(前面已经在 4.6.2 变量和递归 一小节中提到)。 4. 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义定义的一般变量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项CFLAGS)采用大写方式,这并不是要求的。但需要强调一点:对于一个工程,所有Makefile中的变量命名应保持一种风格,否则会显得你是一个蹩脚的程序员(就像代码的变量命名风格一样)。 5. 另外有一些变量名只包含了一个或者很少的几个特殊的字符(符号)。称它们为自动化变量。像“$”、“$@”、“$?”、“$*”等。 参考 9.5.3 自动化变量 一小节 1.1 变量的引用 当我们定义了一个变量之后,就可以在Makefile的很多地方使用这个变量。变量的引用方式是:使用“$(VARIABLE_NAME)”或者“${ VARIABLE_NAME }”来引用一个变量的定义。例如:“$(foo) ”或者“${foo}”就是取变量“foo”的值。美元符号“$”在Makefile中有特殊的含义,所有在命令或者文件名中使用“$”时需要用两个美元符号“$$”来表示。对一个变量的引用可以在Makefile的任何上下文中,目标、依赖、命令、绝大多数指示符和新变量的赋值中。这里有一个例子,其中变量保存了所有.o文件的列表: objects = program.o foo.o utils.o program : $(objects) cc -o program $(objects) $(objects) : defs.h 变量引用的展开过程是严格的文本替换过程,就是说变量值的字符串被精确的展开在此变量被引用的地方。因此规则: foo = c prog.o : prog.$(foo) $(foo) $(foo) -$(foo) prog.$(foo) 被展开后就是: prog.c : prog.c cc -c prog.c 通过这个例子我们可以看到变量的展开过程完全和c语言中的宏展开的过程一样,是一个严格的文本替换过程。上例中在变量“foo”被展开过程中,其值中的前导空格会被忽略。这里举这个例子的目的是为了让我们更清楚地了解变量的展开方式,而不是建议大家按照这样的方式来书写Makefile。在实际书写时,千万不要使用这种方式。否则将会给你带来很多不必要的麻烦。 注意:Makefile中在对一些简单变量的应用,我们也可以不使用“()”和“{}”来标记变量名,而直接使用“$x”的格式来实现,此种用法仅限于变量名为单字符的情况。另外自动化变量也使用这种格式。对于一般多字符变量的引用必须使用括号了标记,否则make将把变量名的首字母作为作为引用(“$PATH”在Makefile中实际上是“$(P)ATH”)。这一点和shell中变量的引用方式不同。shell中变量的引用可以是“${xx}”或者“$xx”格式。但在Makefile中多字符变量名的引用只能是“$(xx)”或者“${xx}”格式。 一般在我们书写Makefile时,各部分变量引用的格式我们建议如下: 1. make变量(Makefile中定义的或者是make的环境变量)的引用使用“$(VAR)”格式,无论“VAR”是单字符变量名还是多字符变量名。 2. 出现在规则命令行中shell变量(一般为执行命令过程中的临时变量,它不属于Makefile变量,而是一个shell变量)引用使用shell的“$tmp”格式。 3. 对出现在命令行中的make变量我们同样使用“$(CMDVAR)” 格式来引用。 例如: # sample Makefile …… SUBDIRS := src foo .PHONY : subdir Subdir : @for dir in $(SUBDIRS); do / $(MAKE) –C $$dir || exit 1; / done …… 1.2 两种变量定义(赋值) GNU make中,一个变量的定义有两种方式(或者称为风格)。我们把这两种方式定义的变量可以看作变量的两种不同风格。变量的这两种不同的风格的区别在于:1. 定义方式;2. 展开时机。下边我们分别对这两种不同的风格进行详细地讨论。 1.2.1 递归展开式变量 第一种风格的变量就是递归方式扩展的变量。这一类型的变量的定义是通过“=”(参考 5.5 如何设置变量 一节)或者使用指示符“define”(参考 5.8 多行定义 一节)定义的变量。对这种变量的引用,在引用的地方是严格的文本替换过程,此变量值的字符串原模原样的出现在引用它的地方。如果此变量定义中存在对其他变量的引用,这些被引用的变量会在它被展开的同时被展开。就是说在变量定义时,变量值中对其他变量的引用不会被替换展开。而是,变量在引用它的地方进行替换展开的同时,它所引用的其它变量才会被替换展开。语言的描述可能比较晦涩,让我们来看一个例子: foo = $(bar) bar = $(ugh) ugh = Huh? all:;echo $(foo) 执行“make”将会打印出“Huh?”。整个变量的替换过程时这样的:首先“$(foo)”被替换为“$(bar)”,接下来“$(bar)”被替换为“$(ugh)”,最后“$(ugh)”被替换为“Hug?”。整个替换的过程是在执行“echo $(foo)”是进行的。 这种类型的变量是其它版本的make所支持的类型。我们可以把这种类型的变量称为“递归展开”式变量。此类型的变量存有它的优点同时也存在其缺点。其优点是: 这种类型变量的定义时,可以引用其它的之前没有定义的变量(可能在后续部分定义,或者是通过make的命令行选项传递的变量)。看一个这样的例子: CFLAGS = $(include_dirs) -O include_dirs = -Ifoo -Ibar “CFLAGS”会在命令中被展开为“-Ifoo -Ibar -O”。我们可以看到在“CFLAGS”定义中使用到了之后定义的变量“include_dirs”。 其缺点是: 1. 使用此风格的变量定义,可能会由于出现变量的递归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败。例如,接上边的例子,我们给这个变量追加值: CFLAGS = $(CFLAGS) –O 它将会导致make进入对变量“CFLAGS”的无限展过程中去(这种定义就是变量的递归定义)。因为一旦后续同样存在对“CLFAGS”定义的追加,展开过程将是套嵌的、不能终止的(在发生这种情况时,make会提示错误信息并结束)。一般在书写Makefile时,使用这种追加变量值的方法也很少使用(也不是我们推荐的方式)。我们可以来看另外一个例子: x = $(y) y = $(x) $(z) 这种情况下同样会导致make陷入到无限的变量展开过程中。因此我们在使用这种风格的变量,当一个变量定义需要引用其它的变量时需要特别的注意,防止变量的展开进入无限的循环。当出现这样的错误时,首先检查你的Makefile中变量是否出现了递归定义。这是它的一个缺点。 2. 第二个缺点:这种风格变量的定义中如果使引用了某一个函数,那么函数总会在其被引用的地方被执行。 这是因为这种风格变量的定义中,对函数引用的替换展开发生在展开它自身的时候,而不是在定义它的时候。这样所带来的问题是,可能可能会使make的执行效率降低,同时对某些变量和函数的引用出现问题。特别是当变量定义中引用了“shell”和“wildcard”函数的情况,可能出现不可控制或者难以预料的错误,因为我们无法确定它在何时会被展开。 1.2.2 直接展开式变量 为了避免“递归展开式”变量存在的问题和不方便。在GNU make中可以使用另外一种风格的变量,我们将它称为“直接展开”式。这种风格的变量使用“:=”来定义变量。在使用“:=”定义变量时,变量值中对另外变量的引用或者函数的引用在定义时被展开(对变量进行替换)。所以在变量被定义以后就是一个实际所需要定义的文本串,其中不再包含任何对其它变量的引用。因此 x := foo y := $(x) bar x := later 就等价于: y := foo bar x := later 需要注意的是:此风格变量在定义时就完成了对所引用的变量的展开,因此它不能实现对其后定义变量的引用。例如: CFLAGS := $(include_dirs) -O include_dirs := -Ifoo -Ibar 由于在变量“include_dirs”的定义出现在“CFLAGS”定义之后。因此在“CFLAGS”的定义中,“include_dirs”的值为空。“CFLAGS”的值为“-O”而不是“-Ifoo -Ibar -O”。这一点也是直接展开式和递归展开式变量的不同点。注意这里的两个变量都是“直接展开”式的。大家不妨试试将其中某一个变量使用递归展开式定义后看一下又会出现什么样的结果。 下边让我们来看一个复杂一点的例子。分析一下直接展开式变量定义(:=)的用法,这里也用到了make的shell函数和变量“MAKELEVEL”(此变量在make的递归调用时代表make的调用深度,参考 4.6.2 变量和递归 一小节)。 其中包括了make的函数、条件表达式和一个系统变量“MAKELEVEL”的使用: ifeq (0,${MAKELEVEL}) cur-dir := $(shell pwd) whoami := $(shell whoami) host-type := $(shell arch) MAKE := ${MAKE} host-type=${host-type} whoami=${whoami} endif 第一行是一个条件判断,说明如果是顶层Makefile,就定义下列变量。否则不定义任何变量。第二、三、四、五行分别定义了一个变量,在进行变量定义时对引用到的其它变量和函数展开。最后结束定义。由于直接展开式的这个优点我们就可以书写这样一个规则: ${subdirs}: ${MAKE} cur-dir=${cur-dir}/$@ -C $@ all 它实现了在不同子目录下变量“cur_dir”使用不同的值(为当前工作目录)。 在复杂的Makefile中,推荐使用直接展开式变量。因为这种风格变量的使用方式和大多数编程语言中的变量使用方式基本上相同。它可以使一个比较复杂的Makefile在一定程度上具有可预测性。而且这种变量允许我们利用之前所定义的值来重新定义它(比如使用某一个函数来对它以前的值进行处理并重新赋值),此方式在Makefile中经常用到。尽量避免和减少递归方式的变量的使用。 1.2.3 如何定义一个空格 使用直接扩展式变量定义我们可以实现将一个前导空格定义在变量值中。一般变量值中的前导空格字符在变量引用和函数调用时被丢弃。直接展开式变量在定义时对引用的其它变量或函数进行展开的特点,使我们可以实现在一个变量中包含前导空格并在引用此变量时对空格加以保护。像这样: nullstring := space := $(nullstring) # end of the line 这里,变量“space”就正好表示一个一个空格。“space”定义的行中的注释在这里使得我们的目的更清晰(明确地描述一个空格字符比较困难),明确的指定我们需要的是一个空格。这是一个很好地实现方式。使用变量“nullstring”标明变量值的开始,采用“#”注释来结束,中间是一个空格字符。 make对变量进行处理时变量值中尾空格是不被忽略的,因此定义一个包含一个或者多个空格的变量定义时,上边的实现就是一个简单并且非常直观的方式。但是需要注意的是对于不包含尾空格的变量的定义,就不能随便使用几个空格之后,在同行中放置它的注释内容。这是千万需要注意的。例如下边的做法就是不正确的: dir := /foo/bar # directory to put the frobs in 变量“dir”的值是“/foo/bar ”(后面有4个空格),这可能并不是你想要实现的。假如一个特定的文件以它作为路径来表示“$(dir)/file”,那么大错特错了。 这里顺便提醒大家,在书写Makefile时。注释内容推荐书写在独立的一行或者多行,这样就可以防止出现这种意外情况的发生,而且注释行独立的行书写时也使得你的Makefile更加清晰,便于别人的预读。对于特殊的定义,比如定义包含一个或者多个空格空格的变量时进行详细地说明和注释。 1.2.4 “?=”操作符 GNU make中,还有一个被称为条件赋值的赋值操作符“?=”。被称为条件赋值是因为:只有此变量在之前没有赋值的情况下才会对这个变量进行赋值。例如: FOO ?= bar 其等价于: ifeq ($(origin FOO), undefined) FOO = bar endif 含义是:如果变量“FOO”在没有定义过,就给它赋值“bar”。否则不改变它的值。 1.3 变量的高级用法 本节讨论一些变量的高级用法,这些高级的用法使我们可以更灵活的使用变量。 1.3.1 变量的替换引用 对于一个已经定义的变量,可以使用“替换引用”将其值使用指定的字符(字符串)进行替换。格式为“$(VAR:A=B)”(或者“${VAR:A=B}”),意思是,替换变量“VAR”中所有“A”字符结尾的字为“B”结尾的字。“结尾”的含义是空格之前(变量值的多个字以空格分开)。而对于变量其它部分的“A”字符不进行替换。例如: foo := a.o b.o c.o bar := $(foo:.o=.c) 在这个定义中,变量“bar”的值就为“a.c b.c c.c”。使用变量的替换引用将变量“foo”以空格分开的值中的所有的字的尾字符“o”替换为“c”,其他部分不变。而且如果在变量“foo”中如果存在“o.o”时,那么变量“bar”的值为“a.c b.c c.c o.c”而不是“a.c b.c c.c c.c”。这一点需要明确。 变量的替换引用其实是函数“patsubst”(参考 7.2 文本处理函数 一节)的一个简化实现。在GNU make中同时提供了这两种方式来实现同样的目的,以兼容其它版本make。 另外一种引用替换的技术使用功能更强大的“patsubst”函数的所有功能。它的格式和上面“$(VAR:A=B)”的格式相类似,不过需要在这里的“A”和“B”中需要包含模式字符“%”。只是它就和“$(patsubst A,B $(VAR))”(可参考 7.2 make的文本处理函数 一小节)所实现功能相同。例如: foo := a.o b.o c.o bar := $(foo:%.o=%.c) 这个例子同样使变量“bar”的值为“a.c b.c c.c”。这种格式的替换引用方式比第一种方式更为通用。 1.3.2 变量的套嵌引用 计算的变量名是一个比较复杂的概念,仅用在那些复杂的Makefile中。通常我们不需要对它的计算过程有深入地了解,只要知道当一个被引用的变量名之中含有“$”时,可以得到另外一个值。你如果是一个比较喜欢追根问底的人,或者你想弄清楚make计算变量的过程。就可以参考本节的内容。 一个变量名(文本串)之中可以包含对其它变量的引用。这种情况我们称之为“变量的套嵌引用”或者“计算的变量名”。例如: 先看一个例子: x = y y = z a := $($(x)) 这个例子中,最终定义了“a”的值为“z”。来看一下变量的引用过程:首先最里边的变量引用“$(x)”被替换为变量名“y”(就是“$($(x))”被替换为了“$(y)”),之后“$(y)”被替换为“z”(就是a := z)。这个例子中(a:=$($(x)))所引用的变量名不是明确声明的,而是由$(x)扩展得到。这里“$(x)”相对于外层的引用就是套嵌的变量引用。 上个例子我们可以看到是一个两层的套嵌引用,具有多层的套嵌引用在Makefile中也是允许的。下边我们在来看一个三层套嵌引用的例子: x = y y = z z = u a := $($($(x))) 这个例子的最终是定义了“a”的值为“u”。它的扩展过程和上边第一个例子的过程相同。首先“$(x)”被替换为“y”,则“$($(x))”就是“$(y)”,“$(y)”在北替换为“z”,所有就是“a:=$(z)”;“$(z)”最后被替换为“u”。 以上两个套嵌引用的例子中没有使用到递归展开式变量。递归展开式变量的变量名的计算过程,也是按照相同的方式被扩展的。我们看一个这样一个例子: x = $(y) y = z z = Hello a := $($(x)) 此例最终实现了“a:=Hello”这么一个定义。其中“$($(x))”被替换成了“$($(y))”,“$($(y)) ”在被替换为“$(z)”,最终就是“a:=Hello”。 这里的$($(x))被替换成了$($(y)),因为$(y)值是“z”,所以,最终结果是:a:=$(z),也就是“Hello”。 递归变量的套嵌引用过程,也可以包含变量的修改引用和函数调用。看下边的例子,使用了make的文本处理函数: x = variable1 variable2 := Hello y = $(subst 1,2,$(x)) z = y a := $($($(z))) 此例同样的实现“a:=Hello”。“$($($(z)))”替换为“$($(y))”,之后再次被替换为“$($(subst 1,2,$(x)))”(“$(x)”的值是“variable1”,所以有“$($(subst 1,2,$(variable1)))”)。函数处理之后为“$(variable2)” 之后对它在进行替换展开。最终,变量“a”的值就是“Hello”。从上边的例子中我们看到,计算的变量名的引用过程存在多层套嵌过程,也是用了文本处理函数。这个复杂的计算变量的过程,会使很多人感到混乱甚至迷惑。上例中所要实现的目的没有直接使用“a:=Hello”来的直观明了。我们在书写Makefile时,应尽量避免使用套嵌的变量引用方式。在一些必需的地方,也最好不要使用高于两级的套嵌引用。使用套嵌的变量引用时,如果涉及到递归展开式的变量引用时需要特别注意(参考 5.2.1 递归展开式变量 一小节)。如果处理不当可能会导致递归展开错误,或者出现一些那一预料的现象。 一个计算的变量名可以不是对一个完整、单一的其它变量的引用。其中可以包含多个变量的引用,也可以包含一些文本字符串。就是说,计算变量的名字可以由一个或者多个变量引用同时加上字符串混合组成。例如: a_dirs := dira dirb 1_dirs := dir1 dir2 a_files := filea fileb 1_files := file1 file2 ifeq "$(use_a)" "yes" a1 := a else a1 := 1 endif ifeq "$(use_dirs)" "yes" df := dirs else df := files endif dirs := $($(a1)_$(df)) 这个例子实现对变量“dirs”的定义,它的可能取值为“a_dirs”、“1_dirs”、“a_files”或者“a_files”四个值其中之一,具体依赖于“use_a”和“use_dirs”的定义。 计算的变量名也可以使用上一小节我们讨论过的“ 变量的替换引用 ”。例如: a_objects := a.o b.o c.o 1_objects := 1.o 2.o 3.o sources := $($(a1)_objects:.o=.c) 这个例子实现对变量“sources”的定义,它的可能取值为“a.c b.c c.c”和“1.c 2.c 3.c”,具体依赖于“a1”的定义。大家自己分析一下计算变量名的过程。 使用嵌套的变量引用的唯一限制是,不能通过指定部分需要调用的函数名称(调用的函数包括了函数名本身和执行的参数)来实现对这个函数的调用。这是因为套嵌引用在展开之前已经完成了对函数名的识别测试。语言的描述可能比较难理解。我们来看一个例子,此例子试图将函数执行的结果赋值给一个变量: ifdef do_sort func := sort else func := strip endif bar := a d b g q c foo := $($(func) $(bar)) 此例的本意是将“sort”或者“strip”(依赖于是否定义了变量“do_sort”)以“a d b g q c”的执行结果赋值变量“foo”。在这里使用了套嵌引用方式来实现,这个实现的结果是:变量“foo”的值为字符串“sort a d b g q c”或者“strip a d g q c”。这是目前版本的make在处理套嵌变量引用时的限制。 计算的变量名可以用在:1. 一个使用赋值操作符定义的变量的左值部分;2. 使用“define”定义的变量名中。例如: dir = foo $(dir)_sources := $(wildcard $(dir)/*.c) define $(dir)_print lpr $($(dir)_sources) endef 在这个例子中我们定义了三个变量:“dir”,“foo_sources”和“foo_print”。 计算的变量名在进行替换时的顺序是:从最里层的变量引用开始,逐步向外进行替换。一层层展开直到最后计算出需要应用的具体的变量,之后进行替换展开得到实际的引用值。 变量的套嵌引用(计算的变量名)在我们的Makefile中应该尽量避免使用。在必需的场合使用时掌握的原则是:套嵌使用的层数越少越好,使用多个两层套嵌引用代替一个多层的套嵌引用。如果在你的Makefile中存在一个层次很深的套嵌引用。会给其他人阅读造成很大的困难。而且变量的多级套嵌引用在某些时候会使简单问题复杂化。 作为一个优秀的程序员,在面对一个复杂问题时,应该是寻求一种尽可能简单、直接并且高效的处理方式来解决,而不是将一个简单问题在实现上复杂化。如果想在简单问题上突出自己使用某种语言的熟练程度,是一种非常愚蠢、且不成熟的行为。 注意: 套嵌引用的变量和递归展开的变量在本质上存在区别。套嵌的引用就是使用一个变量表示另外一个变量,或者更多的层次;而递归展开的变量表示当一个变量存在对其它变量的引用时,对这变量替换的方式。递归展开在另外一个角度描述了这个变量在定义是赋予它的一个属性或者风格。并且我们可以在定义个一个递归展开式的变量时使用套嵌引用的方式,但是建议你的实际编写Makefile时要尽量避免这种复杂的用法。 1.4 变量取值 一个变量可以通过以下几种方式来获得值: ² 在运行make时通过命令行选项来取代一个已定义的变量值。参考 5.7 override指示符 一节 ² 在makefile文件中通过赋值的方式(参考 5.5 如何设置变量 一节)或者使用“define”来为一个变量赋值(参考 5.8 多行定义 一节)。 ² 将变量设置为系统环境变量。所有系统环境变量都可以被make使用。参考 5.9 系统环境变量 一节 ² 自动化变量,在不同的规则中自动化变量会被赋予不同的值。它们每一个都有单一的习惯性用法。参考 9.5.3 自动化变量 一小节 ² 一些变量具有固定的值。参考 9.3 隐含变量 一节 1.5 如何设置变量 Makefile中变量的设置(也可以称之为定义)是通过“=”(递归方式)或者“:=”(静态方式)来实现的。“=”和“:=”左边的是变量名,右边是变量的值。下边就是一个变量的定义语句: objects = main.o foo.o bar.o utils.o 这个语句定义了一个变量“objects”,其值为一个.o文件的列表。变量名两边的空格和“=”之后的空格在make处理时被忽略。 使用“=”定义的变量称之为“递归展开”式变量;使用“:=”定义的变量称为“直接展开”式变量,“直接展开”式的变量如果其值中存在对其变量或者函数的引用,在定义时这些引用将会被进行替换展开(详细可参考 5.2 两种变量定义(赋值) 一节)。 定义一个变量时需要明确以下几点: 1. 变量名之中可以包含函数或者其它变量的引用,make在读入此行时根据已定义情况进行替换展开而产生实际的变量名。参考 5.3.2 变量的套嵌引用 一小节 2. 变量的定义值在长度上没有限制。不过还是需要考虑你的实际情况,保证你的机器上有足够的可用的交换空间来处理一个超常的变量值。变量定义较长时,一个好的做法就是将比较长的行分多个行来书写,除最后一行外行与行之间使用反斜杠(/)连接,表示一个完整的行。这样的书写方式对make的处理不会造成任何影响,便于后期修改维护而且使得你的Makefile更清晰。例如上边的例子就可以这样写: ojects = main.o foo.o / bar.o utils.o 3. 当引用一个没有定义的变量时,make默认它的值为空。 4. 一些特殊的变量在make中有内嵌固定的值(可参考 9.3 隐含变量 一节),不过这些变量允许我们在Makefile中显式得重新给它赋值。 5. 还存在一些由两个符号组成的特殊变量,称之为自动环变量。它们的值不能在Makefile中进行显式的修改。这些变量使用在规则中时,不同的规则中它们会被赋予不同的值。 6. 如果你希望实现这样一个操作,仅对一个之前没有定义过的变量进行赋值。那么可以使用速记符“?=”(条件方式)来代替“=”或者“:=”来实现(可参考 5.2.4 “?=”操作符 一小节)。 1.6 追加变量值 通常对于一个通用变量在定义之后的其他一个地方,需要给它的值进行追加。这也是非常有用的。我们可以在开始给它定义一个基本的值,后续可以不断地根据需要给它增加一些必要值。在Makefile中使用“+=”(追加方式)来实现对一个变量值的追加操作。像下边那样: objects += another.o 这个操作把字符串“another.o”添加到变量“objects”原有值的末尾,使用空格将其分开。因此我们可以看到: objects = main.o foo.o bar.o utils.o objects += another.o 上边的两个操作之后变量“objects”的值成:“main.o foo.o bar.o utils.o another.o”。使用“+=”操作符,就相当于: objects = main.o foo.o bar.o utils.o objects := $(objects) another.o 但是,这两种方式可能在简单一些的Makefile有相同的效果,复杂的Makefile中它们之间的差异就会导致一些问题。为了方便我们调试,了解这两种实现的差异还是很有必要的。 1. 如果被追加值的变量之前没有定义,那么,“+=”会自动变成“=”,此变量就被定义为一个递归展开式的变量。如果之前存在这个变量定义,那么“+=”就继承之前定义时的变量风格(可参考 5.2 两种变量定义 一节)。 2. 直接展开式变量的追加过程:变量使用“:=”定义,之后“+=”操作将会首先替换展开之前此变量的值,尔后在末尾添加需要追加的值,并使用“:=”重新给此变量赋值。实际的过程像下边那样: variable := value variable += more 就是: variable := value variable := $(variable) more 3. 递归展开式变量的追加过程:一个变量使用“=”定义,之后“+=”操作时不对之前此变量值中的任何引用进行替换展开,而是按照文本的扩展方式(之前等号右边的文本未发生变化)替换,尔后在末尾添加需要追加的值,并使用“=”给此变量重新赋值。实际的过程和上边的相类似: variable = value variable += more 相当于: temp = value variable = $(temp) more 当然了,上边的过程并不会存在中间变量:“temp”,这里只是使用它来描述得更形象。这种情况时如果“value”中存在某种引用,情况就有些不同了。看我们通常一个会用到的例子: CFLAGS = $(includes) -O ... CFLAGS += -pg # enable profiling 第一行定义了变量“CFLAGS”,它是一个递归展开式的变量。因此make在处理它的定义时不会对其值中的引用“$(includes)”进行展开,它的替换展开是在变量“CFLAGS”被引用的规则中。因此,变量“include”可以在“CFLAGS”之前没有定义,只要它在实际引用“CFLAGS”之前定义就可以了。如果给“CFLAGS”追加值使用“:=”操作符,我们按照下边那样实现: CFLAGS := $(CFLAGS) -pg # enable profiling 这样似乎好像很正确,但是实际上它在有些情况时却不是你所要实现的。我们来看看,因为“:=”操作符定义的是直接展开式变量,因此变量值中对其它变量或者函数的引用会在定义时进行展开。在这种情况下,如果变量“includes”在之前没有进行定义的话,变量“CFLAGS”的值为“-O -pg”($(includes)被替换展开为空字符)。而其后出现的“includes”的定义对“CFLAGS”将不产生影响。相反的情况,如果我们在这里使用“+=”实现: CFLAGS += -pg # enable profiling 那么变量“CFLAGS”的值就是文本串“$(includes) –O -pg”,因为之前“CFLAGS”定义为递归展开式,所以追加值时不会对其值的引用进行替换展开。因此变量“includes”只要出现在规则对“CFLAGS”的引用之前定义,它都可以对“CFLAGS”的值起作用。对于递归展开式变量的追加,make程序会同样会按照递归展开式的定义来实现对变量的重新赋值,不会发生递归展开式变量展开过程的无限循环。 1.7 override 指示符 通常在执行make时,如果通过命令行定义了一个变量,那么它将替代在Makefile中出现的同名变量的定义。就是说,对于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在Makefile中此变量的值。为了防止命令行变量定义的值替代Makefile中变量定义的值。需要在Makefile中使用指示符“override”来声明这个变量,像下边那样: override VARIABLE = VALUE 或者: override VARIABLE := VALUE 也可以对变量使用追加方式: override VARIABLE += MORE TEXT 对于追加方式需要说明的是:变量在定义时使用了“override”,则后续对它值进行追加时,也需要使用带有“override”指示符的追加方式。否则对此变量值的追加不会生效。 指示符“override”并不是用来调整Makefile和执行时命令参数的冲突,其存在的目的是为了使用户可以改变或者追加那些使用make的命令行指定的变量的定义。从另外一个角度来说,就是实现了在Makefile中增加或者修改命令行参数的一种机制。我们可能会有这样的需求;可以通过命令行来指定一些附加的编译参数,对一些通用的参数或者必需的编译参数我们可以在Makefile中指定,而在命令行中可以指定一些特殊的参数。对待这种需求,我们可以使用指示符“override”来实现。 例如无论命令行指定那些编译参数,必须打开调试开关“-g”,我们的Makefile对“CFLAGS”应该这样写: override CFLAGS += -g 无论通过命令行指定那些编译选项,“-g”参数始终存在。 对于使用“define”定义的变量我们同样也可以使用“override”进行声明。例如: override define foo bar endef 最后我们来看一个例子: # sample Makefile EXEF = foo override CFLAGS += -Wall –g .PHONY : all debug test all : $(EXEF) foo : foo.c ……….. ……….. $(EXEF) : debug.h $(CC) $(CFLAGS) $(addsuffix .c,$@) –o $@ debug : @echo ”CFLAGS = $(CFLAGS)” 执行:make CFLAGS=-O2 将显式结果为编译“foo”的过程是“cc –O2 –Wall –g foo.c –o foo”。执行“make CFLAGS=-O2 debug”可以查看到变量“CFLAGS”的值为“–O2 –Wall –g”。另外,这个例子中,如果把变量“CFLAGS”之前的指示符“override”去掉以后使用相同的命令将得到不同的结果。大家试试看! 1.8 多行定义 定义变量的另外一种方式是使用“define”指示符。它定义一个包含多行字符串的变量,我们就是利用它的这个特点实现一个完整命令包的定义(可参考 4.7 定义命令包 一节)。使用“define”定义的命令包可以作为“eval”函数的参数来使用。参考 7.8 eval函数 一节 本文的前些章节已经不止一次的提到并使用了“define”。相信大家已经有所了解。本节就“define”定义变量从以下几个方面来讨论: 1. “define”定义变量的语法格式:以指示符“define”开始,“endif”结束,之间的所有内容就是所定义变量的值。所要定义的变量名字指示符“define”的同一行之后;指示符所在行的下一行开始一直到“end”所在行的上一行之间的若干行,是变量的值定义。 define two-lines echo foo echo $(bar) endef 如果将变量“two-lines”作为命令包执行时,其相当于: two-lines = echo foo; echo $(bar) 我想大家对这个命令的执行应该是比较熟悉的。它把变量“two-lines”的值作为一个完整的shell命令行来处理(是使用分号“;”分开的在同一行中的两个命令而不是作为两个命令行来处理),保证了变量完整。(关于完整命令行的执行可参考 4.2 命令的执行 一节) 2. 变量的风格:使用“define”定义的变量和使用“=”定义的变量一样,属于“递归展开”式的变量,两者只是在语法上不同。因此“define”所定义的变量值中,对其它变量或者函数引用不会在定义时替换展开,其展开是在“define”定义的变量被引用时进行的。 3. 可以套嵌引用。因为是递归展开式变量,所以在存在嵌套引用时“$(x)”将是变量的值的一部分。 4. 变量值中可以包含:换行符、空格等特殊符号(注意如果定义中某一行是以[Tab]字符开始时,当引用此变量时这一行会被作为命令行来处理)。 5. 可以使用“override”在定义时声明变量:这样可以防止变量的值被命令行指定的值替代。例如: override define two-lines foo $(bar) endef 1.9 系统环境变量 make在运行时,系统的所有环境变量对它都是可见的。在Makefile中,可以引用任何已定义的系统环境变量。(这里我们区分系统环境变量和make的环境变量,系统环境变量是这个系统所有用户所拥有的,而make的环境变量只是对于make的一次执行过程有效,以下正文中出现没有限制的“环境变量”时默认指的是“系统环境变量”,在特殊的场合我们会区分两者)正因为如此,我们就可以设置一个命名为“CFLAGS”的环境变量,用它来指定一个默认的编译选项。我们就可以在所有的Makefile中直接使用这个变量来对c源代码就行编译。通常这种方式是比较安全的,但是它的前提是大家都明白这个变量所代表的含义,没有人在Makefile中把它作其他的用途。当然了,你也可以在你的Makefile中根据你的需要对它进行重新定义。 使用环境变量我们需要注意以下几点: 1. 在Makefile中对一个变量的定义或者以make命令行形式对一个变量的定义,都将覆盖同名的环境变量(注意:它并不改变系统环境变量定义,被修改的环境变量只在make执行过程有效)。而make使用“-e”参数时,Makefile和命令行定义的变量不会覆盖同名的环境变量,make将使用系统环境变量中这些变量的定义值。 2. make的递归调用中,所有的系统环境变量会被传递给下一级make。默认情况下,只有环境变量和通过命令行方式定义的变量才会被传递给子make进程。在Makefile中定义的普通变量需要传递给子make时需要使用“export”指示符来对它声明。参考 4.6.2 变量和递归 一小节 3. 一个比较特殊的是环将变量“SHELL”。在系统中这个环境变量的用途是用来指定用户和系统的交互接口,显然对于make是不合适的。因此在make的执行环境变量“SHELL”的取值不使用同名的环境变量定义,而默认使用“/bin/sh”作为它的命令行解释程序(make执行之前设置它为“/bin/sh”)。(参考 4.2 命令的执行 一小节) 这里不推荐使用环境变量来完成普通变量的工作,特别是在make的递归调用中。任何一个环境变量的错误定义都对系统上的所有make产生影响,甚至是毁坏性的。因为环境变量具有全局的特征。所以尽量不要污染环境变量,造成环境变量名字污染。我想大多数系统管理员都明白环境变量对系统是多么的重要。 我们来看一个例子,结束本节。假如我们的机器名为“server-cc”;我们的Makefile内容如下: # test makefile HOSTNAME = server-http ………… ………… .PHONY : debug debug : @echo “hostname is : $( HOSTNAME)” @echo “shell is $(SHELL)” 1. 执行“make debug”将显示: hostname is : server-http shell is /bin/sh 2. 执行“make –e debug”;将显示: hostname is : server-cc shell is /bin/sh 3. 执行“make –e HOSTNAEM=server-ftp”;将显示: hostname is : server-ftp shell is /bin/sh 记住:除非必须,否则在你的Makefile中不要重置环境变量“SHELL”的值。因为一个不正确的命令行解释程序可能会导致规则定义的命令执行失败,甚至是无法执行!当需要重置它时,必须有充分的理由和配套的规则命令来适应这个新指定的命令行解释程序。 1.10 目标指定变量 在Makefile中定义一个变量,这个变量对此Makefile的所有规则都是有效的。它就像是一个“全局的”变量(仅限于定义它的那个Makefile中的所有规则,如果需要对其它的Makefile中的规则有效,就需要使用“export”对它进行声明)。当然“自动化变量”除外(参考 9.5.3 自动化变量 一节)。 另外一个特殊的变量就是所谓的“目标指定变量(Target-specific Variable)”。此特性允许对于相同变量根据目标指定不同的值,有点类似于自动化变量。目标指定的变量值只在指定它的目标的上下文中有效,对于其他的目标不产生影响。就是说目标指定的变量具有“局部性”。 设置一个目标指定变量的语法为: TARGET ... : VARIABLE-ASSIGNMENT 或者: TARGET ... : override VARIABLE-ASSIGNMENT 一个多目标指定的变量的作用域是所有这些目标的上下文,它包括了和这个目标相关的所有执行过程。 目标指定变量的一些特点: 1. “VARIABLE-ASSIGNMENT”可以使用任何一个有效的赋值方式,“=”(递归)、“:=”(静态)、“+=”(追加)或者“?=”(条件)。 2. 使用目标指定变量值时,目标指定的变量值不会影响同名的那个全局变量的值。就是说目标指定一个变量值时,如果在Makefile中之前已经存在此变量的定义(非目标指定的),那么对于其它目标全局变量的值没有变化。变量值的改变只对指定的这些目标可见。 3. 目标指定变量和普通变量具有相同的优先级。就是说,当我们使用make命令行的方式定义变量时,命令行中的定义将替代目标指定的同名变量定义(和普通的变量一样会被覆盖)。另外当使用make的“-e”选项时,同名的环境变量也将覆盖目标指定的变量定义。因此为了防止目标指定的变量定义被覆盖,可以使用第二种格式,使用指示符“override”对目标指定的变量进行声明。 4. 目标指定的变量和同名的全局变量属于两个不同的变量,它们在定义的风格(递归展开式和直接展开式)上可以不同。 5. 目标指定的变量变量会作用到由这个目标所引发的所有的规则中去。例如: prog : CFLAGS = -g prog : prog.o foo.o bar.o 这个例子中,无论Makefile中的全局变量“CFLAGS”的定义是什么。对于目标“prog”以及其所引发的所有(包含目标为“prog.o”、“foo.o”和“bar.o”的所有规则)规则,变量“CFLAGS”值都是“-g”。 使用目标指定变量我们可以在Makefile实现,对于不同的目标文件使用不同的编译参数。看一个例子: # sample Makefile CUR_DIR = $(shell pwd) INCS := $(CUR_DIR)/include CFLAGS := -Wall –I$(INCS) EXEF := foo bar .PHONY : all clean all : $(EXEF) foo : foo.c foo : CFLAGS+=-O2 bar : bar.c bar : CFLAGS+=-g ……….. ……….. $(EXEF) : debug.h $(CC) $(CFLAGS) $(addsuffix .c,$@) –o $@ clean : $(RM) *.o *.d $(EXES) 这个Makefile文件实现了在编译程序“foo”使用优化选项“-O2”但不使用调试选项“-g”,而在编译“bar”时采用了“-g”但没有“-O2”。这就是目标指定变量的灵活之处。目标指定变量的其它特性大家可以修改这个简单的Makefile来进行验证! 1.11 模式指定变量 GNU make中,除了支持上一节所讨论的模式指定变量之外(参考 5.10 目标指定变量 一节),还支持另外一种方式:模式指定变量(Pattern-specific Variable)。使用目标定变量定义时,此变量被定义在某个具体目标和由它所引发的规则的目标上。而模式指定变量定义是将一个变量值指定到所有符合特定模式的目标上去。对于同一个变量如果使用追加方式,通常一个目标的局部变量值的顺序是:(为所有规则定义的全局值)+(引发它所在规则被执行的目标所指定值)+(它所符合的模式指定值)+(此目标所指定的值)。这个大家也不需要深入了解。 设置一个模式指定变量的语法和设置目标变量的语法相似: PATTERN ... : VARIABLE-ASSIGNMENT 或者: PATTERN ... : override VARIABLE-ASSIGNMENT 和目标指定变量语法的唯一区别就是:这里的目标是一个或者多个“模式”目标(包含模式字符“%”)。例如我们可以为所有的.o文件指定变量“CFLAGS”的值: %.o : CFLAGS += -O 它指定了所有.o文件的编译选项包含“-O”选项,不改变对其它类型文件的编译选项。 需要说明的是:在使用模式指定的变量定义时。目标文件一般除了模式字符(%)以外需要包含某种文件名的特征字符(例如:“a%”、“%.o”、“%.a”等)。当单独使用“%”作为目标时,指定的变量会对任何类型的目标文件都有效。 |