一 Makefile介绍
1.0什么是makefile
makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具.
make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。
首先,我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册,在这个示例中,我们的工程有8个C文件,和3个头文件,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:
1.如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2.如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3.如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,
make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。
1.1 Makefile的规则
在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。
target… : prerequisites …
command
…
…
-------------------------------------------------------------------------------
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites就是,要生成那个target所需要的文件或是目标。
command也就是make需要执行的命令。(任意的Shell命令)
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
1.2 一个示例
正如前面所说的,如果一个工程有3个头文件,和8个C文件,我们为了完成前面所述的那三个规则,我们的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
clean :
rm edit main.o kbd.o command.o display.o
insert.o search.o files.o utils.o
反斜杠(\)是换行符的意思。这样比较便于Makefile的易读。我们可以把这个内容保存在文件为“Makefile”或“makefile”的文件中,
然后在该目录下直接输入命令“make”就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一
下“make clean”就可以了。
在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件
和 .h文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件
生成的,换言之,目标文件是哪些文件更新的。
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
这里要说明一点的是,clean不是一个文件,它只不过是一个动作名字,有点像C语言中的lable一样,其冒号后什么也没有,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等
1.3 make是如何工作的
在默认的方式下,也就是我们只输入make命令。那么,
make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
如果edit所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件声明make的终极任务,也就是执行文件edit了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。
于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如file.c,那么根据我们的依赖性,我们的目标file.o会被重编译(也就是在这个依性关系后面所定义的命令),于是file.o的文件也是最新的啦,于是file.o的文件修改时间要比edit要新,所以edit也会被重新链接了(详见edit目标文件后定义的命令)。
而如果我们改变了“command.h”,那么,kdb.o、command.o和files.o都会被重编译,并且,edit会被重链接
1.4 makefile中使用变量
变量的定义:= ,:=, +=
variable = value #
variable := $(variable) value2 #只取这句前面的值,如果没有定义,则取空
variable += more #追加 每个值之间自动添加空格
变量的使用: $val
variable := $(variable) more
define 设置变量
还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行,这有利于定义一系列的命令(前面我们讲过“命令包”的技术就是利用这个关键字)。
define 指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以[Tab]键开头,那么make就不会把其认为是命令。
下面的这个示例展示了define的用法:
define two-lines
echo foo
echo $(bar)
endef
1.5 使用函数
一、函数的调用语法
函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:
$( )
或是
${ }
这里,就是函数名,make支持的函数不多。是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。函数调用以“
”
开
头
,
以
圆
括
号
或
花
括
号
把
函
数
名
和
参
数
括
起
。
感
觉
很
像
一
个
变
量
,
是
不
是
?
函
数
中
的
参
数
可
以
使
用
变
量
,
为
了
风
格
的
统
一
,
函
数
和
变
量
的
括
号
最
好
一
样
,
如
使
用
“
”开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用“
”开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用“(subst a,b,
(
x
)
)
”
这
样
的
形
式
,
而
不
是
“
(x))”这样的形式,而不是“
(x))”这样的形式,而不是“(subst
a,b,${x})”的形式。因为统一会更清楚,也会减少一些不必要的麻烦。
还是来看一个示例:
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst
(
s
p
a
c
e
)
,
(space),
(space),(comma),$(foo))
在这个示例中,
(
c
o
m
m
a
)
的
值
是
一
个
逗
号
。
(comma)的值是一个逗号。
(comma)的值是一个逗号。(space)使用了
(
e
m
p
t
y
)
定
义
了
一
个
空
格
,
(empty)定义了一个空格,
(empty)定义了一个空格,(foo)的值是“a b c”,
(
b
a
r
)
的
定
义
用
,
调
用
了
函
数
“
s
u
b
s
t
”
,
这
是
一
个
替
换
函
数
,
这
个
函
数
有
三
个
参
数
,
第
一
个
参
数
是
被
替
换
字
串
,
第
二
个
参
数
是
替
换
字
串
,
第
三
个
参
数
是
替
换
操
作
作
用
的
字
串
。
这
个
函
数
也
就
是
把
(bar)的定义用,调用了函数“subst”,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把
(bar)的定义用,调用了函数“subst”,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把(foo)中的空格替换成逗号,所以$(bar)的值是“
a,b,c”。
常用函数
函数调用,很像变量的使用,也是以“$”来标识的
${ }
字符串处理函数
$(subst ,,
$(patsubst ,,
$(strip ) 去空格函数
$(findstring , ) 查找字符串函数
$(filter <pattern…>,
$(filter-out <pattern…>,
$(sort ) 排序函数
$(word ,
$(words
$(firstword
文件名操作函数
$(dir <names…> ) 取目录函数
$(notdir <names…> ) 取文件函数
$(suffix <names…> ) 取后缀函数
$(basename <names…> ) 取前缀函数
$(addsuffix ,<names…> ) 加后缀函数
$(addprefix ,<names…> ) 加前缀函数
$(join , ) 连接函数
foreach 函数
$(foreach ,,
if 函数
$(if , )
或是
$(if ,, )
call函数
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:
$(call ,,,…)
origin函数
origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:
$(origin )
shell函数
shell 函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数
返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
1.6、伪目标
最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”,
clean:
rm *.o temp
正像我们前面例子中的“clean”一样,即然我们生成了许多文件编译文件,我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目标)
因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样。于是整个过程可以这样写:
.PHONY: clean
clean:
rm *.o temp
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目标”。
随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“makeclean”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“makecleanall”和“make cleanobj”和“makecleandiff”命令来达到清除不同种类文件的目的
参考:https://blog.csdn.net/weixin_38391755/article/details/80380786
二 、Android os 中的makefile分析
第一行命令“source build/envsetup.sh”引入了 build/envsetup.sh脚本。该脚本的作用是初始化编译环境,并引入一些辅助的 Shell 函数,这其中就包括第二步使用 lunch 函数。
除此之外,该文件中还定义了其他一些常用的函数,它们如表 1 所示:
表 1. build/envsetup.sh 中定义的常用函数
名称 说明
croot 切换到源码树的根目录
m 在源码树的根目录执行 make
mm Build 当前目录下的模块
mmm Build 指定目录下的模块
cgrep 在所有 C/C++ 文件上执行 grep
jgrep 在所有 Java 文件上执行 grep
resgrep 在所有 res/*.xml 文件上执行 grep
godir 转到包含某个文件的目录路径
printconfig 显示当前 Build 的配置信息
add_lunch_combo 在 lunch 函数的菜单中添加一个条目
第二行命令“lunch ”是调用 lunch 函数,lunch 函数的参数用来指定此次编译的目标设备以及编译类型。
第三行命令“make -j8”才真正开始执行编译。make 的参数“-j”指定了同时编译的 Job 数量,这是个整数,该值通常是编译主机 CPU 支持的并发线程总数的 1 倍或 2 倍(例如:在一个 4 核,每个核支持两个线程的 CPU 上,可以使用 make -j8 或 make -j16)。
在调用 make 命令时,如果没有指定任何目标,则将使用默认的名称为“droid”目标,该目标会编译出完整的Android 系统镜像
1.1Make 文件说明
整个 Build 系统的入口文件是源码树根目录下名称为“Makefile”的文件,当在源代码根目录上调用 make 命令时,make 命令首先将读取该文件。
Makefile 文件的内容只有一行:“include build/core/main.mk”。
该行代码的作用很明显:包含 build/core/main.mk 文件。在 main.mk 文件中又会包含其他的文件,其他文件中又会包含更多的文件,这样就引入了整个 Build 系统
main.mk
最主要的 Make 文件,该文件中首先将对编译环境进行检查,同时引入其他的 Make 文件。另外,该文件中还定义了几个最主要的 Make 目标,例如 droid,sdk,等(参见后文“Make 目标说明”)。
help.mk
包含了名称为 help 的 Make 目标的定义,该目标将列出主要的 Make 目标及其说明。
pathmap.mk
将许多头文件的路径通过名值对的方式定义为映射表,并提供 include-path-for 函数来获取。例如,通过 $(call include-path-for, frameworks-native)
便可以获取到 framework 本地代码需要的头文件路径。
envsetup.mk
配置 Build 系统需要的环境变量,例如:TARGET_PRODUCT,TARGET_BUILD_VARIANT,HOST_OS,HOST_ARCH 等。当前编译的主机平台信息(例如操作系统,CPU 类型等信息)就是在这个文件中确定的。另外,该文件中还指定了各种编译结果的输出路径。
combo/select.mk
根据当前编译器的平台选择平台相关的 Make 文件。
dumpvar.mk
在 Build 开始之前,显示此次 Build 的配置信息。
config.mk
整个 Build 系统的配置文件,最重要的 Make 文件之一。该文件中主要包含以下内容:定义了许多的常量来负责不同类型模块的编译。
定义编译器参数以及常见文件后缀,例如 .zip,.jar.apk。
根据 BoardConfig.mk 文件,配置产品相关的参数。
设置一些常用工具的路径,例如 flex,e2fsck,dx。
definitions.mk
最重要的 Make 文件之一,在其中定义了大量的函数。这些函数都是 Build 系统的其他文件将用到的。例如:my-dir,all-subdir-makefiles,find-subdir-files,sign-package 等,关于这些函数的说明请参见每个函数的代码注释。
distdir.mk
针对 dist 目标的定义。dist 目标用来拷贝文件到指定路径。
dex_preopt.mk
针对启动 jar 包的预先优化。
pdk_config.mk
顾名思义,针对 pdk(Platform Developement Kit)的配置文件
${ONE_SHOT_MAKEFILE}
ONE_SHOT_MAKEFILE 是一个变量,当使用“mm”编译某个目录下的模块时,此变量的值即为当前指定路径下的 Make 文件的路径。
${subdir_makefiles}
各个模块的 Android.mk 文件的集合,这个集合是通过 Python 脚本扫描得到的
。
post_clean.mk
在前一次 Build 的基础上检查当前 Build 的配置,并执行必要清理工作
。
legacy_prebuilts.mk
该文件中只定义了 GRANDFATHERED_ALL_PREBUILT 变量。
Makefile
被 main.mk 包含,该文件中的内容是辅助 main.mk 的一些额外内容
Android 源码中包含了许多的模块,模块的类型有很多种,例如:Java 库,C/C++ 库,APK 应用,以及可执行文件等 。并且,Java 或者 C/C++ 库还可以分为静态的或者动态的,库或可执行文件既可能是针对设备。不同类型的模块的编译步骤和方法是不一样,为了能够一致且方便的执行各种类型模块的编译,在 config.mk 中定义了许多的常量,这其中的每个常量描述了一种类型模块的编译方式,这些常量有:
BUILD_HOST_STATIC_LIBRARY
BUILD_HOST_SHARED_LIBRARY
BUILD_STATIC_LIBRARY
BUILD_SHARED_LIBRARY
BUILD_EXECUTABLE
BUILD_HOST_EXECUTABLE
BUILD_PACKAGE
BUILD_PREBUILT
BUILD_MULTI_PREBUILT
BUILD_HOST_PREBUILT
BUILD_JAVA_LIBRARY
BUILD_STATIC_JAVA_LIBRARY
BUILD_HOST_JAVA_LIBRARY
通过名称大概就可以猜出每个变量所对应的模块类型。(在模块的 Android.mk 文件中,只要包含进这里对应的常量便可以执行相应类型模块的编译。对于 Android.mk 文件的编写请参见后文:“添加新的模块”。)
这些常量的值都是另外一个 Make 文件的路径,详细的编译方式都是在对应的 Make 文件中定义的。这些常量和 Make 文件的是一一对应的,对应规则也很简单:常量的名称是 Make 文件的文件名除去后缀全部改为大写然后加上“BUILD_”作为前缀。例如常量 BUILD_HOST_PREBUILT 的值对应的文件就是 host_prebuilt.mk。
host_static_library.mk
定义了如何编译主机上的静态库。
host_shared_library.mk
定义了如何编译主机上的共享库。
static_library.mk
定义了如何编译设备上的静态库。
shared_library.mk
定义了如何编译设备上的共享库。
executable.mk
定义了如何编译设备上的可执行文件。
host_executable.mk
定义了如何编译主机上的可执行文件。
package.mk
定义了如何编译 APK 文件。
prebuilt.mk
定义了如何处理一个已经编译好的文件 ( 例如 Jar 包 )。
multi_prebuilt.mk
定义了如何处理一个或多个已编译文件,该文件的实现依赖 prebuilt.mk。
host_prebuilt.mk
处理一个或多个主机上使用的已编译文件,该文件的实现依赖 multi_prebuilt.mk。
java_library.mk
定义了如何编译设备上的共享 Java 库。
static_java_library.mk
定义了如何编译设备上的静态 Java 库。
host_java_library.mk
定义了如何编译主机上的共享 Java 库。
不同类型的模块的编译过程会有一些相同的步骤,例如:编译一个 Java 库和编译一个 APK 文件都需要定义如何编译 Java 文件。因此,表 3 中的这些 Make 文件的定义中会包含一些共同的代码逻辑。为了减少代码冗余,需要将共同的代码复用起来,复用的方式是将共同代码放到专门的文件中,然后在其他文件中包含这些文件的方式来实现的。这些包含关系如图 所示。
Make 目标说明
make /make droid
如果在源码树的根目录直接调用“make”命令而不指定任何目标,则会选择默认目标:“droid”(在 main.mk 中定义)。因此,这和执行“make droid”效果是一样的。
droid 目标将编译出整个系统的镜像。从源代码到编译出系统镜像,整个编译过程非常复杂。这个过程并不是在 droid 一个目标中定义的,而是 droid 目标会依赖许多其他的目标,这些目标的互相配合导致了整个系统的编译。
表 4. droid 所依赖的其他 Make 目标的说明
名称
说明
apps_only
该目标将编译出当前配置下不包含 user,userdebug,eng 标签(关于标签,请参见后文“添加新的模块”)的应用程序。
droidcore
该目标仅仅是所依赖的几个目标的组合,其本身不做更多的处理。
dist_files
该目标用来拷贝文件到 /out/dist 目录。
files
该目标仅仅是所依赖的几个目标的组合,其本身不做更多的处理。
prebuilt
该目标依赖于
(
A
L
L
P
R
E
B
U
I
L
T
)
,
(ALL_PREBUILT) ,
(ALLPREBUILT),(ALL_PREBUILT)
的作用就是处理所有已编译好的文件。
**
(
m
o
d
u
l
e
s
t
o
i
n
s
t
a
l
l
)
∗
∗
m
o
d
u
l
e
s
t
o
i
n
s
t
a
l
l
变
量
包
含
了
当
前
配
置
下
所
有
会
被
安
装
的
模
块
(
一
个
模
块
是
否
会
被
安
装
依
赖
于
该
产
品
的
配
置
文
件
,
模
块
的
标
签
等
信
息
)
,
因
此
该
目
标
将
导
致
所
有
会
被
安
装
的
模
块
的
编
译
。
∗
∗
(modules_to_install) ** modules_to_install 变量包含了当前配置下所有会被安装的模块(一个模块是否会被安装依赖于该产品的配置文件,模块的标签等信息),因此该目标将导致所有会被安装的模块的编译。 **
(modulestoinstall)∗∗modulestoinstall变量包含了当前配置下所有会被安装的模块(一个模块是否会被安装依赖于该产品的配置文件,模块的标签等信息),因此该目标将导致所有会被安装的模块的编译。∗∗(modules_to_check)
**
该目标用来确保我们定义的构建模块是没有冗余的。
(
I
N
S
T
A
L
L
E
D
A
N
D
R
O
I
D
I
N
F
O
T
X
T
T
A
R
G
E
T
)
∗
∗
该
目
标
会
生
成
一
个
关
于
当
前
B
u
i
l
d
配
置
的
设
备
信
息
的
文
件
,
该
文
件
的
生
成
路
径
是
:
o
u
t
/
t
a
r
g
e
t
/
p
r
o
d
u
c
t
/
<
p
r
o
d
u
c
t
n
a
m
e
>
/
a
n
d
r
o
i
d
−
i
n
f
o
.
t
x
t
s
y
s
t
e
m
i
m
a
g
e
生
成
s
y
s
t
e
m
.
i
m
g
。
∗
∗
(INSTALLED_ANDROID_INFO_TXT_TARGET) ** 该目标会生成一个关于当前 Build 配置的设备信息的文件,该文件的生成路径是:out/target/product/<product_name>/android-info.txt systemimage 生成 system.img。 **
(INSTALLEDANDROIDINFOTXTTARGET)∗∗该目标会生成一个关于当前Build配置的设备信息的文件,该文件的生成路径是:out/target/product/<productname>/android−info.txtsystemimage生成system.img。∗∗(INSTALLED_BOOTIMAGE_TARGET)
**
生成 boot.img。
**$(INSTALLED_RECOVERYIMAGE_TARGET) 生成 recovery.img。
(
I
N
S
T
A
L
L
E
D
U
S
E
R
D
A
T
A
I
M
A
G
E
T
A
R
G
E
T
)
生
成
u
s
e
r
d
a
t
a
.
i
m
g
。
∗
∗
(INSTALLED_USERDATAIMAGE_TARGET) 生成 userdata.img。 **
(INSTALLEDUSERDATAIMAGETARGET)生成userdata.img。∗∗(INSTALLED_CACHEIMAGE_TARGET)
**
生成 cache.img。
$(INSTALLED_FILES_FILE)
**
该目标会生成 out/target/product/<product_name>/ installed-files.txt 文件,该文件中内容是当前系统镜像中已经安装的文件列表
其他目标
Build 系统中包含的其他一些 Make 目标
主要 Make 目标
make clean
执行清理,等同于:rm -rf out/。
make sdk
编译出 Android 的 SDK。
make clean-sdk
清理 SDK 的编译产物。
make update-api
更新 API。在 framework API 改动之后,需要首先执行该命令来更新 API,公开的 API 记录在 frameworks/base/api 目录下。
make dist
执行 Build,并将 MAKECMDGOALS 变量定义的输出文件拷贝到 /out/dist 目录。
make all
编译所有内容,不管当前产品的定义中是否会包含。
make help
帮助信息,显示主要的 make 目标。
make snod
从已经编译出的包快速重建系统镜像。
make libandroid_runtime
编译所有 JNI framework 内容。
makeframework
编译所有 Java framework 内容。
makeservices
编译系统服务和相关内容。
make <local_target>
编译一个指定的模块,local_target 为模块的名称。
make clean-<local_target>
清理一个指定模块的编译结果。
makedump-products
显示所有产品的编译配置信息,例如:产品名,产品支持的地区语言,产品中会包含的模块等信息。
makePRODUCT-xxx-yyy
编译某个指定的产品。
makebootimage
生成 boot.img
makerecoveryimage
生成 recovery.img
makeuserdataimage
生成 userdata.img
makecacheimage
生成 cache.img
1.2 在 Build 系统中添加新的内容
添加新的产品
当我们要开发一款新的 Android 产品的时候,我们首先就需要在 Build 系统中添加对于该产品的定义。
在 Android Build 系统中对产品定义的文件通常位于 device 目录下(另外还有一个可以定义产品的目录是 vendor 目录,这是个历史遗留目录,Google 已经建议不要在该目录中进行定义,而应当选择 device 目录)。device 目录下根据公司名以及产品名分为二级目录,这一点我们在概述中已经提到过。
通常,
对于一个产品的定义通常至少会包括四个文件:
AndroidProducts.mk,产品版本定义文件,
BoardConfig.mk 以及 verndorsetup.sh。
下面我们来详细说明这几个文件。
AndroidProducts.mk:该文文件中的内容很简单,其中只需要定义一个变量,名称为“PRODUCT_MAKEFILES”,该变量的值为产品版本定义文件名的列表,例如:
产品版本定义文件:顾名思义,该文件中包含了对于特定产品版本的定义。该文件可能不只一个,因为同一个产品可能会有多种版本(例如,面向中国地区一个版本,面向美国地区一个版本)。该文件中可以定义的变量以及含义说明
PRODUCT_NAME
最终用户将看到的完整产品名,会出现在“关于手机”信息中。
PRODUCT_MODEL
产品的型号,这也是最终用户将看到的。
PRODUCT_LOCALES
该产品支持的地区,以空格分格,例如:en_GB de_DE es_ES fr_CA。
PRODUCT_PACKAGES
该产品版本中包含的 APK 应用程序,以空格分格,例如:Calendar Contacts。
PRODUCT_DEVICE
该产品的工业设计的名称。
PRODUCT_MANUFACTURER
制造商的名称。
PRODUCT_BRAND
该产品专门定义的商标(如果有的话)。
PRODUCT_PROPERTY_OVERRIDES
对于商品属性的定义。
PRODUCT_COPY_FILES
编译该产品时需要拷贝的文件,以“源路径 : 目标路径”的形式。
PRODUCT_OTA_PUBLIC_KEYS
对于该产品的 OTA 公开 key 的列表。
PRODUCT_POLICY
产品使用的策略。
PRODUCT_PACKAGE_OVERLAYS
指出是否要使用默认的资源或添加产品特定定义来覆盖。
PRODUCT_CONTRIBUTORS_FILE
HTML 文件,其中包含项目的贡献者。
PRODUCT_TAGS
该产品的标签,以空格分格。
三 、Android.mk介绍
Android.mk是Android提供的一种makefile文件,用来指定诸如编译生成so库名、引用的头文件目录、需要编译的.c/.cpp文件和.a静态库文件等。要掌握jni,就必须熟练掌握Android.mk的语法规范。
1.0、Android.mk文件的用途
一个android子项目中会存在一个或多个Android.mk文件
1、单一的Android.mk文件
直接参考NDK的sample目录下的hello-jni项目,在这个项目中只有一个Android.mk文件
2、多个Android.mk文件
如果需要编译的模块比较多,我们可能会将对应的模块放置在相应的目录中,
这样,我们可以在每个目录中定义对应的Android.mk文件(类似于上面的写法),
最后,在根目录放置一个Android.mk文件,内容如下:
include $(call all-subdir-makefiles)
只需要这一行就可以了,它的作用就是包含所有子目录中的Android.mk文件
3、多个模块共用一个Android.mk
这个文件允许你将源文件组织成模块,这个模块中含有:
-静态库(.a文件)
-动态库(.so文件)
只有共享库才能被安装/复制到您的应用软件(APK)包中
include $(BUILD_STATIC_LIBRARY),编译出的是静态库
include $(BUILD_SHARED_LIBRARY),编译出的是动态库
1.1、自定义变量
以下是在 Android.mk中依赖或定义的变量列表,可以定义其他变量为自己使用,但是NDK编译系统保留下列变量名:
-以 LOCAL_开头的名字(例如 LOCAL_MODULE)
-以 PRIVATE_, NDK_ 或 APP_开头的名字(内部使用)
-小写名字(内部使用,例如‘my-dir’)
如果为了方便在 Android.mk 中定义自己的变量,建议使用 MY_前缀,一个小例子:
MY_SOURCES := foo.c
ifneq ($(MY_CONFIG_BAR),)
MY_SOURCES += bar.c
endif
LOCAL_SRC_FILES +=
(
M
Y
S
O
U
R
C
E
S
)
注
意
:
‘
:
=
’
是
赋
值
的
意
思
;
′
+
=
′
是
追
加
的
意
思
;
‘
(MY_SOURCES) 注意:‘:=’是赋值的意思;'+='是追加的意思;‘
(MYSOURCES)注意:‘:=’是赋值的意思;′+=′是追加的意思;‘’表示引用某变量的值。
1.2、GNU Make系统变量
这些 GNU Make变量在你的 Android.mk 文件解析之前,就由编译系统定义好了。注意在某些情况下,NDK可能分析 Android.mk 几次,每一次某些变量的定义会有不同。
(1)CLEAR_VARS: 指向一个编译脚本,几乎所有未定义的 LOCAL_XXX 变量都在"Module-description"节中列出。必须在开始一个新模块之前包含这个脚本:include$(CLEAR_VARS),用于重置除LOCAL_PATH变量外的,所有LOCAL_XXX系列变量。
(2)BUILD_SHARED_LIBRARY: 指向编译脚本,根据所有的在 LOCAL_XXX 变量把列出的源代码文件编译成一个共享库。
注意,必须至少在包含这个文件之前定义 LOCAL_MODULE 和 LOCAL_SRC_FILES。
(3)BUILD_STATIC_LIBRARY: 一个 BUILD_SHARED_LIBRARY 变量用于编译一个静态库。静态库不会复制到的APK包中,但是能够用于编译共享库。
示例:include
(
B
U
I
L
D
S
T
A
T
I
C
L
I
B
R
A
R
Y
)
注
意
,
这
将
会
生
成
一
个
名
为
l
i
b
(BUILD_STATIC_LIBRARY) 注意,这将会生成一个名为 lib
(BUILDSTATICLIBRARY)注意,这将会生成一个名为lib(LOCAL_MODULE).a 的文件
(4)TARGET_ARCH: 目标 CPU平台的名字
(5)TARGET_PLATFORM: Android.mk 解析的时候,目标 Android 平台的名字.详情可考/development/ndk/docs/stable- apis.txt.
android-3 -> Official Android 1.5 system images
android-4 -> Official Android 1.6 system images
android-5 -> Official Android 2.0 system images
(6)TARGET_ARCH_ABI: 暂时只支持两个 value,armeabi 和 armeabi-v7a。。
(7)TARGET_ABI: 目标平台和 ABI 的组合,
1.3、模块描述变量
下面的变量用于向编译系统描述你的模块。应该定义在’include $(CLEAR_VARS)'和’include
(
B
U
I
L
D
X
X
X
X
X
)
′
之
间
。
(BUILD_XXXXX)'之间。
(BUILDXXXXX)′之间。(CLEAR_VARS)是一个脚本,清除所有这些变量。
(1)LOCAL_PATH: 这个变量用于给出当前文件的路径。
必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH := $(call my-dir)
如当前目录下有个文件夹名称 src,则可以这样写
(
c
a
l
l
s
r
c
)
,
那
么
就
会
得
到
s
r
c
目
录
的
完
整
路
径
这
个
变
量
不
会
被
(call src),那么就会得到 src 目录的完整路径 这个变量不会被
(callsrc),那么就会得到src目录的完整路径这个变量不会被(CLEAR_VARS)清除,因此每个 Android.mk 只需要定义一次(即使在一个文件中定义了几个模块的情况下)。
(2)LOCAL_MODULE: 这是模块的名字,它必须是唯一的,而且不能包含空格。
必须在包含任一的$(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。
(3)LOCAL_SRC_FILES: 这是要编译的源代码文件列表。
只要列出要传递给编译器的文件,因为编译系统自动计算依赖。注意源代码文件名称都是相对于 LOCAL_PATH的,你可以使用路径部分,例如:
LOCAL_SRC_FILES := foo.c toto/bar.c\
Hello.c
文件之间可以用空格或Tab键进行分割,换行请用""
如果是追加源代码文件的话,请用LOCAL_SRC_FILES +=
注意:可以LOCAL_SRC_FILES := KaTeX parse error: Double subscript at position 73: …。 (4)LOCAL_C_̲INCLUDES: 可选变量…(TARGET_ROOT_OUT)
至于LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的区别,暂时还不清楚。
(9)LOCAL_JNI_SHARED_LIBRARIES:定义了要包含的so库文件的名字,如果程序没有采用jni,不需要
LOCAL_JNI_SHARED_LIBRARIES := libxxx 这样在编译的时候,NDK自动会把这个libxxx打包进apk; 放在youapk/lib/目录下
四Android.bp相关介绍
早期的Android系统都是采用Android.mk的配置来编译源码,从Android 7.0开始引入Android.bp。很明显Android.bp的出现就是为了替换掉Android.mk。
再来说一说跟着Android版本相应的发展演变过程:
Android 7.0引入ninja和kati
Android 8.0使用Android.bp来替换Android.mk,引入Soong
Android 9.0强制使用Android.bp
转换关系图如下:
;
通过Kati将Android.mk转换成ninja格式的文件,通过Buleprint+ Soong将Android.bp转换成ninja格式的文件,通过androidmk将Android.mk转换成Android.bp,但针对没有分支、循环等流程控制的Android.mk才有效。
这里涉及到Ninja, kati, Soong, bp概念,接下来分别简单介绍一下。
- Ninja
ninja是一个编译框架,会根据相应的ninja格式的配置文件进行编译,但是ninja文件一般不会手动修改,而是通过将Android.bp文件转换成ninja格文件来编译。 - Android.bp
Android.bp的出现就是为了替换Android.mk文件。bp跟mk文件不同,它是纯粹的配置,没有分支、循环等流程控制,不能做算数逻辑运算。如果需要控制逻辑,那么只能通过Go语言编写。 - Soong
Soong类似于之前的Makefile编译系统的核心,负责提供Android.bp语义解析,并将之转换成Ninja文件。Soong还会编译生成一个androidmk命令,用于将Android.mk文件转换为Android.bp文件,不过这个转换功能仅限于没有分支、循环等流程控制的Android.mk才有效。 - Blueprint
Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong负责Android编译而设计的工具,而Blueprint只是解析文件格式,Soong解析内容的具体含义。Blueprint和Soong都是由Golang写的项目,从Android 7.0,prebuilts/go/目录下新增Golang所需的运行环境,在编译时使用。 - Kati
kati是专为Android开发的一个基于Golang和C++的工具,主要功能是把Android中的Android.mk文件转换成Ninja文件。代码路径是build/kati/,编译后的产物是ckati。
Android.bp文件配置规则
【模块和属性】
Android.bp描述的编译对象都是以模块为组织单位的,定义一个模块从模块的类型开始,模块有不同的类型,模块包含一些属性,下面举一个例子来具体说明:
cc_binary {
name: ”avbctl”,
defaults: [“avb_defaults”],
static_libs: [
“libavb_user”,
“libfs_mgr”,
],
shared_libs: [“libbase”],
srcs: [“tools/avbctl/avbctl.cc”],
}
上面例子中的cc_binary就是模块类型,表示该模块目标为二进制可执行文件。如果要编译一个APP,那么就使用android_app模块,要编译一个动态库,那么就使用cc_library_shared.soong工具支持的模块类型在android/build/soong/androidmk/cmd/androidmk/android.go中可以查到,有以下:
●模块类型后面用大括号“{}”将模块的所有属性包裹起来。
●每个属性的名字和值用中间用冒号连接起来,属性值要用双引号””包裹起来(如果属性值是变量,变量不需要加双引号):name: ”avbctl”表示模块name属性的值为avbctl,就是类比Android.mk中的LOCAL_MODULE := avbctl。模块的name属性是必须的,内容必须是独一无二的。如果属性被定义为数组,需要用中括号“[]”将数组的各元素包裹起来,每个元素中间用逗号“,”连接,一般常用的属性有name,srcs,cflags, cppflags, shared_libs,static_libs。
●查看全部支持的模块和各个模块支持的属性定义,请查看这个网址:https://ci.android.com/builds/submitted/6504066/linux/latest/view/soong_build.html。
●cc_defaults模块比较特殊,它表示该模块的属性可以被其他模块重复引用,类似于我们的头文件被其他cpp文件引用,举例:
cc_defaults {
name: “gzip_defaults”,
shared_libs: [“libz”],
stl: “none”,
}
cc_binary {
name: “gzip”,
defaults: [“gzip_defaults”],
/这里等价于
shared_libs: [“libz”],
stl: “none”,/
srcs: [“src/test/minigzip.c”],
}
●属性可以使用列表数组的形式,也可以使用unix通配符,例如:”*.java”
●每一条完整的属性定义语句加上逗号“,”表示结束
●注释包括单行注释//和多行注释/**/
●最重要的一点:目前android编译系统同时支持mk和bp两种,但是这两种是彼此单独运行的,所以bp中依赖的目标,例如动态库静态库目标,如果库是源代码的形式存在的,那么库的编译脚本必须也是通过bp文件编译才能被找到,否则用mk文件编译库,bp会提示找不到依赖的库目标