Makefile 是 C/C++ 工程中绕不开的构建工具。很多人平时只会在模板上改一改,遇到复杂一点的变量赋值、函数调用就容易卡住。本文整理了一份常见 Makefile 规则和函数用法,适合当作速查手册,需要时翻出来对照着写。
01 Makefile 通用规则
一个简单的 Makefile 文件包含一系列的“规则”,其样式如下:
目标(target):依赖(dependings)
<tab>命令(command)
- 目标(target):通常是要生成的文件的名称,可以是可执行文件或 OBJ 文件,也可以是一个执行的动作名称,比如
clean。
- 依赖(dependings):是用来产生目标的材料,比如源文件。一个目标经常会有多个依赖。
- 命令(command):是生成目标时执行的动作。一个规则可以包含多条命令,每条命令占一行。
需要注意:每个命令行前面必须是一个 Tab 字符,也就是说命令行第一个字符必须是 Tab。这是最容易出错的地方之一。
通常,如果一个依赖发生了变化,就需要规则调用命令来更新或创建目标。不过并非所有目标都有依赖,比如 clean 这个目标的作用是清除文件,它就没有依赖。
规则一般用来解释怎样以及何时重建目标。make 会先调用命令处理依赖,进而才能创建或更新目标。当然,规则也可以用于解释怎样以及何时执行一个动作,比如打印提示信息。
一个 Makefile 文件可以包含规则以外的其他文本,但一个简单的 Makefile 文件只需要包含规则即可。虽然真实的规则比这个例子复杂,但格式完全一样。
02 Makefile 文件里的赋值方法
变量的定义语法形式如下:
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate += deferred or immediate
define immediate
deferred
endif
在 GNU make 中,对变量的赋值有两种方式:延时变量和立即变量。
区别在于定义方式和扩展时机不同。延时变量在使用时才展开,也就是真正使用时这个变量的值才确定;立即变量在定义时值就已经确定了。
使用 =、?= 定义,或者使用 define 指令定义的变量是延时变量;使用 := 定义的变量是立即变量。
还要注意,?= 仅仅在变量还没有定义的情况下有效,也就是说 ?= 被用来定义第一次出现的延时变量。
03 Makefile 常用函数
函数调用的格式如下:
$(function arguments)
其中 function 是函数名,arguments 是函数的参数。参数和函数名之间用空格或 Tab 隔开。如果有多个参数,它们之间用逗号隔开。这些空格和逗号并不是参数值的一部分。
内核的 Makefile 中会用到大量函数,下面介绍一些常用函数。
3.1 字符串替换和分析函数
3.1.1 $(subst from,to,text)
在文本 text 中使用 to 替换每一处 from。
例如:
$(subst ee,EE,feet on the street)
结果为:
fEEt on the strEEt
3.1.2 $(patsubst pattern,replacement,text)
寻找 text 中符合格式 pattern 的字,用 replacement 替换它们。pattern 和 replacement 中可以使用通配符。
例如:
$(patsubst %.c,%.o,x.c.c bar.c)
结果为:
x.c.o bar.o
3.1.3 $(strip string)
去掉字符串前导和结尾的空格,并将中间的多个空格压缩为单个空格。
例如:
$(strip a b c )
结果为:
a b c
3.1.4 $(findstring find,in)
在字符串 in 中搜寻 find。如果找到,返回值就是 find,否则返回空字符串。
例如:
$(findstring a,a b c)
$(findstring a,b c)
结果分别为 a 和空字符串。
3.1.5 $(filter pattern...,text)
返回 text 中由空格隔开且匹配格式 pattern... 的字,去除不符合格式的字。
例如:
$(filter %.c %.s,foo.c bar.c baz.s ugh.h)
结果为:
foo.c bar.c baz.s
3.1.6 $(filter-out pattern...,text)
返回 text 中由空格隔开且不匹配格式 pattern... 的字,去除符合格式的字。它是函数 filter 的反函数。
例如:
$(filter-out %.c %.s,foo.c bar.c baz.s ugh.h)
结果为:
ugh.h
3.1.7 $(sort list)
将 list 中的字按字母顺序排序,并去掉重复的字。输出由单个空格隔开的字列表。
例如:
$(sort foo bar lose)
返回值为:
bar foo lose
3.2 文件名函数
3.2.1 $(dir names...)
抽取 names... 中每一个文件名的路径部分。路径部分包括从文件名的首字符到最后一个斜杠(含斜杠)之前的所有字符。
例如:
$(dir src/foo.c hacks)
结果为:
src/ ./
3.2.2 $(notdir names...)
抽取 names... 中每一个文件名中除路径部分外的一切字符,也就是真正的文件名。
例如:
$(notdir src/foo.c hacks)
结果为:
foo.c hacks
3.2.3 $(suffix names...)
抽取 names... 中每一个文件名的后缀。
例如:
$(suffix src/foo.c src-1.0/bar.c hacks)
结果为:
.c .c
3.2.4 $(basename names...)
抽取 names... 中每一个文件名中除后缀外的一切字符。
例如:
$(basename src/foo.c src-1.0/bar hacks)
结果为:
src/foo src-1.0/bar hacks
3.2.5 $(addsuffix suffix,names...)
参数 names... 是一系列文件名,文件名之间用空格隔开;suffix 是一个后缀名。函数会把 suffix 的值附加在每一个独立文件名的后面,完成后将文件名串联起来,文件名之间用单个空格隔开。
例如:
$(addsuffix .c,foo bar)
结果为:
foo.c bar.c
3.2.6 $(addprefix prefix,names...)
参数 names... 是一系列文件名,文件名之间用空格隔开;prefix 是一个前缀名。函数会把 prefix 的值附加在每一个独立文件名的前面,完成后将文件名串联起来,文件名之间用单个空格隔开。
例如:
$(addprefix src/,foo bar)
结果为:
src/foo src/bar
3.2.7 $(wildcard pattern)
参数 pattern 是一个文件名格式,可以包含通配符,通配符用法和 shell 中的类似。函数 wildcard 的结果是一列与格式匹配且真实存在的文件名称,文件名之间用一个空格隔开。
例如,假设当前目录下有文件 1.c、2.c、1.h、2.h,则:
c_src := $(wildcard *.c)
结果为:
1.c 2.c
3.3 其他函数
3.3.1 $(foreach var,list,text)
前两个参数 var 和 list 会首先扩展,而最后一个参数 text 此时不扩展。接着,list 扩展后得到的每个字都会赋给变量 var。然后 text 引用该变量进行扩展,因此 text 每次扩展的结果都不相同。
函数的结果是由空格隔开的 text 在 list 中多次扩展后得到的新列表。也就是说,text 多次扩展后的字会串联起来,字与字之间由空格隔开。
下面是一个简单例子,把变量 files 的值设置为 dirs 中所有目录下的所有文件列表:
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))
3.3.2 $(if condition,then-part[,else-part])
首先把第一个参数 condition 的前导空格和结尾空格去掉,然后扩展。如果扩展结果为非空字符串,则条件 condition 为“真”;如果扩展结果为空字符串,则条件 condition 为“假”。
如果条件为“真”,就计算第二个参数 then-part 的值,并将该值作为整个函数 if 的值。
如果条件为“假”,并且第三个参数存在,就计算第三个参数 else-part 的值,并将该值作为整个函数 if 的值;如果第三个参数不存在,函数 if 将什么也不计算,返回空值。
需要注意:只能计算 then-part 和 else-part 二者之一,不能同时计算。这样做有可能产生副作用,比如调用 shell 函数时。
3.3.3 $(origin variable)
变量 variable 是一个查询变量的名称,不是对该变量的引用。所以这里不能采用 $ 和圆括号的格式书写该变量。当然,如果需要使用非常量的文件名,可以在文件名中使用变量引用。
函数 origin 的结果是一个字符串,用来表示该变量是如何定义的:
undefined:变量从未定义;
default:变量是缺省定义;
environment:变量作为环境变量定义,选项 -e 没有打开;
environment override:变量作为环境变量定义,选项 -e 已打开;
file:变量在 Makefile 中定义;
command line:变量在命令行中定义;
override:变量在 Makefile 中用 override 指令定义;
automatic:变量是自动变量。
3.3.4 $(shell command arguments)
函数 shell 是 make 与外部环境进行通讯的工具。函数 shell 的执行结果和在控制台上执行 command arguments 的结果相似。不过如果 command arguments 的结果中含有换行符或回车符,它们在函数 shell 的返回结果中会被处理成单个空格;如果返回结果末尾是换行符或回车符,则会被去掉。
例如,假设当前目录下有文件 1.c、2.c、1.h、2.h,则:
c_src := $(shell ls *.c)
结果为:
1.c 2.c
04 写在最后
这一节的内容不要求一次记住全部函数用法。你可以在阅读内核、bootloader、应用程序里的 Makefile 时,把它当作手册随时查询。掌握基本规则和几个高频函数之后,大部分日常构建需求都能应对。