语言: 日本語 | Español | Français | Português | 中文 | English
上一章 | 下一章 | 目录 | 英文原版(gnu.org)

6 变量的使用方法

变量(variable)是在 makefile 中定义的名字,用来表示一个称为(value)的字符串。通过显式请求,这些值会被展开并插入到目标、前置条件 (prerequisite)、命令 (recipe) 以及 makefile 的其他各处。(在 make 的某些其他版本中,变量也被称为(macro)。)

makefile 中的变量和函数,原则上在读取时即被展开。但有几个例外:命令(recipe)内部、使用 ‘=’ 的变量定义的右边,以及使用 define 指令的变量定义的主体,在此时不会被展开。展开变量时所得到的值,是展开那一刻为止最新一次定义的值。换句话说,变量是动态确定作用域的(动态作用域)。

变量可以表示文件名列表、传给编译器的选项、要运行的程序、查找源文件的目录、写入输出的目录等等,凡是你能想到的几乎一切。

变量名可以是不含 ‘:’、‘#’、‘=’ 或空白的任意字符序列。但是,含有字母、数字、下划线以外字符的变量名应当谨慎使用,因为在某些 shell 中,这类名字的变量无法通过环境传递给子 make(请参阅向子make传递变量)。以 ‘.’ 加大写字母开头的变量名,在 make 的未来版本中可能被赋予特殊含义。

变量名区分大小写。‘foo’、‘FOO’、‘Foo’ 分别指向不同的变量。

变量名习惯上使用大写字母,但本书建议:对于在 makefile 内部使用的变量采用小写字母,而把大写字母保留给控制隐含规则的参数,或用户应当在命令选项中覆盖的参数(请参阅覆盖变量)。

少数变量的名字只有一个符号字符或仅几个字符。这些被称为自动变量(automatic variable),各有特定的专门用途。请参阅自动变量

6.1 变量引用的基础

要插入变量的值,就写一个美元符号,后面跟用圆括号或花括号括起来的变量名。也就是说,‘$(foo)’ 和 ‘${foo}’ 都是对变量 foo 的正确引用。正因为 ‘$’ 具有这样的特殊含义,所以当你想在文件名或命令中表示单独一个美元符号本身时,必须写成 ‘$$’。

变量引用可以用在任何上下文中:目标、前置条件、命令、大多数指令,以及新变量的值。下面是一个常见的例子,把构成某个程序的所有目标文件(object)的名字放进一个变量:

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)

可以用来编译 C 程序 prog.c。由于在变量赋值中值前面的空白会被忽略,所以 foo 的值恰好是 ‘c’。(可别真的这样去写你的 makefile!)

如果美元符号后面跟着的不是美元符号、左圆括号或左花括号,那么后面那一个字符会被当作变量名。因此,你可以用 ‘$x’ 引用变量 x。不过,这种写法可能引起混淆(例如 ‘$foo’ 会被解释为变量 f 后面跟着字符串 oo)。因此,除非省略括号能显著提高可读性,否则我们建议对所有变量(即便是单字母变量)都用圆括号或花括号括起来。省略括号常常能提高可读性的一处情形,就是自动变量(请参阅自动变量)。

6.2 变量的两种种类

在 GNU make 中,变量获得值的方式有若干不同,我们称之为变量的种类(flavor)。种类的区别在于:如何处理在 makefile 中赋给它的值,以及当该变量随后被使用并展开时这些值如何被管理。

6.2.1 递归展开变量的赋值

第一种种类的变量是递归展开(recursively expanded)变量。这类变量用带 ‘=’ 的行(请参阅设置变量)或 define 指令(请参阅定义跨多行的变量)来定义。你指定的值被原封不动地存储下来。如果它的值含有对其他变量的引用,那么每当这个变量(在展开某个其他字符串的过程中)被插入时,这些引用都会被展开。当这种情况发生时,就称为递归展开(recursive expansion)。

例如,

foo = $(bar)
bar = $(ugh)
ugh = Huh?

all:;echo $(foo)

会输出 ‘Huh?’。因为 ‘$(foo)’ 展开为 ‘$(bar)’,后者又展开为 ‘$(ugh)’,最终展开为 ‘Huh?’。

这种种类的变量是 make 的大多数其他版本唯一支持的种类。它既有优点也有缺点。优点(多数人会这么说)在于,例如:

CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar

能如预期地工作:当 ‘CFLAGS’ 在命令中被展开时,会展开成 ‘-Ifoo -Ibar -O’。一个主要的缺点是,你无法像下面这样在变量末尾追加内容:

CFLAGS = $(CFLAGS) -O

因为这会导致变量展开陷入无限循环。(实际上 make 会检测到无限循环并报错。)

另一个缺点是,定义中引用的任何函数(请参阅用于变换文本的函数)都会在每次变量被展开时执行。这使得 make 运行变慢;更糟的是,它会让 wildcard 函数和 shell 函数的结果变得不可预测,因为你无法轻易控制它们何时被调用,甚至无法控制被调用多少次。

6.2.2 简单展开变量的赋值

为了避免递归展开变量的问题与不便,还有另一种种类:简单展开变量。

简单展开变量(simply expanded variable)用带 ‘:=’ 或 ‘::=’ 的行来定义(请参阅设置变量)。在 GNU make 中两者是等价的;但 POSIX 标准只规定了 ‘::=’ 这一形式(对 ‘::=’ 的支持是在 POSIX Issue 8 中加入 POSIX 标准的)。

简单展开变量的值,在变量被定义时只扫描一次,此时若有对其他变量或函数的引用便就地展开。一旦该展开完成,变量的值就再也不会被展开了。当变量被使用时,其值会被原样逐字复制,作为展开的结果。如果值中含有变量引用,那么展开结果将包含这个变量被定义时它们的那些值。因此,

x := foo
y := $(x) bar
x := later

等价于下面这样:

y := foo bar
x := later

下面是一个稍微复杂些的例子,展示了 ‘:=’ 与 shell 函数结合使用的情形。(请参阅函数 shell。)这个例子还展示了 MAKELEVEL 变量的用法,该变量在从一层传递到另一层时会发生变化。(关于 MAKELEVEL 的详情,请参阅向子make传递变量。)

ifeq (0,${MAKELEVEL})
whoami    := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

这样使用 ‘:=’ 的好处在于,典型的“进入某个目录”的命令可以写成这样:

${subdirs}:
        ${MAKE} -C $@ all

简单展开变量的行为很像大多数编程语言中的变量,因而能让复杂的 makefile 编程更可预测。借助简单展开变量,你可以用一个变量自身的值(或者用某个展开函数对自身值加工后的结果)来重新定义它,也能更高效地使用展开函数(请参阅用于变换文本的函数)。

此外,借助简单展开变量,你还能在变量值的开头放入受控的空白。输入值开头的空白字符,会在进行变量引用和函数调用的替换之前被去掉。也就是说,如果你想在变量值开头包含空白,就用变量引用来保护它们,如下所示:

nullstring :=
space := $(nullstring) # end of the line

这里变量 space 的值恰好是一个空格。注释 ‘# end of the line’ 在此仅仅是为了清晰而加上的。由于变量值末尾的空白字符不会被去掉,所以只在行尾放一个空格也能达到相同的效果(只是会很难读)。在变量值末尾放空白时,最好像这样在行尾加一条注释,以表明你的意图。反过来,如果你不想在变量值末尾保留任何空白字符,就必须注意,不要在一些空白之后随手在行尾加上一条注释,例如这样:

dir := /foo/bar    # directory to put the frobs in

这里,变量 dir 的值会变成 ‘/foo/bar    ’(末尾有 4 个空格),这恐怕不是你想要的。(想象一下用这个定义去写诸如 ‘$(dir)/file’ 这样的用法吧!)

6.2.3 即时展开变量的赋值

还有一种赋值形式会进行即时展开,但与简单赋值不同,所得到的变量是递归展开变量,也就是说每次使用时都会被重新展开。为了避免意外结果,值在被即时展开之后会自动加上引号:展开后值中所有的 $ 都会被转换成 $$。这种赋值使用 ‘:::=’ 运算符。例如,

var = first
OUT :::= $(var)
var = second

会使 OUT 变量含有文本 ‘first’。而下面这种情况,

var = one$$two
OUT :::= $(var)
var = three$$four

则会使 OUT 变量含有文本 ‘one$$two’。由于值在变量被赋值时就展开,所以结果首先是 var 第一个值的展开,即 ‘one$two’;然后在赋值完成之前,值被重新转义,从而得到最终结果 ‘one$$two’。

此后,OUT 变量被当作递归展开变量,因此在使用时会被重新展开。

这看上去与 ‘:=’ / ‘::=’ 运算符在功能上等价,但有几点不同:

第一,赋值之后,该变量是普通的递归展开变量。当你用 ‘+=’ 追加时,右边的值不会被即时展开。如果你希望 ‘+=’ 运算符即时展开右边,就应当改用 ‘:=’ / ‘::=’ 赋值。

第二,这些变量比简单展开变量略微低效一些。因为使用时它们需要被重新展开,而不只是被复制。不过,由于所有变量引用都已被转义,这次展开只是单纯地解除值的转义,并不会展开任何变量或运行任何函数。

下面再举一个例子:

var = one$$two
OUT :::= $(var)
OUT += $(var)
var = three$$four

此后,OUT 的值是文本 ‘one$$two $(var)’。当这个变量被使用时会被展开,结果是 ‘one$two three$four’。

这种赋值风格等价于传统 BSD make 的 ‘:=’ 运算符。如你所见,它与 GNU make 的 ‘:=’ 运算符的行为略有不同。:::= 运算符是在 POSIX 规范的 Issue 8 中加入的,以提供可移植性。

6.2.4 条件变量赋值

还有一个用于变量的赋值运算符 ‘?=’。它被称为条件变量赋值运算符,因为只有当变量尚未被定义时它才起作用。下面这条语句,

FOO ?= bar

与下面这段完全等价(请参阅函数 origin):

ifeq ($(origin FOO), undefined)
  FOO = bar
endif

请注意,被设为空值的变量仍被视为“已定义”。因此,‘?=’ 不会设置该变量。

6.3 变量引用的高级功能

本节介绍若干高级功能,你可以用它们以更灵活的方式引用变量。

6.3.1 替换引用

替换引用(substitution reference)在插入变量的值时,会对其施加你所指定的改动。它的形式是 ‘$(var:a=b)’(或 ‘${var:a=b}’),其含义是:取变量 var 的值,把该值中每个单词末尾的 a 都替换为 b,再插入所得到的字符串。

我们说“单词末尾”,意思是:要被替换的 a 必须后面紧跟空白,或者位于值的末尾,才会被替换;值中其他位置出现的 a 不会被改动。例如,

foo := a.o b.o l.a c.o
bar := $(foo:.o=.c)

会把 ‘bar’ 设为 ‘a.c b.c l.a c.c’。请参阅设置变量

替换引用是 patsubst 展开函数(请参阅用于字符串替换和分析的函数)的简写形式。‘$(var:a=b)’ 等价于 ‘$(patsubst %a,%b,var)’。为了与 make 的其他实现兼容,我们在 patsubst 之外还提供了替换引用。

另一种替换引用让你能充分发挥 patsubst 函数的全部能力。它的形式与上面所述的 ‘$(var:a=b)’ 相同,只是此时 a 必须含有一个 ‘%’ 字符。这种情况等价于 ‘$(patsubst a,b,$(var))’。关于 patsubst 函数的说明,请参阅用于字符串替换和分析的函数。例如,

foo := a.o b.o l.a c.o
bar := $(foo:%.o=%.c)

会把 ‘bar’ 设为 ‘a.c b.c l.a c.c’。

6.3.2 计算得到的变量名

计算得到的变量名是一个高级概念,在较为复杂的 makefile 编程中非常有用。在简单情形下你无需考虑它们,但它们有时会极为有用。

变量可以在另一个变量名内部被引用。这被称为计算得到的变量名(computed variable name),或嵌套变量引用(nested variable reference)。例如,

x = y
y = z
a := $($(x))

会把 a 定义为 ‘z’。‘$($(x))’ 内部的 ‘$(x)’ 展开为 ‘y’,所以 ‘$($(x))’ 展开为 ‘$(y)’,后者又展开为 ‘z’。这里,要引用的变量名并没有被显式写出,而是通过对 ‘$(x)’ 的展开计算得出。此处的引用 ‘$(x)’ 嵌套在外层变量引用之中。

前面的例子展示了两层嵌套,但嵌套可以有任意多层。例如,下面是三层的例子:

x = y
y = z
z = u
a := $($($(x)))

这里,最内层的 ‘$(x)’ 展开为 ‘y’,所以 ‘$($(x))’ 展开为 ‘$(y)’,后者又展开为 ‘z’。此时剩下 ‘$(z)’,它成为 ‘u’。

变量名内部对递归展开变量的引用,会照常被重新展开。例如,

x = $(y)
y = z
z = Hello
a := $($(x))

会把 a 定义为 ‘Hello’。‘$($(x))’ 变成 ‘$($(y))’,后者变成 ‘$(z)’,再变成 ‘Hello’。

嵌套变量引用与其他引用一样,也可以包含带修饰的引用和函数调用(请参阅用于变换文本的函数)。例如,使用 subst 函数(请参阅用于字符串替换和分析的函数),

x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))

最终会把 a 定义为 ‘Hello’。很难想象真有人愿意去写这么错综复杂的嵌套引用,但它确实能工作。‘$($($(z)))’ 展开为 ‘$($(y))’,后者变成 ‘$($(subst 1,2,$(x)))’。这会从 x 取得值 ‘variable1’,并通过替换把它改成 ‘variable2’,于是整个字符串变成一个简单的变量引用 ‘$(variable2)’,其值为 ‘Hello’。

计算得到的变量名不一定只由一个变量引用构成。它可以包含多个变量引用,以及一些不变的文本。例如,

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))

会根据 use_ause_dirs 的设置,给 dirs 赋予与 a_dirs1_dirsa_files1_files 之一相同的值。

计算得到的变量名也可以用在替换引用之中:

a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o

sources := $($(a1)_objects:.o=.c)

会根据 a1 的值,把 sources 定义为 ‘a.c b.c c.c’ 或 ‘1.c 2.c 3.c’。

对这种嵌套变量引用的用法,唯一的限制是:不能用它来指定要调用的函数名的一部分。这是因为,对是否为可识别的函数名的判定,发生在嵌套引用展开之前。例如,

ifdef do_sort
func := sort
else
func := strip
endif

bar := a d b g q c

foo := $($(func) $(bar))

不会把 ‘a d b g q c’ 作为参数交给 sort 函数或 strip 函数,而是会试图把变量 ‘sort a d b g q c’ 或 ‘strip a d b g q c’ 的值赋给 ‘foo’。如果将来证明这种改变是个好主意,这一限制可能会被去掉。

计算得到的变量名,也可以用在变量赋值的左边,或者用在 define 指令之中。例如:

dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print =
lpr $($(dir)_sources)
endef

这个例子定义了 ‘dir’、‘foo_sources’、‘foo_print’ 这几个变量。

请注意,嵌套变量引用递归展开变量(请参阅变量的两种种类)是颇为不同的两回事。尽管在进行 makefile 编程时,这两者会以复杂的方式被组合在一起使用。

6.4 变量如何获得它们的值

变量可以通过若干不同的方式获得值:

6.5 设置变量

要从 makefile 设置一个变量,就写一个以变量名开头的行,后面跟赋值运算符 ‘=’、‘:=’、‘::=’、‘:::=’ 之一。运算符后面的内容,在去掉行首空白后,就成为它的值。例如,

objects = main.o foo.o bar.o utils.o

定义了一个名为 objects 的变量,使其值为 ‘main.o foo.o bar.o utils.o’。变量名前后,以及 ‘=’ 之后紧跟的空白,都会被忽略。

用 ‘=’ 定义的变量是递归展开变量。用 ‘:=’ 或 ‘::=’ 定义的变量是简单展开变量;这些定义中可以含有变量引用,它们会在定义生效之前被展开。用 ‘:::=’ 定义的变量是即时展开变量。各赋值运算符在变量的两种种类中作了说明。

变量名中可以含有函数引用和变量引用,它们会在读取该行时被展开,以求得实际要使用的变量名。

变量值的长度没有限制,除了计算机的内存量之外。为了可读性,你可以把一个变量的值拆分到多个物理行(请参阅拆分长行)。

大多数变量名,只要你从未设置过它们,就被认为以空字符串作为值。有些变量具有非空的内置初始值,但你也可以用通常的方式设置它们(请参阅隐含规则所使用的变量)。有些特殊变量会在每条规则中被自动设置为新值,它们被称为自动变量(请参阅自动变量)。

如果你想只在变量尚未被设置时才设置它的值,可以用简写运算符 ‘?=’ 代替 ‘=’。下面对变量 ‘FOO’ 的两种设置是相同的(请参阅函数 origin):

FOO ?= bar

ifeq ($(origin FOO), undefined)
FOO = bar
endif

使用 shell 赋值运算符 ‘!=’,可以执行一段 shell 脚本并把变量设为它的输出。这个运算符首先求值右边,然后把求得的结果交给 shell 去执行。如果执行结果以换行结尾,这个换行会被去掉;其余所有换行都会被替换为空格。这样得到的字符串,随后被放入指定的递归展开变量中。例如,

hash != printf '\043'
file_list != find . -name '*.c'

如果执行结果中可能产生 $,而你又不希望紧随其后的内容被解释为 make 的变量引用或函数引用,那么你必须在执行的过程中把每个 $ 替换为 $$。或者,你也可以用 shell 函数调用,把运行某个程序的结果设给一个简单展开变量。请参阅函数 shell。例如,

hash := $(shell printf '\043')
var := $(shell find . -name "*.c")

shell 函数的情形一样,刚刚启动的 shell 脚本的退出状态会被存入 .SHELLSTATUS 变量。

6.6 向变量追加文本

常常会有这样的需要:向一个已经定义的变量的值再追加 (append) 一些文本。这可以用一个含有 ‘+=’ 的行来做,像这样:

objects += another.o

这会取变量 objects 的值,并向它追加文本 ‘another.o’(如果它已有值,则会在前面加一个空格)。因此,

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

但在你使用更复杂的值时,二者会有重要的差别。

当所讨论的变量此前未被定义时,‘+=’ 的行为与普通的 ‘=’ 完全相同:它定义一个递归展开变量。然而,当此前已有定义时,‘+=’ 究竟做什么,取决于你最初把它定义成了哪种种类的变量。关于变量的两种种类的说明,请参阅变量的两种种类

当你用 ‘+=’ 向变量的值追加时,make 本质上就好像你把这段额外文本包含在了该变量最初的定义之中。如果你最初用 ‘:=’ 或 ‘::=’ 定义它,使它成为简单展开变量,那么 ‘+=’ 会追加到那个简单展开的定义上,并且像 ‘:=’ 一样,在把新文本追加到旧值之前先展开它(关于 ‘:=’ 或 ‘::=’ 的完整说明,请参阅设置变量)。事实上,

variable := value
variable += more

与下面这段完全等价:

variable := value
variable := $(variable) more

另一方面,当你对一个最初用普通 ‘=’ 或 ‘:::=’ 定义为递归展开变量的变量使用 ‘+=’ 时,make 会把未经展开的文本追加到现有值之上,无论现有值是什么。这意味着

variable = value
variable += more

大致等价于:

temp = value
variable = $(temp) more

当然,它实际上并不会去定义一个叫 temp 的变量。这一点的重要性,在变量的旧值含有变量引用时就会体现出来。看看下面这个常见的例子:

CFLAGS = $(includes) -O
…
CFLAGS += -pg # enable profiling

第一行定义了 CFLAGS 变量,其中带有对另一个变量 includes 的引用。(CFLAGS 被 C 编译的规则所使用;请参阅内置规则一览。)用 ‘=’ 来定义,使 CFLAGS 成为递归展开变量,这意味着当 make 处理 CFLAGS 的定义时,‘$(includes) -O不会被展开。因此,includes 不必此时就已定义其值才会生效,只要在对 CFLAGS 的任何引用之前定义就行。如果不用 ‘+=’ 而想要向 CFLAGS 的值追加,你或许会这样做:

CFLAGS := $(CFLAGS) -pg # enable profiling

这相当接近了,但并非我们想要的。使用 ‘:=’ 会把 CFLAGS 重新定义为简单展开变量;这意味着 make 会在设置变量之前先展开文本 ‘$(CFLAGS) -pg’。如果 includes 此时尚未定义,我们就会得到 ‘ -O -pg’,而之后再定义 includes 也不会有任何效果。反过来,通过使用 ‘+=’,我们把 CFLAGS 设为未展开的值 ‘$(includes) -O -pg’。这样我们就保留了对 includes 的引用,因此如果该变量在之后某个时刻被定义,像 ‘$(CFLAGS)’ 这样的引用仍会使用它的值。

6.7 override 指令

如果一个变量已经用命令参数设置过(请参阅覆盖变量),那么 makefile 中对它的普通赋值会被忽略。如果即便该变量已用命令参数设置,你仍想在 makefile 中设置它,你可以使用 override 指令,它是这样一个行:

override variable = value

override variable := value

要向在命令行上定义的变量追加更多文本,可以这样做:

override variable += more text

请参阅向变量追加文本

带有 override 标志的变量赋值,其优先级高于除另一个 override 之外的所有赋值。此后对该变量的、未标记 override 的赋值或追加都会被忽略。

override 指令并不是为了让 makefile 与命令参数之间的争斗升级而设计的。它是为了让你能够更改用户用命令参数指定的值,或者在其之上有所增添。

例如,假设你希望在运行 C 编译器时总是带上 ‘-g’ 开关,但对于其他开关,则仍想让用户像往常一样能在命令参数中指定。你可以使用下面这个 override 指令:

override CFLAGS += -g

override 指令也可以与 define 指令一起使用。其做法正如你所预期的那样:

override define foo =
bar
endef

请参阅定义跨多行的变量

6.8 定义跨多行的变量

设置变量值的另一种方法是使用 define 指令。这个指令具有一种特别的语法,允许在值中包含换行字符,这对于定义定型的命令序列(请参阅定义定型命令),以及定义供 eval 使用的 makefile 语法片段(请参阅函数 eval),都很方便。

define 指令的同一行,后面跟着要定义的变量名和一个(可选的)赋值运算符,此外别无其他。要赋给变量的值出现在随后的几行中。值的结束由仅含 endef 一词的行来标示。

除了语法上的这点不同之外,define 的工作方式与其他任何变量定义完全相同。变量名中可以含有函数引用和变量引用,它们会在读取该指令时被展开,以求得实际要使用的变量名。

紧挨着 endef 之前的最后一个换行不会被包含在值中。如果你想让值含有末尾的换行,就必须放一个空行。例如,为了定义一个含有换行字符的变量,你必须使用两个空行,而不是一个:

define newline


endef

你也可以根据喜好省略变量赋值运算符。如果省略,make 会把它当作 ‘=’,并创建一个递归展开变量(请参阅变量的两种种类)。当使用 ‘+=’ 运算符时,与其他任何追加操作一样,值会被追加到先前的值上:用一个空格分隔旧值和新值。

你可以嵌套 define 指令:make 会跟踪嵌套的指令,如果它们没有全部用 endef 正确闭合,就会报错。请注意,以命令前缀字符开头的行被视为命令的一部分,所以出现在这类行上的任何 defineendef 字符串都不会被视为 make 指令。

define two-lines
echo foo
echo $(bar)
endef

当用在命令中时,前面这个例子在功能上等价于:

two-lines = echo foo; echo $(bar)

因为用分号分隔的两条命令,其行为很像两条各自独立的 shell 命令。不过请注意,使用两个各自独立的行意味着 make 会启动两次 shell,为每一行运行一个独立的子 shell。请参阅命令的执行

如果你想让用 define 所做的变量定义优先于命令行的变量定义,可以把 override 指令与 define 一起使用:

override define two-lines =
foo
$(bar)
endef

请参阅override 指令

6.9 取消变量的定义

如果你想清除一个变量,把它的值设为空通常就足够了。展开这样一个变量,无论它此前是否被设置过,都会得到相同的结果(空字符串)。但是,如果你正在使用 flavor 函数(请参阅函数 flavor)或 origin 函数(请参阅函数 origin),那么从未被设置过的变量与具有空值的变量之间是有区别的。在这类情形下,你可能想用 undefine 指令,让某个变量看起来仿佛从未被设置过。例如,

foo := foo
bar = bar

undefine foo
undefine bar

$(info $(origin foo))
$(info $(flavor bar))

这个例子对两个变量都会打印“undefined”。

如果你想取消一个命令行的变量定义,可以把 override 指令与 undefine 一起使用,这与对变量定义的做法类似:

override undefine CFLAGS

6.10 来自环境的变量

make 的变量可以来自运行 make 的环境。make 在启动时看到的每一个环境变量,都会被转换成一个同名同值的 make 变量。但是,makefile 中的显式赋值,或者命令参数中的赋值,会覆盖环境。(如果指定了 ‘-e’ 标志,那么来自环境的值会覆盖 makefile 中的赋值。请参阅选项一览。但这并不是推荐的做法。)

因此,只要在环境中设置变量 CFLAGS,你就能让大多数 makefile 中所有的 C 编译都使用你偏好的编译器开关。对于具有标准或惯例含义的变量来说,这是安全的,因为你知道不会有 makefile 把它们用作别的用途。(不过请注意,这并非完全可靠;有些 makefile 会显式设置 CFLAGS,因此不受环境中那个值的影响。)

make 运行命令时,makefile 中定义的一部分变量会被放入 make 所调用的每条命令的环境中。默认情况下,只有来自 make 自身环境的变量,或在其命令行上设置的变量,才会被放入命令的环境。要传递其他变量,你可以使用 export 指令。全部细节请参阅向子make传递变量

对来自环境的变量的其他使用方式,是不推荐的。让 makefile 的运行依赖于在其控制之外设置的环境变量,是不明智的,因为这会使不同的用户用同一个 makefile 得到不同的结果。这与大多数 makefile 的根本目的相违背。

这类问题尤其容易发生在变量 SHELL 上。SHELL 通常存在于环境中,用来指定用户所选择的交互式 shell。让这个选择影响到 make 是非常不可取的;因此,make 以一种特殊的方式处理 SHELL 环境变量。请参阅选择 shell

6.11 针对目标的变量值

make 中的变量值通常是全局的,也就是说(当然,除非被重新设置),无论在哪里求值都是相同的。例外是用 let 函数(请参阅函数 let)或 foreach 函数(请参阅函数 foreach)定义的变量,以及自动变量(请参阅自动变量)。

另一个例外是针对目标的变量值(target-specific variable value)。这个功能让你可以根据 make 当前正在构建的目标,为同一个变量定义不同的值。与自动变量一样,这些值只在该目标的命令的上下文中(以及在其他针对目标的赋值中)可用。

像下面这样设置一个针对目标的变量值:

target … : variable-assignment

针对目标的变量赋值,可以加上特殊关键字 exportunexportoverrideprivate 中的任何一个或全部作为前缀;它们只对该变量的这一个实例施加各自通常的行为。

如果指定多个 target,会为目标列表中的每个成员分别创建一份针对目标的变量值。

variable-assignment 可以是任何有效的赋值形式:递归(‘=’)、简单(‘:=’ 或 ‘::=’)、即时(‘::=’)、追加(‘+=’)或条件(‘?=’)。出现在 variable-assignment 中的所有变量,都在目标的上下文中求值:因此,此前定义的任何针对目标的变量值都会生效。请注意,这个变量实际上与任何“全局”的值是相互独立的:两个变量不必是同一种种类(递归还是简单)。

针对目标的变量与任何其他 makefile 变量具有相同的优先级。在命令行上提供的变量(以及在 ‘-e’ 选项生效时来自环境的变量)会优先。指定 override 指令,可以让针对目标的变量值得到优先。

针对目标的变量还有一个特殊功能:当你定义一个针对目标的变量时,该变量值也会对这个目标的所有前置条件、以及它们的前置条件,如此递归地生效(除非那些前置条件用它们自己针对目标的变量值覆盖了它)。例如,下面这样一条语句,

prog : CFLAGS = -g
prog : prog.o foo.o bar.o

会在 prog 的命令中把 CFLAGS 设为 ‘-g’,而且还会在创建 prog.ofoo.obar.o 的命令中,以及创建它们的前置条件的命令中,把 CFLAGS 设为 ‘-g’。

但请注意,在 make 的一次调用中,某个前置条件至多只会被构建一次。如果同一个文件是多个目标的前置条件,而那些目标各自对同一个针对目标的变量持有不同的值,那么最先被构建的目标会引发对该前置条件的构建,该前置条件会从第一个目标继承到针对目标的值。来自其他目标的针对目标的值会被忽略。

6.12 针对模式的变量值

除了针对目标的变量值(请参阅针对目标的变量值)之外,GNU make 还支持针对模式的变量值。在这种形式中,变量会为任何匹配所指定模式的目标而定义。

像下面这样设置一个针对模式的变量值:

pattern … : variable-assignment

其中 pattern 是一个 %-模式。与针对目标的变量值一样,如果指定多个 pattern,会为每个模式分别创建一份针对模式的变量值。variable-assignment 可以是任何有效的赋值形式。除非指定了 override,否则任何命令行的变量设置都会优先。

例如,

%.o : CFLAGS = -O

会为所有匹配模式 %.o 的目标,把 CFLAGS 赋值为 ‘-O’。

如果某个目标匹配不止一个模式,那么匹配的针对模式的变量中,词干(stem)较长的会被先解释。这使得较具体的变量优先于较通用的变量。例如,

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

lib/%.o: CFLAGS := -fPIC -g
%.o: CFLAGS := -g

all: foo.o lib/bar.o

在这个例子中,第二个定义也适用于目标 lib/bar.o,但用于更新 lib/bar.o 的却是 CFLAGS 变量的第一个定义。词干长度相同的针对模式的变量,会按它们在 makefile 中被定义的顺序来考虑。

针对模式的变量,会在为该目标显式定义的针对目标的变量之后、为父目标定义的针对目标的变量之前被搜索。

6.13 抑制继承

如前几节所述,make 的变量会被前置条件继承。这个功能让你可以根据是哪些目标引发了某个前置条件的重新构建,来改变该前置条件的行为。例如,你可以在 debug 目标上设置一个针对目标的变量,那么运行 ‘make debug’ 时该变量就会被 debug 的所有前置条件继承;而(例如)仅仅运行 ‘make all’ 则不会有这次赋值。

然而,有时你可能并不想让某个变量被继承。针对这类情形,make 提供了 private 修饰符。尽管这个修饰符可以用于任何变量赋值,但用在针对目标和针对模式的变量上最有意义。任何被标记 private 的变量,对它本地的目标可见,但不会被该目标的前置条件继承。被标记 private 的全局变量,在全局作用域中可见,但不会被任何目标继承,因而在任何命令中都不可见。

作为示例,看看下面这个 makefile:

EXTRA_CFLAGS =

prog: private EXTRA_CFLAGS = -L/usr/local/lib
prog: a.o b.o

由于 private 修饰符,a.ob.o 不会继承来自 prog 目标的 EXTRA_CFLAGS 变量赋值。

6.14 其他特殊变量

GNU make 支持一些具有特殊性质的变量。

MAKEFILE_LIST

保存 make 所解析的每个 makefile 的名字,按其被解析的顺序排列。名字会在 make 即将开始解析该 makefile 之前被追加。因此,如果一个 makefile 做的第一件事就是查看这个变量的最后一个词,那它就是当前 makefile 的名字。不过,一旦当前 makefile 使用了 include,最后一个词就会是刚刚被包含进来的 makefile。

假设有一个名为 Makefile 的 makefile,内容如下:

name1 := $(lastword $(MAKEFILE_LIST))

include inc.mk

name2 := $(lastword $(MAKEFILE_LIST))

all:
        @echo name1 = $(name1)
        @echo name2 = $(name2)

那么你可以预期看到下面的输出:

name1 = Makefile
name2 = inc.mk
.DEFAULT_GOAL

设置当命令行上未指定任何目标时所使用的默认目标(请参阅用于指定目标的参数)。借助 .DEFAULT_GOAL 变量,你可以查看当前的默认目标,可以通过清空它的值来重启默认目标选择算法,或者显式设置默认目标。下面的例子展示了这几种情形:

# Query the default goal.
ifeq ($(.DEFAULT_GOAL),)
  $(warning no default goal is set)
endif

.PHONY: foo
foo: ; @echo $@

$(warning default goal is $(.DEFAULT_GOAL))

# Reset the default goal.
.DEFAULT_GOAL :=

.PHONY: bar
bar: ; @echo $@

$(warning default goal is $(.DEFAULT_GOAL))

# Set our own.
.DEFAULT_GOAL := foo

这个 makefile 会输出:

no default goal is set
default goal is foo
default goal is bar
foo

请注意,给 .DEFAULT_GOAL 赋两个或更多目标名是无效的,会导致错误。

MAKE_RESTARTS

这个变量只在本次 make 实例发生重启时才被设置(请参阅Makefile如何被重新生成)。它含有本实例已重启的次数。请注意,这与递归(由 MAKELEVEL 变量计数的那个)是两回事。你不应当设置、修改或导出这个变量。

MAKE_TERMOUT
MAKE_TERMERR

make 在启动时会检查标准输出和标准错误输出是否会把它们的输出显示在终端上。如果是,它会分别把 MAKE_TERMOUTMAKE_TERMERR 设为终端设备的名字(如果无法判定,则设为 true)。一旦被设置,这些变量会被标记为待导出。这些变量不会被 make 改变,如果已经被设置,也不会被修改。

这些值可以用来(尤其是与输出同步(请参阅并行执行时的输出)结合使用)判定 make 自身是否正在向终端写入;例如,可以测试它们来决定是否强制命令为其输出着色。

如果你调用一个子 make 并重定向它的标准输出或标准错误输出,那么倘若你的 makefile 依赖这些变量,重置或取消导出这些变量就是你的责任。

.RECIPEPREFIX

这个变量的值的第一个字符,被用作 make 认定为引入一行命令的那个字符。如果变量为空(默认就是如此),那个字符就是标准的制表符(Tab)。例如,下面是一个有效的 makefile:

.RECIPEPREFIX = >
all:
> @echo Hello, world

.RECIPEPREFIX 的值可以被改变任意多次;一旦设置,它就会对此后解析的所有规则持续生效,直到被修改为止。

.VARIABLES

展开为到目前为止已定义的所有全局变量的名字列表。这包括具有空值的变量,以及内置变量(请参阅隐含规则所使用的变量),但不包括任何仅在针对目标的上下文中定义的变量。请注意,你赋给这个变量的任何值都会被忽略;它总是返回它那个特殊的值。

.FEATURES

展开为本版本 make 所支持的特殊功能列表。可能的值包括(但不限于)以下这些:

archives

支持使用特殊文件名语法的 ar(归档)文件。请参阅make 更新归档文件

check-symlink

支持 -L(--check-symlink-times)标志。请参阅选项一览

else-if

支持非嵌套的“else if”条件分支。请参阅条件分支的语法

extra-prereqs

支持 .EXTRA_PREREQS 特殊目标。

grouped-target

支持显式规则的分组目标语法。请参阅一条规则中的多个目标

guile

可将 GNU Guile 用作内嵌的扩展语言。请参阅GNU Guile 的集成

jobserver

支持借助“作业服务器”(jobserver)的增强并行构建。请参阅并行执行

jobserver-fifo

支持借助命名管道、通过“作业服务器”(jobserver)进行的增强并行构建。请参阅嵌入 GNU make

load

支持用于创建自定义扩展的可动态加载对象。请参阅动态对象的加载

notintermediate

支持 .NOTINTERMEDIATE 特殊目标。请参阅嵌入 GNU make

oneshell

支持 .ONESHELL 特殊目标。请参阅使用单个 shell

order-only

支持仅排序的前置条件。请参阅前置条件的类型

output-sync

支持 --output-sync 命令行选项。请参阅选项一览

second-expansion

支持前置条件列表的二次展开。

shell-export

支持把 make 变量导出给 shell 函数。

shortest-stem

在多个适用的选项中,采用“最短词干”方式来选择使用哪个模式。请参阅模式如何匹配

target-specific

支持针对目标和针对模式的变量赋值。请参阅针对目标的变量值

undefine

支持 undefine 指令。请参阅取消变量的定义

.INCLUDE_DIRS

展开为 make 为查找被包含的 makefile 而搜索的目录列表(请参阅包含其他Makefile)。请注意,改变这个变量的值并不会改变实际被搜索的目录列表。

.EXTRA_PREREQS

这个变量中的每个词,都是一个新的前置条件,会被添加到设置了它的那些目标上。这些前置条件与普通前置条件的不同之处在于,它们不会出现在任何自动变量(请参阅自动变量)中。这使你能够定义不影响命令的前置条件。

考虑一条链接程序的规则:

myprog: myprog.o file1.o file2.o
       $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)

现在假设你想增强这个 makefile,使得编译器的更新会引发程序的重新链接。你可以把编译器添加为前置条件,但必须确保它不会作为参数被传给链接命令。你需要类似下面这样的东西:

myprog: myprog.o file1.o file2.o $(CC)
       $(CC) $(CFLAGS) $(LDFLAGS) -o $@ \
           $(filter-out $(CC),$^) $(LDLIBS)

接着考虑有多个额外前置条件的情形:它们就都得被过滤掉。使用 .EXTRA_PREREQS 和针对目标的变量,可以得到一个更简单的解决办法:

myprog: myprog.o file1.o file2.o
       $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
myprog: .EXTRA_PREREQS = $(CC)

如果你想给一个不便修改的 makefile 添加前置条件,这个功能也很有用。你可以创建一个新文件,例如 extra.mk:

myprog: .EXTRA_PREREQS = $(CC)

然后调用 make -f extra.mk -f Makefile 即可。

.EXTRA_PREREQS 设为全局,会使这些前置条件被添加到所有目标(那些没有用针对目标的值自行覆盖它的目标)上。请注意,make 足够聪明,不会把列在 .EXTRA_PREREQS 中的某个前置条件添加为它自身的前置条件。


上一章 | 下一章 | 目录 | 英文原版(gnu.org)