make 是用来构建和管理工程的工具,它是一个命令,需要解释一个称为 makefile 中的指令,makefile 是描述工程中所有文件的编译、链接的规则。
make 可以根据依赖文件和目标文件的最后修改时间,来决定哪些文件需要更新,哪些文件不需要更新,这样能高效地构建工程。
makefile 主要是由规则来组成的,一个基本的规则描述如下:
tab
减缩进。一个 makefile 中可以包含其它的 makefile,格式为:
include
指示符告诉 make 程序暂停读取当前的 makefile,而转去读取 include 指定的一个或多个文件,完成之后再回到当前 makefile 继续读取。
include 所包含的文件如果不是绝对路径,那么 make 将会在从当前目录下寻找,不存在时会根据命令行选项 -I 或 --include-dir
指定的目录下搜索,如果也找不到会搜索系统的目录(如果其存在):/usr/local/include、/usr/include 等。如果都找不到,给出警告提示但也不会立即退出,而是会继续处理 makefile 内容,当完成读取所有的 makefile 文件后,make 试图使用规则来创建未找到的文件,如果没有规则能创建它,则 make 将会提示致命错误并退出。
可以是用 -include 来代替 include
,忽略由于包含文件找不到提示的错误。
GNU make 执行分为两个阶段:
如何决定哪些目标需要更新呢? 首先,make比较目标文件和所有的依赖文件的时间戳。如果目标的时间戳比所有依赖文件的时间戳更新(依赖文件在上一次执行make之后没有被修改),那么什么也不做。否则(依赖文件中的某一个或者全部在上一次执行make后已经被修改过),规则所定义的重建目标的命令将会被执行。
上节说到 makefile 中一个基本规则的结构,其实规则就是描述了何种情况下使用什么命令来重建一个特定的文件,该文件称为”目标文件“,规则中所罗列的其它文件称为目标的”依赖“,规则中的命令则是用来更新或者创建目标文件的方法。
上面其实也说到一个规则的通常语法,这里只是再强调几点:
Tab
字符开始。#
开始,但前面不能有 tab
字符,否则就被当场命令了。$
有特殊的含义(变量或函数的引用),若在规则中需要使用 $
,需要写两个连续的 $$
。\
来独起一行,但斜线后不能有任何空格。有两种不同类型的依赖:
makefile 中书写规则时,order-only 依赖使用管道符号 |
开始,左边是常规依赖文件,右边是 order-only 依赖文件,格式如下:
makefile 中通配符的用法和含义与 shell 中完全相同,如:
* | 任意字符串 |
? | 仅与一个任意的字符匹配 |
[...] | 同方括号中指定的任意一个字符匹配 |
[!...] | 与所有不在方括号中所有字符匹配 |
但并不是这些通配符可以出现在任何地方,通常只有两种场合:
例如:
除了上面这两种场合之外,使用通配符可能与你预期的不一样,例如以通配符定义了一个变量,在规则对引用该变量:
当目录下存在 .o 文件时,那么这些 .o 文件就是目标的依赖文件,目标 foo 会被重建;若不存在 .o 文件时,执行规则时会得到类似于”没有创建 *.o 文件的规则“ 的错误提示。可以使用 wildcard 函数来解决,后面再学习。
在一个大的工程中,通常将源代码和目标代码(.o 和可执行文件)放在不同的目录来管理,这时我们希望 make 提供目录自动搜索依赖文件的功能,指定依赖文件的搜索目录,当工程目录结构发生改变时不需要修改 makefile 的规则,只需要更改依赖文件的搜索目录即可。有以下几种方式可以达到我们的目标:
VPATH
,通过该变量可以指定依赖文件的搜索路径,在当前目录找不到依赖文件时,会自动依次搜索该变量所指定的目录vpath
来指定,但它不是变量而是 make 的一个关键字,有三种使用方法:
vpath PATTERN DIRECTORIES
为符合模式 PATTERN 的文件指定搜索目录 DIRECTORIESvpath PATTERN
清除之前为符合模式 PATTERN 设置的文件搜索路径vpath
清除所有被设置的文件搜索路径这有个问题是,通过目录搜索得到的目标的依赖文件可能在其它目录,但已经存在的规则命令却不知道在哪个目录,为了写出正确的规则命令,我们需要使用自动化变量,稍后再学习。
每条规则中命令和 shell 命令行是一致的,make 时会按顺序时一条一条地执行命令。
通常 make 会将其执行的命令在执行前输出到屏幕上,当我们以 @
字符放置在命令行前,那么该条命令只执行而不会输出到屏幕上,如果该条命令有输出,那么还是会输出到屏幕的。如:
还有两个实用技巧:
make -n/-just-print 只显示命令但不会执行,有利于我们调试 makefile
make -s/-slient
全面禁止命令的输出当规则的目标需要被构建或更新时,make 会一条条地执行其后的命令,一般使用环境变量 SHELL 所定义的系统 shell 来执行命令。
如果你需要让上一条命令的结果作用于下一条命令时,你应该使用分号来分割这两条命令;而不能写成单独的两行。如:
当一条命令执行完之后,make 会检测命令的返回码,如果返回成功,make 会执行下一条命令;如果命令出错了,make 会终止当前规则,有可能就终止所有规则的执行。但有时候命令的出错并不是表示错误,如果需要忽略命令的错误导致的终止的影响,有几种方法:
-
,前面在介绍 include
包含时已经提到过这种用法;make -i/-ignore-errors
命令会全局忽略所有命令行的错误;.IGNORE
标识的目标规则中的所有命令会忽略错误;make -k/-keep-going
如果某条规则命令出错终止该条规则,但会继续执行其它规则。在大的工程中,我们会将不同的模块或功能的源文件放到不同的目录中,我们可以在每个目录下写一个该目录的 makefile,在工程的根目录写一个“总控makefile”,这样有利于使我们的 makefile 变得简洁而且更加容易维护。
如有一个子目录叫 subdir,该目录下有个 makefile 来指明这个目录文件的编译规则,对于总控 makefile 可以这样写:
总控 makefile 中定义的变量(如果显示声明)会传递到下级的 makefile 中的,但不会覆盖到下级 makefile 定义的同名变量,除非指定了 -e
参数。
但有两个变量 SHELL 和 MAKEFLAGS
无论你是否 export,都会传递到下级 makefile 中的。
makefile 中的变量类似于 C/C++ 中的宏,代表一个文本字符串,在执行时会自动原模原样地在使用的地方展开,与宏不同的是你可以在 makefile 中改变其值。下面是变量的一些基础知识:
":"、"#"、"=" 或空字符,
大小写敏感$
,最好用 () 或 {}
括起来在定义一个变量时需要给它赋值,有四种赋值的方式:=、:=、?=、+=,主要区别在于:
=
操作符赋值,左侧是为定义的新变量,右侧为引用已定义的变量的值,该已定义的变量不一定非要出现在新变量之前,也可以在之后定义:=
操作符赋值,这种赋值方法前面的变量就不能引用后面定义的变量了?=
操作符赋值,含义是如果左侧的变量没定义过,那么就定义该变量;否则什么也不做 +=
操作符赋值可以在原变量后追加值,如果之前变量没定义过,就自动变成 =
赋值;如果之前变量定义过,则 +=
会继承前面赋值的操作符,例如前面使用 :=
来赋值的,那么这次的 +=
也是以 :=
来赋值
下面介绍两种变量的高级用法:
$(var:a=b) 或者 ${var:a=b}
,意思是把 var 中所有以 a 字符结尾的的 a 替换成 b。$($(var))
将变量 var 的值当成一个变量,再取变量的值,可以组合更深。目标和依赖都是一系列的文件,如何书写一个命令完成不同的依赖文件生成相应的目标呢?自动化变量就可以帮助我们完成这个功能。
下面是所有自动化变量及其说明:
变量
|
说明
|
---|---|
$@ |
规则中的目标文件集,在模式规则中,如果有多个目标,该变量匹配目标中模式定义的集合 |
$% |
仅当目标是函数库中的文件,表示规则中的目标成员名,例如目标是 foo.a(bar.o) ,那么 $% 表示 bar.o ;如果不是函数库文件,其值为空 |
$< |
依赖目标中的第一个目标名字,如果依赖目标是以模式(即 % )定义的,那么将是符合模式的一系列文件集,是一个个取出来 |
$? |
所有比目标新的依赖目标的文件集,以空格隔开 |
$^ |
所有的依赖目标的集合,以空格隔开,如果依赖目标有重复的,那么会去除重复 |
$+ |
与 |
$* |
表示目标模式中 % 及其前面的部分,如果目标是 dir/a.foo.c 并且目标模式定义为 a.%.c ,那么该变量就表示 dir/a.foo |
上表中四个变量( $@、$<、$%、$*
)在扩展是只会有一个文件,其他三个的值是一个文件列表。
makefile 中可以使用函数来处理变量,函数调用的返回值可以当作变量来使用。
函数调用与变量引用很像,都是以 $
来标识的,如下:
其中 <function>
是函数名, <arguments>
是函数的参数,参数之间以逗号隔开。
下面介绍常用的字符串处理函数:
函数名
|
原型
|
功能
|
返回值
|
示例
|
---|---|---|---|---|
subst |
$(subst <from>, <to>, <text>) |
|
返回被替换过的字符串 |
$(subst ee, EE, feet on street) 返回:fEEt on strEEt |
patsubst | $(patsubst <pattern>, <replacement>, <text>) |
把 <text> 中的单词(以空格、tab或回车、换行 分隔)匹配 <pattern> 模式的替换成 <replacement> |
返回被替换之后的字符串 |
$(patsubst %.c,%.o,x.c.c bar.c) 返回:x.c.o bar.o |
strip | $(strip <string>) |
去掉<string> 字符串中的开头和结尾的空字符 |
返回被去掉空字符的字符串 |
$(strip a b c ) 返回:a b c |
findstring | $(findstring <find>, <in>) |
在字符串 <in> 中查找 <find> 字串 |
找到返回 <find>,否则返回空字符串 |
$(findstring a, a b c) 返回:a |
filter | $(filter <pattern...>, <text>) |
以 <pattern> 模式过滤 <text> 字符串中的单词,保留符合 <pattern> 单词 |
返回符合<pattern> 单词 |
|
sort |
$(sort <list>) |
$(sort foo bar lose) 返回:bar foo lose |
||
word |
$(word <n>, <text>) |
取字符串 <text> 的第 <n> 个单词 |
||
wordlist |
$(wordlist <s>, <e>, <text>) |
取字符串 <text> 中的第 <s> 到第 <e> 个单词 |
||
words |
$(words <text>) |
统计 <text> 字符串中单词的个数 |
||
firstword |
$(firstword <text>) |
取字符串 <text> 中的第一个单词 |
函数名
|
函数原型
|
功能
|
返回值
|
示例
|
---|---|---|---|---|
dir |
$(dir <names...>) |
从文件名序列 <names> 中取出目录部分 |
||
notdir |
$(notdir <names...>) |
从文件名序列 <names> 中取出非目录部分 |
||
suffix |
$(suffix <names...>) |
从文件名序列 <names> 中取出文件名的后缀 |
||
basename |
$(basename <names...>) |
从文件名序列 <names> 中取出文件名的前缀 |
||
addsuffix |
$(addsuffix <suffix>, <names...>) |
把后缀 <suffix> 添加到 <names> 中每个文件名后 |
||
addprefix |
$(addprefix <prefix>, <names...>) |
把前缀 <prefix> 添加到 <names> 中每个文件名前 |
||
join |
$(join <list1>, <list2>) |
把 <list2> 中的单词对应加到 <list1> 单词的后面 |
||
wildcard | $(wildcard <pattern>) |
列出所有当前路径下所有匹配 <pattern> 模式的文件名 |
函数名
|
函数原型
|
功能
|
返回值
|
示例
|
---|---|---|---|---|
shell |
$(shell <commands>) |
使用 shell 命令来执行 <commands> |
返回 <commands> 命令执行的结果 |
|