0%

【Tool#0x00】GNU Make 笔记

最近做ICS PA,需要看项目源码,毕竟看懂了才能往里面加东西。
借此机会学习一下GNU Make,主要是Makefile的语法和特性。于是将基础的一些高级用法(嗯?)做了一些总结放在这个笔记里。

参考资料:跟我一起写Makefile

make需要一个makefile来指示编译使用的规则,并将会根据源文件的修改日期来自动判断程序的哪一些部分需要被重新编译。

Makefile

Makefile由若干条 rule 组成,每一条 rule 都具有如下格式:

1
2
3
target: [prerequisities]
recipe (the beginning tab is necessary)
...

target是一个目标文件/可执行文件名,也可以是一个操作名(比如clean)
对于伪目标文件可以用 .PHONY: <name> 来设置。
make会将第一个目标作为其默认目标,不管它是伪的还是真的。

Makefile由五个组件组成:

  1. 显式规则:由编写者明显指出文件名、依赖文件以及生成命令
  2. 隐式规则:由make自动推断依赖文件(a.o会自动推断出a.c)
  3. 变量定义:类似于C宏,都是替换字符串
  4. 文件指示:使用include来引用另一个makefile
  5. 注释:用#表示注释

通配符使用

基础通配符如 *, ?, ~ 在make中也适用。
如果需要强行展开上述通配符,可以使用 wildcard函数,如 $(wildcard *.c)*

文件搜寻

可以用VPATH来指定make搜索依赖文件或目标文件的目录。
一种方式是类似环境变量一样,在makefile中加入:
VPATH = src:../headers,其中:表示多个VPATH

另一种方式更为灵活:
vpath <pattern> <directories>
<pattern>需要包含 % 字符。 % 的意思是匹配零或若干字符,(需引用 % ,使用 \ )例如, %.h 表示所有以 .h 结尾的文件。<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。

执行命令

在执行命令时,如果前后命令存在依赖关系,比如cd到某个目录后执行一些指令,那么应该把两条命令写在同一行,并用分号分隔。

由于make会检测每个命令的返回码(比如 return 0 或者 exit(-1)),如果检测到任何一个非0值,make都会终止执行当前规则。
为了防止这种终止,一种方法是在会出错的指令开头加上负号,一种方法是在执行make的时候加上 -i 或 --ignore-errors 参数,另一种方式是使用 .IGNORE 表示忽略该规则中所有命令的错误。

嵌套make

make也可以作为一个指令来嵌套执行,如

1
2
subsystem:
$(MAKE) -C subdir

总控Makefile的变量可以传递到下级的Makefile中,只要使用export声明即可。
有两个特殊变量会往下传递:SHELLMAKEFLAGS

-w or --print-directory 参数会让make在过程中输出进入目录的信息,在用 -C 指定子目录时会默认开启。

命令包

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

使用方法和变量一样,如 $(run-yacc)

使用变量

变量命名规范:推荐使用大小写搭配(驼峰),防止与全大写的系统变量冲突。不能包含冒号、井号、等于号和空字符。

变量定义

变量可以使用后面的变量来定义,这就允许了极度危险的递归定义(当然make可以检测到这种错误)。
为了禁用后面的变量来定义前面的变量,可以用 := 对变量进行赋值(阿姆斯特朗回旋加速阿姆斯特朗炮)。这样一来,若有引用后面才定义但前面没有定义过的变量,那么它将直接被忽略。

此外,注意变量定义时注释符#的使用。
如果这样定义一个变量:

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

那么dir的值将会是 /foo/bar ,在后面跟了四个空格!

变量使用

可以用 += 来扩充变量。
使用方法如 $(var_name)${var_name}。加上括号是为了更安全地使用变量。

条件判断

1
2
3
4
5
ifeq (a, b)
command1
else
command2
endif

如果a和b相等的话(equal),就执行command1。

类似ifeq的条件关键字有:

  1. ifeq:相等
  2. ifneq:不相等
  3. ifdef:检查变量是否有值
  4. ifndef:检查变量是否为空

使用函数

函数调用的方法和变量类似:
$(<function> <arguments>)${<function> <arguments>}

函数使用方法请见 这个链接

隐含规则

隐含规则在设计到多种语言时会显得重要,然而我只会C/C++/ASM。
详见 隐含规则 — 跟我一起写Makefile 1.0 文档

隐含规则会根据一些环境变量生成编译语句。常用的环境变量有:
$(CC) $(CFLAGS) $(CXX) $(CPPFLAGS)

模式规则

模式规则的特殊之处在于其目标定义中含有 % ,表示长度任意的非空字符串。目标中的 % 表示对于文件名的匹配,如果在依赖定义中也含有 % ,就会根据依赖名来匹配目标文件名。

在命令部分,我们可以使用一些自动化变量来匹配符合模式的文件。下面是所有的自动化变量及其说明:

  • $@ : 一个一个取出目标文件

  • $% : 仅当目标是函数库文件中,表示规则中的目标文件。例如,如果一个目标是 foo.a(bar.o) ,那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件(Unix下是 .a ,Windows下是 .lib ),那么,其值为空。

  • $< : 一个一个取出依赖文件

  • $? : 所有比目标文件新的依赖文件的集合。以空格分隔。

  • $^ : 所有的依赖文件的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。

  • $+ : 这个变量很像 $^ ,也是所有依赖文件的集合。只是它不去除重复的依赖目标。

  • $* : 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是 a.%.b ,那么, $* 的值就是 dir/foo 。这个变量对于构造有关联的文件名是比较有效。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是make所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么 $* 就是空值。

比如下面这条模式会给每一个匹配的依赖文件生成一条规则:

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

例如当前目录下有一个叫做 sdb.c 的(依赖)文件,则根据模式规则,对于它生成的规则如下:

1
2
sdb.o: sdb.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) sdb.c -o sdb.o

模式规则非常强大,若编写得当,可以省去一大堆维护项目Makefile的功夫。

Make

一些有用的参数:(其实用到了查man也行)

-f <file>--file=<file>--makefile=<file>
指定需要执行的makefile。

-n--just-print--dry-run--recon

不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

-t--touch

这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。

-q--question

这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。

-W <file>--what-if=<file>--assume-new=<file>--new-file=<file>

这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。