Makefile基础教程 10

一、实验介绍--Makefile 变量

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

1.1 实验内容

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

1.2 实验知识点

  1. 变量的定义及展开时机。
  2. 递归展开变量使用"="define定义,在使用时展开。
  3. 递归展开变量的定义与书写顺序无关,但也会产生难于调试和函数重复调用的问题。
  4. 直接展开变量使用":="定义,在make读入当前行时立即展开。5.+=操作符可以对变量进行追加,展开方式与变量原始的赋值方式一致。6.?=操作符可以在变量未定义时进行赋值。
  5. 变量的替换引用可以将变量展开的内容进行字符串替换。
  6. 系统环境变量对makefile来说是可见的,但文件中的同名变量会覆盖环境变量,可以使用-e选项避免覆盖。
  7. 命令行变量比makefile中的普通变量具有更高的优先级,可以使用override关键字防止makefile中的同名变量被命令行指定变量覆盖。
  8. 目标指定变量仅在包括依赖项在内的上下文可见,类似于局部变量,优先级高于普通变量。
  9. 自动化变量可以根据具体目标和依赖项自动生成相应的文件列表。

1.3 实验环境

Ubuntu系统, GNU gcc工具,GNU make工具

1.4 适合人群

本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

1.5 代码获取

可以通过以下命令获取代码:

$ git clone https://github.com/darmac/make_example.git

二、实验原理

依据 makefile 的基本规则进行正反向实验,学习和理解规则的使用方式。

三、开发准备

进入实验楼课程即可。

四、项目文件结构

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

五、实验步骤

5.1 make 的递归执行示例

5.1.1 抓取源代码

使用如下 cmd 获取 GitHub 源代码并进入相应章节:

cd ~/Code/
git clone https://github.com/darmac/make_example.git
cd make_example/chapter9
5.1.2 递归展开式变量

makefile 变量就是一个名字,代表一个文本字符串。变量有两种定义方式:递归展开式变量和直接展开式变量。变量在makefile的读入阶段被展开成字符串。

递归展开式变量可以通过"=""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引用后面才定义到的a3,a3则引用a1

loop规则用到b1,b22个变量,二者相互引用。

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

cd style;make recur

终端打印:

a1:abc
a2:abc
a3:abc

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

再测试loop命令:

make loop

终端打印:

makefile:9: *** Recursive variable 'b1' references itself (eventually).  Stop.

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

从上面测试可以看出递归展开式的优点:此变量对引用变量的定义顺序无关。缺点则是:多个变量在互相引用时可能导致无限递归。

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

5.1.3 直接展开式变量

直接展开式变量通过":="进行定义,对其它变量的引用和函数的引用都将在定义时被展开。

文件direct.mkmakefile中的"="替换为":=",重新执行recurloop规则:

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

终端打印:

a1:abc
a2:
a3:abc
b1:
b2:

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

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

5.1.4 变量追加和条件赋值

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

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

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演示了条件赋值的使用。

分别执行四条规则:

make -f append.mk dir;make -f append.mk recur;make -f append.mk def;make -f append.mk cond

终端打印:

a1:aa1 _a1st _a2
append.mk:16: *** Recursive variable 'b1' references itself (eventually).  Stop.
append.mk:19: *** Recursive variable 'c1' references itself (eventually).  Stop.
d1:dd1
d2:dd2

请自行分析每一行打印与其原因。

实验过程如下图所示:

5.1

5.2 变量的替换

5.2.1 替换引用

对于已经定义的变量,可以使用"替换引用"对其指定的字符串进行替换。

替换引用的格式为$(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

终端打印:

vari_a: fa.o fb.o fc.o f.o.o
vari_b: fa.c fb.c fc.c f.o.c
vari_c: fa.c fb.c fc.c f.o.c
vari_d: fa.o fb.o fc.o f.c.o

vari_b中的.o后缀被替换成了.c后缀,f.o.o被替换未f.o.c,这表明只有后缀会被替换,字符串的其它部分保持不变。

vari_c则是使用模式符号替换后缀,结果与vari_b一致。

vari_d使用模式符号将前缀f.o替换为f.c

5.2.2 环境变量的使用

对于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

终端打印:

def
abc

说明makefile自定义变量优先级高于环境变量。我们也可以在makefile中取消CC变量的定义或者修改PATH变量定义看看会发生什么状况。

5.2.3 防止环境变量被覆盖

可以使用-e选项防止环境变量被同名变量覆盖,如上述实验加入-e选项:

make -f envi.mk -e

终端打印:

def
5.2.4 命令行变量

与环境变量不同,在执行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_a和 vari_c是递归展开式变量,vari_b和 vari_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

终端打印:

vari_a: va
vari_b: vb
vari_c: hij zzz
vari_d: lmn zzz
vari_e: ve

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

同时,用override定义的变量在进行修改时也需要使用override,否则修改不会生效,验证方法如下:

make -f override.mk

终端打印:

vari_a: abc
vari_b: def
vari_c: hij zzz
vari_d: lmn zzz
vari_e:

可见命令行没有传入变量,但vari_cvari_d仍然无法追加不用override指示符时的"+= zzz"

实验过程如下图所示:

5.2

5.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的值,大家可以根据前面所述的规则推测每个目标分别会打印什么信息。

进入target目录,执行make

cd ../target;make

终端打印:

pre_a : all_target
pre_a : pat
pre_b : all_target
pre_b : pat
file_c : all_target
file_c : def
all : all_target
all : def

由于终极目标all指定了vari_a"all_target",因此在整个目标重建过程中vari_a都以目标指定变量的形式出现。vari_b仅在模式目标pre_%中被定义,因此对pre_apre_b来说,vari_bpat,但对file_%all目标而言,vari_b是全局变量,展开后为def

我们也可以单独以pre_afile_c为目标,看看内容有什么区别:

make pre_a

终端打印:

pre_a : abc
pre_a : pat

再执行:

make file_c

终端打印:

file_c : abc
file_c : def

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

实验过程如下图所示:

5.3

5.4 自动化变量

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

总共有七种自动化变量:

$@:目标名称

$%:若目标名为静态库,代表该静态库的一个成员名,否则为空

$<:第一个依赖项名称

$?:所有比目标文件新的依赖项列表

$^:所有依赖项列表,重名依赖项被忽略

$+:包括重名依赖项的所有依赖项列表

$*:模式规则或静态模式规则中的茎,也即"%"所代表的部分

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)

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

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

终极目标all的依赖项包括pre_a pre_b pre_c lib和库文件libadd.a,其中重复包含了一次pre_a依赖项。

模式规则pre_%利用静态模式依赖于对应的depen_%规则,打印匹配到的茎,并生成目标文件,库文件规则打印$%并打包生成libadd.a

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

export CC=gcc

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

cd ../auto;make

终端打印:

makefile:17: target `pre_a' given more than once in the same rule.
use depen rule to build:depen_a
touch depen_a
$*(in pre_a):a
touch pre_a
use depen rule to build:depen_b
touch depen_b
$*(in pre_b):b
touch pre_b
use depen rule to build:depen_c
touch depen_c
$*(in pre_c):c
touch pre_c
gcc    -c -o add.o add.c
gcc    -c -o minus.o minus.c
$?(in libadd.a): add.o minus.o
$%(in libadd.a): add.o
ar r libadd.a add.o
ar: creating libadd.a
$?(in libadd.a): add.o minus.o
$%(in libadd.a): minus.o
ar r libadd.a minus.o
$?(in lib): add.o minus.o
touch lib
$@: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_c lib libadd.a
$*:
$%:

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_a pre_b被手动更新过,现在打印的$?内容为pre_a pre_b

上述七个自动化变量除了直接引用外,还可以在其后增加D或者F字符获取目录名和文件名,

如:$(@D)表示目标文件的目录名,$(@F)表示目标文件的文件名。这种用法非常简单,也适用于所有的自动化变量,请大家自行实验测试。

实验过程如下图所示:

5.4A

5.4B

六、实验总结

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

七、课后习题

请自行设计实验测试自动化变量的目录名和文件名的获取。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值