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,具体为:

  1. 读入一个逻辑行,可以包含若干物理行
  2. 忽略注释
  3. 如果某行以recipe的前缀(缺省为Tab键)开头,且当前处于rule的上下文中,在当前recipe中添加这行。
  4. 展开immediate元素
  5. 扫描分隔符,例如:或者=,判断该行是宏还是rule
  6. 生成结果,读下一行

3.9 二次展开

高阶玩家可以用.SECONDEXPANSION打开make的二次展开开关。

4. 书写Rule

rule高速make解释器两个消息:

  1. 什么时候target过期
  2. 怎么重新生成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有两个作用:

  1. 规定recipe被调用的顺序,也就是说,先执行prerequisite的recipe,再执行target的recipe
  2. 规定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 通配符陷阱

通配符有时候并不是你想象的效果,需要更多技巧。

  1. 在定义变量时通配符并不适用
    下面的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)
  1. 路径的通配符
    在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有一套自己的规则选择是否采用,具体为:

  1. 如果在指定的路径下找不到目标文件,则make开始执行路径搜索
  2. 如果路径搜索成功,则路径暂时(tentatively)存储下来
  3. 一个target的所有prerequisite使用相同的规范测试
  4. 处理完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的处理是:

  1. 找当前目录下的libname.so
  2. 如果找不到,找当前目录下的libname.a
  3. 如果找不到,在vpath和VPATH指定的路径找
  4. 如果找不到,在/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个问题:

  1. 即使某个子文件夹下的make失败,其他文件夹还是继续build
  2. 因为这是同一条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的作用,再来说说它的用法。

  1. 一般不把.PHONY标签作为其他非.PHONYtarget的prerequisite,否则每次make这条rule都会执行。
  2. .PHONYtarget的prerequisite可以是任意,既可以也可以是.PHONY标签,也可以是真实target。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值