GNU Makefile中的变量赋值方式=、?=、:=和+=有什么区别?

956

能否给出一个清晰的解释,说明Makefile中变量赋值的工作原理。

冒号和双冒号有什么区别:

 VARIABLE = value
 VARIABLE ?= value
 VARIABLE := value
 VARIABLE += value
我已经阅读了GNU Make手册中的章节,但是对我来说仍然不太清楚。
6个回答

1238

延迟集合

VARIABLE = value

这是一种常规的变量设置方式,但是如果在value字段中提到了任何其他变量,则这些变量将递归地按照它们在使用该变量时的值展开,而不是它们在声明时的值。

立即设置

VARIABLE := value

在简单展开变量值的情况下设置变量 - 在声明时展开其中的值。

如果不存在则惰性设置

VARIABLE ?= value

仅在变量没有值时设置变量。当访问VARIABLE时,总是会计算value的值。它相当于:

ifeq ($(origin VARIABLE), undefined)
  VARIABLE = value
endif

请查看文档获取更多细节。

Append

VARIABLE += value
将提供的值追加到现有值上(如果变量不存在,则将其设置为该值)

30
A += B 这个操作会改变 B 的值吗?也就是说如果我先执行 A += B,然后 B += C,那么 A 的值是否会变成 ${B} 和 ${C} 的连接结果? - Anton Daneyko
25
正如手册中链接的部分所述,+= 操作符根据原始赋值语句具有的任何简单或递归语义进行操作。因此,它会展开 RHS,但它是立即展开还是以延迟的方式展开取决于 LHS 变量的类型。 - Etan Reisner
7
当你说变量值被扩展时,意思是该变量的值被替换为它所代表的内容。 - Sashko Lykhenko
4
请看这里以获取"expansion"的含义: http://www.gnu.org/software/make/manual/make.html#Flavors - Umair R
7
“set if absent” 是“立即设置”,还是“惰性设置”?我能否进行“惰性设置”,或者“立即设置”呢? - Woodrow Barlow
显示剩余9条评论

338

使用=会使变量被赋值。如果变量已经有了一个值,它将被替换。这个值在使用时会被扩展。例如:

HELLO = world
HELLO_WORLD = $(HELLO) world!

# This echoes "world world!"
echo $(HELLO_WORLD)

HELLO = hello

# This echoes "hello world!"
echo $(HELLO_WORLD)

使用:=与使用=类似。然而,它不是在使用时扩展值,而是在赋值时扩展值。例如:

HELLO = world
HELLO_WORLD := $(HELLO) world!

# This echoes "world world!"
echo $(HELLO_WORLD)

HELLO = hello

# Still echoes "world world!"
echo $(HELLO_WORLD)

HELLO_WORLD := $(HELLO) world!

# This echoes "hello world!"
echo $(HELLO_WORLD)

使用 ?= 语法为变量赋值,仅当变量之前未被赋值时才会生效。如果变量之前已经被赋空值 (VAR=),则该变量视为已被设置。否则,其行为与 = 相同。

使用 += 语法与使用 = 相似,但是不会替换变量的值,而是将新值添加到当前值的末尾,并在它们之间添加一个空格。如果变量之前使用了 :=,那么它将被展开。当使用该变量时,结果值也将被展开。例如:

HELLO_WORLD = hello
HELLO_WORLD += world!

# This echoes "hello world!"
echo $(HELLO_WORLD)

如果使用类似于HELLO_WORLD = $(HELLO_WORLD) world!,将导致递归,这很可能会结束执行您的Makefile。 如果使用A:= $(A) $(B),则结果将不完全相同,因为B会被:=扩展,而+=不会导致B被扩展。


7
因此,这样做的一个后果是VARIABLE = literalVARIABLE := literal始终是等效的。我理解得对吗? - aiao
4
@aiao,是的,字面值在它们的使用中是不变的。 - Sebastian
1
微妙的区别是:?:可以提高递归调用Makefile的性能。例如,如果$? = $(shell some_command_that_runs_long_time),在递归调用中,这将只被评估一次,从而提高构建性能。:=将会更慢,因为该命令不必要地运行多次。 - KeshV

79

我建议你使用"make"进行一些实验。这里有一个简单的演示,展示了=:=之间的区别。

/* Filename: Makefile*/
x := foo
y := $(x) bar
x := later

a = foo
b = $(a) bar
a = later

test:
    @echo x - $(x)
    @echo y - $(y)
    @echo a - $(a)
    @echo b - $(b)

make test 打印出:

x - later
y - foo bar
a - later
b - later bar

点击这里查看更详细的解释。


5
最好在每个食谱前面使用“@”符号,以避免结果中出现混淆的重复。 - Alexandro de Oliveira
4
Make 不支持 /* ... */ 块注释。 - yoonghm

39

当你使用 VARIABLE = value 时,如果 value 实际上是另一个变量的引用,则该值仅在使用 VARIABLE 时确定。下面通过一个例子来说明:

VAL = foo
VARIABLE = $(VAL)
VAL = bar

# VARIABLE and VAL will both evaluate to "bar"

当你使用VARIABLE := value时,你将获得value的当前值。例如:

VAL = foo
VARIABLE := $(VAL)
VAL = bar

# VAL will evaluate to "bar", but VARIABLE will evaluate to "foo"

使用VARIABLE ?= val表示只有在VARIABLE没有被设置的情况下才会设置其值。如果它已经被设置了,那么设置值的操作将被延迟到VARIABLE被使用时(如示例1)。

VARIABLE += value仅将value附加到VARIABLE上。实际的value值是在最初使用=:=时确定的。


实际上,在您的第一个示例中,VARIABLE 是 $(VAL),而 VAL 是 bar。当使用VARIABLE时,它会被扩展。 - strager
1
是的,这些注释解释了它们在使用时会发生什么。 - mipadi
啊,我猜你已经纠正了它,或者是我把“evaluate”误读成了“be”。 - strager

10
在上面的答案中,理解“值在声明/使用时会被展开”的含义非常重要。像*.c这样的值并不包含任何展开。只有当此字符串被命令使用时,它才可能触发某些通配。类似地,像$(wildcard *.c)$(shell ls *.c)这样的值并不包含任何展开,并且即使我们在变量定义中使用了:=,它也会在定义时完全求值。
请在您拥有一些C文件的目录中尝试以下Makefile:
VAR1 = *.c
VAR2 := *.c
VAR3 = $(wildcard *.c)
VAR4 := $(wildcard *.c)
VAR5 = $(shell ls *.c)
VAR6 := $(shell ls *.c)

all :
    touch foo.c
    @echo "now VAR1 = \"$(VAR1)\"" ; ls $(VAR1)
    @echo "now VAR2 = \"$(VAR2)\"" ; ls $(VAR2)
    @echo "now VAR3 = \"$(VAR3)\"" ; ls $(VAR3)
    @echo "now VAR4 = \"$(VAR4)\"" ; ls $(VAR4)
    @echo "now VAR5 = \"$(VAR5)\"" ; ls $(VAR5)
    @echo "now VAR6 = \"$(VAR6)\"" ; ls $(VAR6)
    rm -v foo.c

运行make将触发一个规则,创建一个额外的(空的)C文件,称为foo.c,但这6个变量中没有一个变量的值包含foo.c


这是一个很好的调用,并且在声明时有很多扩展示例,如果能提供一个使用时的示例和一些扩展说明就更有用了。 - Robert Monfera

9
最受赞同的答案可以改进。
让我参考GNU Make手册"设置变量""Flavors",并添加一些注释。
递归展开变量
引用文字中包含其他变量时,所指定的值会被逐字安装;每当此变量在展开某个其他字符串的过程中被替换时,这些引用就会被展开。发生这种情况时,它被称为递归展开。
foo = $(bar)
注意: 每次评估foo时,foo将会被扩展为$(bar)的值,可能导致不同的值。当然你不能称其为“懒惰”! 如果在午夜执行此操作,则可能会让您感到惊讶:
# This variable is haunted!
WHEN = $(shell date -I)

something:
    touch $(WHEN).flag

# If this is executed on 00:00:00:000, $(WHEN) will have a different value!
something-else-later: something
    test -f $(WHEN).flag || echo "Boo!"

简单扩展变量

VARIABLE := value
VARIABLE ::= value

使用“:=”或“::=”定义的变量是简单展开变量。
使用“:=”或“::=”定义的行定义了简单展开变量[...]。在GNU make中,这两种形式是等效的;但是,只有“::=”形式被POSIX标准[...] 2012描述。
一旦定义了变量,简单展开变量的值就会被全部扫描,展开对其他变量和函数的引用。
没有什么需要补充的。它立即被评估,包括递归扩展变量的递归扩展。
注意:如果VARIABLE引用ANOTHER_VARIABLE:
VARIABLE := $(ANOTHER_VARIABLE)-yohoho

如果在此赋值之前未定义ANOTHER_VARIABLE,则ANOTHER_VARIABLE将扩展为空值。

如果未设置,则进行赋值

FOO ?= bar

等同于

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

如果变量没有被设置过,那么$(origin FOO)等于undefined

注意:如果FOO在makefile、shell环境或命令行覆盖中被设置为空字符串,则不会分配bar

追加

VAR += bar

追加

当所涉及的变量之前未定义时,'+=' 的作用就像普通的 '=' 一样:它定义了一个递归展开的变量。然而,当存在先前的定义时,'+=' 的确切作用取决于您最初定义的变量类型。

因此,这将打印 foo bar

VAR = foo
# ... a mile of code
VAR += $(BAR)
BAR = bar
$(info $(VAR))

但是这将打印foo:
VAR := foo
# ... a mile of code
VAR += $(BAR)
BAR = bar
$(info $(VAR))

问题在于+=的行为取决于变量VAR之前分配的类型。

多行值

将多行值分配给变量的语法如下:

define VAR_NAME :=
line
line
endef

或者

define VAR_NAME =
line
line
endef

赋值运算符可以省略,这时它会创建一个递归扩展的变量。
define VAR_NAME
line
line
endef

最后一个 endef 前的换行符会被移除。

额外奖励: shell 赋值运算符‘!=’

 HASH != printf '\043'

是相同的

HASH := $(shell printf '\043')

不要使用它。$(shell) 调用更易读,而在 makefile 中同时使用两者是不鼓励的。至少,$(shell) 遵循 Joel 的建议,使错误代码显然错误。

当定义宏以被环境变量(如CFLAGS/CPPFLAGS/LDFLAGS)覆盖时,?==是否等效? - shadowtalker
1
@shadowtalker,不行。将变量默认设置为环境变量,但“=”会覆盖任何现有值。 还要注意的是,命令行覆盖变量(在命令行上跟随make)会覆盖Makefile中的每个赋值,除了覆盖 - Victor Sergienko
这个听起来正确吗?=:=会覆盖环境变量,环境变量会覆盖=?,命令行中的“override variables”会同时覆盖两者,而override指令则会覆盖以上所有。环境变量和命令行覆盖的交互可能会在您已经非常详尽的答案中提供非常有用的澄清。 - shadowtalker
1
这是准确的。谢谢。我认为这有点超出了问题的范围。另一方面,也没有问题是“如何获取Makefile变量的值?”也许值得提出这个问题。 - Victor Sergienko
我采纳了你的建议:https://stackoverflow.com/a/68825174/2954547 - shadowtalker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接