Makefile 语法详解 (1)- 文件搜索、伪目标与命令执行

本文内容是之前的文章 Makefile 简介 的补充,详细介绍了 Makefile 中的文件搜索(即通过 VPATH 和 vpath 进行源文件的搜索)、伪目标(定义多个生成目标)以及执行多条命令的一些做法。

文件搜索

在一些源文件较多的大工程中,通常会把源文件分类并存放在不同的目录中 (比如自定义的头文件放在 include 目录,源文件放在 src 目录),而当 make 需要去找寻文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉 make,让 make 自动去搜索。

Makefile 文件中的特殊变量 VPATH 就是完成这个功能的,如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,make 就会在当前目录找不到的情况下,到所指定的目录中去找寻文件

1
VPATH = src:../headers

上面的的定义指定两个目录,src../headers,make 会按照这个顺序进行搜索。目录由 : 分隔;然,当然,在此之前会在当前目录查找

另一个设置文件搜索路径的方法是使用 make 的 vpath 关键字(全小写),这不是变量,这是一个 make 的关键字,这和上面提到的那个 VPATH 变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

1、vpath <pattern> <directories> 在目录 <directories> 中搜索符合模式 <pattern> 的文件

2、vpath <pattern> 清除符合模式 <pattern> 的文件的搜索路径

3、vpath 清除所有已被设置好了的文件搜索目录。

vpath 使用方法中的 <pattern> 需要包含 % 字符。% 的意思是匹配零或若干字符,如,%.h 表示所有以 .h 结尾的文件。<pattern> 指定了要搜索的文件集,而 <directories> 则指定了 <pattern> 的文件集的搜索的目录。例如:

vpath %.h ../headers 表示要 make 在 ../headers 目录下搜索所有以 .h 结尾的文件。(如果某文件在当前目录没有找到的话)

我们可以连续地使用 vpath 语句,以指定不同搜索策略。如果连续的 vpath 语句中出现了相同的 <pattern>,或是被重复了的 <pattern>,那么,make 会按照 vpath 语句的先后顺序来执行搜索。如:

1
2
3
vpath %.c foo
vpath %.c blish
vpath %.c bar

其表示 “.c” 结尾的文件,先在 foo 目录,然后是 blish ,最后是 bar 目录。

1
2
vpath %.c foo:bar
vpath %.c blish

而上面的语句则表示 .c 结尾的文件,先在 foo 目录,然后是 bar 目录,最后才是 blis 目录。

伪目标

最早先的一个例子中,我们提到过一个 clean 的目标,这是一个 “伪目标”,因为并不生成 “clean” 这个文件

1
2
clean:
rm *.o temp

为了避免伪目标名称和文件重名的这种情况,可以使用一个特殊的标记 .PHONY 来显式地指明一个目标是伪目标,如下所示

.PHONY : clean

只要有这个声明,不管是否有 clean 文件,要运行 clean 这个目标,只能运行 make clean。于是整个过程可以这样写:

1
2
3
.PHONY : clean
clean :
rm *.o temp

伪目标一般没有依赖的文件,但是也可以为伪目标指定所依赖的文件。伪目标同样可以作为 “默认目标”,只要将其放在第一个。一个常用的做法就是,如果你的 Makefile 需要一次生成若干个可执行文件,可以通过伪目标实现,如下所示

1
2
3
4
5
6
7
8
9
10
11
all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o

prog2 : prog2.o
cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o

由于 Makefile 中的第一个目标会被作为其默认目标,上面声明的伪目标 all 会作为默认目标,但由于 all 又是一个伪目标,所以不会有 all 文件产生, 但是会生成其依赖的三个文件

从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子:

1
2
3
4
5
6
7
8
9
10
.PHONY : cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
rm program

cleanobj :
rm *.o

cleandiff :
rm *.diff

make cleanall 将清除所有要被清除的文件。cleanobjcleandiff 这两个伪目标有点像 “子程序” 的意思。我们可以输入 make cleanallmake cleanobjmake cleandiff 命令来达到清除不同种类文件的目的。

命令执行

执行连续命令

执行多条命令时可以分多行写;但是如果要让上一条命令的结果应用在下一条命令时,应该使用分号或 && 分隔这两条命令。比如第一条命令是 cd 命令,并且希望第二条命令在 cd 之后的基础上运行,那么就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:

1
2
3
4
5
6
7
8
# 示例一:
exec:
cd /usr/lib/
pwd

# 示例二:
exec:
cd /usr/lib/; pwd 或 cd /usr/lib/ && pwd

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

嵌套执行 make

在一些大的工程中,往往会把不同模块或是不同功能的源文件放在不同的目录中,可以在每个目录中都书写一个该目录的 Makefile,这有利于 Makefile 变得更加地简洁且更容易维护,而不至于把所有的东西全部写在一个 Makefile 中,这个技术对于我们模块编译和分段编译有着非常大的好处。

例如,有一个子目录叫 subdir,这个目录下有个 Makefile 文件,来指明了这个目录下文件的编译规则。那么我们总控的 Makefile 可以这样书写:

1
2
3
4
5
subsystem:
cd subdir && $(MAKE)

subsystem:
$(MAKE) -C subdir

$(MAKE) 是自定义的宏变量,不直接使用 make 命令,而是定义 $(MAKE) 这个宏变量的原因是 make 有时需要一些参数,所以定义成一个变量比较利于维护。

如果要传递变量到下级 Makefile 中,那么可以使用这样的声明 export variable_name 如果不想让某些变量传递到下级 Makefile 中,那么可以这样声明 unexport variable_name 如果你要传递所有的变量,那么,只要一个 export 就行了; 后面什么也不用跟,表示传递所有的变量。

定义命令包

如果 Makefile 中出现一些相同命令序列,那么可以为这些相同的命令序列定义成一个变量。定义这种命令序列的语法以 define 开始,以 endef 结束,如:

1
2
3
4
5
6
7
8
define run-action
action 1
action 2
action 3
endef

foo.o : foo.c
$(run-action)

这里的 run-action 是这个命令包的名字,在 defineendef 中的三行就是命令序列;可以看到,使用这个命令包就好像使用变量一样。