跟我一起写Makefile读书笔记(二)

第六部分 书写命令
第七部分 使用变量

第六部分 书写命令

每条规则中的命令和操作系统中Shell的命令是一致的。make会按顺序一条一条的执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号后面的。在命令行之间的空格或是空行会被忽略,但是若该空格或是空行以[Tab]键开头,那么make会认为其是一个空命令。

make的命令默认是被 “/bin/sh” ————UNIX的标准Shell解释执行。可以指定其他的Shell。

显示命令

通常, make 会把其要执行的命令行在命令执行前输出到屏幕上。如果使用 “@” 字符在命令行前,那么这个命令将不被 make 显示出来。最具代表性的例子是我们用这个功能来在屏幕显示一些信息。比如:
@echo 正在编译XXX模块
当 make 执行时,会输出“正在编译XXX模块”字串,但不会输出命令,如果没有“@”,那么,make 将输出“echo 正在编译XXX模块”

如果 make 执行时,带入 make 参数 “-n” 或 “–just-print” ,那么其只是显示命令,但不会执行命令,这个功能有利于调试 Makefile ,便于知道书写的命令执行起来是什么样子的或是什么顺序的。

make 参数“-s”或“–slient”则是全面禁止命令的显示。

命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make 会一条一条的执行其后的命令。需要注意的是,如果希望上一条命令的结果应用于下一条命令时,应该使用分号分隔这两条命令。比如第一条命令是 cd 命令,如果希望第二条命令 pwd 命令在 cd 之后运行,那么就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分割。如:
示例一:

1
2
3
exec:
cd /home/rilzob
pwd

示例二:

1
2
exec:
cd /home/rilzob; pwd

当我们执行“ make exec”时,第一个例子中的 cd 没有作用,pwd 会打印出当前的 Makefile 目录,而第二个例子中, cd 就起作用了, pwd会打印出“/home/rilzob”。

命令出错

每当命令运行后,make 会检测每个命令的返回码,如果命令返回成功,那么make 会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成。如果一个规则中的某个命令出错了(命令退出码非零),那么 make 就会终止执行当前规则,这将有可能终止所有规则的执行。

有些时候,命令的出错并不表示就是错误的。例如 mkdir 命令,我们一定要建立一个目录,如果命令不存在,那么 mkdir 就成功执行,如果目录存在,就会出错。使用 mkdir 的原因是一定要有这样一个目录,于是我们不希望 mkdir 出错而终止规则的运行。

可以在 Makefile 的命令行前加一个减号 “-” (在[Tab]键之后)来忽略命令的出错,标记为不管命令出不出错都认为是成功的。

还有一个全局的办法,是给 make 加上 “-i” 或是 “–ignore-errors” 参数,那么 Makefile中所有命令都会忽略错误。而如果一个规则是以 “.IGNORE” 作为目标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,可以根据需要选择。

make 还有一个参数是 “-k” 或是 “–keep-going” ,这个参数的意思是如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其他规则。

嵌套执行make

在一些大的工程中,为了让 Makefile 变得更加简洁,而不至于把所有东西全部写在一个 Makefile 中,可以把不同模块模式不同功能的源文件放在不同的目录中,在每个目录中都有一个该目录的 Makefile。这么做同样会便于模块编译和分段编译。

比如当前目录下有一个子目录 subdir ,这个目录下有个 Makefile 来指明该子目录的便以规则,那么总控的 Makefile 可以这样书写:

1
2
subsystem:
cd subdir && $(MAKE)

其等价于:

1
2
subsystem:
$(MAKE) -C subdir

这两个例子的意思都是先进入“subdir”目录,然后执行 make 命令。定义 $(MAKE) 宏变量的作用是,可能 make 需要一些参数,所以定义成一个变量更易于维护。

该 Makefile 叫做 “总控Makefile”,总控 Makefile 的变量可以传递到下级的 Makefile中(如果有显示的声明),但是不会覆盖下层 Makefile 中所定义的变量,除非指定 “-e” 参数。

希望传递某些变量到下级 Makefile 的声明方式:

1
export <variable ... >

不希望传递某些变量到下级 Makefile 的声明方式:

1
unexport <variable>

示例:

1
export variable := value

其等价于:

1
2
variable := value
export variable

如果要传递所有的变量,那么只需一个export即可,后面什么也不用跟,表示传递所有的变量。

需要注意的是,有两个变量Shell和MAKEFLAGS总是要传递到下层 Makefile 中,特别是 MAKEFIAGS 变量(书中这部分可能有错误,书中是 MAKEFILES 变量),其中包含了 make 的参数信息,如果执行“总控 Makefile”时有 make 参数或是在上层Makefile中定义了这个变量,那么 MAKEFLAGS变量将会是这些参数,并且会传递到下层 Makefile中,这是一个系统级的环境变量。

同样 make 命令中也有几个参数并不往下传递,它们是-C-f-h-o-W(有关 Makefile 参数的细节将在后面说明),如果不想往下传递参数,那么可以这样:

1
2
subsystem:
cd subdir && $(MAKE) MAKEFLAGS =

还有一些比较有用的参数:

  • -w或是--print-directory 会在 make 的过程中输出一些信息,让你看到目前的工作目录。比如,下级 make 目录是/home/rilzob/gnu/make,如果使用make -w来执行,那么进入该目录时会看到
    make: Entering directory '/home/rilzob/gnu/make',完成下层 make 后离开目录时,会看到make: Leaving directory '/home/rilzob/gnu/make'
  • 使用-C参数来指定 make 下层 Makefile时,-w会被自动打开。
  • 参数中有-s(--slient)或是--no-print-directory,那么-w总是失效的。

定义命令包

语法

1
2
define ...
endef

define开始,endef结束。

作用

将 Makefile 中相同的命令序列定义为一个变量。

示例

1
2
3
4
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

这里“run-yacc”是这个命令包的名字,不要和 Makefile 中的变量重名。这个命令包中的第一个命令是运行 Yacc 程序,因为 Yacc 程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改名字。

1
2
foo.c : foo.y
$(run-yacc)

使用这个命令包像使用变量一样。在这个命令包的使用中,命令包“run-yacc”中的$^就是“foo.y”,$@就是“foo.c”(有关这种以“$”开头的特殊变量,会在后面介绍),make在执行命令包时,命令包中的每个命令会被依次独立执行。

第七部分 使用变量

  • Makefile 中定义的变量代表一个文本字串,在 Makefile 中执行的时候其会自动原模原样地展开在所使用的地方,类似于C/C++中的宏。
  • 在 Makefile 中变量可以使用在“目标”,“依赖目标”、“命令”或是 Makefile 的其它部分中,这点与C/C++所不同。

变量的命名规则:

  • 变量的命名字可以包含字符、数字、下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等)。
  • 变量是大小写敏的,“foo”、“Foo”和“FOO”是三个不同的变量名。传统的 Makefile 的变量名是全大写的命名方式,但作者推荐使用大小写搭配的变量名,如:MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。
  • 有些变量是很奇怪的字符串,如$<$@等,这些是自动化变量,会在后面介绍。

变量的基础

变量在声明时需要给与初值。
变量在使用时需要在变量名前加上”$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来,如果要使用真实的“$”字符,那么需要使用“$$”来表示。

变量可以使用在规则中的“目标”、“依赖”、“命令”以及新的变量中。

示例:

1
2
3
4
5
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)

$(objects) : defs.h

变量会在使用它的地方精确地展开,就像C/C++中的宏一样,例如:

1
2
3
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)

展开后得到:

1
2
prog.o : prog.c
cc -c prog.c

这里只是举个例子来表明 Makefile 中的变量在使用处展开的真实样子,千万不要在 Makefile 里真这么干,可见其就是一个“替代”原理。

另外,给变量加上括号完全是为了更加安全地使用这个变量,在上面的例子中如果不给变量加上括号也可以,但作者强烈建议给变量加上括号。

变量中的变量

在定义变量的值时,可以使用其他变量来构造变量的值,在 Makefile 中有两种方法来用变量定义变量的值。

“=”号

第一种方式就是简单的使用“=”号,“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧的变量不一定已定义好的值,也可以使用后面定义的值。

1
2
3
4
5
6
foo = $(bar)
bar = $(ugh)
ugh = Hub?

all:
echo $(foo)

执行make all将会打出变量$(foo)的值是Huh?$(foo)的值是$(bar)$(bar)的值是$(ugh)*$(ugh)的值是Huh?)。可见,变量是可以使用后面的变量定义。

这个功能既有优点也有缺点。
优点是可以把变量的真实值推到后面来定义,如:

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

CFLAGS在命令中被展开时,会是-Ifoo -Ibar -O

缺点是递归定义。

1
CFLAGS = $(CFLAGS) -O


1
2
A = $(B)
B = $(A)

make 会陷入无限的变量展开过程中,当然 make 有能力检测这样的定义,并会报错。并且这种方式会使 make 运行非常慢,更糟糕的是,它会使得wildcardshell(make的两个函数)发生不可预知的错误,因为不会知道这两个函数被调用多少次。

“:=”号

使用 make 的另一种变量定义方法就可以避免上面的这种问题,这种方法就是使用“:=”号。

1
2
3
x := foo
y := $(x) bar
x := later

其等价于:

1
2
y := foo bar
x := later

使用“:=”与使用“=”的不同点:
前面的变量不能使用后面的变量,只能使用前面已定义好的变量。
示例:

1
2
y := $(x) bar
x := later

y的值是“bar”,而不是“foo bar”

特殊说明

定义一个变量其值为空格

因为在操作符的右边很难描述一个空格,所以需要以下技巧。

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

nullstring是一个Empty变量,其中说明也没有,而space的值是一个空格。先用一个Empty变量来标明变量的值开始了,而后面采用“#”注释符来表示定义的终止,通过这样的方法可以定义出其值是一个空格的变量。

书中这部分还介绍了一个“#”的特性,但是我并没有看懂也没有找到相关资料

“?=”号

1
FOO ?= bar

其含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语句将什么也不做,其等价于:

1
FOO = bar

变量高级用法

这里介绍了两种变量的高级使用方法。

变量值的替换

变量值的替换就是替换变量中的共有的部分。变量值的替换方法有两种,一种是变量值的替换,另一种是把变量的值再当成变量。

第一种方法格式是$(var:a=b)或是${var:a=b},其意思是,把变量var中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。

示例:

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

这个示例先定义了一个$(foo)变量,而第二行的意思是“$(foo)”中所有以“.o”字串“结尾”全部替换为“.c”,因此$(bar)的值就是“a.c b.c c.c”。

另一种变量替换的技术是以“静态模式(参见前面章节)定义的,如:

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

这依赖于被替换字串中有相同模式,模式中必须含有一个“%”字符。这个例子中同样$(bar)的值就是“a.c b.c c.c”。

把变量的值再当成变量

通过如下示例,就会清楚如何把变量的值再当成变量。

1
2
3
x = y
y = z
a := $($(x))

在这个例子中$(x)的值是y,所以$($(x))就是$(y),于是$(a)的值就是z。(注意,是x = y而不是x = $(y)

使用上“在变量定义中使用变量”的第一个方式,再看一个例子:

1
2
3
4
x = $(y)
y = z
z = Hello
a := $($(x))

这里的$($(x))被替换成了$($(y)),因为$(y)的值是z,所以最终结果是a := $(z),也就是Hello。(注意这里代码中写的是x = $(y),但实际却相当于$(x) = $(y),因此说$($(x))被替换成了$($(y)))。

在这种方式中,可以做到使用多个变量来组成一个变量的名字,然后再取其值:

1
2
3
4
first_second = Hello
a = first
b = second
all = $($a_$b)

这里的$a_$b组成了first_second,于是$(all)的值就是Hello

再给出一个与第一种高级用法“变量值的替换”结合的例子:

1
2
3
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o = .c)

在这个例子中,如果$(a1)的值是“a”,那么$sources的值就是“a.c b.c c.c”;同理,如果$(a1)的值是“1”,那么$sources的值就是“1.c 2.c 3.c”

“把变量的值再当成变量”这种技术,同样可以用在操作符的左边:

1
2
3
4
5
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef

这个例子中定义了三个变量:dirfoo_sourcesfoo_print

追加变量值(“+=”操作符)

使用“+=”操作符给变量追加值:

1
2
objects = main.o foo.o bar.o utils.o
objects += another.o

上述代码就是对下述代码的模拟,但更为简洁:

1
2
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o

如果变量之前没有定义过,那么“+=”会自动变成“=”,如果前面有变量定义,那么“+=”会继承于前次操作的赋值符。如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符,如:

1
2
variable := value
variable += more

等价于:

1
2
variable := value
variable += more

但如果是这种情况:

1
2
variable = value
variable += more

由于前次的赋值符是“=”,所以“+=”也会以“=”来作为赋值,那么岂不会是发生变量的递归定义,然而 make 会自动这个问题。

override指示符

如果有变量通常是由 make 的命令行参数设置的,那么 Makefile 中对这个变量的赋值会被忽略。可以使用override指示符来在 Makefile 中设置这类参数的值。

语法是:

1
2
3
override <variable> = <value>
override <variable> := <value>
override <variable> += <more text>

下节会介绍多行变量,在这里提前说一下,对于多行变量的定义使用define指示符,在define指示符之前同样可以使用override指示符,如:

1
2
3
override define foo
bar
endef

多行变量

设置变量的值还可以使用define关键字,使用define关键字设置变量的值可以有换行,这有利于定义一系列的命令(前面介绍过的“命令包”就是利用这个关键字)。

define关键字后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字或是其它变量。因为命令需以[Tab]键开头,若define定义的命令变量中没有以[Tab]键开头,那么 make 就会不把其认为是命令。

示例:

1
2
3
4
define two-lines # two-lines是变量的名字
echo foo # foo是变量的值
echo $(bar)
endef

环境变量

make 运行时的系统环境变量可以在 make 开始运行时被载入到 Makefile 文件中,但若 Makefile 中已定义了这个变量或是这个变量由 make 命令行带入,那么系统的环境变量的值将被覆盖。(类似于全局变量和局部变量的概念)
如果 make 指定了“-e”参数,那么系统环境变量将覆盖 Makefile 中定义的变量

如果在环境变量中设置某一环境变量,那么就可以在所有的 Makefile 中使用这个变量,这对使用统一的编译参数有比较大的好处。

嵌套调用时的环境变量

当 make 嵌套调用时(参见前面的“嵌套调用”章节),上层 Makefile 中定义的变量会以系统环境变量的方式传递到下层的 Makefile 中。默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层 Makefile 传递,则需要使用 export 关键字来表明。(参见前面章节)

作者并不建议把许多变量都定义在系统环境中。因为这样在执行不用的 Makefile 时,拥有的是同一套系统变量,这可能会带来更多的麻烦。

目标变量(规则变量)

目标变量的值依赖于规则的目标和依赖目标的定义。并且同样可以为某个目标设置局部变量,这种变量被称为“Target-specific Variable”,它可以和 Makefile 中定义的变量同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效,而不会影响规则链以外的全局变量的值。

其语法是:

1
2
<target ...> : <variable-assignment>
<target ...> : override <variable-assignment>

<variable-assignment>可以是前面讲过的各种赋值表达式,如“=”、“:=”、“+=”或是“?=”。第二个语法是针对于 make 命令行带入的变量,或是系统环境变量。这个特性十分有用,借此我们可以设置一个变量,该变量会作用到由这个目标所引发的所有的规则中去。如:

1
2
3
prog : CFLAGS = -g  # prog对应上述语法的<target...>,CFLAGS = -g则对应上述语法中的<variable-assignment>
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o

在这个示例中,不管全局的$(CFLAGS)的值是什么,在prog目标以及其所引发的所有规则中(prog.o foo.o bar.o的规则中),$(CFLAGS)的值都是-g

模式变量

在GNU的 make 中,还支持模式变量(Pattern-specific Variable)。目标变量可以将变量定义在某个目标上,模式变量则在这上面更进一步,可以把变量定义在符合这种模式的所有目标上。

make 的“模式”一般是至少含有一个“%”的,例如:

1
%.o : CFLAGS = -O

上述指令的意思是给所有以[.o]结尾的目标定义目标变量。

其语法与目标变量的语法一样,是:

1
2
<pattern ...> : <variable-assignment>
<pattern ...> : override <variable-assignment>

override同样是针对于系统环境传入的变量,或是 make 命令行指定的变量。