本文是之前介绍GNU Make的一个进阶,介绍了一些稍微高级的用法。

Makefile Phony Target

使用Phony Target最重要的原因是避免在我们的Makefile中定义的只执行命令的target[此target目的为了执行对应的命令,而不需要创建这个target]和工作目录下的实际文件出现名字冲突。比如:

clean:
    rm *.o 

执行make clean这个命令,就会删除所有.o文件。但是如果当前路径存在名为clean的文件,make clean就不会执行rm *.o文件。将一个目标声明为伪目标的方法是将它作为特殊目标.PHONY的依赖。这样就避免了文件和target重名问题。

clean:
    rm *.o 
.PHONY: clean

Makefile 双冒号

双冒号规则允许在多个规则中为同一个目标指定不同的重建目标的命令。例如:

Newprog :: foo.c
    $(CC) $(CFLAGS) $< -o $@
Newprog :: bar.c
    $(CC) $(CFLAGS) $< -o $@ 

如果foo.c文件被修改,执行make以后将根据foo.c文件重建目标Newprog。而如果bar.c被修改那么Newprog将根据bar.c被重建。很明显,如果是单冒号,这两个规则有冲突就会产生错误。需要明确的是:Makefile中,一个目标可以出现在多个规则中。但是这些规则必须是同一类型的规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在两种不同类型的规则中

递归展开式变量 VS 直接展开式变量

递归方式扩展的变量。这一类型变量的定义是通过=或者使用指示符define定义的。这种变量的引用,在引用的地方是严格的文本替换过程,此变量值的字符串原模原样的出现在引用它的地方。比如,

foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)

执行make将会打印出Huh?。整个变量的替换过程时这样的,首先$(foo)被替换为$(bar),接下来$(bar)被替换为$(ugh),最后$(ugh)被替换为Hug?整个替换的过程是在执行echo $(foo)时完成的

这种类型变量在定义时,可以引用其它的之前没有定义的变量[可能在后续部分定义,或者是通过make的命令行选项传递的变量]。比如,bar就是在foo后面才定义的。但是使用此风格的变量定义,可能会由于出现变量的递归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败[比如,出现两个$(bar)的定义]。

为了避免递归展开式变量存在的问题和不方便。GNU make支持另外一种风格的变量,称为直接展开式。这种风格的变量使用:=定义。在使用:=定义变量时,变量值中对其他量或者函数的引用在定义变量时被展开[对变量进行替换]。所以变量被定义后就是一个实际需要的文本串,其中不再包含任何变量的引用

x := foo
y := $(x) bar
x := later 

就直接等价于

y := foo bar
x := later 

此风格变量在定义时就完成了对所引用变量和函数的展开,因此不能实现对其后定义变量的引用。

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

这样的写法就会报错。因为定义y的时候,x值未定义,无法读取x的值。

Makefile override

通常在执行make时,如果通过命令行定义了一个变量,那么它将替代在Makefile中出现的同名变量的定义。就是说,对于一个在Makefile中使用常规方式[使用=:=或者define]定义的变量,我们可以在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在Makefile中此变量的值。如果不希望命令行指定的变量值替代在Makefile中的变量定义,那么我们需要在Makefile中使用指示符”override”来对这个变量进行声明。比如,无论命令行指定那些编译参数,编译时必须打开“-g”选项,那么在Makefile中编译选项“CFLAGS”应该这样定义:

override CFLAGS += -g 

这样,在执行make时无论在命令行中指定了哪些编译选项[指定CFLAGS的值],编译时”-g”参数始终存在

变量在定义时使用了override,则后续对它值进行追加时,也需要使用带有override指示符的追加方式。否则对此变量值的追加不会生效。

Makefile ?=

a ?= b定义变量的方式是,只有a不存在的情况下才会对a进行定义赋值,否则不会改变a的值。

Makefile Implicit Rule

隐含规则为make提供了重建一类目标文件通用方法,它不需要在Makefile中明确地给出重建特定目标文件所需要的细节描述。例如:make对C文件的编译过程是由.c源文件编译生成.o目标文件。当 Makefile中出现一个.o文件目标时,make会使用这个通用的方式将后缀为.c的文件编译称为目标的.o文件。通常make会对那些没有命令行的规则、双冒号规则寻找一个隐含规则来执行。三个常见的隐含规则有:

  • 当编译c程序时 N.o自动由N.c生成,执行命令为$(CC) -c $(CPPFLAGS) $(CFLAGS)

  • 编译c++程序 N.o自动由N.cc生成,执行命令为$(CXX) -c $(CPPFLAGS) $(CFLAGS)。建议使用.cc作为C++源文件的后缀

  • 链接单一的object文件 N自动由N.o生成,通过C编译器使用链接器[GUN ld],执行命令是$(CC) $(LDFLAGS) N.o $(LOADLIBES) $(LDLIBS)此规则仅适用由一个源文件直接产生可执行文件的情况。当需要有多个源文件共同来创建一个可执行文件时,需要在Makefile中增加隐含规则的依赖文件

在复杂的场合,目标文件和源文件之间不存在向上边那样的名称对应关系时[xx.c对应,因此隐含规则在进行链接时,自动将x.c作为其依赖文件]。这时,需要在Makefile中明确给出描述目标依赖关系的规则[所以make的隐含规则的最大前提就是名称对应]。

Makefile自动化变量

  • $@表示规则的target文件名。如果目标是一个文档文件[Linux中,一般称.a文件为文档文件,也称为静态库文件],那么它代表这个文档的文件名。在多目标模式规则中,它代表的是那个触发规则被执行的target文件名。

  • $%当规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是foo.a(bar.o),那么,$%的值就为bar.o$@的值为foo.a如果目标不是静态库文件,其值为空[所以感觉并不是特别常用]。

  • $<规则的第一个依赖文件名。如果是一个目标文件使用隐含规则来重建,则它代表由隐含规则加入的第一个依赖文件。

  • $?所有比目标文件更新的依赖文件列表,空格分割。如果目标是静态库文件名,代表的是库成员[.o文件]。

  • $^规则的所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有库成员[.o文件]名。一个文件可重复的出现在目标的依赖中,变量$^只记录它的一次引用情况,就是说变量$^会去掉重复的依赖文件

  • $+类似$^,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。

  • $*在模式规则和静态模式规则中,代表”茎”。”茎”是目标模式中”%”所代表的部分(当文件名中存在目录时,”茎”也包含目录[斜杠之前]部分。例如:文件dir/a.foo.b,当目标的模式为a.%.b时,$*的值为dir/a.foo。”茎”对于构造相关文件名非常有用。

以上罗列的自动量变量中。其中有四个在规则中代表文件名[$@ $% $< $*]。而其它三个[$? $^ $+]在规则中代表一个文件名列表。

Makefile的通用约定

所有的Makefile中应该包含这样一行:SHELL = /bin/sh。其目的是为了避免变量SHELL在有些系统上可能继承同名的系统环境变量而导致错误。虽然在GNU版本的 make中不会出现这种问题[GNU make中变量SHELL的默认值是/bin/sh,它不同于系统环境变量SHELL]

小心处理后缀和隐含规则。不同make可识别后缀和隐含规则可能不同,它可能会导致混乱或者错误。因此在特定Makefile中明确限定可识别的后缀是一个不错的主意。在Makefile中应该这样做:

.SUFFIXES:
.SUFFIXES: .c .o

第一行首先取消掉 make 默认的可识别后缀列表,第二行重新指定可识别的后缀列表。

Makefile 常用命名方式:

  • all: 作为 Makefile 的顶层目标,一般此目标作为默认的终极目标。

  • clean: 这个伪目标定义了一组命令,这些命令的功能是删除所有由make创建的文件。

  • mostlyclean: 和”clean”伪目标功能相似。区别在于它所定义的删除命令不会全部删除由make生成的文件。比如说不需要删除某些库文件。

  • distclean/realclean/clobber: 同样类似于伪目标clean,但它们所定义的删除命令所删除的文件更多。可以包含非make创建的文件。例如,编译之前系统的配置文件、链接文件等。

  • install: 将make成功创建的可执行文件拷贝到shell环境变量PATH指定的某个目录。比如,应用可执行文件被拷贝到目录/usr/local/bin,库文件拷贝到目录/usr/local/lib目录下。

  • uninstall: 删除所有已安装文件[由install创建的文件拷贝]。规则所定义的命令不能修改编译目录下的文件,仅仅是删除安装目录下的文件。像install目标的命令一样,

  • print: 打印出所有被更改的源文件列表。

  • tar: 创建一个tar文件[归档文件包]。

  • shar: 创建一个源代码的shell文档[shar 文件]。

  • dist: 为源文件创建发布的压缩包,可以使各种压缩方式的发布包。

  • TAGS: 创建当前目录下所有源文件的符号信息[tags]文件,这个文件可被vim使用。

  • check/test: 对Makefile最后生成的文件进行检查。


文中部分内存容来自GNU Make Book特此声明。