Makefile语法[2/10]
详细内容可就太细了,不容易记忆,这里点到为止,具体使用的时候翻阅手册的对应章节。
3. Makefile总述
3.1 Makefile中有什么
目前为止,你应该已经搞清楚什么是make,什么是Makefile了。make是解释器,Makefile是具体规则,make阅读Makefile,Makefile也称为“data base"。
Makefile包含5个方面的内容:
- 显式规则
- 隐式规则
- 变量
- 文件指示(directive)
其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个Multi-Line Variable,即定义变量值可以包含多行字符的变量。 - 注释
3.1.1 断行
GNU make无所谓你的一行有多长,但是太长了不好阅读,也不符合编程规范。请用反斜杠\
断行,高阶玩家可以用.POSIX
控制断行的逻辑。
3.2 make怎么找Makefile
make怎么知道哪个是它要的配置文件呢?
- 用特殊的文件名
查找的顺序为:GNUmakefile, makefile and Makefile (官网建议用“Makefile”) - 用选项指定
你想用自定义的名字,那么用-f name
或者--file=name
3.3 include其他makefile文件
在当前makefile文件中包含其他makefile的语法是:
include filenames ...
include
(注意没有井号#)前面的空格会被解释器忽略,但是别用Tab
,否则make还以为它是个recipe呢!
filenames的格式可以是shell中合法的相对路径或者绝对路径;
除了其他makefile文件,include还可以引入变量。
假设同路径下有a.mk b.mk c.mk三个文件,而变量的值是bish bash,则:
include foo *.mk $(bar)
会被make展开成
include foo a.mk b.mk c.mk bish bash
接下来,make会在相应位置把include的文件展开,还记得吗,make中一行的长度可以无限长,尽情展开。
include的常见应用场景有:
- 不同路径下的makefile文件们共用一套变量定义
- 希望根据源文件自动生成依赖
最佳实践是,在一个makefile文件中列出依赖项,在主makefile中包含这个文件。
make去哪找这些文件呢?按顺序:
- 当前目录
- make命令的参数
-I
或者--include-dir
指定的路径 - prefix/include (通常为/usr/local/include)
- /usr/gnu/include
- /usr/local/include
- /usr/include
make先在单前makefile文件中一行行解读,尝试一个个展开,如果一时找不到先不报错,如果一直到结束还是没找到,make就真得认为这是一个fatal的错误了。
如果你想让make别管哪些找不到的文件,记得在include前加减号-
,这个用法我们在之前2.7小节的-rm
已经见过了。
3.4 环境变量MAKEFILES
如果设置了环境变量MAKEFILES,make会做一个类似include的动作,但是一般不建议使用。首先,其中的target不生效;其次,影响也大,不好部署、移植;最后,你可能有多个工程要编译,要是不小心设了这个环境变量,你可能无意中引入了一堆,产生意想不到的bug。
3.5 Makefile文件remade的过程
有时makefile文件可以由其它文件重新生成,如从RCS或SCCS文件生成等。
3.6 Makefile冲突
有时候,不同的makefile文件可能内容差不多,类似c++中父类和子类。这时可以include节省一些相同的语句。
但是,如果两个makefile文件中对同名target有不同的规则,make就糊涂了,非法操作。
高阶玩法,可以在主makefile文件(要包含其它makefile文件的那个)中,用通配符格式规则说明只有在依靠当前makefile文件中的信息不能重新创建target时,make才搜寻其它的makefile文件。
举例来说,%
是通配符,匹配任意值。
假设GNUmakefile文件中有以下语句:
foo:
frobnicate > foo
%: force
@$(MAKE) -f Makefile $@
force: ;
make foo
执行时,会运行具体的菜谱frobnicate > foo
;
make bar
执行时,并没有bar的显式规则,则会匹配%
那条规则,会运行推导出的菜谱make -f Makefile bar
,也就是跑去按照Makefile(不再是GNUmakefile)文件生成bar。
这个force干嘛的?bar依赖force,而target force并没有具体的recipe,这样force就不会又去匹配%
那条规则,不会死循环;
force有什么用呢,给target bar强行写一个依赖force,能强制make即使已经存在文件bar,也做菜。
这里真的有点绕,技巧性强,或者说,经验性强。
3.7 make怎么读Makefile文件(没懂!)
GNU的make工作时的执行步骤入下:(想来其它的make也是类似)
1、读入所有的Makefile。
2、读入被include的其它Makefile。
3、初始化文件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的target创建依赖关系链。
6、根据依赖关系,决定哪些目标要重新生成。
7、执行生成命令。
1-5步为第一个阶段read-in,6-7为第二个阶段target-update。
We say that expansion is immediate if it happens during the first phase: make
will expand that part of the construct as the makefile is parsed. We say that expansion is deferred if it is not immediate. Expansion of a deferred construct part is delayed until the expansion is used: either when it is referenced in an immediate context, or when it is needed during the second phase.
当然,这个工作方式你不一定要清楚,但是知道这个方式你也会对make更为熟悉。有了这个基础,后续部分也就容易看懂了。
变量赋值规则
条件指示必须是立即式的;
rule规则总是:
immediate : immediate ; deferred
deferred
3.8 怎么解析Makefile
怎理解recipe?菜谱,比如你要做一道红烧肉,target是红烧肉,prerequisite是肉、酱油、糖、麻绳,recipe是:肉焯水切块,煎至两面发黄,加酱油、糖大火炖烂,小火慢炖收汁,麻绳捆扎装碟。也就是这个recipe包含6个命令。
这个rule可以写成:
红烧肉 : 肉,酱油,糖,麻绳
肉焯水
切块
煎肉
加酱油糖大火炖烂
小火慢炖
麻绳捆扎装碟
GNU make按行解析Makefile,具体为:
- 读入一个逻辑行,可以包含若干物理行
- 忽略注释
- 如果某行以recipe的前缀(缺省为Tab键)开头,且当前处于rule的上下文中,在当前recipe中添加这行。
- 展开immediate元素
- 扫描分隔符,例如
:
或者=
,判断该行是宏还是rule - 生成结果,读下一行
3.9 二次展开
高阶玩家可以用.SECONDEXPANSION
打开make的二次展开开关。
4. 书写Rule
rule高速make解释器两个消息:
- 什么时候target过期
- 怎么重新生成target
4.1 Rule语法
我们早就学过rule了(2.1小节),它的一般形式包含target、prerequisite、recipe三部分,主makefile中的第一个target是默认goal,一些程序员也特意加个标签all
,用make all
生成整个软件工程。
形如:
targets : prerequisites
recipe
...
或
targets : prerequisites ; recipe
recipe
...
一条rule的targets一般只有一个,偶尔也有人写多个,多个targets用空格隔开(4.9小节),多个prerequisite也用空格隔开;
target可以包含通配符(4.3小节),target也可以是数组a的其中一个元素a(m)(11.1小节);
recipe在第5章专门介绍;
如果想在target或者prerequisite中用美元符号,那就要写成$$
(打开二次展开开关时要写成$$$$
),毕竟make中$
用于对变量取值;
4.2 prerequisite类型
一般来说,prerequisite有两个作用:
- 规定recipe被调用的顺序,也就是说,先执行prerequisite的recipe,再执行target的recipe
- 规定target和prerequisite的依赖关系
有时候,你只想规定调用顺序,不想强迫target更新,这时用到|
符号,|
符号后面的prerequisite只规定顺序:
targets : normal-prerequisites | order-only-prerequisites
这个有什么应用场景呢,这么绕?
例如,你想把target放到一个单独的文件夹,在make之前并不存在这个文件夹。
这种场景下,你希望在生成target之前创建这个文件夹,这就是顺序问题;
但是,文件夹中任何文件被添加、删除、修改、重命名等都会更新文件夹的时间戳;
我们当然不想文件夹中任何风吹草动,都导致所有的target都重新生成一遍,这就用到了order-only-prerequisites。
OBJDIR := objdir
OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)
$(OBJDIR)/%.o : %.c # 通配符匹配
$(COMPILE.c) $(OUTPUT_OPTION) $<
all: $(OBJS)
$(OBJS): | $(OBJDIR) # order-only-prerequisites
$(OBJDIR):
mkdir $(OBJDIR)
其中addprefix <prefix>, <name1 name2 ...>
命令的功能是在每个name前加上前缀。
4.3 文件名的通配符语法
make
中的通配符(wildcard characters)与Bourne shell的语法一样:
*
匹配0个或任意多个字符,也就是可以匹配任何内容
?
匹配一个任意字符
[list]
匹配list中的一个任意字符
[-list]
匹配除list中的一个任意字符
[c1-c2]
匹配c1-c2中的一个任意字符,闭区间
{string1, string2, ...}
匹配string1或string2或更多的一个任意字符串
注意,通配符看起来类似正则表达式,但是并不同,而且涉及的有且仅有上述几种。
如果你真的只是想用字符本身,记得用反义斜杠\
,例如:a\*b
的含义就是"a*b"这个字符串。
make中除了通配符外,还有其他几个特殊符号的用法:
文件名前面的~
或者~/
或者~your_user_name
代表主路径,linux、windows用户的主路径都可以设置,例如
~/bin
等效于/home/hjj/bin
;~hjj/code
等效于/home/hjj/code
;
4.3.1 通配符的例子
通配符可以在recipe、prerequisite中,但是在定义变量时不适用,例如:
objects = *.o # 这里不适用,就是简单的符号
objects2 := $(wildcard *.o) # 这样可以强行使用通配符功能
clean:
rm -f *.o
print: *.c # print依赖所有的.c文件
lpr -p $? # $?列出比目标文件(print)更新的所有依赖文件,并由lpr命令提交给打印机
touch print # 更新print的时间戳,如果没有则建立print文件。
4.3.2 通配符陷阱
通配符有时候并不是你想象的效果,需要更多技巧。
- 在定义变量时通配符并不适用
下面的objects
就是普通字符串"*.o"的含义;
在foo这条rule的prerequisite中用$
取变量的值,得到普通字符串"*.o",此时make会在prerequisite的解析过程推导通配符;
如果路径下有几个.o文件,例如a.o, aaa.o则匹配成功,功能ok;
如果路径下并没有.o文件,则匹配失败,这是make会认为这就是个普通字符串,也就是认为target foo依赖文件*.o;
哪有这么奇怪的文件呢,这根本不是你想要的效果!陷进。
objects = *.o
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)
正确的用法是,调用函数wildcard(见4.3.3小节),在匹配不到的时候忽略这一项,而不是认为一字不差照抄一个普通字符串:
objects := $(wildcard *.o)
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)
- 路径的通配符
在MS-DOS、MS-Windows等操作系统中,路径的分隔符是反斜杠,\
,例如c:\foo\bar\z.c
;
在Unix风格的操作系统中,路径的分隔符是正斜杠,/
,例如c:/foo/bar/z.c
;
当在路径中想要使用通配符时,必须使用Unix风格,因为在通配符中反斜杠用于转义字符;
4.3.3 函数wildcard
这是我们第一次接触make中的function。
通配符在recipe和prerequisite中会自动展开,但是不适用于变量定义和函数传参;
在这两种场景下,如果你还是想用通配符,就需要借助function wildcard,形如
$(wildcard pattern...)
用法举例:
objects := $(patsubst %.c,%.o,$(wildcard *.c))
foo : $(objects)
cc -o foo $(objects)
其中patsubst
是make中的又一个函数,具体的功能参考8.2小节,也就是字符串替换。
4.4 prerequisite的搜索路径
大型软件工程中,源文件和bin文件通常在不同的文件夹进行管理,make有一套文件搜索的规定,在你重新组织文件夹的时候只要根据规则设置搜索路径即可,而不需要修改makefile文件本身。
4.4.1 VPATH变量
make中的特殊变量VPATH
(注意大小写)指定target和prerequisite的搜索路径,多个路径用冒号:
或者空格分割。
VPATH = src:../headers
4.4.2 vpath提令
VPATH变量太粗了不是吗,vpath指令能更精细的指定搜索路径,用法有三种:
# 为符合pattern模式的文件名,指定搜索路径
vpath pattern directories
# 清除符合pattern模式的文件名的vpath路径
vpath pattern
# 清除所有的vpath路径
vpath
pattern怎么写呢?pattern必须是包含%
的字符串,%
用于匹配0个或任意多个字符,也就是可以匹配任何内容,类似shell通配符*
的用法。
如果文件名能匹配多个pattern怎么办?make按行阅读makefile文件,先匹配上哪个pattern就以那个为准。
4.4.3 make搜索文件夹的过程
当通过Directory Search找prerequisite时,找到的路径名可能不是在prerequisite列表中实际提供的路径名,make有一套自己的规则选择是否采用,具体为:
- 如果在指定的路径下找不到目标文件,则make开始执行路径搜索
- 如果路径搜索成功,则路径暂时(tentatively)存储下来
- 一个target的所有prerequisite使用相同的规范测试
- 处理完prerequisite的搜索之后,target有可能rebuilt
a. 如果target不需要rebuilt,make决定采用通过路径搜索找到的path
b. 如果target需要rebuilt, make抛弃通过路径搜索找到的path
这是在弄啥呢?如果target过期了,说明曾经编成功过,那make认为原来的依赖也都还存在,别找了,还用原来的。
这个规则看起来太麻烦了,简而言之:如果某个文件并不存在,但是通过路径搜索找到了一个,那就用它,甭管target要不要rebuilt。
可以用特殊变量GPATH
通知make,别把找到的路径抛弃了(thrown away),用它。
4.4.4 书写带Directory Search的recipe
如果某个prerequisite是通过路径搜索找到的,那么在这个rule的recipe不能再改变。
所以在rule中要小心书写,别写错了。怎么写呢?
make提供了一个特殊自动变量$^
,表示当前rule的所有prerequisite;
类似的,make提供另一个特殊自动变量$@
,表示target的值;$<
表示第一个prerequisite。
VPATH = src:../headers
foo.o : foo.c defs.h hack.h
cc -c $(CFLAGS) $< -o $@
4.4.5 路径搜索与隐式规则
隐式规则在找文件的时候,也会去找VPATH或者vpath指定的路径。
4.4.6 链接库的路径搜索
这里不得不补充一下,关于程序的编译和链接。
摘抄如下:
【一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。
总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是:Link 2001错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File。】
言归正传,在make中,prerequisite中指定library时不直接使用文件名,比入libname.a会写成-lname
。
当prerequisite中有形如-lname
的字段时,make的处理是:
- 找当前目录下的libname.so
- 如果找不到,找当前目录下的libname.a
- 如果找不到,在vpath和VPATH指定的路径找
- 如果找不到,在/lib、/usr/lib、prefix/lib(例如在unix系统下的/usr/local/lib)找
举例来说,如果当前路径和vpath、VPATH没有相应库文件,在/usr/lib下有libcurses.a,没有libcurses.so,则:
foo : foo.c -lcurses
cc $^ -o $@
当需要生成foo时,就会执行命令cc foo.c /usr/lib/libcurses.a -o foo
。
高级玩家可以用.LIBPATTERNS
特殊变量定制库文件的搜索模式,缺省为lib%.so lib%.a
。
4.5 Phony Targets
也就是之前提的标签,例如clean
,它在target的位置,但它并不是一个文件名。它只是一个代名词,当想要执行一定recipe时可以把这组命令起个名字做指代。特殊指令.PHONY
明确告诉make解释器,某个变量是标签而不是文件名。
.PHONY
的作用有2个:避免同名文件冲突,提高编译性能。
第一个作用,避免同名文件冲突,这个例子我们在2.5小节已经看到过了;第二个作用,提高编译性能,原理是.PHONY
告诉make不要进行隐式规则的搜索。
SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
这段脚本的本意是,遍历3个子文件夹,分别执行预设的make指令。但它存在2个问题:
- 即使某个子文件夹下的make失败,其他文件夹还是继续build
- 因为这是同一条rule,所以无法利用make并行编译的能力(详见5.4小节)。
用.PHONY
声明子文件夹(必须这么做,因为foo bar baz子文件夹肯定已经存在了,否则就啥也不执行了),就可以解决上述问题:
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo: baz
回忆4.2小节prerequisite的两个作用(规定依赖,规定顺序),subdirs: $(SUBDIRS)
这句大家都是标签而不是文件名,因此这条rule就是在规定顺序。在用户执行make subdirs
时,就会先去执行foo bar baz的rule。
$(SUBDIRS):
$(MAKE) -C $@
这些子文件夹的标签并没有依赖,所以会直接调用recipe,也就是进行子文件夹内的make。而且这实际上是3个rule,可以发挥make的并行能力。
根据最后一句脚本,执行foo文件夹之前会先执行baz文件夹。
解释完.PHONY
的作用,再来说说它的用法。
- 一般不把
.PHONY
标签作为其他非.PHONY
target的prerequisite,否则每次make这条rule都会执行。 .PHONY
target的prerequisite可以是任意,既可以也可以是.PHONY
标签,也可以是真实target。