本文内容,全部转自老罗系列:http://blog.youkuaiyun.com/luoshengyang/article/details/18466779
图4 完整的文件依赖关系图
整个工程只有一个Makefile,听起来似乎是一件很疯狂的事情,因为这个Makefile可能会变得无比庞大和复杂。其实不用担心,我们可以按照模块来将这个Makefile划分成一个个Makefile片段(fragement),然后通过Makefile的include指令来将这些Makefile片段组装在一个Makefile中。与递归Makefile相比,每一个模块现在拥有的是一个Makefile片段,而不是一个Makefile文件。这正是Android编译系统的设计思想和原则,也就是说,我们平时所编写的Android.mk编译脚本都只不过是整个Android编译系统的一个Makefile片段。
在使用Android编译系统之前,我们需要打开一个shell进入到Android源码根目录中,并且在该shell中将build/envsetup.sh脚本文件source进来。脚本文件build/envsetup.sh被source到当前shell的过程中,会在vendor和device两个目录将厂商指定的envsetup.sh也source到当前shell当中,这样就可以获得厂商提供的产品配置信息。此外,脚本文件build/envsetup.sh还提供了以下几个重要的命令来帮助我们编译Android源码:
1. lunch
用来初始化编译环境,例如设置环境变量和指定目标产品型号。Lunch命令在执行的时候,主要做两件事情。第一件事情是设置TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_BUILD_TYPE和TARGET_BUILD_APPS等环境变量,用来指定目标产品类型和编译类型。第二件事情是通过make命令执行build/core/config.mk脚本,并且通过加载另外一个脚本build/core/dumpvar.mk打印出当前的编译环境配置信息。注意,build/core/config.mk和build/core/dumpvar.mk均为Makefile脚本,因此它们可以通过make命令来执行。另外,build/core/config.mk脚本还会加载一个名称为BoradConfig.mk的脚本以及build/core/envsetup.mk脚本来配置目标产品型号的相关信息。
2. m
相当于是在执行make命令。对整个Android源码进行编译。
3. mm
如果是在Android源码根目录下执行,那么就相当于是执行make命令对整个源码进行编译。如果是在Android源码根目录下的某一个子目录执行,那么就在会在从该子目录开始,一直往上一个目录直至到根目录,寻找是否存在一个Android.mk文件。如果存在的话,那么就通过make命令对该Android.mk文件描述的模块进行编译。
4. mmm
后面可以跟一个或者若干个目录。如果指定了多个目录,那么目录之间以空格分隔,并且每一个目录下都必须存在一个Android,mk文件。如果没有在目录后面通过冒号指定模块名称,那么在Android.mk文件中描述的所有模块都会被编译,否则只有指定的模块会被编译。如果需要同时指定多个模块,那么这些模块名称必须以逗号分隔。它的语法如下所示:
- mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]
(1). build/core/config.mk
该文件根据lunch命令所配置的产品信息在build/target/board、vendor或者device目录中找到对应的BoradConfig.mk文件,以及通过加载build/core/product_config.mk文件在build/target/product、vendor或者device目录中找到对应的AndroidProducts.mk文件,来进一步对编译环境进行配置,以便接下来编译指定模块时可以获得必要的信息。
(2). build/core/definitions.mk
该文件定义了在编译过程需要调用到的各种自定义函数。
(3). 指定的Android.mk
这些指定的Android.mk环境是由mmm命令通过环境变量ONE_SHOT_MAKEFILE传递给build/core/main.mk文件使用的。这些Android.mk文件一般还会通过环境变量BUILD_PACKAGE、BUILD_JAVA_LIBRARY、BUILD_STATIC_JAVA_LIBRARY、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY、BUILD_EXECUTABLE和BUILD_PREBUILT将build/core/package.mk、build/core/java_library.mk、build/core/static_java_library.mk、build/core/shared_library.mk、build/core/static_library.mk、build/core/executable.mk和build/core/prebuilt.mk等编译片段模板文件加载进来,来表示要编译是APK、Java库、Linux动态库/静态库/可执行文件或者预先编译好的文件等等。
(4). build/core/Makefile
该文件包含了用来制作system.img、ramdisk.img、boot.img和recovery.img等镜像文件的脚本。
对Android编译环境进行初始化很简单,分为两步。第一步是打开一个终端,并且将build/envsetup.sh加载到该终端中:
- $ . ./build/envsetup.sh
- including device/asus/grouper/vendorsetup.sh
- including device/asus/tilapia/vendorsetup.sh
- including device/generic/armv7-a-neon/vendorsetup.sh
- including device/generic/armv7-a/vendorsetup.sh
- including device/generic/mips/vendorsetup.sh
- including device/generic/x86/vendorsetup.sh
- including device/lge/mako/vendorsetup.sh
- including device/samsung/maguro/vendorsetup.sh
- including device/samsung/manta/vendorsetup.sh
- including device/samsung/toroplus/vendorsetup.sh
- including device/samsung/toro/vendorsetup.sh
- including device/ti/panda/vendorsetup.sh
- including sdk/bash_completion/adb.bash
http://www.gnu.org/s/bash/manual/bash.html#Programmable-Completion。
第二步是执行命令lunch,如下所示
我们看到lunch命令输出了一个Lunch菜单,该菜单列出了当前Android源码支持的所有设备型号及其编译类型。例如,第一项“full-eng”表示的设备“full”即为模拟器,并且编译类型为“eng”即为工程机。
当我们选定了一个Lunch菜单项序号(1-16)之后,按回车键,就可以完成Android编译环境的初始化过程。例如,我们选择1,可以看到以下输出:
我们可以看到,lunch命令帮我们设置好了很多环境变量。通过设置这些环境变量,就配置好了Android编译环境。
总体来说,Android编译环境初始化完成之后,获得了以下三样东西:
1. 将vendor和device目录下的vendorsetup.sh文件加载到了当前终端;
2. 新增了lunch、m、mm和mmm等命令;
3. 通过执行lunch命令设置好了TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_BUILD_TYPE和TARGET_BUILD_APPS等环境变量。
一. 文件build/envsetup.sh的加载过程
文件build/envsetup.sh是一个bash shell脚本,从它里面定义的函数hmm可以知道,它提供了lunch、m、mm和mmm等命令供我们初始化编译环境或者编译Android源码。
函数hmm主要完成三个工作:
1. 调用另外一个函数gettop获得Android源码的根目录T。
2. 通过cat命令显示一个Here Document,说明$T/build/envsetup.sh文件加载到当前终端后所提供的主要命令。
3. 通过sed命令解析$T/build/envsetup.sh文件,并且获得在里面定义的所有函数的名称,这些函数名称就是$T/build/envsetup.sh文件加载到当前终端后提供的所有命令。
注意,sed命令是一个强大的文本分析工具,它以行为单位为执行文本替换、删除、新增和选取等操作。函数hmm通过执行以下的sed命令来获得在$T/build/envsetup.sh文件定义的函数的
函数hmm主要完成三个工作:
1. 调用另外一个函数gettop获得Android源码的根目录T。
2. 通过cat命令显示一个Here Document,说明$T/build/envsetup.sh文件加载到当前终端后所提供的主要命令。
3. 通过sed命令解析$T/build/envsetup.sh文件,并且获得在里面定义的所有函数的名称,这些函数名称就是$T/build/envsetup.sh文件加载到当前终端后提供的所有命令。
注意,sed命令是一个强大的文本分析工具,它以行为单位为执行文本替换、删除、新增和选取等操作。函数hmm通过执行以下的sed命令来获得在$T/build/envsetup.sh文件定义的函数的
二. lunch命令的执行过程 lunch命令实际上是定义在文件build/envsetup.sh的一个函数,
1. 检查是否带有参数,即位置参数$1是否等于空。如果不等于空的话,就表明带有参数,并且该参数是用来指定要编译的设备型号及其编译类型的。如果等于空的话,那么就调用另外一个函数print_lunch_menu来显示Lunch菜单项,并且通过调用read函数来等待用户输入。无论通过何种方式,最终变量answer的值就保存了用户所指定的备型号及其编译类型。
2. 对变量answer的值的合法性进行检查。如果等于空的话,就将它设置为默认值“full-eng”。如果不等于空的话,就分为三种情况考虑。第一种情况是值为数字,那么就需要确保该数字的大小不能超过Lunch菜单项的个数。在这种情况下,会将输入的数字索引到数组LUNCH_MENU_CHOICES中去,以便获得一个用来表示设备型号及其编译类型的文本。第二种情况是非数字文本,那么就需要确保该文本符合<product>-<variant>的形式,其中<product>表示设备型号,而<variant>表示编译类型 。第三种情况是除了前面两种情况之外的所有情况,这是非法的。经过合法性检查后,变量selection代表了用户所指定的备型号及其编译类型,如果它的值是非法的,即它的值等于空,那么函数lunch就不往下执行了。
3. 接下来是解析变量selection的值,也就是通过sed命令将它的<product>和<variant>值提取出来,并且分别保存在变量product和variant中。提取出来的product和variant值有可能是不合法的,因此需要进一步通过调用函数check_product和check_variant来检查。一旦检查失败,也就是函数check_product和check_variant的返回值$?等于非0,那么函数lunch就不往下执行了。
4. 通过以上合法性检查之后,就将变量product和variant的值保存在环境变量TARGET_PRODUCT和TARGET_BUILD_VARIANT中。此外,另外一个环境变量TARGET_BUILD_TYPE的值会被设置为"release",表示此次编译是一个release版本的编译。另外,前面还有一个环境变量TARGET_BUILD_APPS,它的值被函数lunch设置为空,用来表示此次编译是对整个系统进行编译。如果环境变量TARGET_BUILD_APPS的值不等于空,那么就表示此次编译是只对某些APP模块进行编译,而这些APP模块就是由环境变量TARGET_BUILD_APPS来指定的。
5. 调用函数set_stuff_for_environment来配置环境,例如设置Java SDK路径和交叉编译工具路径等。
6. 调用函数printfconfig来显示已经配置好的编译环境参数。
Android编译系统环境是由build/core/config.mk、build/core/envsetup.mk、build/core/product_config.mk、AndroidProducts.mk和BoardConfig.mk等文件来完成的。这些mk文件涉及到非常多的细节,
Android编译环境初始化完成后,我们就可以用m/mm/mmm/make命令编译源代码了。当然,这要求每一个模块都有一个Android.mk文件。Android.mk实际上是一个Makefile脚本,用来描述模块编译信息。Android编译系统通过整合Android.mk文件完成编译过程。
从前面这篇文章可以知道,lunch命令其实是定义在build/envsetup.sh文件中的函数lunch提供的。与lunch命令一样,m、mm和mmm命令也分别是由定义在build/envsetup.sh文件中的函数m、mm和mmm提供的,而这三个函数又都是通过make命令来对源代码进行编译的。事实上,命令m就是对make命令的简单封装,并且是用来对整个Android源代码进行编译,而命令mm和mmm都是通过make命令来对Android源码中的指定模块进行编译。
m: 函数m调用函数gettop得到的是Android源代码根目录T。在执行make命令的时候,先通过-C选项指定工作目录为T,即Android源代码根目录,接着又将执行命令m指定的参数$@作为命令make的参数。从这里就可以看出,命令m实际上就是对命令make的简单封装。
mm: 函数mm首先是判断当前目录是否就是Android源码根目录,即当前目录下是否存在一个build/core/envsetup.mk文件和一个Makefile文件。如果是的话,就将命令mm当作是一个普通的make命令来执行。否则的话,就调用函数findmakefile从当前目录开始一直往上寻找是否存在一个Android.mk文件。如果在寻找的过程中,发现了一个Android.mk文件,那么就获得它的绝对路径,并且停止上述寻找过程。
由于接下来执行make命令时,我们需要指定的是要编译的Android.mk文件的相对于Android源码根目录路径,因此函数mm需要将刚才找到的Android.mk绝对文件路径M中与Android源码根目录T相同的那部分路径去掉。这是通过sed命令来实现的,也就是将字符串M前面与字符串T相同的子串删掉。
最后,将找到的Android.mk文件的相对路径设置给环境变量ONE_SHOT_MAKE,表示接下来要对它进行编译。另外,函数mm还将make命令目标设置为all_modules。这是什么意思呢?我们知道,一个Android.mk文件同时可以定义多个模块,因此,all_modules就表示要对前面指定的Android.mk文件中定义的所有模块进行编译。
mmm:
其中,dir-1、dir-2、dir-N都是包含有Android.mk文件的目录。在最后一个目录dir-N的后面可以带一个冒号,冒号后面可以通过逗号分隔一系列的模块名称module-1、module-2和module-M,用来表示要编译前面指定的Android.mk中的哪些模块。
当在Android源码中定义的各个模块都编译好之后,我们还需要将编译得到的文件打包成相应的镜像文件,例如system.img、boot.img和recorvery.img等,这样我们才可以将这些镜像烧录到目标设备去运行。
system.img镜像文件描述的是设备上的system分区,即/system目录,它是在build/core/Makefile文件中生成的
boot.img:build/core/main.mk文件定义了boot.img镜像文件的依赖规则,我们可以通过执行make bootimage命令来执行。其中,bootimage是一个伪目标,它依赖于INSTALLED_BOOTIMAGE_TARGET
boot.img的存在就是为BootLoader的第一阶段提供第二阶段、Kernel镜像、Ramdisk镜像,以及相应的启动参数等等
ramdisk.img:build/core/main.mk文件定义了ramdisk.img镜像文件的依赖规则,我们可以通过执行make ramdisk命令来执行
userdata.img镜像描述的是Android系统的data分区,即/data目录,里面包含了用户安装的APP以及数据等等。
build/core/main.mk文件定义了userdata.img镜像文件的依赖规则,我们可以通过执行make userdataimage命令来执行。
recovery.img是设备进入recovery模式时所加载的镜像。recovery模式就是用来更新系统的,我们可以认为我们的设备具有两个系统。一个是正常的系统,它由boot.img、system.img、ramdisk.img和userdata.img等组成,另外一个就是recovery系统,由recovery.img组成。平时我们进入的都是正常的系统,只有当我们需要更新这个正常的系统时,才会进入到recovery系统。因此,我们可以将recovery系统理解为在Linux Kernel之上运行的一个小小的用户空间运行时。这个用户空间运行时可以访问我们平时经常使用的那个系统的文件,从而实现对它的更新。
在build/core/Makefile文件中,定义了一个伪目标recoveryimage,用来生成recovery.img 由于recovery.img和boot.img都是用来启动系统的,因此,它们的内容是比较类似,例如都包含有Kernel及其启动参数、 Ramdisk,以及可选的BootLoader第二阶段