10 Makefile 变量进阶

868 阅读6分钟

本章节将介绍 make 的变量定义风格,变量的替换引用,环境变量、命令行变量、目标指定变量的使用及自动化变量的使用。

知识点

  • 不同的变量风格和赋值风格
  • 变量的替换引用,环境变量、命令行变量的使用
  • 目标指定变量的使用
  • 自动化变量的使用

项目结构

本章实验涉及到的代码文件位于 /home/project/make_example-master/chapter9 目录下,请在 Terminal 中通过 cd 命令切换至该目录后再进行实验学习。

chapter9 的目录结构如下:

├── auto #自动化变量的使用
│   ├── add.c
│   ├── makefile
│   └── minus.c
├── rep #变量的替换
│   ├── envi.mk
│   ├── makefile
│   └── override.mk
├── style #变量和赋值风格
│   ├── append.mk
│   ├── direct.mk
│   └── makefile
└── target #目标指定变量
    └── makefile

1️⃣ make 的递归执行示例

makefile 变量就是一个名字,代表一个文本字符串。变量有两种定义方式:

  1. 直接展开式变量:直接展开式变量通过「:=」进行定义,对其它变量的引用和函数的引用都将在定义时被展开。
  2. 递归展开式变量:可以通过「=」和「define」进行定义,在变量定义过程中,对其它变量的定义不会立即展开,而是在变量被规则使用到时才进行展开。

递归展开式变量

chapter9/style/ 目录下的 makefile 文件演示了递归展开式变量的定义和使用方式,文件内容如下:

#this makefile is for recursively vari test

.PHONY:recur loop

a1 = abc
a2 = $(a3)
a3 = $(a1)

b1 = $(b2)
b2 = $(b1)

recur:
    @echo "a1:"$(a1)
    @echo "a2:"$(a2)
    @echo "a3:"$(a3)

loop:
    @echo "b1:"$(b1)
    @echo "b2:"$(b2)

文件中 recur 规则用到 3 个变量,a1 是直接定义字符串,a2 引用后面才定义到的 a3a3 则引用 a1loop 规则用到 b1b2 两个变量,二者相互引用。

现在进入 style 目录,测试 recur 规则:

cd style;make recur

Terminal 的输出结果如下图:

可见 a1a2a3 的值是一致的,变量的展开与定义顺序无关。

再测试 loop 命令:

make loop

Terminal 的输出结果如下图:

make 因为两个变量的无限递归而报错退出。

从上面测试可以看出

  • 递归展开式的优点是此变量对引用变量的定义顺序无关

  • 缺点则是多个变量在互相引用时可能导致无限递归

  • 除此之外,递归展开式变量中若有函数引用,每次引用该变量都会导致函数重新执行,效率较低。


直接展开式变量

直接展开式变量通过":="进行定义,对其它变量的引用和函数的引用都将在定义时被展开。 文件direct.mkmakefile中的"="替换为":=",重新执行recurloop规则:

a1 := abc
a2 := $(a3)
a3 := $(a1)

b1 := $(b2)
b2 := $(b1)

分别执行 recurloop 规则:

make -f direct.mk recur;make -f direct.mk loop

Terminal 的输出结果如下图所示:

从测试结果可以看出,由于 a2b1 都引用了尚未定义的变量,因此被展开为空。

使用直接展开式变量可以避免无限递归问题和函数重复展开引发的效率问题,并且更符合一般的程序设计逻辑且便于调试,因此推荐用户尽量使用直接展开式变量。


变量追加和条件赋值

  • 使用+=赋值符号可以对变量进行追加变量追加时的赋值风格与变量定义时一致,若追加的是未定义变量,则默认以递归展开式风格进行赋值

  • 使用?=赋值符号可以对变量进行条件赋值若变量未被定义则会对变量进行赋值,否则不改变变量的当前定义。

style/ 目录下的 append.mk 文件分别演示了追加赋值和条件赋值的使用方式,内容如下:

#this makefile is for += test

.PHONY:dir recur

a1 := aa1
a1 += _a1st
a2 := _a2
a1 += $(a2)
a1 += $(a3)
a3 += $(a1)

b1 = bb1
b1 += _b1st
b2 = _b2
b1 += _b2
b1 += $(b3)
b3 += $(b1)

c1 += $(c2)
c2 += $(c1)

d1 ?= dd1
d2 = dd2
d2 ?= dd3

dir:
    @echo "a1:"$(a1)

recur:
    @echo "b1:"$(b1)

def:
    @echo "c1:"$(c1)

cond:
    @echo "d1:"$(d1)
    @echo "d2:"$(d2)
  • dirrecur 规则演示了直接展开式变量和递归展开式变量使用追加赋值的区别。
  • def 规则演示了未定义变量追加赋值的默认风格。
  • cond 演示了条件赋值的使用。

现在一个一个试试:

1. dir 演示了直接展开式变量

输出a1变量,但是a1变量+=

a1 := aa1
a1 += _a1st
a2 := _a2
a1 += $(a2)
a1 += $(a3)
a3 += $(a1)

使用+=赋值符号可以对变量进行追加变量追加时的赋值风格与变量定义时一致,若追加的是未定义变量,则默认以递归展开式风格进行赋值

这里有定义aa1,在后面增加a1st变为 aa1_a1st
然后又+=一个定义的变量a2变为 aa1_a1st_a2
后面加a3,但是a3未定义,a3加a1
结果为aa1_a1st_a2

图片.png


2. reccur 演示递归展开式变量使用追加赋值

输出b1变量

b1 = bb1
b1 += _b1st
b2 = _b2
b1 += _b2
b1 += $(b3)
b3 += $(b1)

使用+=赋值符号可以对变量进行追加变量追加时的赋值风格与变量定义时一致,若追加的是未定义变量,则默认以递归展开式风格进行赋值

首先b1=bb1
然后b1+= ,加了_b1st变为 bb1_b1st
加b2 变为 bb1_b1s _b2
加上b3,这时候b3未定义
b3又等于b3=b3+b1

会报错

图片.png

这里错误提示是自己调用自己,make因为无限递归而报错退出。


3. def 规则演示了未定义变量追加赋值的默认风格

输出c1

使用+=赋值符号可以对变量进行追加变量追加时的赋值风格与变量定义时一致,若追加的是未定义变量,则默认以递归展开式风格进行赋值

c1 += $(c2)
c2 += $(c1)

首先c1未定义,c1=c1+c2
c2=c2+c1
这样就重复调用自己,无限递归出错

图片.png


4. cond 演示了条件赋值的使用。

输出d1,d2

使用?=赋值符号可以对变量进行条件赋值若变量未被定义则会对变量进行赋值,否则不改变变量的当前定义。

d1 ?= dd1
d2 = dd2
d2 ?= dd3

首先d1 赋值,d1未被定义,改变它为dd1
然后d2=dd2
d2赋值,d2已经被定义了,所以不改变它的值,还是dd2

图片.png


2️⃣ 变量的替换

替换引用

对于已经定义的变量,可以使用「替换引用」对其指定的字符串进行替换。 替换引用的格式为 $(VAR:A=B)它可以将变量 VAR 中所有 A 结尾的字符替换为 B 结尾的字符。也可以使用模式符号将符合 A 模式的字符替换为 B 模式。

文件夹 chapter9/rep/ 中的 makefile 文件用以验证变量的替换引用,内容如下:

.PHONY:all

vari_a := fa.o fb.o fc.o f.o.o
vari_b := $(vari_a:.o=.c)
vari_c := $(vari_a:%.o=%.c)
vari_d := $(vari_a:f.o%=f.c%)

all:
    @echo "vari_a:" $(vari_a)
    @echo "vari_b:" $(vari_b)
    @echo "vari_c:" $(vari_c)
    @echo "vari_d:" $(vari_d)

文件中分别对不同的变量进行替换引用和模式替换引用。

现在进入 rep 目录并测试。

cd ../rep;make

Terminal 的输出结果如下图:

  • vari_b中的.o后缀被替换成了.c后缀,f.o.o被替换为f.o.c` ,这表明只有后缀会被替换,字符串的其它部分保持不变.
  • vari_c则是使用模式符号替换后缀,结果与vari_b 一致。
  • vari_d使用模式符号将前缀f.o替换为f.c

环境变量的使用

对于makefile来说,系统下的环境变量都是可见的。若文件中的变量名与环境变量名一致,默认引用文件中的变量。

文件envi.mk演示了变量CC与环境变量CC发生冲突时的执行情况:

.PHONY:all

CC := abc

all:
    @echo $(CC)

文件定义一个 CC 变量并赋值为 abc,执行终极目标时打印 CC 变量的内容。

我们先 export 一个环境变量 CC,再执行 envi.mk 观察两个变量是否有区别。

export CC=def;echo $CC;make -f envi.mk

输出图如下:

图片.png

说明 makefile 自定义变量优先级高于环境变量


防止环境变量被覆盖

我们可以尝试在 makefile 中取消 CC 变量的定义或者修改 PATH 变量定义然后观察会发生什么状况。

为了防止环境变量被同名变量覆盖可以使用 -e 选项,现在在刚才执行的命令后添加 -e 选项并重新执行一次并观察执行结果。

make -f envi.mk -e

终端打印:

def

命令行变量

与环境变量不同,在执行make时指定的命令行变量会覆盖makefile中同名的变量定义, 如果希望变量不被覆盖则需要使用override关键字。 override.mk文件演示了命令行参数的覆盖和override关键字的使用:

.PHONY:all

vari_a = abc
vari_b := def

override vari_c = hij
override vari_d := lmn

vari_c += xxx
vari_d += xxx

override vari_c += zzz
override vari_d += zzz

all:
    @echo "vari_a:" $(vari_a)
    @echo "vari_b:" $(vari_b)
    @echo "vari_c:" $(vari_c)
    @echo "vari_d:" $(vari_d)
    @echo "vari_e:" $(vari_e)

可见 vari_avari_c 是(=)递归展开式变量,vari_bvari_d 是(:=)直接展开式变量,vari_e 是未定义变量。

现在从命令行传入 vari_avari_e 并查看变量最终的展开值。

make -f override.mk vari_a=va vari_b=vb vari_c=vc vari_d=vd vari_e=ve

Terminal 的输出结果如下图:

从结果上可以看出,无论哪种风格的变量,都需要使用 override 指示符才能防止命令行定义的同名变量覆盖

同时,override 定义的变量在进行修改时也需要使用 override,否则修改不会生效,

验证方法如下:

make -f override.mk

Terminal 的输出结果如下图:

可见命令行没有传入变量,但 vari_cvari_d 变量仍然无法追加不用 override 指示符时的 xxx,我们可以尝试在 vari_c += xxx 这一行前面添加 override 然后再执行一次命令:

override vari_c += xxx

终端输入:

make -f override.mk

Terminal 的输出结果如下图:

可以看到 xxx 已经被成功的追加到了 vari_c 的变量后面。


3️⃣ 目标指定变量和模式指定变量

makefile 中定义的变量通常时对整个文件有效,类似于全局变量。除了普通的变量定义以外,

  • 目标指定变量定义在目标依赖项处,仅对目标上下文可见。这里的目标上下文也包括了目标依赖项的规则
  • 目标指定变量还可以定义在模式目标中,称为模式指定变量
  • 当目标中使用的变量既在全局中定义,又在目标中定义时,目标定义优先级更高
  • 需要注意:目标指定变量与全局变量是两个变量,它们的值互不影响。

chapter9/target/ 目录下的 makefile 文件演示了目标指定变量的用法,其内容如下:

.PHONY:all

vari_a=abc
vari_b=def

all:vari_a:=all_target

all:pre_a pre_b file_c
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)

pre_%:vari_b:=pat
    pre_%:
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)

file_%:
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)

makefile 中定义了 vari_avari_b 两个全局变量,目标 all 指定了一个同名的 vari_a 变量,模式目标 pre_% 指定了一个同名的 vari_b 变量。

每个目标的规则中都打印它们能看到的 vari_avari_b 的值,大家可以根据前面所述的规则推测每个目标分别会打印什么信息。

现在通过 cd 命令进入 target 目录,执行 make 命令:

cd ../target;make

Terminal 的输出内容如下图:

  • 由于终极目标all指定了vari_a"all_target",因此在整个目标重建过程中vari_a都以目标指定变量的形式出现。

  • vari_b仅在模式目标pre_%中被定义,因此对pre_apre_b来说,vari_bpat,但对file_%all目标而言,vari_b是全局变量,展开后为def


我们也可以单独以 pre_afile_c 为目标,然后观察与前面的输出内容有什么区别。

先对 pre_a 进行单独执行。

make pre_a

再对 file_c 进行单独的执行。

make file_c

Terminal 的输出结果如下图所示:

由于此时并非处于 all 目标的上下文中,所以 all 指定的 vari_a 变量失效,取而代之的是原有的值abc,而 pre_% 指定了 vari_b 变量,所以对 pre_a 来说,vari_b 变量依然是 pat


4️⃣ 自动化变量($..)

在模式规则中,一个模式目标可以匹配多个不同的目标名,但工程重建过程中经常需要指定一个确切的目标名,为了方便获取规则中的具体的目标名和依赖项,makefile 中需要用到自动化变量,自动化变量的取值是根据具体所执行的规则来决定的,取决于所执行规则的目标和依赖文件名。

自动化变量总共有七种包括:

  1. $@:目标名称。
  2. $%:若目标名为静态库,代表该静态库的一个成员名,否则为空。
  3. $<:第一个依赖项名称。
  4. $?:所有比目标文件新的依赖项列表。
  5. $^:所有依赖项列表,重名依赖项被忽略。
  6. $+:包括重名依赖项的所有依赖项列表。
  7. $*:模式规则或静态模式规则中的茎,也即"%"所代表的部分。

文件夹 chapter9/auto/ 中的 makefile 文件演示了七种自动化变量的用法,文件内容如下:

# $@ $^ $% $< $? $* $+

.PHONY:clean

PRE:=pre_a pre_b pre_a pre_c

all:$(PRE) lib -ladd
    @echo "$$""@:"$@
    @echo "$$""^:"$^
    @echo "$$""+:"$+
    @echo "$$""<:"$<
    @echo "$$""?:"$?
    @echo "$$""*:"$*
    @echo "$$""%:"$%
    @touch $@

$(PRE):pre_%:depen_%
    @echo "$$""*(in $@):"$*
    touch $@

depen_%:
    @echo "use depen rule to build:"$@
    touch $@

lib:libadd.a(add.o minus.o)
    @echo "$$""?(in $@):" $?
    touch $@

libadd.a(add.o minus.o):add.o minus.o
    @echo "$$""?(in $@):" $?
    @echo "$$""%(in $@):" $%
    $(AR) r $@ $%

clean:
    $(RM) pre_* depen_* *.a *.o lib all

从内容上看终极目标 all 的依赖项包括 pre_apre_bpre_clib 和库文件 libadd.a,其中重复包含了一次 pre_a 依赖项。模式规则 pre_% 利用静态模式,依赖于对应的 depen_% 规则,打印匹配到的「茎」,并生成目标文件。库文件规则打印 $% 并打包生成 libadd.a

由于此处会用到$(CC)进行编译,而我们之前将环境变量CC赋值为"def",现在需要将其修改回来:

export CC=gcc

注意:在执行 make 命令之前需要对 makefile 文件中的一处内容进行修改,该内容的位置在目标 libadd.a(add.o minus.o) 的命令部分。具体修改内容是添加选项 -U ,修改后的内容如下:

$(AR) r $@ $%

若没有添加 -U 选项则在执行 make 的过程中,Terminal 会打印出下面的警告:

现在进入 auto 目录并执行 make 命令:

cd ../auto;make

Terminal 的输出结果如下图所示:

make首先重建pre_a pre_b pre_c依赖项,并打印匹配到的茎a b c,接下来重建lib规则,libadd.a在重建过程中打印$%,从打印和打包命令可以看出$%展开后仅为add.o这一项文件,但静态文件目标会依据给定的文件列表展开多次。最后,make执行终极目标all的命令列表,分别打印其自动化变量,并生成all文件。 请大家仔细观察不同规则下自动化变量的变化。由于这是初次建立终极目标,因此$?得到的依赖项列表是全部的依赖项。使用touch命令更新pre_a pre_b再次测试:

touch pre_a pre_b;make

终端打印:

makefile:17: target `pre_a' given more than once in the same rule.
$@:all
$^:pre_a pre_b pre_c lib libadd.a
$+:pre_a pre_b pre_a pre_c lib libadd.a
$<:pre_a
$?:pre_a pre_b
$*:
$%:

由于 pre_apre_b 被手动更新过,现在打印的 $? 内容为 pre_apre_b

上述七个自动化变量除了直接引用外,还可以在其后增加 D 或者 F 字符获取目录名和文件名, 如:$(@D) 表示目标文件的目录名,$(@F) 表示目标文件的文件名。这种用法非常简单,也适用于所有的自动化变量。


本次介绍了 make 的变量定义风格,变量的替换引用,环境变量、命令行变量、目标指定变量的使用及自动化变量的使用。