Quan Zhuo's Blog

Makefile 语法详解

2016-06-06

本文介绍 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


评论