本文介绍 GNU make 的 Makefile 语法。
写 Makefile
Makefile 告诉 make 程序,如何编译,链接,以及处理程序。
Makefile 的组成
Makefile 中包含 5 类语句:显式规则,隐式规则,变量定义,指令和注释。下面主要从这五个 方面描述 Makefile 的语法。
Makefile 文件名
默认情况下,make 程序按照以下文件名查找顺序,查找 Makefile:
GUNmakefile,makefile 和 Makefile
你的 makefile 通常应该叫做 makefile
或者 Makefile
。推荐叫做 Makefile
因为它
出现在文件夹列表的前面,并且靠近 README 文件。
如果指定的文件名不是以一个斜线开始,并且文件没有在当前文件夹中找到,会搜索其他几个文件
夹:首先会搜索通过选项 -I 或者 –include-dir 包含的文件夹。然后搜索以下文件夹:
/usr/local/include /usr/gnu/include /usr/local/include /usr/include
当仍然找不
到时,make 会继续处理其他的内容,当所有的内容都读入之后,会尝试构建没有找到的 Makefile,
如果无法构建出该 makefile , make 会报错并退出。
包含其它 Makefile
Makefile 可以使用 include 指令包含其它的 Makefile。当 make 程序遇到 include 指令的 时候,它会暂停读取当前 Makefile,而去读取出现在 include 指令中的所有的 Makefile。在 处理 include 指令的时候会进行通配符以及变量的扩展。例如:如果存在三个 Makefile,a.mk, b.mk,c.mk,并且 $(bar) 扩展为 bish 和 bash,那么以下表达式:
include *.mk $(bar)
等同于:
include a.mk b.mk c.mk bish bash
include 指令中可以包含多个 Makefile,多个 Makefile 之间以空格分隔。include 指令前面 可以存在空白,空白将会被 make 忽略。但是第一个字符一定不能是 TAB 键。
可以使用 -include
来代替 include
忽略包含文件未找到的错误。-
意思是告诉 make,
忽略操作错误,make 继续执行。
-include FILENAMES...
变量 MAKEFILES
Makefile 如何被重构
Make如何读取 Makefile
写规则(rule)
Makefile 的规则决定了什么时候以及如何更新规则的目标(target)。大多数情况下,每条规则 只有一个目标。规则还列出了目标所依赖的前提条件,以及用于更新或者创建目标的命令。
除了用于决定默认目标外,规则的顺序无关紧要。如果你没有显式地指定一个默认目标,那么 Makefile 中的第一个目标就是默认目标。但是存在两种特殊情况:如果第一个目标以一个点(.) 开始,那么它不能被当做默认目标,除非它同时包含了一个或者多个斜线(/);模式规则的目标也 不能作为默认目标。因此,基于这些理由,我们在写 Makefile 的时候,通常将第一个规则的目标 指定为我们要构建的整个程序的名字。
Rules 语法
在文件名中使用通配符
前提条件的搜索目录
伪(Phony)目标
伪目标并不是一个真正的文件名。它仅仅是你想要执行某一系列动作名字。使用伪目标有两个原因: 避免有一个文件和你要执行的动作同名;提升性能。
如果你写了一个不会创建目标文件的规则,那么该规则的命令每一次都会被执行,例如:
clean:
rm *.o temp
因为 rm 命令并不会创建一个叫做 clean 的文件,因此 make 认为 clean 目标始终需要生成,
当你每次执行 make clean
的时候,rm 命令都会被执行。
没有 Recipe 或 Prerequisites 的 Rule
使用空的 Target 文件记录事件
特殊的内建目标名
-
.DELETE_ON_ERROR
只要在 makefile 中定义了 .DELETE_ON_ERROR 作为一个目标,那么 make 将会在一个目标 的 Recipe 的退出码不为 0 的时候,删除这个这条规则的目标。就像它收到了一个信号一样。
具有多条规则的目标
一个文件可以作为多个规则的目标。所有规则的依赖项合并到一起组成了这个文件的依赖。如果该文 件比任何一个依赖项要旧,make 的时候就会执行规则进行重新构建该文件。
虽然可以存在多条规则共享同一个目标的情况。但是这些规则中只能有一条规则提供 Recipe。如果 有多个规则都指定了 Recipe 去构建同一个目标,那么 make 会使用最后一个 Recipe 并且会打印 出错误信息。
AOSP 构建系统的默认目标就是使用这种技术来使默认目标 droid
依赖于多个依赖项,在
build/core/main.mk
文件中存在下面的规则声明:
# This is the default target. It must be the first declared target.
.PHONY: droid
DEFAULT_GOAL := droid
$(DEFAULT_GOAL): droid_targets
.PHONY: droid_targets
droid_targets:
droid_targets: apps_only
# Building a full system-- the default is to build droidcore
droid_targets: droidcore dist_files
在上面的代码中:droid 是声明的第一个目标,因此是默认目标。droid 依赖于 droid_targets, 而 droid_targets 又在两条规则中分别依赖于 apps_only 和 droidcore,dist_files。
静态模式规则
双冒号规则
自动生成前提条件
写 Recipe
Recipe 语法
Recipe 回显
通常情况下,make 会在每一行指令执行前将它们打印出来。这个特性被称为回显,就好像是你自己 在输入命令一样。
但是,如果一行命令以 @
符号开头,那么那一行的回显就会被关闭。这个特性通常会用在本来就
是为了输出一些文本的命令前面,例如 echo
:
@echo About to make distribution files
但是,如果在调用 make 的时候传递了 -n
或者 --just-print
参数,以 @ 打头的行也会被
显示出来。
Recipe 执行
并行执行
错误
中断或者杀死 make
make 的递归使用
使用空的 Recipe
变量
变量是在 Makefile 中定义的用来代表一串文本的名字。变量可以用于 Makefile 中的任何位置。 在一些其它版本的 make 中,变量被称之为宏定义。
Makefile 中所有的变量和函数都会在被读取的时候被扩展,除了位于 recipe 中的变量引用,使 用等于号(”=”) 的变量定义的右边部分的变量引用,以及 define 指令内部的变量引用。也就是 说当变量引用位于 Recipe 中,或者位于 “=” 变量定义的的右边时,或者位于 define 变量定义 的变量体中时,变量不会在读取的时候被扩展。而出现在其它地方的变量引用都会在 Makefile 被 读取的时候就被扩展。
变量名可以是除了 ‘:’,’#’,’=’ 以及空格之外的任意字符序列。但是,变量名最好不要包含除了 字母、数字、下划线之外的其它字符。以大写字母和英文句点 ‘.’ 打头的变量可能在未来的 make 中被赋予特殊意义。另外,变量名区分大小写。有一些特殊的变量名只包含一个或者很少的标点符号, 这些是自动变量,将在后面的章节中介绍。
变量引用
要引用一个变量,先写一个美元符号,接着写一对儿大括号或者小括号,然后将变量的名字写在括号
内,例如:${foo}, $(foo)
都是对变量 foo 的引用。因此,如果要在文件名或者 Recipe 中
使用美元符号,就必须写两个美元符号:$$
。
变量引用可以用于任何的上下文中:目标,前提条件,Recipe,指令,新的变量值等等,下面是一个 例子:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
变量引用其实就是文本替换,因此,下面的代码片段:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
可以用于编译 c 程序 prog.c。因为变量值前面的空格会被忽略,因此 foo 变量的值就是一个字符
c
。虽然这可以工作,但是实际运用中请不要写这样的代码。
如果一个美元符号 $
后面跟的字符不是美元符号,大开括号,小开括号,那么就会将那个字符当
做一个变量的名字来对待。因此,你也可以使用 $x
来引用变量 x
。但是,除了自动变量,强
烈建议不要使用这种用法。
两种不同行为的变量
给 GNU make 变量赋值有两种不同的方式。我们称之为两种不同行为的变量。这两种行为的区别在于 它们是如何定义的以及它们是如何被展开的。
第一种变量是 递归展开 变量。这种变量使用 =
或者使用 define
指令定义。你给变量
赋的值按照字面意思传递给变量;如果变量的值中包含有对其它变量的引用,那么这些被包含的变量
只有在该包含变量被置换的时候才会被展开。例如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)
会显示 “Huh?”。 ‘$(foo)’ 扩展为 ‘$(bar)’,‘$(bar)’ 又扩展为 ‘$(ugh)’, ‘$(ugh)’ 最终 扩展为 ‘Huh?’。
简单展开 变量使用 :=
和 ::=
定义。两种格式对于 GNU make 来说是相同的;然而,
只有格式 ::=
是 POSIX 标准支持的。
简单展开变量在 makefile 被读取的时候就被展开。展开任何对其它变量的引用。展开之后,变量 就不包含对任何其它变量的引用了;因此:
x := foo
y := $(x) bar
x := later
等同于
y := foo bar
x := later
简单扩展变量通常使复杂的 makefile 编程编的更加可控,因为它们就像大多数编程语言中的变量 一样工作。它们允许你使用一个变量的值来重新定义该变量。
你也可以使用简单扩展变量来在变量中引入前导空格。由于变量值中的前导空格会在变量被替换的时 候被忽略;因此你可以使用音量引用来阻止前导空格被忽略,例如:
nullstring :=
space := $(nullstring) # end of the line
make 变量值尾部的空白并不会在变量扩展的时候被忽略变量 。上面的例子中, nullstring 变量
为空,因此 space 的值是一个空格。后面的注释:”# end of the line” 仅仅是为了使代码更加
清晰可读。仅仅在 $(nullstring)
的尾部加上一个空格,不加后面的注释也具有同样的效果,但
是会使代码难以理解,因为我们看不出来尾部到底有几个空格。下面的例子中:
dir := /foo/bar # directory to put the frobs in
dir 变量的值为 /foo/bar
(尾部有 4 个空格)。
还有另外一种变量赋值操作符:?=
。这是一个条件赋值操作符,只会在变量还没定义的时候才会
生效,语句:FOO ?= bar
跟下面的语句相同:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
注意:设置为空格变量任然是已定义变量,因此 ?=
不会设置为空的变量。
引用变量的高级特性
变量如何得到值
设置变量
往变量追加文本
### override 指令
定义跨多行的多行变量
环境变量
Target 特定变量
Pattern 特定变量
其他特殊变量
GNU make 支持一些特殊的变量,下面分别介绍
-
MAKEFILE_LIST
该变量包含 make 已经解析过的 makefile 的名字,按照解析的顺序。当 make 开始解析 makefile 之前,该 makefile 的名字就追加到变量
MAKEFILE_LIST
中。因此,如果你在 makefile 中做的第一件事情是检查变量MAKEFILE_LIST
的最后一个单词,那么它将是当前 makefile 的名字。一旦 makefile 包含了其它的 makefile,变量MAKEFILE_LIST
的值 的最后一个单词就是刚才包含的 makefile 的名字。例如,如果你有一个 makefile 叫做 Makefile:name1 := $(lastword $(MAKEFILE_LIST)) include inc.mk name2 := $(lastword $(MAKEFILE_LIST)) all: @echo name1 = $(name1) @echo name2 = $(name2)
那么输出将为:
name1 = Makefile name2 = inc.mk
Makefile 中的条件部分
使用函数处理文本
通过函数可以在 makefile 中进行文本处理工作:计算出要操作的文件的名字,或者要执行的命令等。 通常在函数调用中使用函数,传递给函数一些文本作为参数。函数调用的结果替换函数调用,就像变量 替换一样。
函数调用语法
使用函数处理字符串
-
strip string
移除字符串 string 的前导或者尾部空白字符,如果 string 内部有连续的多个空白字符,将 多个空白字符替换为一个空格。因此
$(strip a b c )
的结果是a b c
。strip 函数与条件语句中配合使用十分有用。当使用
ifeq
或者ifneq
和一个空字符串 比较的时候,通常我们希望只包含空白的字符串能够匹配空字符串。下面的一段程序可能不会有 想要的结果:.PHONY: all ifneq "$(needs_made)" "" all: $(needs_made) else all:;@echo "Nothing to make !" endif
将变量引用 $(needs_made) 替换为函数调用
$(strip $(needs_made))
可以是程序更加 健壮。
文件名操作函数
下面的各个函数都会对文件名进行一些特定的转化。函数的参数被当做一系列的以空格分隔的文件名, 前导和后置的空格被忽略。给函数提供的所有参数都以相同的方式处理,处理的结果以空格连接。
-
$(addprefix prefix, names...)
参数被当做一系列的以空格分隔的文件名;prefix 被当做一个单元。prefix 的值被添加到每 一个参数的前面,所有参数的扩展结果以一个空格连接在一起。例如:
$(addprefix src/,foo bar)
的结果是
src/foo src/bar
。
条件操作函数
make 选项
使用参数执行 Makefile
使用参数指定 Goals
Goals 是 make 最终将会更新的对象,目标(Goals)的前提条件或者前提条件的前提条件也会被更
新。默认情况下,目标是 makefile 中的第一个 target(不包含以 . 开头的 target)。因此,
通常在写 makefile 的时候,我们一般都将第一个 target 设定为编译整个程序,这样就使默认的
目标就可以编译所有的程序。如果 makefile 中的第一条规则有多个目标,那么只有第一个目标会
称为默认目标,即 Goal。你也可以在 makefile 中使用变量 .DEFAULT_GOAL
变量来自定义默认
目标。
我们也可以在命令行上面指定 goal。将你希望构建的对象作为参数在命令行上传递给 make,可以
传递一个或者多个目标,如果传递了多个目标,所有的目标都会按照它们传递给 make 的顺序构建。
make 会将你在命令行上面指定的所有目标赋给变量 MAKECMDGOALS
。如果没有在命令行上面指定
目标,那么该变量就为空。
不执行
使用 隐式规则
旧式风格的后缀规则
后缀规则是 make 旧式风格的 隐式规则。后缀规则已经被弃用,因为模式规则比后缀规则更加通用, 更加清晰。后缀规则仍然存在于 GNU make 中是为了保持向后兼容性。它们有两种:双后缀和单后 缀。
双后缀规则由一对后缀定义:目标后缀和源代码后缀。它会匹配以目标后缀结尾的任何文件。相应的
依赖文件由目标文件的名字将目标后缀替换为源代码后缀生成。目标后缀和源代码后缀分别为 ‘.o’
和 ‘.c’ 的双后缀规则等同于模式规则:%.o : %.c
。