文档下载链接:《GNU_Make中文手册》
make是一个命令工具,它解释Makefile中的规则。在Makefile文件中描述了整个工程所有文件的编译、链接等规则。make执行时,根据Makefile的规则检查文件的修改情况,决定是否执行定义的动作(那些修改过的文件将会被重新编译),这是GNU make的执行依据。
准备知识
编译:把高级语言书写的代码转换为机器可识别的机器指令。编译时,编译器检查高级语言的语法、函数与变量的声明是否正确。通常,一个高级语言的源文件都可对应一个目标文件。目标文件在Linux中默认后缀为“.o”(如“foo.c”的目标文件为“foo.o”)。
为了和规则的目标文件相区别,本文将编译高级语言后生成的目标文件称为.o文件。
链接:将多.o文件,或者.o文件和库文件链接成为可被操作系统执行的可执行程序(Linux环境下,可执行文件的格式为“ELF”格式)。链接器不检查函数所在的源文件,只检查所有.o文件中定义的符号。将.o文件中使用的函数和其它.o或者库文件中的相关符号进行合并,对所有文件中的符号进行重新安排(重定位),并链接系统相关文件(程序启动文件等)最终生成可执行程序。链接过程使用GNU 的“ld”工具。
静态库:又称为文档文件(Archive File)。它是多个.o文件的集合。Linux中静态库文件的后缀为“.a”。静态库中的各个成员(.o文件)没有特殊的存在格式,仅仅是一个.o文件的集合。使用“ar”工具维护和管理静态库。
共享库:也是多个.o文件的集合,但是这些.o文件时有编译器按照一种特殊的方式生成(Linux中,共享库文件格式通常为“ELF”格式。共享库已经具备了可执行条件)。模块中各个成员的地址(变量引用和函数调用)都是相对地址。使用此共享库的程序在运行时,共享库被动态加载到内存并和主程序在内存中进行连接。多个可执行程序可共享库文件的代码段(多个程序可以共享的使用库中的某一个模块,共享代码,不共享数据)。另外共享库的成员对象可被执行(由libdl.so提供支持)。
参考 info ld了解更加详细的关于ld的说明和用法。
当使用make工具进行编译时,工程中以下几种文件在执行make时将会被编译(重新编译):
a. 所有的源文件没有被编译过,则对各个C源文件进行编译并进行链接,生成最后的可执行程序;
b. 每一个在上次执行make之后修改过的C源代码文件在本次执行make时将会被重新编译;
c. 头文件在上一次执行make之后被修改,则所有包含此头文件的C源文件在本次执行make时将会被重新编译。
Makefile规则包含了文件之间的依赖关系和更新此规则目标所需要的命令。一个简单的Makefile描述规则如下表示:
TARGET... : PREREQUISITES...
COMMAND
...
...
target:规则的目标。可以是.o文件、也可以是可执行程序文件名等。另外,目标也可以是一个make执行的动作的名称,如目标“clean”,我们称这样的目标是“伪目标”。
prerequisites:规则的依赖。生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。
command:规则的命令行。规则所要执行的动作(任意的shell命令或者是可在shell下执行的程序)。一个规则可以有多个命令行,每一条命令占一行。
注意:每一个命令行必须以[Tab]字符开始,[Tab]字符告诉make此行是一个命令行。这也是书写Makefile中容易产生,而且比较隐蔽的错误。
一个Makefile文件中通常还包含了除规则以外的东西。一个最简单的Makefile可能只包含规则。make程序根据规则的依赖关系,决定是否执行规则所定义的命令的过程我们称之为执行规则。
示例:我们将书写一个简单的Makefile,来描述如何创建最终的可执行文件“edit”,此可执行文件依赖于8个C源文件和3个头文件。Makefile文件的内容如下:
首先,书写时可以将一个较长行使用反斜线(\)来分解为多行,这样可以使我们的Makefile书写清晰、容易阅读理解。但需要注意:反斜线之后不能有空格(这也是大家最容易犯的错误,错误比较隐蔽)。
在这个Makefile中,我们的目标(target)就是可执行文件“edit”和那些.o文件(main.o,kbd.o,…);依赖(prerequisites)就是冒号后面的那些.c 文件和 .h文件。所有的.o文件既是依赖(相对于可执行程序edit)又是目标(相对于.c和.h文件)。
所有的命令行必需以[Tab] 字符开始,但并不是所有的以[Tab]键出现行都是命令行。但make程序会把出现在第一条规则之后的所有以[Tab]字符开始的行都作为命令行来处理。
Makefile中把那些没有任何依赖只有执行动作的目标称为“伪目标”(phony targets)。目标“clean”没有任何依赖文件,它只有一个目的,就是通过这个目标名来执行它所定义的命令。
Make如何工作
默认的情况下,make执行的是Makefile中的第一个规则,此规则的第一个目标称之为“最终目的”或者“终极目标”(就是一个Makefile最终需要更新或者创建的目标)。就是说,第一个规则的第一个目标称为最终目标或者是终极目标。
当在shell提示符下输入“make”命令以后,make读取当前目录下的Makefile文件,并将Makefile文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。
make在执行这个规则所定义的命令之前,首先处理目标“edit”的所有的依赖文件(例子中的那些.o文件)的更新规则(以这些.o文件为目标的规则)。对这些.o文件为目标的规则处理有下列三种情况:
a. 目标.o文件不存在,使用其描述规则创建它;
b. 目标.o文件存在,目标.o文件所依赖的.c源文件、.h文件中的任何一个比目标.o文件“更新”(在上一次make之后被修改)。则根据规则重新编译生成它;
c. 目标.o文件存在,目标.o文件比它的任何一个依赖文件(的.c源文件、.h文件)“更新”(它的依赖文件在上一次make之后没有被修改),则什么也不做。
这些.o文件所在的规则之所以会被执行,是因为这些.o文件出现在“终极目标”的依赖列表中。在Makefile中一个规则的目标如果不是“终极目标”所依赖的(或者“终极目标”的依赖文件所依赖的),那么这个规则将不会被执行,除非明确指定执行这个规则。
完成了对.o文件的创建(第一次编译)或者更新之后,make程序将处理终极目标“edit”所在的规则,分为以下三种情况:
a. 目标文件“edit”不存在,则执行规则以创建目标“edit”。
b. 目标文件“edit”存在,其依赖文件中有一个或者多个文件比它“更新”,则根据规则重新链接生成“edit”。
c. 目标文件“edit”存在,它比它的任何一个依赖文件都“更新”,则什么也不做。
总结一下
a. 首先,对于一个Makefile文件,“make”解析终极目标所在的规则,根据其依赖文件依次(按照依赖文件列表从左到右的顺序)寻找创建这些依赖文件的规则。
b. 其次,为第一个依赖文件(main.o)寻找创建规则,如果第一个依赖文件依赖于其它文件(main.c、defs.h),则同样为这个依赖文件寻找创建规则(创建main.c和defs.h的规则,通常源文件和头文件已经存在,也不存在重建它们的规则)……,直到为所有的依赖文件找到合适的创建规则。
c. 最后,make从最后一层规则(上例目标为main.o的规则)回退开始执行,最终完成终极目标的第一个依赖文件的创建和更新。之后对第二个、第三个、第四个……终极目标的依赖文件执行同样的过程(上例的的顺序是“main.o”、“kbd.o”、“command.o”……)。
创建或者更新每一个规则依赖文件的过程,类似于c语言中的递归过程。对于任意一个规则执行的过程都是按照依赖文件列表顺序,对于规则中的每一个依赖文件,使用同样方式(按照同样的过程)去重建它,在完成对所有依赖文件的重建之后,最后一步才是重建此规则的目标。
更新(或者创建)终极目标的过程中,如果任何一个规则执行出错,make就立即报错并退出。整个过程make只是负责执行规则,一个规则的依赖关系是否正确、描述重建目标的规则命令行是否正确,make不做任何错误检查。
因此,要正确的编译一个工程,需要在提供给make程序的Makefile中来保证其依赖关系的正确性、和执行命令的正确性。
自动推导规则
在使用make编译.c源文件时,编译.c源文件规则的命令可以不用明确给出。make存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。它执行命令“cc -c”来编译.c源文件。此默认规则称为make的隐含规则。
这样,在书写Makefile时,就可以省略掉描述.c文件和.o依赖关系的规则,而只需要给出那些特定的规则描述(.o目标所需要的.h文件)。因此上边的例子就可以以更加简单的方式书写,其中使用了变量“objects”。Makefile内容如下:
假如只有一个.c的源文件mtest.c,那么其Makefile可以简化为“mtest:”。更加简单的方法是:直接在shell里输入“make mtest”,无需Makefile文件!
#使用规范写法
mtest: mtest.c
cc -o mtest mtest.c
#使用隐含规则
mtest:
清除工作目录过程文件
在实际应用时,清除当前目录中编译过程中产生的临时文件使用如下写法:
.PHONY : clean
clean :a. 通过“.PHONY”特殊目标将“clean”目标声明为伪目标。避免当磁盘上存在一个名为“clean”文件时,目标“clean”所在规则的命令无法执行。
b. 在命令行之前使用“-”,意思是忽略命令“rm”的执行错误。
Makefile的内容
在一个完整的Makefile中,包含了5个东西:显式规则、隐含规则、变量定义、指示符和注释。
> 显式规则:它描述了在何种情况下如何更新一个或者多个被称为目标的文件(Makefile的目标文件)。书写Makefile时需要明确地给出目标文件、目标的依赖文件列表以及更新目标文件所需要的命令(有些规则没有命令,这样的规则只是纯粹的描述了文件之间的依赖关系)。
> 隐含规则:它是make根据一类目标文件(典型的是根据文件名的后缀)而自动推导出来的规则。make根据目标文件的名,自动产生目标的依赖文件并使用默认的命令来对目标进行更新(建立一个规则)。
> 变量定义:使用一个字符或字符串代表一段文本串,当定义了一个变量以后,Makefile后续在需要使用此文本串的地方,通过引用这个变量来实现对文本串的使用。
> Makefile指示符:指示符指明在make程序读取makefile文件过程中所要执行的一个动作。其中包括:
a. 读取一个文件,读取给定文件名的文件,将其内容作为makefile文件的一部分;
b. 决定(通常是根据一个变量的得值)处理或者忽略Makefile中的某一特定部分;
c. 定义一个多行变量。
> 注释:Makefile中“#”字符后的内容被作为是注释内容(和shell脚本一样)处理。如果此行的第一个非空字符为“#”,那么此行为注释行。注释行的结尾如果存在反斜线(\),那么下一行也被作为注释行。一般在书写Makefile时推荐将注释作为一个独立的行,而不要和Makefile的有效行放在一行中书写。当在Makefile中需要使用字符“#”时,可以使用反斜线加“#”(\#)来实现(对特殊字符“#”的转义),其表示将“#”作为一字符而不是注释的开始标志。
包含其它makefile文件
Makefile中包含其它文件使用关键字“include”,和c语言一致。“include”指示符告诉make暂停读取当前的Makefile,而转去读取“include”指定的一个或者多个文件,完成以后再继续当前Makefile的读取。Makefile中指示符“include”书写在独立的一行,其形式如下:
include FILENAMES...
FILENAMES是shell所支持的文件名(可以使用通配符)。包含进来的Makefile中,如果存在变量或者函数的引用,它们将会被展开。
一个例子,存在三个.mk文件a.mk、b.mk、c.mk,“$(bar)”被扩展为“bish bash”,则:
include foo *.mk $(bar)
等价于
include foo a.mk b.mk c.mk bish bash
指示符“include”使用场合
a. 有多个不同的程序,由不同目录下的几个独立的Makefile来描述其重建规则。它们需要使用一组通用的变量定义或者模式规则。通用的做法是将这些共同使用的变量或者模式规则定义在一个文件中,在需要使用的Makefile中使用指示符“include”来包含此文件。
b. 当根据源文件自动产生依赖文件时,我们可以将自动产生的依赖关系保存在另外一个文件中,主Makefile使用指示符“include”包含这些文件。这样的做法比直接在主Makefile中追加依赖文件的方法要明智的多。
如果指示符“include”指定的文件在绝对路径(如/usr/src/Makefile...)和当前目录下都找不到,make将试图在以下目录下查找:
首先,查找命令行选项“-I”或者“--include-dir”指定的目录;如果未找到,则继续依此搜索以下目录(如果其存在):“/usr/gnu/include”、“/usr/local/include”和“/usr/include”。
当在这些目录下都没有找到“include”指定的文件时,make将会提示一个包含文件未找到的告警提示,但是不会立刻退出。当完成读取整个Makefile后,make将试图使用规则来创建通过指示符“include”指定的但未找到的文件,当不能创建它时(没有创建这个文件的规则),make将提示致命错误并退出,会输出类似如下错误提示:
Makefile:错误的行数:未找到文件名:提示信息(No such file or directory)
Make: *** No rule to make target ‘
两种方式的比较
在Makefile中使用“-include”代替“include”,可忽略由于包含文件不存在或者无法创建时的错误提示。“-”的意思是告诉make,忽略此操作的错误。make继续执行。
使用“include FILENAMES...”:make程序处理时,如果“FILENAMES”列表中的任何一个文件不能正常读取而且不存在一个创建此文件的规则时,make程序将会提示错误并退出。
使用“-include FILENAMES...”:当所包含的文件不存在或者不存在一个规则去创建它,make程序会继续执行,只有真正由于不能正确完成终极目标的重建时(某些必需的目标无法在当前已读取的makefile文件内容中找到正确的重建规则),才会提示致命错误并退出。
为了和其它的make程序进行兼容,也可以使用“sinclude”来代替“-include”(GNU所支持的方式)。
变量 MAKEFILES
如果在当前环境,定义了一个“MAKEFILES”环境变量,make执行时首先将此变量的值作为需要读入的Makefile文件,多个文件之间使用空格分开。类似使用指示符“include”包含其它Makefile文件一样,如果文件名非绝对路径而且当前目录也不存在此文件,make会在一些默认的目录去寻找。它和使用“include”的区别:
a. 环境变量指定的makefile文件中的“目标”不会被作为make执行的“终极目标”;
b. 环境变量所定义的文件列表,在执行make时,如果不能找到其中某一个文件(不存在或者无法创建)。make不会提示错误,也不退出。就是说环境变量“MAKEFILES”定义的包含文件是否存在不会导致make错误(这是比较隐蔽的地方)。
c. make在执行时,首先读取的是环境变量“MAKEFILES”所指定的文件列表,之后才是工作目录下的makefile文件,“include”所指定的文件是在make发现此关键字的时、暂停正在读取的文件而转去读取“include”所指定的文件。
实际应用中很少设置此变量。因为一旦设置了此变量,在多级make调用时;由于每一级make都会读取“MAKEFILES”变量所指定的文件,将导致执行出现混乱。不过,我们可以使用此环境变量来指定一个定义了通用“隐含规则”和变量的文件,比如设置默认搜索路径;通过这种方式设置的“隐含规则”和定义的变量可以被任何make进程使用(有点象C语言中的全局变量)。
推荐的做法:在需要包含其它makefile文件时,使用指示符“include”来实现。
变量 MAKEFILE_LIST
make程序在读取多个makefile文件时,在对这些文件进行解析执行之前,make读取的文件名将会被自动依次追加到变量“MAKEFILE_LIST”的定义域中。可以通过测试此变量的最后一个字来获取当前make程序正在处理的makefile文件名。
Make如何解析makefile文件
GUN make的执行过程分为两个阶段。
第一阶段:读取所有的makefile文件(包括“MAKIFILES”变量指定的、指示符“include”指定的、以及命令行选项“-f(--file)”指定的makefile文件),内建所有的变量、明确规则和隐含规则,并建立所有目标和依赖之间的依赖关系结构链表。
第二阶段:根据第一阶段已经建立的依赖关系结构链表,决定哪些目标需要更新,并使用对应的规则来重建这些目标。
变量和函数的展开问题,是书写Makefile时容易犯错和引起大家迷惑的地方之一。
在make执行的第一阶段中如果变量和函数被展开,那么称此展开是“立即”的,此时所有的变量和函数被展开在需要构建的结构链表的对应规则中(此规则在建立链表是需要使用)。
其他的展开称之为“延后”的,这些变量和函数不会被“立即”展开,而是直到后续某些规则须要使用时或者在make处理的第二阶段它们才会被展开。
> 变量取值 <
变量定义解析的规则如下:
IMMEDIATE = DEFERRED
IMMEDIATE ?= DEFERRED
IMMEDIATE := IMMEDIATE
IMMEDIATE += DEFERRED or IMMEDIATE
define IMMEDIATE
DEFERRED
Endef
当变量使用追加符(+=)时,如果此前这个变量是一个简单变量(使用 :=定义的)则认为它是立即展开的,其它情况时都被认为是“延后”展开的变量。
> 条件语句 <
所有使用到条件语句在产生分支的地方,make程序会根据预设条件将正确地分支展开。就是说,条件分支的展开是“立即”的。其中,包括:“ifdef”、“ifeq”、“ifndef”和“ifneq”所确定的所有分支命令。
> 规则定义 <
所有的规则在make执行时,都按照如下的模式展开:
IMMEDIATE : IMMEDIATE ; DEFERRED
DEFERRED
其中,规则中目标和依赖如果引用其他的变量,则被立即展开。而规则的命令行中的变量引用会被延后展开。此模板适合所有的规则,包括明确规则、模式规则、后缀规则、静态模式规则。
Make执行过程总结
a. 依次读取环境变量“MAKEFILES”定义的makefile文件列表;
b. 读取工作目录下的makefile文件(依次“GNUmakefile”,“makefile”,“Makefile”,首先找到那个就读取那个);
c. 依次读取工作目录makefile文件中使用指示符“include”包含的文件;
d. 查找重建所有已读取的makefile文件的规则(如果存在一个目标是当前读取的某一个makefile文件,则执行此规则重建此makefile文件,完成以后从第一步开始重新执行);
e. 初始化变量值,展开那些需要立即展开的变量和函数,并根据预设条件确定执行分支;
f. 根据“终极目标”以及其他目标的依赖关系,建立依赖关系链表;
g. 执行除“终极目标”以外的所有的目标的规则(规则中如果依赖文件中任一个文件的时间戳比目标文件新,则使用规则所定义的命令重建目标文件);
h. 执行“终极目标”所在的规则。
Makefile文件中第一个规则的第一个目标,将会被作为make的“终极目标”。有两种情况的例外:
a. 目标名以点号“.”开始且其后不存在斜线“/”(“./”被认为是当前目录;“../”被认为是上一级目录);
b. 模式规则的目标。
当这两种目标所在的规则是Makefile的第一个规则时,它们并不会被作为“终极目标”。
“终极目标”是执行make的唯一目的,其所在的规则作为第一个被执行的规则。而其它的规则是在完成重建“终极目标”的过程中被连带出来的,所以这些目标所在规则在Makefile中的顺序无关紧要。
因此,我们书写的makefile的第一个规则应该就是重建整个程序或者多个程序的依赖关系和执行命令的描述。
规则语法
通常规则的语法格式如下:
TARGETS : PREREQUISITES
COMMAND
...
或者:
TARGETS : PREREQUISITES ; COMMAND
COMMAND
...
规则中“TARGETS”可以是空格分开的多个文件名,也可以是一个标签(例如:执行清空的“clean”)。“TARGETS”的文件名可以使用通配符,格式“A(M)”表示档案文件(Linux下的静态库.a文件)的成员“M”。。通常,规则只有一个目标文件(建议这么做),偶尔会在一个规则中需要多个目标。
书写规则是我们需要注意的几点:
a. 规则的命令部分有两种书写方式:
(1). 命令与(目标:依赖)描述在同一行。命令使用分号“;”和依赖文件列表分开。
(2). 命令与(目标:依赖)描述在不同行。当作为独立的命令行时此行必须以[Tab]字符开始。
在Makefile中,在第一个规则之后出现的所有以[Tab]字符开始的行,都会被当作命令来处理。
b. Makefile中符号“$”有特殊的含义(表示变量或者函数的引用),在规则中需要使用符号“$”的地方,需要书写两个连续的(“$$”)。
c. 对于Makefile中一个较长的行,建议使用反斜线“\”将其书写到几个独立的物理行上。
一个规则告诉“make”两件事:1.目标在什么情况下已经过期; 2. 如果需要重建目标时,如何去重建这个目标。规则的中心思想是:目标文件的内容是由依赖文件决定,依赖文件的任何一处改动,将导致目前已经存在的目标文件的内容过期。规则的命令为重建目标提供方法。这些命令运行在系统shell之上。
有时,需要定义一个这样的规则:在更新目标(目标文件已经存在)时,只需要根据依赖文件中的部分来决定目标是否需要被重建,而不是在依赖文件的任何一个被修改后都重建目标。
为实现这一目的,对规则的依赖进行分类:一类是依赖文件被更新后,需要更新规则的目标;另一类是更新这些依赖,可不需要更新规则的目标。我们把第二类称为:“order-only”依赖。
书写规则时,“order-only”依赖使用管道符号“|”开始,作为目标的一个依赖文件。规则依赖列表中管道符号“|”左边的是常规依赖,管道符号右边的就是“order-only”依赖。这样的规则书写格式如下:
TARGETS : NORMAL-PREREQUISITES| ORDER-ONLY-PREREQUISITES
文件名使用通配符
Makefile中表示文件名时,可使用的通配符有:“*”、“?”和“[…]”。在Makefile中通配符的用法和含义和Linux(unix)的Bourne shell完全相同,但是仅限于以下两种场合:
a. 可以用在规则的目标、依赖中,make在读取Makefile时会自动对其进行匹配处理(通配符展开);
b. 可出现在规则的命令中,通配符的通配处理是在shell在执行此命令时完成的。
除这两种情况之外,不能直接使用通配符,而是需要通过函数“wildcard”。如果规则的一个文件名包含统配字符(“*”、“.”等字符),使用反斜线(\)进行转义处理。
另外需要注意:在Linux(unix)中,以波浪线“~”开始的文件名有特殊含义。单独使用它或者其后跟一个斜线(~/),代表了当前用户的宿主目录。例如“~/bin”代表“/home/username/bin/”(当前用户宿主目录下的bin目录)。
在规则中,通配符会被自动展开。但在变量定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN...) 。在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式字符并返回空。
目录搜索
在一个较大的工程中,一般会将源代码和二进制文件(.o文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用make提供的目录搜索依赖文件功能(在指定的若干个目录下自动搜索依赖文件)。
一般搜索(变量VPATH)
通过变量“VPATH”,可指定依赖文件的搜索路径,当规则的依赖文件在当前目录不存在时,make会在此变量所指定的目录下去寻找这些依赖文件。其实“VPATH”变量所指定的是Makefile中所有文件的搜索路径,包括了规则的依赖文件和目标文件。
定义变量“VPATH”时,使用空格或者冒号(:)将多个需要搜索的目录分开。make搜索目录的顺序是按照变量“VPATH”定义中的目录顺序进行的。例如对变量的定义如下:
VPATH = src:../headers
这样我们就为所有规则的依赖指定了两个搜索目录,“src”和“../headers”。
选择性搜索(关键字vpath)
当需要为不类型的文件指定不同的搜索目录时,使用make的“vpath”关键字(全小写的)。它不是一个变量,而是一个make的关键字。它可以为不同类型的文件(由文件名区分)指定不同的搜索目录,使用方法有三种:
a. vpath PATTERN DIRECTORIES
为所有符合模式“PATTERN”的文件,指定搜索目录“DIRECTORIES”。多个目录使用空格或者冒号(:)分开。
b. vpath PATTERN
清除之前为符合模式“PATTERN”的文件设置的搜索路径。
c. vpath
清除所有已被设置的文件搜索路径。
“PATTERN”表示了具有相同特征的一类文件,使用模式字符“%”表示匹配一个或者多个字符,而“DIRECTORIES”则指定了搜索此类文件目录。例如:
vpath %.h ../headers
其含义是:Makefile中出现的.h文件;如果不能在当前目录下找到,则到目录“../headers”下寻找。注意:这里指定的路径仅限于在Makefile文件内容中出现的.h文件。并不能指定源文件中包含的头文件所在的路径(在.c源文件中所包含的头文件路径需要使用gcc的“-I”选项来指定,可参考gcc的info文档)。
目录搜索的机制
规则中一个依赖文件可以通过目录搜寻找到,可能得到的是文件的完整路径名,它却并不是规则中列出的文件名。因此,使用目录搜索所到的完整的文件路径名可能需要废弃。make在解析Makefile文件执行规则时对文件路径保存或废弃所依据的算法如下:
a. 首先,如果规则的目标文件在Makefile文件所在的目录(工作目录)下不存在,那么就执行目录搜寻;
b. 如果目录搜寻成功,在指定的目录下存在此规则的目标。那么搜索到的完整的路径名就被作为临时的目标文件被保存;
c. 对于规则中的所有依赖文件使用相同的方法处理。
d. 完成第三步的依赖处理后,make程序就可以决定规则的目标是否需要重建,两种情况时后续处理如下:
如果需要make在执行时,无论该目标是否需要重建,都使用搜索到的目标文件完整路径名,将目标文件在已存在的目录存下进行重建,我们可以使用“GPATH”变量来指定这些目标所在的目录。“GPATH”变量和“VPATH”变量具有相同的语法格式。a) . 规则的目标不需要重建:那么通过目录搜索得到的所有完整的依赖文件路径名有效,同样,规则的目标文件的完整的路径名同样有效。就是说,当规则的目标不需要被重建时,规则中的所有的文件完整的路径名有效。已经存在的目标文件所在的目录不会被改变。
b) . 规则的目标需要重建:那么通过目录搜索所得到的目标文件的完整的路径名无效,规则中的目标文件将会被在工作目录下重建。就是说,当规则的目标需要重建时,规则的目标文件会在工作目录下被重建,而不是在目录搜寻时所得到的目录。这里,必须明确:此种情况只有目标文件的完整路径名失效,依赖文件的完整路径名是不会失效的,否则将无法重建目标。
命令行和搜索目录
书写命令时,我们必须保证,当依赖文件在其它目录下被发现时,规则的命令能够正确执行。解决这个问题的方式是在规则的命令行中使用“自动化变量”。
a. 自动化变量“$^”:代表所有通过目录搜索得到的依赖文件的完整路径名(目录 + 一般文件名)列表;
b. 自动化变量“$@”:代表规则的目标;
c. 自动化变量“$<”:代表规则中通过目录搜索得到的依赖文件列表的第一个依赖文件;
对于一个规则我们可以如下描述:
foo.o : foo.c
cc -c $(CFLAGS)$^ -o$@
变量“CFLAGS”是编译.c文件时gcc的编译选项,可以在Makefile中给它指定明确的值、也可以使用隐含的定义值。
规则的依赖文件列表中可以包含头文件,而在命令行中不需要使用这些头文件(这些头文件,只有在make程序决定目标是否需要重建时,才有意义)。这时,我们可以使用“$<”。
VPATH = src:../headers
foo.o : foo.cdefs.h hack.h
cc -c $(CFLAGS)$< -o$@
隐含规则和搜索目录
通过变量“VPATH”、或者关键字“vpath”指定的搜索目录,对于隐含规则同样有效。如果能够在一个可以搜索的目录中找到文件,make会使用根据搜索到的文件完整的路径名去重建目标,相应的命令中的文件名都是使用目录搜索得到的完整的路径名。就是说,在搜索到的目录中去创建目标,而不是当前目录。
库文件和搜索目录
Makefile中程序链接的静态库、共享库,同样也可以通过搜索目录得到。在书写规则的依赖时,指定一个类似“-lNAME”的依赖文件名。
搜索名为“libNAME.so”共享库文件的过程:
a. 首先,在当前工作目录下搜索;
b. 接着,在“VPATH”或者“vpath”指定的目录下搜索;
c. 然后,在系统库文件所在目录下搜索,顺序是:“/lib”、“/usr/lib”和“PREFIX/lib”(在Linux系统中为“/usr/local/lib”)。
如果在上述目录都未找到“libNAME.so”,那么make将会按照以上的搜索顺序查找名字为“libNAME.a”的文件。
需要注意的是:“-lNAME”只是告诉了链接器在生成目标时,需要链接的库文件,并没有告诉make程序其依赖的库文件应该如何重建。当所有的搜索目录中不存在库“libFoo”时,Make将提示“没有规则可以创建目标“XXX”需要的目标“-lFoo”。
在规则的依赖列表中如果出现“-lNAME”格式的依赖时,表示需要搜索的依赖文件名为“libNAME.so”和“libNAME.a”,这是由变量“.LIBPATTERNS”指定的。默认情况时,“.LIBPATTERNS”的值为:“lib%.so lib%.a”。
Makefile伪目标
使用伪目标有两点原因:a. 避免定义的只执行命令的目标和实际文件出现名字冲突;b. 提高执行make时的效率。
对于第一点,将它作为特殊目标“.PHONY”的依赖。无论在当前目录下是否存在“clean”这个文件,输入“make clean”之后,“rm”命令都会被执行。而且,被声明为伪目标后,make不会试图去查找隐含规则来创建它。这就提高了make的执行效率。
在书写伪目标规则时,首先需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标“clean”的完整书写格式应该如下:
.PHONY: clean
clean:
rm *.o temp
伪目标的另一种使用场合是在make的并行和递归执行过程中。
在一个目录下如果需要创建多个可执行程序,我们可以将所有程序的重建规则在一个Makefile中描述。因为Makefile中第一个目标是“终极目标”,约定的做法是使用一个称为“all”的伪目标来作为终极目标,它的依赖文件就是那些需要创建的程序。下边就是一个例子:
#sample Makefile
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
执行make时,目标“all”被作为终极目标。为了完成对它的更新,make会创建(不存在)或者重建(已存在)目标“all”的所有依赖文件(prog1、prog2和prog3)。当需要单独更新某一个程序时,我们可以通过make的命令行选项来明确指定需要重建的程序(例如:“make prog1”)。
通常在清除文件的伪目标所定义的命令中,“rm”使用选项“–f”(--force)来防止在缺少删除文件时出错并退出,使“make clean”过程失败。也可以在“rm”之前加上“-”来防止“rm”错误退出,这种方式时make会提示错误信息但不会退出。为了不看到这些讨厌的信息,需要使用上述的第一种方式。
另外make存在一个内嵌隐含变量“RM”,它被定义为:“RM = rm –f”。因此在书写“clean”规则的命令行时可以使用变量“$(RM)”来代替“rm”。
Makefile的特殊目标
在Makefile中,有一些名字,当它们作为规则的目标时,具有特殊含义。它们是一些特殊的目标,GNU make所支持的特殊的目标有:
.SUFFIXES目标“.PHONY”的所有依赖被作为伪目标。伪目标所在规则定义的命令,无论目标文件是否存在都会被无条件执行。
特殊目标“SUFFIXES”的所有依赖,指出了一系列在后缀规则中需要检查的后缀名(就是当前make需要处理的后缀)。
.DEFAULT
.PRECIOUSMakefile中,目标“.DEFAULT”所在规则定义的命令,被用在重建那些没有具体规则的目标(明确规则和隐含规则),使用“.DEFAULT”所指定的命令。
.INTERMEDIATE目标“.PRECIOUS”的所有依赖文件在执行过程中被中断时,make不会删除它们。而且如果目标的依赖文件是中间过程文件,同样不会被删除。这一点,目标“.PRECIOUS”和目标“.SECONDAY”实现的功能相同。
另外,目标“.PRECIOUS”的依赖文件也可以是一个模式,例如“%.o”。这样可以保留有规则创建的中间过程文件。
.SECONDARY目标“.INTERMEDIATE”的依赖文件在make时被作为中间过程文件对待,没有任何依赖文件的目标“.INTERMEDIATE”没有意义。
.DELETE_ON_ERROR目标“.SECONDARY”的依赖文件被作为中间过程文件对待,并且不会被自动删除。没有任何依赖文件的目标“.SECONDARY”的含义是:将所有的文件作为中间过程文件(不会自动删除任何文件)。
.IGNORE如果在Makefile中存在特殊目标“.DELETE_ON_ERROR”,如果规则的命令执行错误,将删除已经被修改的目标文件。
.LOW_RESOLUTION_TIME目标“.IGNORE”指定的依赖文件,则忽略创建这个文件所执行命令的错误。给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。
.SILENT目标“.LOW_RESOLUTION_TIME”的依赖文件被make认为是低分辨率时间戳文件。给目标“.LOW_RESOLUTION_TIME”指定命令是没有意义的。
通常文件的时间辍都是高分辨率的,make在处理依赖关系时、对规则目标-依赖文件的高分辨率的时间戳进行比较,判断目标是否过期。类似“cp -p”这样的命令,在根据源文件创建目的文件时,所产生的目的文件的高分辨率时间辍的细粒度部分被丢弃(来源于源文件)。这个特殊的目标主要作用是,弥补系统在没有提供修改文件高分辨率时间戳机制的情况下,某些命令在make中的一些缺陷。
对于静态库文件(文档文件)成员的更新也存在这个问题。make在创建或者更新静态库时,会自动将静态库的所有成员作为目标“.LOW_RESOLUTION_TIME”的依赖。
出现在目标“.SILENT”的依赖列表中的文件,make在创建这些文件时,不打印出重建此文件所执行的命令。同样,给目标“.SILENT”指定命令行是没有意义的。现行版本make支持目标“.SILENT”的功能和用法是为了和旧版本的兼容。在当前版本中如果需要禁命令执行过程的打印,可使用make的命令行参数“-s”或者“--silent”。
.EXPORT_ALL_VARIABLES
此目标应该作为一个简单的没有依赖的目标,其功能含义是将之后所有的变量传递给子make进程。
.NOTPARALLEL
Makefile中,如果出现目标“.NOPARALLEL”,则所有命令按照串行方式执行,即使存在make的命令行参数“-j”。但在递归调用的子make进程中,命令可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将被忽略。
所有定义的隐含规则后缀作为目标出现时,都被视为特殊目标,两个后缀串联起来也是如此,例如“.c.o”。这样的目标被称为后缀规则的目标,这种定义方式是已经过时的定义隐含规则的方法(目前,这种方式还被用在很多地方)。原则上,如果将其分为两个部分、并将它们加到后缀列表中,任何目标都可采用这种方式来表示。实际中,后缀通常以“.”开始,因此,以上的这些特别目标同样是以“.”开始。
多目标与多目标规则
一个规则中可以有多个目标,规则所定义的命令对所有的目标有效。一个具有多目标的规则相当于多个规则。规则的命令对不同的目标的执行效果不同,因为在规则的命令中可能使用了自动环变量“$@”。多目标规则意味着所有的目标具有相同的依赖文件。多目标通常用在以下两种情况:
a. 仅需要一个描述依赖关系的规则,不需要在规则中定义命令。例如
kbd.o command.o files.o: command.h
这个规则实现了同时给三个目标文件指定一个依赖文件。
b. 对于多个具有类似重建命令的目标。重建这些目标的命令并不需要是完全相同,因为可以在命令行中使用自动环变量“$@”来引用具体的目标,完成对它的重建。例如规则:
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
其等价于:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
例子中的“generate”根据命令行参数来决定输出文件的类型。使用了make的字符串处理函数“subst”来根据目标产生对应的命令行选项。
虽然在多目标的规则中,可以根据不同的目标使用不同的命令(在命令行中使用自动化变量“$@”)。但是,多目标的规则并不能做到,根据目标文件自动改变依赖文件。需要实现这个目的是,要用到make的静态模式。
Makefile中,一个文件可以作为多个规则的目标(多个规则中只能有一个规则定义命令)。这种情况时,以这个文件为目标的规则的所有依赖文件将会被合并成此目标一个依赖文件列表。
对于一个多规则的目标,重建此目标的命令只能出现在一个规则中(可以是多条命令)。某些情况,需要对相同的目标使用不同的规则中所定义的命令,我们需要使用另外一种方式——“双冒号”规则来实现。
一个仅仅描述依赖关系的规则,可用来给出一个或做多个目标文件的依赖文件。例如,我们使用多目标的方式来书写Makefile:
objects = foo.o bar.o
foo.o : defs.h
bar.o : defs.h test.h
$(objects) : config.h
Makefile中变量“objects”,定义为所有的需要编译生成的.o文件的列表。在源文件增加或者删除了包含的头文件以后,不用修改已经存在的Makefile规则,只需要增加或者删除某一个.o文件依赖的头文件。在一个大的工程中,对于一个单独目录下的.o文件的依赖规则,建议使用此方式。规则中头文件的依赖描述规则也可以使用gcc自动产生。
我们也可通过一个变量来增加目标的依赖文件,使用make的命令行来指定某一个目标的依赖头文件,例如:
extradeps=
$(objects) : $(extradeps)
它的意思是:如果我们执行“make extradeps=foo.h”,那么“foo.h”将作为所有的.o文件的依赖文件。当然我们只执行“make”的话,就没有指定任何文件作为.o文件的依赖文件。
在多规则的目标中,如果目标的任何一个规则没有定义重建此目标的命令,make将会寻找一个合适的隐含规则来重建此目标。
静态模式
静态模式规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造依赖文件。静态模式规则比多目标规则更通用,它不需要多个目标具有相同的依赖。但是静态模式规则中的依赖文件必须是相类似的而不是完全相同的。
首先,我们来看一下静态模式规则的基本语法:
TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...
COMMANDS
...
从目标模式(TAGET-PATTERN)的目标名字中抽取一部分字符串(称为“茎”)。使用“茎”替代依赖模式(PREREQ-PATTERNS)中的相应部分来产生对应目标的依赖文件。
看一个例子,根据相应的.c文件来编译生成“foo.o”和“bar.o”文件:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
例子中,规则描述了所有的.o文件的依赖文件为对应的.c文件,对于目标“foo.o”,取其茎“foo”替代对应的依赖模式“%.c”中的模式字符“%”之后可得到目标的依赖文件“foo.c”。
如果一个文件列表中的一部分符合某一种模式,而另外一部分符合另外一种模式,这种情况下我们可以使用“filter”函数来对这个文件列表进行分类,在分类之后对确定的某一类使用模式规则。例如:
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
其中;$(filter %.o,$(files))的结果为“bar.o lose.o”。“filter”函数过滤不符合“%.o”模式的文件名而返回所有符合此模式的文件列表。
我们通过另外一个例子来看一下自动环境变量“$*”在静态模式规则中的使用方法:
bigoutput littleoutput : %output : text.g
generate text.g -$* > $@
当执行此规则的命令时,自动环变量“$*”被展开为“茎”,在这里就是“big”和“little”。
静态模式规则对一个较大工程的管理非常有用。它可以对整个工程的同一类文件的重建规则进行一次定义,从而实现对整个工程中此类文件指定相同的重建规则。通常的做法是将生成同一类目标的模式,定义在一个make.rules的文件中,在工程各个模块的Makefile中包含此文件。
静态模式和隐含规则
Makefile中,静态模式规则和被定义为隐含规则的模式规则经常使用。两者相同的地方:都用目标模式和依赖模式来构建目标的规则中的文件依赖关系;两者不同的地方:在执行时使用它们的时机。
隐含规则:可被用在任何和它相匹配的目标上,当存在多个隐含规则和目标模式相匹配时,只执行其中的一个规则,具体执行哪一个规则取决于定义规则的顺序。
静态模式规则:只能用在规则中明确指出的那些文件的重建过程中。不能用在除此之外的任何文件的重建过程中,并且它对指定的每一个目标来说是唯一的。
静态模式规则相比隐含模式规则,有以下两个优点:
a. 不能根据文件名通过词法分析进行分类的文件,我们可以明确列出这些文件,并使用静态模式规则来重建其隐含规则。
b. 对于无法确定工作目录内容,并且不能确定是否此目录下的无关文件会使用错误的隐含规则而导致make失败的情况。当存在多个适合此文件的隐含规则时,使用哪一个隐含规则取决于其规则的定义顺序。这种情况下我们使用静态模式规则就可以避免这些不确定因素,因为静态模式中,指定的目标文件有明确的规则来描述其依赖关系和重建命令。
双冒号规则
双冒号规则:使用“::”代替普通规则的“:”得到的规则。双冒号规则允许在多个规则中,为同一个目标指定不同的重建目标的命令。
首先需要明确:Makefile中,一个目标可以出现在多个规则中,但必须是同一类型的规则(要么都是普通规则,要么都是双冒号规则)。双冒号规则和普通规则的处理的不同点表现在以下几个方面:
a. 对于一个没有依赖而只有命令行的双冒号规则,当引用此目标时,规则的命令将会被无条件执行;而普通规则,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。
b. 当同一个文件作为多个双冒号规则的目标时,这些不同的规则会被独立的处理;而不是像普通规则那样,合并所有的依赖到一个目标文件。
我们来看一个例子,在我们的Makefile中包含以下两个规则:
Newprog :: foo.c
$(CC) $(CFLAGS) $< -o $@
Newprog :: bar.c
$(CC) $(CFLAGS) $< -o $@
如果“foo.c”文件被修改,执行make以后将根据“foo.c”文件重建目标“Newprog”;而如果“bar.c”被修改那么“Newprog”将根据“bar.c”被重建。回想一下,如果以上两个规则为普通规时出现的情况是什么?(make将会出错并提示错误信息)。
GNU make的双冒号规则,给我们提供一种根据依赖的更新情况而执行不同的命令,来重建同一目标的机制。一般这种需要的情况很少,所以双冒号规则的使用比较罕见。
自动产生依赖
Makefile中,有时需要书写一些规则来描述一个.o文件和头文件的依赖关系。gcc通过“-M”选项来实现此功能,使用“-M”选项gcc将自动找寻源文件中包含的头文件,并生成文件的依赖关系。
需要明确的是:如果在“main.c”中包含了标准库的头文件,使用gcc的“-M”选项时,其输出结果中也包含对标准库的头文件的依赖关系描述。当不需要考虑标准库头文件时,对于gcc需要使用“-MM”选项。
在旧版本的make中,使用编译器此项功能通常的做法是:在Makefile中书写一个伪目标“depend”的规则,来定义自动产生依赖关系文件的命令。输入“make depend”将生成一个称为“depend”的文件,其中包含了所有源文件的依赖规则描述。Makefile中使用“include”指示符包含这个文件。
在新版本的make中,推荐的方式是:为每一个源文件产生一个描述其依赖关系的makefile文件。对于一个源文件“NAME.c”,对应的这个makefile文件为“NAME.d”。“NAME.d”中描述了文件“NAME.o”所要依赖的所有头文件。采用这种方式,只有源文件在修改之后,才会重新使用命令生成新的依赖关系描述文件“NAME.d”。
我们可以使用如下的模式规则,来自动生成每一个.c文件对应的.d文件:
%.d: %.c
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
此规则的含义是:所有的.d文件依赖于同名的.c文件。
第一行:使用c编译器自动生成依赖文件($<)的头文件的依赖关系,并输出为一个临时文件,“$$$$”表示当前进程号。
第二行:使用sed处理第二行已产生的那个临时文件,并生成此规则的目标文件。这里sed完成了如下的转换过程。例如对已一个.c源文件,将编译器产生的依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
第三行:删除临时文件。
Makefile中对当前目录下.d文件处理可以参考如下:
sources = foo.c bar.c
sinclude $(sources:.c=.d)
变量“sources”定义了当前目录下的需要编译的源文件。变量引用置换“$(sources : .c=.d)”的功能是根据变量“source”指定的.c文件自动产生对应的.d文件,并在当前Makefile文件中包含这些.d文件。.d文件和其它的makefile文件一样,make在执行时读取并试图重建它们。
需要注意的是:include指示符的书写顺序,因为在这些.d文件中已经存在规则。当一个Makefile使用指示符include这些.d文件时,应该注意它应该出现在终极目标之后,以免.d文件中的规则被是Makefile的终极规则。