Android 编译之make基础

引言

做android系统相关的开发已经有几年了,很早就想梳理一下make相关的知识。想详细介绍一下android.mk,讲讲android编译系统从android.mk到android.bp的变迁历史。但着手来写的时候发现还是必须要先介绍一下make的基础知识。又由于本人技术、知识水平所限,没有能力以完全的原创文章来介绍make的基础知识。因此,以徐海兵老师的《GNU make 中文手册》为主要素材,辅以部分自己的理解和使用实例来为大家介绍make的基础知识。读者也可以直接阅读《GNU make 中文手册》。

Android编译知识的梳理文章共三篇:

本文是第一篇,主要介绍make的基础知识。

1. make简介

1.1 make的起源

make是一个自动化构建工具。著名计算机科学家斯图亚特·费尔德曼(Stuart Feldman)于1977年在贝尔实验室(Bell Labs)创立了make。make通过读取一种叫做Makefile的文件,将源代码自动构建成可执行程序和库文件,Makefile中定义了目标程序的依赖关系和生成目标程序的相关规则。由于make被包含在Unix系统中,随着GNU/Linux从Unix衍生出来并发扬光大,GNU/Linux保留并扩展了原始的make,加入了许多内置函数和自动变量等等,形成了GNU make。现在被广泛使用的各种GNU/Linux发行版本,例如Ubuntu, Debian, CentOS等等,它们包含的实际上都是GNU make。

1.2 make的用途

make早期主要用于构建C语言开发的项目,后来逐渐发展,广泛用于构建C、C++、java等各种语言开发的项目。android 6.0及以下版本的系统源码就是使用大量Makefile(android.mk)来构建的。后来虽然google极力推广android.bp,但实际上各个原厂的android9.0、android10.0系统源码代码中仍有不少的android.mk。

2. Makefile简介

2.1 make命令和Makefile简介

Java项目中常用的构建工具 ant、maven、gradle都有自己的命令工具、构建规则、配置文件,例如,ant的命令工具为ant,配置文件为xml文件;gradle的命令工具为gradlew,配置文件为build.gradle。同样的,make作为自动化构建的祖师爷,也有着自己的命令工具、构建规则、配置文件。

make的命令工具是make,GNU/Linux中已经集成了GNU make,并且make已经被写入了系统环境变量。GNU/Linux下make的路径一般为:/usr/bin/make。make的配置文件为Makefile,在 Makefile 文件中描述了工程的构建规则,由命令工具make来解释其中的规则。

make 在执行时,需要读取至少一个Makefile文件。Makefile文件告诉 make 以何种方式编译源代码和链接程序。Makefile文件中描述了整个工程所有文件的依赖关系、编译规则。Makefile 像 C/C++、java等编程语言一样,有自己的书写格式、关键字、函数。在 Makefile 中可以使用系统 shell 所提供的任何命令来完成想要的工作。

实际使用make时,我们只需要在项目的目录下创建一个Makefile,然后在终端命令行中输入make,即可执行make进行编译。举个简单的例子:
我们在temp目录下新建一个Makefile,编辑它,在Makefile中输入:

first:
    echo first makefile

然后保存Makefile,再开启一个终端命令行,cd到temp目录,输入make,回车,即可执行:
qxt@ubuntu:~/temp$ make
echo first makefile
first makefile

可以看到执行make后,执行了我们在Makefile中编写的规则,打印了“first makefile”。当然,这个Makefile没有构建任何项目代码,仅仅打印了一个字符串。实际项目中的Makefile要复杂得多,后面我们会一步步介绍。

2.2 Makefile的构成

一个完整的 Makefile 一般包含 4 个元素:指示符、规则、变量、注释。

  • 指示符:
    指示符是Makefile中含有特殊含义的字符串。Makefile中的指示符包括一系列的关键字、内置函数、自动化变量、环境变量。指示符指明在 make 程序读取 makefile 文件过程中所要执行的一个动作。其中包括:
    a. 读取一个文件,读取给定文件名的文件,将其内容作为makefile文件的一部分。
    b. 决定(通常是根据一个变量的得值)处理或者忽略Makefile中的某一特定部分
    c. 定义一个多行变量。

  • 规则:
    规则又分为显式规则和隐含规则。
    显式规则:
    它描述了在何种情况下如何更新一个或者多个被称为目标的文件(Makefile 的目标文件)。书写 Makefile 时需要明确地给出目标文件、目标的依赖文件列表以及更新目标文件所需要的命令(有些规则没有命令,这样的规则只是纯粹的描述了文件之间的依赖关系)。
    隐含规则:
    它是make根据一类目标文件(典型的是根据文件名的后缀)而自动推导出来的规则。make根据目标文件的名,自动产生目标的依赖文件并使用默认的命令来对目标进行更新(建立一个规则)。

  • 变量:
    使用一个变量名(一个字符或字符串)代表一个变量值(另一个字符或字符串),当定义了一个变量以后,Makefile后续在需要使用这个变量值内容的地方,可以通过引用这个变量名来实现。

  • 注释:
    Makefile 中“#”字符后的内容被作为是注释内容(和 shell 脚本一样)处理。如果此行的第一个非空字符为“#”,那么此行为注释行。注释行的结尾如果存在反斜线(\),那么下一行也被作为注释行。一般在书写 Makefile时推荐将注释作为一个独立的行,而不要和 Makefile 的有效行放在一行中书写。当在 Makefile 中需要使用字符“#”时,可以使用反斜线加“#”(\#)来实现(对特殊字符“#”的转义),其表示将“#”作为一字符而不是注释的开始标志。

3. Makefile中的指示符

Makefile中的指示符包括一系列的关键字、内置函数、自动化变量、环境变量。

3.1 Makefile中的关键字

define: define用于定义变量。
endef: endef是定义变量的结束符,define和endef成对使用。
ifdef: ifdef判断变量是否已定义。
ifndef: ifndef判断变量是否未定义。
ifeq: ifeq判断两个变量是否相等。
ifneq: ifneq判断两个变量是否不相等。
else: 条件语句的分支处理。
endif: 条件语句的结束符。
include: include用于包含其他Makefile文件。
sinclude: sinclude等价于-include,用于兼容非GNU make。
override: override用于重载变量。
export: export将一个变量和它的值加入到当前工作的环境变量中。
unexport: unexport与export作用相反。

3.2 Makefile中的内置函数

subst
patsubst
strip
findstring
filter
filter-out
sort
word
words
wordlist
firstword
dir
notdir
suffix
basename
addsuffix
addprefix
join
wildcard
error
warning
shell
origin
foreach
call
if
eval
value
常用的内置函数的用法在接下来的章节中会介绍。

3.3 Makefile中的自动化变量

$@:表示规则的目标文件名。如果目标是一个文档文件(Linux中,一般称.a文件为文档文件,也称为静态库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是哪个触发规则被执行的目标文件名。

$%: 当规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是“foo.a(bar.o)”,那么,“$%”的值就为“bar.o”,“$@”的值为“foo.a”。如果目标不是静态库文件,其值为空。

$<: 规则的第一个依赖文件名。如果是一个目标文件使用隐含规则来重建,则它代表由隐含规则加入的第一个依赖文件。

$?: 所有比目标文件更新的依赖文件列表,空格分割。如果目标是静态库文件名,代表的是库成员(.o文件)。

$^:规则的所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有库成员(.o文件)名。一个文件可重复的出现在目标的依赖中,变量“$^”只记录它的一次引用情况。就是说它会去掉重复的依赖文件。

$+: 类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合.

$*:在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时,“茎”也包含目录(斜杠之前)部分)。

$(@D):表示目标文件的目录部分(不包括斜杠)。如果“$@”是“dir/foo.o”,那么“$(@D)”的值为“dir”。如果“$@”不存在斜杠,其值就是“.”(当前目录)。注意它和函数“dir”的区别!

$(@F): 目标文件的完整文件名中除目录以外的部分(实际文件名)。如果“$@”为“dir/foo.o”,那么“$(@F)”只就是“foo.o”。“$(@F)”等价于函数“$(notdir $@)”。

$(*D): 代表目标“茎”中的目录部分。

$(*F): 代表目标“茎”中的文件名部分。

$(%D): 当以如“archive(member)”形式静态库为目标时,表示库文件成员“member”名中的目录部分。它仅对这种形式的规则目标有效。

$(%F): 当以如“archive(member)”形式静态库为目标时,表示库文件成员“member”名中的文件名部分。它仅对这种形式的规则目标有效。

$(<D): 表示规则中第一个依赖文件的目录部分。

$(<F): 表示规则中第一个依赖文件的文件名部分。

$(^D): 表示所有依赖文件的目录部分(不存在同一文件)

$(^F): 表示所有依赖文件的文件部分(不存在同一文件)

$(+D): 表示所有依赖文件的目录部分(可存在重复文件)。

$(+F): 表示所有依赖文件的文件部分(可存在重复文件)。

$(?D): 表示被更新的依赖文件的目录部分。

$(?F): 表示被更新的依赖文件的文件名部分。

3.4 Makefile中的环境变量

MAKEFILES
MAKEFILES_LIST
VPATH
SHELL
MAKESHELL
MAKE
MAKELEVEL
MAKEFLAGS
MAKECMDGOALS
CURDIR
SUFFIXES
.LIBPATTERNS

4. MakeFile规则

介绍规则时,我们以显式规则为主,关于隐含规则可参考《GNU make中文手册》第十章 使用隐含规则。

4.1 规则简介

规则就是描述在什么情况下、如何重建目标文件,通常规则中包括了目标的依赖文件和重建目标的命令。make 执行重建目标的命令,来创建或者重建规则的目标。注意,一个目标也可能是另外一个目标的依赖文件。
一个简单的 Makefile 描述规则组成:

TARGET... : PREREQUISITES... 
COMMAND 
... 
... 
  • target(目标):
    目标通常是最后需要生成的文件名或者为了实现这个目的而必需的中间文件名。可以是.o文件、也可以是最后的可执行程序的文件名等。另外,目标也可以是一个make执行的动作的名称。例如,常见的目标“clean”,它不代表一个真正的文件名,在执行 make 时可以指定它来执行其所在规则定义的命令。我们称这样的目标为“伪目标”,伪目标前需要用".PHONY : [target]"声明,例如".PHONY : clean"。

  • prerequisites(依赖):
    依赖是生成目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。

  • command(命令):
    命令是规则所要执行的动作,可以是任意的 shell 命令或者是可在shell 下执行的程序。一个规则可以有多个命令行,每一条命令占一行。注意:每一个命令行必须以[Tab]字符开始,[Tab]字符告诉 make 此行是一个命令行。命令就是在任何一个目标的依赖文件发生变化后重建目标的动作描述。一个目标可以没有依赖而只有动作。比如 Makefile 中的目标“clean”,它没有依赖,只有命令。它所定义的命令用来删除 make 过程产生的中间文件(进行清理工作)。

4.2 规则的书写

书写规则时我们需要注意的几点:

  • (1). 规则的命令部分有两种书写方式:
    a. 命令可以和目标的依赖描述放在同一行。命令在依赖文件列表后并使用分号(;)和依赖文件列表分开。
    b. 命令在目标的依赖描述的下一行,作为独立的命令行。当作为独立的命令行时此行必须以[Tab]字符开始。在 Makefile 中,在第一个规则之后出现的所有以[Tab]字符开
    始的行都会被当作命令来处理。
  • (2). Makefile 中符号“$”有特殊的含义(表示变量或者函数的引用),在规则中需
    要使用字符“$”的地方,需要书写两个连续的(“$$”)。
  • (3). Makefile中可以将一个较长行使用反斜线(\)来分解为多行,这样可以使Makefile书写清晰、容易阅读理解。注意,反斜线之后不能有空格。
  • (4). Makefile 中表示文件名时可使用通配符。可使用的通配符有:“*”、“?”和“[…]”。在 Makefile 中通配符的用法和含义和 Linux基本相同。

4.3 目标的创建和更新

make 通过比较规则的目标和依赖的最后修改时间来决定哪些文件需要更新、哪些文件不需要更新。对需要更新的文件 make 就执行数据库中所记录的相应命令(在 make 读取 Makefile 以后会建立一个编译过程的描述数据库。此数据库中记录了所有各个文件之间的相互关系,以及它们的关系描述)来重建它,对于不需要重建的文件 make 什么也不做。

根据目标的规则处理目标的创建和更新时有如下原则:

  • (1). 目标文件不存在,使用其描述规则创建它;
  • (2). 目标文件存在,目标文件所依赖的文件中的任何一个比目标文件“更新”(依赖文件在上一次 make 之后被修改)。则根据规则重新编译生成目标文件;
  • (3). 目标文件存在,目标文件比它的任何一个依赖文件“更新”(依赖文件在上一次 make 之后没有被修改),则什么也不做。
  • (4). 默认的情况下,make执行的是Makefile中的第一个规则,此规则的第一个目标称之为“最终目的”或者“终极目标”,也就是一个Makefile最终需要更新或者创建的目标。
  • (5). 一个目标如果不是“终极目标”所依赖的(或者“终极目标”的依赖文件所依赖的),那么这个目标的规则将不会被执行,除非明确指定执行。

4.4 规则举例

#sample Makefile 
edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o 
    cc -o edit main.o kbd.o command.o display.o \
             insert.o search.o files.o utils.o 
main.o : main.c defs.h 
    cc -c main.c 
kbd.o : kbd.c defs.h command.h 
    cc -c kbd.c 
command.o : command.c defs.h command.h 
    cc -c command.c 
display.o : display.c defs.h buffer.h 
    cc -c display.c 
insert.o : insert.c defs.h buffer.h 
    cc -c insert.c 
search.o : search.c defs.h buffer.h 
    cc -c search.c 
files.o : files.c defs.h buffer.h command.h 
    cc -c files.c 
utils.o : utils.c defs.h 
    cc -c utils.c 
.PHONY : clean
clean : 
    rm edit main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o

例子中,edit是终极目标,edit依赖于多个.o文件,并且规则中定义了链接.o 文件生成目标“edit”的命令。

首次执行make时,由于这些.o文件并不存在,在执行链接命令之前,会首先处理目标“edit”的所有的依赖文件的规则(以这些.o 文件为目标的规则)。然后在所有依赖的.o文件生成后,再执行链接命令。

在首次执行make后,如果更改了源文件“insert.c”后执行make,“insert.o”将被更新,之后终极目标“edit”将会被重生成;如果我们修改了头文件“command.h”之后运行“make”,那么“kbd.o”、“command.o”和“files.o”将会被重新编译,之后同样终极目标“edit”也将被重新生成。

例子中的目标clean,由于它没有被终极目标edit所依赖。命令行只输入make时它不会被执行,只有通过命令行指定重建目标,即“make clean”,clean的规则才会被执行。

一个最简单的 Makefile 可能只包含规则,但一个 Makefile 文件中通常还包含了除规则以外的很多东西。有些 Makefile 中的规则可能看起来非常复杂,但是无论规则是多么的复杂,它都符合规则的基本格式,后续我们会一步一步的展开。

5. Makefile中的变量

5.1 变量的定义

在 Makefile 中,变量是一个名字,变量的值为一个文本字符串。在 Makefile 的目标、依赖、命令中引用变量的地方,变量会被它的值所取代。Makefile 的变量很像是 C 语言中的宏。Makefile 中变量可以使用“=”(包括“=”、“:=”、“+=”)和关键字“define”来定义。变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。

变量的命名规则:

  • (1). 变量名是不包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是,尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在make的后续版本中被赋予特殊含义,并且这样命名的变量对于一些shell来说是不能被作为环境变量来使用的。

  • (2). 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。Makefile 传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义的一般变量(例如:目标文件列表 objects)使用小写方式,而对于一些参数列表(例如:编译选项 CFLAGS)采用大写方式,但这并不是要求的。但建议对于一个工程,所有 Makefile 中的变量命名应保持一种风格。

  • (3). 另外有一些变量名只包含了一个或者很少的几个特殊的字符(符号)。称它们为
    自动化变量。像“$<”、“$@”、“$?”、“$*”等。

5.2 变量的赋值

变量赋值可以分为两大类,一类是使用“=”(包括“=”、“:=”、“+=”)对变量进行赋值,另一类是使用define关键字在定义的时候就进行赋值。具体区别接下来会一一介绍。

5.2.1 =

使用“=”定义和赋值的变量称为递归展开式变量。使用“=”定义的变量在定义时,变量值中引用的其他变量不会被替换展开;在引用该变量的地方,替换展开该变量时,它变量值中所引用的其它变量才会被一同替换展开。例如:

foo = $(bar) 
bar = $(ugh) 
ugh = Huh? 
all:;echo $(foo) 

执行“make”将会打印出“Huh?”。整个变量的替换过程时这样的:首先“$(foo)”被替换为“$(bar)”,接下来“$(bar)”被替换为“$(ugh)”,最后“$(ugh)”被替换为“Hug?”。整个替换的过程是在执行“echo $(foo)”时完成的。

递归展开式变量的优缺点:

  • 优点: 在定义变量时,可以引用其它的之前没有定义的变量(可能在后续部分定义,或者是通过 make 的命令行选项传递的变量)。
  • 缺点:使用此风格的变量定义,可能会由于出现变量的递归定义而导致 make 陷入到无限的变量展开过程中,最终使 make 执行失败。例如:
CFLAGS = $(CFLAGS) –O
5.2.2 :=

使用“:=”定义和赋值的变量称为直接展开式变量。使用“:=”定义的变量,变量值中对其他量或者函数的引用在定义变量时被展开(对变量进行替换)。所以变量被定义后就是一个字符串,其中不再包含任何变量的引用。例如:

x := foo 
y := $(x) bar 
x := later 

就等价于:

y := foo bar 
x := later
5.2.3 +=

“+=”通常用于追加变量值,当然也可以使用“+=”定义变量并直接追加变量值。
定义并追加赋值:

objects += another.o

也可以用于追加,例如:

objects = main.o foo.o bar.o utils.o 
objects += another.o 

上边的两个操作之后变量“objects”的值就为:“main.o foo.o bar.o utils.o another.o”。

5.2.4 define

define关键字常用于定义一个包含多行字符串的变量。“define”定义变量的语法格式:以关键字“define”开始,到关键字“endif”结束,之间的所有内容就是所定义变量的值。所要定义的变量名和关键字“define”在同一行,使用空格分开;关键字“define”所在行的下一行开始一直到“endif”所在行的上一行之间的若干行,是变量值。例如:

define two-lines 
echo foo 
echo $(bar) 
endef 

5.3 变量的引用

当我们定义了一个变量之后,就可以在 Makefile 的很多地方使用这个变量。变量的引用方式是:“$(VARIABLE_NAME)”或者“${ VARIABLE_NAME }”。例如:

objects := main.o foo.o bar.o utils.o 
echo $(objects)

输出的是objects变量的值:main.o foo.o bar.o utils.o

6. Makefile中的流程控制

6.1 Makefile中的条件判断

Makefile和C语言一样有条件语句,可以根据一个变量的值来控制 make 执行或者忽略 Makefile 的特定部分。条件语句可以是两个不同变量、或者变量和常量值的比较。Makefile 中使用条件控制可以使处理更加灵活和高效。

在“ifxxx”(ifxxx包括ifeq、ifneq、ifdef、ifndef)” 和 “else”、“endif”组成的条件语句中:

  • (1). “ifxxx”表示条件语句的开始,并指定了一个比较条件(相等/不相等/已定义/未定义)。之后是用圆括号括包围的、使用逗号“,”分割的两个参数,和关键字“ifxxx”用空格分开。参数中的变量引用在进行变量值比较时被展开。“ifxxx”之后就是当条件满足make 需要执行的,条件不满足时忽略。
  • (2). “else”之后就是当条件不满足时的执行部分。“else”还可以和“ifxxx”组合使用,用于处理条件语句的多个分支。不是所有的条件语句都需要此部分。
  • (3). “endif”表示一个条件语句的结束,任何一个条件表达式都必须以“endif”结
    束。
    例如:
var_a := a
var_b := b
var_c := c

ifeq ($(var_a),$(var_b)) 
var_c += $(var_a) 
else 
var_c += $(var_b) 
endif

echo $(var_c)

Makefile中条件语句有多个分支时,可以用else ifxxx(ifxxx包括ifeq、ifneq、ifdef、ifndef)处理多个分支。但需要注意else ifxxx之间的换行问题,不换行的else ifxxx和换行的else ifxxx是不同的:

  • 不换行的else ifxxx,仅需要一个endif与条件语句开头的ifxxx配对。
  • 换了行的else ifxxx,出现一个ifxxx就需要一个配对的endif。
    例如:
    else ifeq不换行:
target_arch := arm64
ifeq ($(target_arch),arm64)
    src_files := libs/arm64-v8a/libExample.so
else ifeq ($(target_arch),arm) 
    src_files := libs/armeabi-v7a/libExample.so
else ifeq ($(target_arch),x86_64) 
    src_files := libs/x86-64/libExample.so
else
    src_files := libs/x86/libExample.so
endif
so:
    echo $(src_files);

else ifeq换行:

target_arch := arm64
ifeq ($(target_arch),arm64)
    src_files := libs/arm64-v8a/libExample.so
else
ifeq ($(target_arch),arm)
    src_files := libs/armeabi-v7a/libExample.so
else
ifeq ($(target_arch),x86_64)
    src_files := libs/x86-64/libExample.so
else
    src_files := libs/x86/libExample.so
endif
endif
endif
so:
    echo $(src_files);

很明显,换行的else ifeq的写法看起来非常恶心,稍有不注意就有可能漏掉某一个endif。因此在实际使用时,应当尽量使用不换行的else ifeq,这样可以使Makefile看起来更加清晰。

6.2 Makefile中的循环执行

Makefile使用循环通常有两种方式,一种是利用规则的命令行,在命令行中使用shell循环;另外一种是使用make的内置函数foreach。

6.2.1 使用shell循环
SUBDIRS = foo bar baz 
subdirs: 
    for dir in $(SUBDIRS); \
        do \
          echo $$dir; \
        done

输出:
foo
bar
baz

6.2.2 使用foreach
src := main test util log
src_files := $(foreach f, $(src), $(f).c)
list:
    echo $(src_files);

输出:
main.c test.c util.c log.c

6.3 Makefile中的递归执行

Makefile的递归执行指的是:在 Makefile 中使用“make”作为一个命令来执行本身或者其它 Makefile 文件的过程。递归调用在一个存在有多级子目录的项目中非常有用。例如,当前目录下有一个“subdir”子目录,subdir目录中有描述此目录编译规则的 Makefile 文件。在执行 make 时,只需要从当前目录开始递归调用它的所有子目录,即可完成编译。

6.3.1 递归遍历子目录

例如,在当前目录/home/qxt/temp中有一个Makefile和一个子目录subdir,subdir中也有它自己的Makefile。
qxt@ubuntu:~/temp$ tree
├── Makefile
└── subdir
   └── Makefile

Makefile:

list: list_sub
    echo `pwd`;
list_sub: 
    $(MAKE) -C subdir

这里大写的MAKE是make环境变量,MAKE代表make命令的实际路径。不直接使用make,而使用环境变量MAKE,是为了Makefile可以兼容不同版本的make。因为不同版本的make,make命令的实际路径可能是不同的,环境变量可以为我们屏蔽掉这个差异。

这里的“-C”选项是指定更新make的当前工作目录。make中有一个环境变量“CURDIR”,“CURDIR”代表 make 的当前工作目录。当使用“-C”选项进入一个子目录后,“CURDIR”将被重新赋值。
subdir/Makefile:

list:
        echo `pwd`;

执行make后输出:
make -C subdir
make[1]: 进入目录“/home/qxt/temp/subdir”
echo `pwd`;
/home/qxt/temp/subdir
make[1]: 离开目录“/home/qxt/temp/subdir”
echo `pwd`;
/home/qxt/temp/

6.3.2 递归传递变量

递归时还可以将上层目录的Makefile中定义变量传给子目录的Makefile。我们稍微改造一下上面的Makefile代码:
Makefile:

export base_path = temp
list: list_sub
    echo `pwd`;
list_sub: 
    $(MAKE) -C subdir

subdir/Makefile:

list:
    echo current path:`pwd`, base patch:$(base_path)

执行make后输出:
make -C subdir
make[1]: 进入目录“/home/qxt/temp/subdir”
echo current path:`pwd`, base patch:temp
current path:/home/qxt/temp/subdir, base patch:temp
make[1]: 离开目录“/home/qxt/temp/subdir”
echo `pwd`;
/home/qxt/temp
可以看到,变量base_path定义前加了export之后,可以传递到子目录的Makefile中使用。

7. Makefile中的函数

make的函数是make中最重要的内容。make 的函数提供了处理文件名、变量、文本和命令的方法。使用函数可以使Makefile书写的更加灵活和健壮。可以在需要的地方地调用函数来处理指定的文本,将需要处理的文本作为函数的参数,调用函数后,使用函数的处理结果。

7.1 函数的调用

make 函数的调用格式类似于变量的引用,以“$”开始表示一个引用。语法
格式如下:
$(FUNCTION ARGUMENTS)
或者:
${FUNCTION ARGUMENTS}

对于函数调用的格式有以下几点说明:

  • (1). “FUNCTION”是需要调用的函数名,只能是make 的内置函数名。自定义的函数需要通过 make 的“call”函数来间接调用。

  • (2). “ARGUMENTS”是函数的参数,参数和函数名之间使用若干个空格或者[tab]字符分割。如果存在多个参数时,参数之间使用逗号“,”分开。

  • (3). 以“$”开头,使用成对的圆括号或花括号把函数名和参数括起。参数中存在变量或者函数的引用时,对它们所使用的分界符(圆括号或者花括号)建议和引用函数的相同,不使用两种不同的括号。推荐在变量引用和函数引用中统一使用圆括号。
    例如,在 Makefile 中应该这样来书写“$(sort $(x))”;而不是“$(sort ${x})”。

  • (4). 函数处理参数时,参数中如果存在对其它变量或者函数的引用,首先对这些引用进行展开得到参数的实际内容。而后才对它们进行处理。参数的展开顺序是按照参数的先后顺序来进行的。

  • (5). 书写时,函数的参数不能出现逗号“,”和空格。这是因为逗号被作为多个参数的分隔符,前导空格会被忽略。在实际书写 Makefile 时,当有逗号或者空格作为函数的参数时,需要把它们赋值给一个变量,在函数的参数中引用这个变量来实现。例如:

comma:= , 
empty:= 
space:= $(empty) $(empty) 
foo:= a b c 
bar:= $(subst $(space),$(comma),$(foo)) 

这样我们就实现了“bar”的值是“a,b,c”。

7.2 文本处理函数

7.2.1 subst 函数

函数的语法:$(subst FROM,TO,TEXT)

函数功能:字符串替换,把字串“TEXT”中的“FROM”字符替换为“TO”。

返回值:替换后的新字符串。

示例:
$(subst ee,EE,feet on the street)
替换“feet on the street”中的“ee”为“EE”,结果得到字符串“fEEt on the strEEt”。

7.2.2 patsubst 函数

函数的语法:$(patsubst PATTERN,REPLACEMENT,TEXT)

函数功能:模式替换,搜索“TEXT”中以空格分开的单词,将符合模式“PATTERN”的替换为“REPLACEMENT”。参数“PATTERN”中可以使用模式通配符“%”来代表一个单词中的若干字符。如果参数“REPLACEMENT”中也包含一个“%”,那么“REPLACEMENT”中的“%”将是“PTATTERN”中的那个“%”所代表的字符串。在“PTATTERN”和“REPLACEMENT”中,只有第一个“%”被作为模式字符来处理,之后出现的不再作模式字符(作为一个字符)。在参数中如果需要将第一个出现的“%”作为字符本身而不作为模式字符时,可使用反斜杠“\”进行转义处理。

返回值:替换后的新字符串。
函数说明:参数“TEXT”单词之间的多个空格在处理时被合并为一个空格,并忽略前导和结尾空格。

示例:
$(patsubst %.c,%.o,x.c.c bar.c)
把字串“x.c.c bar.c”中以.c 结尾的单词替换成以.o 结尾的字符。函数的返回结果是“x.c.o bar.o”

7.2.3 strip 函数

函数的语法:$(strip STRINT)

函数功能:去空格,去掉字串(若干单词,使用若干空字符分割)“STRINT”开头和结尾的空字符,并将其中多个连续空字符合并为一个空字符。

返回值:无前导和结尾空字符、使用单一空格分割的多单词字符串。

函数说明:空字符包括空格、[Tab]等不可显示字符。

示例:
STR = a b c
LOSTR = $(strip $(STR))
结果是“a b c”。

7.2.4 findstring 函数

函数的语法:$(findstring FIND,IN)

函数功能:查找字符串,搜索字串“IN”,查找“FIND”字串。

返回值:如果在“IN”之中存在“FIND”,则返回“FIND”,否则返回空。

函数说明:字串“IN”之中可以包含空格、[Tab]。搜索需要是严格的文本匹配。

示例:
$(findstring a,a b c)
$(findstring a,b c)
第一个函数结果是字“a”;第二个值为空字符。

7.2.5 filter 函数

函数的语法:$(filter PATTERN…,TEXT)

函数功能:过滤,过滤掉字串“TEXT”中所有不符合模式“PATTERN”的单词,保留所有符合此模式的单词。可以使用多个模式。模式中一般需要包含模式字符“%”。存在多个模式时,模式表达式之间使用空格分割。

返回值:空格分割的“TEXT”字串中所有符合模式“PATTERN”的字串。

函数说明:“filter”函数可以用来去除一个变量中的某些字符串,我们下边的例子中就是用到了此函数。

示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
使用“$(filter %.c %.s,$(sources))”的返回值给 cc 来编译生成目标“foo”,函数返回值为“foo.c bar.c baz.s”。

7.2.6 filter-out 函数

函数的语法:$(filter-out PATTERN...,TEXT)

函数功能:反过滤,和“filter”函数实现的功能相反。过滤掉字串“TEXT”中所有符合模式“PATTERN”的单词,保留所有不符合此模式的单词。可以有多个模式。存在多个模式时,模式表达式之间使用空格分割。

返回值:空格分割的“TEXT”字串中所有不符合模式“PATTERN”的字串。

函数说明:“filter-out”函数也可以用来去除一个变量中的某些字符串,(实现和“filter”函数相反)。

示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects))
实现了去除变量“objects”中“mains”定义的字串(文件名)功能。它的返回值
为“foo.o bar.o”。

7.2.7 sort 函数

函数的语法:$(sort LIST)

函数功能:排序,给字串“LIST”中的单词以首字母为准进行排序(升序),并取掉重复的单词。

返回值:空格分割的没有重复单词的字串。

函数说明:两个功能,排序和去字串中的重复单词。可以单独使用其中一个功能。

示例:
$(sort foo bar lose foo)
返回值为:“bar foo lose” 。

7.2.8 word 函数

函数的语法:$(word N,TEXT)

函数功能:取单词,取字串“TEXT”中第“N”个单词(“N”的值从 1 开始)。

返回值:返回字串“TEXT”中第“N”个单词。

函数说明:如果“N”值大于字串“TEXT”中单词的数目,返回空字符串。如果“N” 为 0,出错!

示例:
$(word 2, foo bar baz)
返回值为“bar”。

7.2.9 wordlist 函数

函数的语法:$(wordlist S,E,TEXT)

函数功能:取字串,从字串“TEXT”中取出从“S”开始到“E”的单词串。“S”和“E”表示单词在字串中位置的数字。

返回值:字串“TEXT”中从第“S”到“E”(包括“E”)的单词字串。

函数说明:“S”和“E”都是从 1 开始的数字。当“S”比“TEXT”中的字数大时,返回空。如果“E”大于“TEXT”字数,返回从“S”开始,到“TEXT”结束的单词串。如果“S”大于“E”,返回空。

示例:
$(wordlist 2, 3, foo bar baz)
返回值为:“bar baz”。

7.2.10 words 函数

函数的语法:$(words TEXT)

函数功能:统计单词数目,计算字串“TEXT”中单词的数目。

返回值:“TEXT”字串中的单词数。

示例:
$(words, foo bar)
返回值是“2”。所以字串“TEXT”的最后一个单词就是:$(word $(words TEXT),TEXT)。

7.2.11 firstword函数

函数的语法:$(firstword NAMES…)

函数功能:取首单词,取字串“NAMES…”中的第一个单词。

返回值:字串“NAMES…”的第一个单词。

函数说明:“NAMES”被认为是使用空格分割的多个单词(名字)的序列。函数忽略“NAMES…”中除第一个单词以外的所有的单词。

示例:
$(firstword foo bar)
返回值为“foo”。

7.3 文件名处理函数

7.3.1 dir函数

函数的语法:$(dir NAMES…)

函数功能:取目录,从文件名序列“NAMES…”中取出各个文件名的目录部分。文件名的目录部分就是包含在文件名中的最后一个斜线(“/”)(包括斜线)之前的部分。

返回值:空格分割的文件名序列“NAMES…”中每一个文件的目录部分。

函数说明:如果文件名中没有斜线,认为此文件为当前目录(“./”)下的文件。

示例:
$(dir src/foo.c hacks)
返回值为“src/ ./”。

7.3.2 notdir 函数

函数的语法:$(notdir NAMES…)

函数功能:取文件名,从文件名序列“NAMES…”中取出非目录部分。目录部分是指最后一个斜线(“/”)(包括斜线)之前的部分。删除所有文件名中的目录部分,只保留非目录部分。

返回值:文件名序列“NAMES…”中每一个文件的非目录部分。

函数说明:如果“NAMES…”中存在不包含斜线的文件名,则不改变这个文件名。

示例:
$(notdir src/foo.c hacks)
返回值为:“foo.c hacks”。

7.3.3 suffix 函数

函数的语法:$(suffix NAMES…)

函数功能:取后缀,从文件名序列“NAMES…”中取出各个文件名的后缀。后缀是文件名中最后一个以点“.”开始的(包含点号)部分,如果文件名中不包含一个点号,则为空。

返回值:以空格分割的文件名序列“NAMES…”中每一个文件的后缀序列。

函数说明:“NAMES…”是多个文件名时,返回值是多个以空格分割的单词序列。如果文件名没有后缀部分,则返回空。

示例:
$(suffix src/foo.c src-1.0/bar.c hacks)
返回值为“.c .c”。

7.3.4 basename 函数

函数的语法:$(basename NAMES…)

函数功能:取前缀,从文件名序列“NAMES…”中取出各个文件名的前缀部分(点号之后的部分)。前缀部分指的是文件名中最后一个点号之前的部分。

返回值:空格分割的文件名序列“NAMES…”中各个文件的前缀序列。如果文件没有前缀,则返回空字串。

函数说明:如果“NAMES…”中包含没有后缀的文件名,此文件名不改变。如果一个文件名中存在多个点号,则返回值为此文件名的最后一个点号之前的文件名部分。

示例:
$(basename src/foo.c src-1.0/bar.c /home/jack/.font.cache-1 hacks)
返回值为:“src/foo src-1.0/bar /home/jack/.font hacks”。

7.3.5 addsuffix 函数

函数的语法:$(addsuffix SUFFIX,NAMES…)

函数功能:加后缀,为“NAMES…”中的每一个文件名添加后缀“SUFFIX”。参数“NAMES…”为空格分割的文件名序列,将“SUFFIX”追加到此序列的每一个文件名的末尾。

返回值:以单空格分割的添加了后缀“SUFFIX”的文件名序列。

示例:
$(addsuffix .c,foo bar)
返回值为“foo.c bar.c”。

7.3.6 addprefix 函数

函数的语法:$(addprefix PREFIX,NAMES…)

函数功能:加前缀,为“NAMES…”中的每一个文件名添加前缀“PREFIX”。参数“NAMES…”是空格分割的文件名序列,将“SUFFIX”添加到此序列的每一个文件名之前。

返回值:以单空格分割的添加了前缀“PREFIX”的文件名序列。

示例:
$(addprefix src/,foo bar)
返回值为“src/foo src/bar”。

7.3.7 join 函数

函数的语法:$(join LIST1,LIST2)

函数功能:单词连接,将字串“LIST1”和字串“LIST2”各单词进行对应连接。就是将“LIST2”中的第一个单词追加“LIST1”第一个单词字后合并为一个单词;将“LIST2”中的第二个单词追加到“LIST1”的第一个单词之后并合并为一个单词,……依次列推。

返回值:单空格分割的合并后的字(文件名)序列。

函数说明:如果“LIST1”和“LIST2”中的字数目不一致时,两者中多余部分将被作为返回序列的一部分。

示例 1:
$(join a b , .c .o)
返回值为:“a.c b.o”。
示例 2:
$(join a b c , .c .o)
返回值为:“a.c b.o c”。

7.3.8 wildcard 函数

函数的语法:$(wildcard PATTERN)

函数功能:获取匹配模式文件名,列出当前目录下所有符合模式“PATTERN”格式的文件名。

返回值:空格分割的、存在当前目录下的所有符合模式“PATTERN”的文件名。

函数说明:“PATTERN”使用shell可识别的通配符,包括“?”(单字符)、“*”(多字符)等。

示例:
$(wildcard *.c)
返回值为当前目录下所有.c 源文件列表。

7.4 foreach 函数

函数的语法:$(foreach VAR,LIST,TEXT)

函数功能:循环函数,类似于 Linux 的 shell 中的for 语句。如果“VAR”和“LIST”存在变量或者函数的引用,首先展开变量“VAR”和“LIST”的引用;而表达式“TEXT”中的变量引用不展开。执行时把“LIST”中使用空格分割的单词依次取出赋值给变量“VAR”,然后执行“TEXT”表达式。重复直到“LIST”的最后一个单词(为空时结束)。“TEXT”中的变量或者函数引用在执行时才被展开,因此如果在“TEXT”中存在对“VAR”的引用,那么“VAR”的值在每一次展开时将会到的不同的值。

返回值:空格分割的多次表达式“TEXT”的计算的结果。

函数说明:函数中参数“VAR”是一个局部的临时变量,它只在“foreach”函数的上下文中有效,它的定义不会影响其它部分定义的同名“VAR”变量的值。在函数的执行过程中它是一个“直接展开”式变量。

示例:
src := main test util log
src_files := $(foreach f, $(src), $(f).c)
list:
echo $(src_files);
返回值为:main.c test.c util.c log.c

7.5 call 函数

函数语法:$(call VARIABLE,PARAM,PARAM,...)

函数功能:在执行时,将它的参数“PARAM”依次赋值给临时变量“$(1)”、“$(2)”(这些临时变量定义在“VARIABLE”的值中,参考下边的例子)…… call 函数对参数的数目没有限制,也可以没有参数值,没有参数值的“call”没有任何实际存在的意义。执行时变量“VARIABLE”被展开为在函数上下文有效的临时变量,变量定义中的“$(1)”作为第一个参数,并将函数参数值中的第一个参数赋值给它;变量中的“$(2)”一样被赋值为函数的第二个参数值;依此类推(变量$(0)代表变量“VARIABLE”本身)。之后对量“VARIABLE” 表达式的计算值。

返回值:参数值“PARAM”依次替换“$(1)”、“$(2)”…… 之后变量“VARIABLE”定义的表达式的计算值。

函数说明:

  1. 函数中“VARIBLE”是一个变量名,而不是变量引用。因此,通常“call”函数中的“VARIABLE”中不包含“$”(当然,除非此变量名是一个计算的变量名)。
  2. 当变量“VARIBLE”是一个 make 内嵌的函数名时(如“if”、“foreach”、“strip”等),对“PARAM”参数的使用需要注意,因为不合适或者不正确的参数将会导致函数的返回值难以预料。
  3. 函数中多个“PARAM”之间使用逗号分割。
  4. 变量“VARIABLE”在定义时不能定义为直接展开式!只能定义为递归展开式。

示例 :
reverse = $(2) $(1)
foo = $(call reverse,a,b)
变量“foo”的值为“ba”

7.6 shell 函数

函数语法:$(shell COMMAND PARAM)

函数功能:函数“shell”所实现的功能和shell中的引用(``)相同。实现对命令的扩展。这就意味着需要一个shell 命令作为此函数的参数,函数的返回结果是此命令在shell中的执行结果。make仅仅对它的返回结果进行处理;make将函数返回结果中的所有换行符(“\n”)或者一对“\n\r”替换为单空格;并去掉末尾的回车符号(“\n”)或者“\n\r”。进行函数展开式时,它所调用的命令(它的参数)得到执行。除对它的引用出现在规则的命令行和递归变量的定义中以外,其它决大多数情况下,make是在读取解析Makefile时完成对函数shell的展开。

返回值:函数“shell”的参数(一个 shell 命令)在 shell 环境中的执行结果。

函数说明:函数本身的返回值是其参数的执行结果,没有进行任何处理。对结果的处理是由 make 进行的。当对函数的引用出现在规则的命令行中,命令行在执行时函数才被展开。展开时函数参数(shell 命令)的执行是在另外一个 shell进程中完成的,因此需要对出现在规则命令行的多级“shell”函数引用需要谨慎处理,否则会影响效率(每一级的“shell”函数的参数都会有各自的 shell进程)。

示例 1:
contents := $(shell cat foo)
将变量“contents”赋值为文件“foo”的内容,文件中的换行符在变量中使用空格代替。

除了上面介绍的这些函数以外,常用的函数还有if、value、eval、origin等等,这里就不一一介绍,有兴趣的童鞋可以阅读《GNU make 中文手册》。

8. 结语

实际使用make时,我们不必掌握全部的make知识,相信大多数人也都是记不全的。我们只需要掌握make的大多数关键字、常用的内置函数、常用的环境变量,了解Makefile变量的使用,理解Makefile的规则,理解Makefile规则的目标是如何被创建和更新的就可以满足大多数日常开发需求了。实在遇到不常用的make用法,可以再查阅参考资料。

原则上本文不是一篇原创文章,本文大多数内容来自《GNU make 中文手册》。本文充其量是一篇读书笔记,对《GNU make 中文手册》原文进行了摘抄,摘抄加上自己的一点东西,重新排序、整理成文。

本文的目的,是对make的知识进行梳理,为接下来介绍android.mk打下基础。本文介绍了make中个人认为比较重要的内容,但对make的很多内容都没有详细介绍。因为make的知识比较多,多到足以写成一本书,而这并不在本人的知识和时间的允许范围内。有更高需求的童鞋,强烈建议阅读原文:《GNU make 中文手册》。

9. 本文参考

《GNU make 中文手册》ver3.8 翻译整理:徐海兵 2004-09-11
感谢原作者徐海兵老师的辛勤付出!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349