Make 及 Android 编译系统介绍
本篇文档主要分三个部分, 第一部分是 Makefile 的写法, 第二部分概括性地介绍一下 Android 编译 过程, 第三部分以 libstagefright.so 为例, 介绍编译单个模块的整个流程.
一、Makefile 的写法
1.1 Make
Make 是一种组织程序构建过程的工具. 以 C 程序构建过程为例, 整个构建过程有预处理, 编译, 汇编, 链接等诸多步骤. 当需要构建的程序非常大时, 就需要类似 Make 的工具, 把整个构建过程有条不紊地 组织起来.
Make 通过读取 Makefile 配置文件进行工作, Makefile 是一系列构建规则的集合.
举个例子:
假定现在在某个 src
文件夹下, 文件夹中有例子中的各种 *.h
, *.c
文件, 还有一个名为 Makefile
的文件, Makefile
中的内容就是上面例子中的文本. 那么在命令行执行 make
命 令就会把 *.c
的源文件编译成目标文件, 并最终生成可执行文件 edit
, 也就完成了一次程序构 建过程. 也可以执行 make clean
命令, 这个命令相当于执行 rm edit main.o insert.o search.o files.o utils.o
, 会把 edit
及构建 edit
的过程中生成的目标文件都删除.
从上面的例子中, 可以提取出 Makefile 中的核心结构如下:
其中,
target 是一个构建目标, 可以是目标文件, 可执行文件, 也可以就是一个"标签"
prerequisites 是生成构建目标 target 所需要的依赖
commands 是生成构建目标 target 需要执行的命令
1.2 Makefile 的语法规则
1.2.1 包含其他 Makefile 文件
两条语句的区别是, 使用 -include <filename>
时, 如果找不到需要包含的文件, make 不会报 错, 会继续执行.
另外, 执行 make
命令时, 可以使用 -l
或者 --include-dir
参数, 来指定去哪些目录寻找 <filename>
对应的文件.
1.2.2 使用变量
makefile 中可以使用 =
或者 :=
来定义变量, 使用 $(var)
来获取变量 var
的值:
=
和 :=
的区别在于, =
可以使用整个 makefile 中任意位置的变量定义新变量, 而 :=
只能 使用在新变量定义之前的变量:
可以使用 +=
操作符对变量进行追加:的
这里 +=
中的 =
是 :=
还是 =
与变量在定义时使用的符号有关.
1.2.3 使用条件判断
makefile 支持简单的条件判断:
这里的 ifeq-else-endif
就是一组完整的条件判断关键字, 类似的关键字还有 ifneq
, ifdef
, ifndef
, 它们的含义分别是: 如果等于, 如果不等于, 如果已定义, 如果未定义.
1.2.4 使用函数
makefile 支持一些 make 内置的函数, 函数调用的语法是:
其中多个 arguments 以逗号隔开, 比如:
makefile 支持的字符串处理函数有:
$(subst <from>,<to>,<text>)
: 把 text 中的 from 换成 to$(strip <string>)
: 去除 string 首尾的空格$(sort <list>)
: 排序 list 字符串中的单词, $(sort foo bar lose) 返回 bar foo lose$(firstword <text>)
: 取 text 中的首个单词其他字符串函数: patsubst, findstring, filter, filter-out, word, wordlist, words
makefile 支持的文件名操作函数有:
$(dir <names...>)
: 从文件名序列中取出目录部分, 包括反斜杠 /$(notdir <names...>)
: 从文件名序列中取出非目录部分$(suffix <names...>)
: 取后缀函数, 包括 .$(basename <names...>)
: 取前缀部分, . 之前的所有内容$(addsuffix <suffix>,<names...>)
: 把后缀 加到 中的每个单词后面$(addprefix <prefix>,<names...>)
: 把前缀 加到 中的每个单词前面$(join <list1>,<list2>)
: 把 中的单词对应地加到 的单词后面,比如
$(join aaa bbb , 111 222 333)
返回值是 aaa111 bbb222 333
还有很多可用的函数, 可参考 GNU make , 不一一列举了.
1.2.5 使用隐式规则
下面是一个 makefile:
如果相关源文件存在, 执行 make foo
就会得到编译目标 foo
. 这个 makefile 并没有指出如何 构建 foo
的依赖 foo.o
和 bar.o
, make 内置的一些隐含规则会去寻找 foo.c
和 bar.c
进而生成 foo.o
和 bar.o
. 除了 make 的内置规则, 也可以定义自己的模式规则, 具体写法可参考 GNU make.
1.3 Android Makefile
Android 源码中有很多名为 Android.mk 的文件, 这些文件是组织 Android 源码编译的 makefile. 一个典型的 Android.mk 如下:
在最后一行 include $(BUILD_SHARED_LIBRARY)
之前, 都在定义或者清除变量, BUILD_SHARED_LIBRARY
的值是某一个文件的文件名, 把该文件包含进来, 并由该 文件中相关的语句执行编译, 生成动态库的过程.
由此可见, Android makefile 与普通 makefile 的区别, 只是 Android makefile 中有一些具有特定含 义的变量. 考虑到 Android 源码中定义了相当多的模块, 使用相同的变量名使得 Android makefile 的 书写和管理变得非常方便.
Android makefile 中常见的变量如下:
LOCAL_PATH := $(call my-dir)
, 这条语句获取当前 Android.mk 所在的路径, 并将其赋给
LOCAL_PATH
include $(CLEAR_VARS)
, 这条语句会将构建过程中使用的变量置为空, 避免之前的模块编译时定义的变量, 对此次编译产生影响.
LOCAL_SRC_FILES
, 是编译当前模块需要的源文件.LOCAL_C_INCLUDES
, 是查找头文件的路径.LOCAL_CFLAGS
, 是编译时的选项.LOCAL_STATIC_LIBRARIES
, 是编译模块依赖的静态库.LOCAL_SHARED_LIBRARIES
, 是编译模块依赖的动态库.LOCAL_MODULE
, 是当前编译的模块的名字.LOCAL_MODULE_TAGS
, 是模块的标签, 决定当前模块是否被编译进某个产品include $(BUILD_SHARED_LIBRARY)
, 将编译动态库的 makefile 文件包含进来, 决定如何通过上面的源文件, 依赖库等编译出当前编译的模块.
二、Android 编译流程介绍
2.1 Android 编译系统简介
在 Android 7.0 之前, 整个 Android 编译系统就是通过 makefile 来组织的, 模块目录下的 Android.mk
就是编译模块对应的 makefile.
从 Android 7.0 开始, Android 引入了新的编译配置文件 Android.bp
, bp
是 blueprint 的缩写, blueprint 文件与 Android.mk
文件功能类似, 都是描述通过哪些源文件, 哪些依赖库 等将对应模块编译出来. Android.bp
是一种类似于 json
格式的文件, 相对于 Android.mk
写法更简单, 统一.
Android.bp
是 Soong/Ninja 组织的编译系统的配置文件, blueprint 和 Soong 是两个转换工具 用来将 Android.bp
转换成 Ninja 格式的文件, 与 make 对应的工具 ninja 会读取 Ninja 格式的 文件来进行编译过程. 在 Android.bp
完全取代 Android.mk
之前, Android 项目中的 Android.mk
文件会被叫做 kali 的转换工具转成 Ninja 格式的文件, 也由 ninja 读取并进行相应的 编译过程.
2.2 Android 编译流程介绍
2.2.1 在当前 Shell 环境下引入相关变量和函数
进行 Android 源码编译的第一步是执行 source build/envsetup.sh
, envsetup.sh
是一个 shell 脚本文件, 其中定义了很多重要函数:
与编译相关的函数, 比如 lunch
, m
, mm
, mmm
, mma
, mmma
等;
一些功能性的函数, 比如 printconfig
打印出当前的配置信息, print_lunch_menu
打印出 当前可选的编译产品列表, croot
回到源码的根目录, cgrep
在所有的 C 文件中查找, jgrep
在所有的 Java 文件中查找等.
2.2.2 选择编译的产品
编译的第二步执行命令 lunch
, lunch
是第一步中引入的函数, 执行时可跟参数. 如果执行时 没有传入参数, 则会先使用 print_lunch_menu
函数打印出当前支持的所有产品, 再读取命令行 的输入作为选择. 确定了编译的产品之后, lunch
函数会对相关的环境变量进行设置, 并最终调用 print_config
函数打印出当前产品的配置.
2.2.3 执行 make 命令进行编译
做完前两步之后, 在源码根目录下执行 make
命令, 就开始了对整个 Android 源码的编译. 也可以进 到某一个模块目录下, 执行 mm
等单个模块的编译命令来编译单个模块. 编译单个模块的情形, 在本文的第三部分中进行介绍.
根据 make 的规则, make 在当前目录下寻找并读取 Makefile 文件, 根目录下 Makefile 中的内容 如下:
也就是把 build/make/core/main.mk
包含了进来, main.mk
才是 make 真正的入口.
在 main.mk
中定义了很多编译目标, 其中第一个也是默认的编译目标是 droid
:
因此, 在命令行执行 make
就等价于执行 make droid
, 从上面也可以看到, droid
依赖 的目标是 droid_targets
. 而 droid_targets
又依赖于 droid_core
, apps_only
, blueprint_tools
, dist_files
等, 层层依赖, 最终包含了整个 Android.
main.mk
中还定义了其他的编译目标, 比如 clean
, sdk
, all
, update-api
, snod
等, 其中 make snod
可以快速地重新打包 image.
除了定义了诸多编译目标, main.mk
中还引入了很多其他 makefile, 其中比较重要的有 config.mk
和 definitions.mk
.
config.mk
中又包含了其他的 makefile:
pathmap.mk
: 定义了一组值为特定目录下子目录名字的变量, 以便在某些模块的 makefile 中使用
BUILD_STATIC_LIBRARY:= $(BUILD_SYSTEM)/static_library.mk
这里看到了之前Android.mk
介绍中的BUILD_STATIC_LIBRARY
变量, 也确实看到了它的值是一个makefile 文件,
static_library.mk
中定义了如何编译静态库shared_library.mk
,java_library.mk
,package.mk
等功能都与static_library.mk
类似.
definitions.mk
中定义了一些名字, 比如 my-dir
, 用在
中用来获取当前 makefile 所在的路径. 还有 all-java-files-under
等, 可以获取当前目录下 所有 Java 文件等.
这一部分简单介绍了一下 Android 编译的整体情况, 总之就是, 所有的 makefile 层层包含, 其中定义 的大量编译目标最终组成了一棵编译目标树, makefile 解析完成后再逐步从目标树的叶子开始, 一层一层地向目标树根部编译, 最终完成了整个编译过程.
三、编译单个模块的流程介绍
这一部分以 libstagefright.so
的编译过程为例, 详细介绍编译单个模块的全过程. 为了简化 流程, 避开 Soong/Ninja 编译系统的格式转换等过程, 这里的介绍基于 Android 5.1 的源码.
待续...
Last updated
Was this helpful?