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
3vpath %.c foo
vpath %.c blish
vpath %.c bar
其表示 “.c” 结尾的文件,先在 foo
目录,然后是 blish
,最后是 bar
目录。1
2vpath %.c foo:bar
vpath %.c blish
而上面的语句则表示 .c
结尾的文件,先在 foo
目录,然后是 bar
目录,最后才是 blis
目录。
伪目标
最早先的一个例子中,我们提到过一个 clean
的目标,这是一个 “伪目标”,因为并不生成 “clean” 这个文件1
2clean:
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
11all : 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
将清除所有要被清除的文件。cleanobj
和 cleandiff
这两个伪目标有点像 “子程序” 的意思。我们可以输入 make cleanall
和 make cleanobj
和 make 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
5subsystem:
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
8define run-action
action 1
action 2
action 3
endef
foo.o : foo.c
$(run-action)
这里的 run-action
是这个命令包的名字,在 define
和 endef
中的三行就是命令序列;可以看到,使用这个命令包就好像使用变量一样。